From 1544a666c30792d40d15e35c43d37d74a13734da Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 3 Dec 2025 23:31:37 +0530 Subject: [PATCH 1/4] Reminder Widget dynamic positioning --- ...LockScreenReminderWidgetPanelManager.swift | 50 ++++++++++++++----- .../LockScreenTimerWidgetManager.swift | 9 ++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift b/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift index 4a82398..27a0a8f 100644 --- a/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift +++ b/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift @@ -1,6 +1,7 @@ import AppKit import SwiftUI import SkyLightWindow +import QuartzCore @MainActor final class LockScreenReminderWidgetPanelManager { @@ -29,6 +30,20 @@ final class LockScreenReminderWidgetPanelManager { window.contentView = nil } + func refreshPosition(animated: Bool) { + guard let window, window.isVisible, let screen = NSScreen.main else { return } + let target = frame(for: window.frame.size, on: screen) + if animated { + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.22 + context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + window.animator().setFrame(target, display: true) + } + } else { + window.setFrame(target, display: true) + } + } + private func render(snapshot: LockScreenReminderWidgetSnapshot, makeVisible: Bool) { guard let screen = NSScreen.main else { return } if !makeVisible, window == nil { @@ -84,26 +99,35 @@ final class LockScreenReminderWidgetPanelManager { let screenFrame = screen.frame let originX = screenFrame.midX - (size.width / 2) let musicTop = LockScreenPanelManager.shared.latestFrame?.maxY ?? (screenFrame.midY - 32) - - let defaultWeatherBottom = screenFrame.midY + (screenFrame.height * 0.14) - let weatherBottom = LockScreenWeatherPanelManager.shared.latestFrame?.minY ?? min(screenFrame.maxY - 120, max(defaultWeatherBottom, musicTop + size.height + 80)) + let timerFrame = LockScreenTimerWidgetPanelManager.shared.latestFrame let marginAboveMusic: CGFloat = 16 + let marginAboveTimer: CGFloat = 24 let marginBelowWeather: CGFloat = 16 - let constrainedWeatherBottom = max(weatherBottom, musicTop + size.height + marginAboveMusic + marginBelowWeather) - let centerY = (musicTop + constrainedWeatherBottom) / 2 - let lowerBound = screenFrame.minY + 80 - let upperBound = constrainedWeatherBottom - marginBelowWeather - size.height + let baseLowerBound = musicTop + marginAboveMusic + let timerLowerBound = timerFrame.map { $0.maxY + marginAboveTimer } + let minYFloor = screenFrame.minY + 80 + let lowerBound = max(timerLowerBound ?? baseLowerBound, minYFloor) - var proposedY = centerY - (size.height / 2) - if proposedY < lowerBound { - proposedY = lowerBound - } - if upperBound > lowerBound { - proposedY = min(proposedY, upperBound) + let defaultWeatherBottom = screenFrame.midY + (screenFrame.height * 0.14) + let minimumWeatherBottom = lowerBound + size.height + marginBelowWeather + let weatherBottom = LockScreenWeatherPanelManager.shared.latestFrame?.minY ?? min(screenFrame.maxY - 120, max(defaultWeatherBottom, minimumWeatherBottom)) + + let upperBound = weatherBottom - marginBelowWeather - size.height + let clampedUpperBound = max(upperBound, lowerBound) + + let prefersTopAlignment = timerFrame != nil + var proposedY: CGFloat + + if prefersTopAlignment { + proposedY = clampedUpperBound + } else { + proposedY = (lowerBound + clampedUpperBound) / 2 } + proposedY = max(lowerBound, min(proposedY, clampedUpperBound)) + return NSRect(x: originX, y: proposedY, width: size.width, height: size.height) } } diff --git a/DynamicIsland/managers/LockScreenTimerWidgetManager.swift b/DynamicIsland/managers/LockScreenTimerWidgetManager.swift index bfc2b65..c71716a 100644 --- a/DynamicIsland/managers/LockScreenTimerWidgetManager.swift +++ b/DynamicIsland/managers/LockScreenTimerWidgetManager.swift @@ -101,6 +101,7 @@ final class LockScreenTimerWidgetPanelManager { private var hasDelegated = false private let animator = LockScreenTimerWidgetAnimator() private var hideTask: Task? + private(set) var latestFrame: NSRect? private init() {} @@ -109,11 +110,13 @@ final class LockScreenTimerWidgetPanelManager { let window = ensureWindow() let frame = targetFrame(on: screen) window.setFrame(frame, display: true) + latestFrame = frame window.alphaValue = 1 window.orderFrontRegardless() hideTask?.cancel() hideTask = nil animator.isPresented = true + LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: true) } func hide(animated: Bool = true) { @@ -125,6 +128,8 @@ final class LockScreenTimerWidgetPanelManager { if delay == 0 { window.orderOut(nil) hideTask = nil + latestFrame = nil + LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: true) return } @@ -133,6 +138,8 @@ final class LockScreenTimerWidgetPanelManager { await MainActor.run { window?.orderOut(nil) self?.hideTask = nil + self?.latestFrame = nil + LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: true) } } } @@ -150,6 +157,8 @@ final class LockScreenTimerWidgetPanelManager { } else { window.setFrame(frame, display: true) } + latestFrame = frame + LockScreenReminderWidgetPanelManager.shared.refreshPosition(animated: animated) } func refreshRelativeToMusicPanel(animated: Bool) { From f4b0231c23821adefc65370cdbff3b5d4f812f7f Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 4 Dec 2025 00:01:53 +0530 Subject: [PATCH 2/4] Update README --- ReadMe.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ReadMe.md b/ReadMe.md index 55c535e..db6970e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -152,6 +152,8 @@ Atoll builds upon the work of several open-source projects and draws inspiration - [**Stats**](https://github.com/exelban/stats) - source implementation for CPU temperature monitoring via SMC (System Management Controller) access, frequency sampling through IOReport bindings, and per-core CPU utilisation tracking. The system metrics collection architecture derives from Stats project readers. +- [**Open Meteo**](https://open-meteo.com) - weather apis for the lock screen widgets + ## Contributors From 106f431682c6f90c49bd90860a8df8262482cde8 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 4 Dec 2025 13:55:14 +0530 Subject: [PATCH 3/4] Update Settings View and stuff --- DynamicIsland/Localizable.xcstrings | 9 + .../components/Settings/SettingsView.swift | 158 ++---------------- .../DynamicIslandExtensionManager.swift | 62 ------- 3 files changed, 25 insertions(+), 204 deletions(-) delete mode 100644 DynamicIsland/managers/DynamicIslandExtensionManager.swift diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index dbb878a..90e667c 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -2033,6 +2033,7 @@ } }, "Add manually" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -6674,6 +6675,7 @@ } }, "Description" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -6790,6 +6792,7 @@ "isCommentAutoGenerated" : true }, "Disable" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -8581,6 +8584,7 @@ "isCommentAutoGenerated" : true }, "Extensions" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -10537,6 +10541,7 @@ } }, "Installed extensions" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -13893,6 +13898,7 @@ } }, "No extension installed" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -15721,6 +15727,7 @@ "isCommentAutoGenerated" : true }, "Restart" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -15863,6 +15870,7 @@ } }, "Running" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { @@ -19150,6 +19158,7 @@ } }, "Uninstall" : { + "extractionState" : "stale", "localizations" : { "ar" : { "stringUnit" : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 833c730..cbbb5f8 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -34,7 +34,6 @@ private enum SettingsTab: String, CaseIterable, Identifiable { case downloads case shelf case shortcuts - case extensions case about var id: String { rawValue } @@ -58,7 +57,6 @@ private enum SettingsTab: String, CaseIterable, Identifiable { case .downloads: return "Downloads" case .shelf: return "Shelf" case .shortcuts: return "Shortcuts" - case .extensions: return "Extensions" case .about: return "About" } } @@ -82,7 +80,6 @@ private enum SettingsTab: String, CaseIterable, Identifiable { case .downloads: return "square.and.arrow.down" case .shelf: return "books.vertical" case .shortcuts: return "keyboard" - case .extensions: return "puzzlepiece.extension" case .about: return "info.circle" } } @@ -106,7 +103,6 @@ private enum SettingsTab: String, CaseIterable, Identifiable { case .downloads: return .gray case .shelf: return .brown case .shortcuts: return .orange - case .extensions: return .blue case .about: return .secondary } } @@ -233,7 +229,6 @@ private struct SettingsForm: View { } struct SettingsView: View { - @StateObject var extensionManager = DynamicIslandExtensionManager() @State private var selectedTab: SettingsTab = .general @State private var searchText: String = "" @StateObject private var highlightCoordinator = SettingsHighlightCoordinator() @@ -260,11 +255,13 @@ struct SettingsView: View { .padding(.horizontal, 12) List(filteredTabs, selection: selectionBinding) { tab in + let isComingSoon = tab == .downloads NavigationLink(value: tab) { HStack(spacing: 10) { sidebarIcon(for: tab) + .opacity(isComingSoon ? 0.45 : 1) Text(tab.title) - .foregroundStyle(Color.primary) + .foregroundStyle(isComingSoon ? Color.secondary : Color.primary) if tab == .osd { Spacer() Text("ALPHA") @@ -276,11 +273,15 @@ struct SettingsView: View { Capsule() .fill(Color.orange) ) + } else if isComingSoon { + Spacer() + comingSoonTag() } } .padding(.vertical, 4) } .tag(tab) + .disabled(isComingSoon) .listRowInsets(EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 10)) } .listStyle(SidebarListStyle()) @@ -303,13 +304,12 @@ struct SettingsView: View { .opacity(0) // Invisible, but reserves space for a consistent look between tabs .disabled(true) } - .environmentObject(extensionManager) .environmentObject(highlightCoordinator) .formStyle(.grouped) .frame(width: 700) .onChange(of: searchText) { _, newValue in let matches = tabsMatchingSearch(newValue) - guard let firstMatch = matches.first else { return } + guard let firstMatch = matches.first(where: { $0 != .downloads }) else { return } if !matches.contains(resolvedSelection) { selectedTab = firstMatch } @@ -347,7 +347,10 @@ struct SettingsView: View { private var selectionBinding: Binding { Binding( get: { resolvedSelection }, - set: { selectedTab = $0 } + set: { newValue in + guard newValue != .downloads else { return } + selectedTab = newValue + } ) } @@ -382,7 +385,6 @@ struct SettingsView: View { .downloads, .shelf, .shortcuts, - .extensions, .about ] @@ -402,10 +404,11 @@ struct SettingsView: View { } private var searchSuggestions: [SettingsSearchEntry] { - Array(searchEntries(matching: searchText).prefix(8)) + Array(searchEntries(matching: searchText).filter { $0.tab != .downloads }.prefix(8)) } private func handleSearchSuggestionSelection(_ suggestion: SettingsSearchEntry) { + guard suggestion.tab != .downloads else { return } highlightCoordinator.focus(on: suggestion) selectedTab = suggestion.tab } @@ -583,12 +586,6 @@ struct SettingsView: View { SettingsSearchEntry(tab: .battery, title: "Show power status notifications", keywords: ["notifications", "power"], highlightID: SettingsTab.battery.highlightID(for: "Show power status notifications")), SettingsSearchEntry(tab: .battery, title: "Show power status icons", keywords: ["power icons", "charging icon"], highlightID: SettingsTab.battery.highlightID(for: "Show power status icons")), - // Downloads - SettingsSearchEntry(tab: .downloads, title: "Show download progress", keywords: ["downloads", "progress"], highlightID: SettingsTab.downloads.highlightID(for: "Show download progress")), - SettingsSearchEntry(tab: .downloads, title: "Enable Safari Downloads", keywords: ["safari", "browser"], highlightID: SettingsTab.downloads.highlightID(for: "Enable Safari Downloads")), - SettingsSearchEntry(tab: .downloads, title: "Download indicator style", keywords: ["indicator", "style"], highlightID: SettingsTab.downloads.highlightID(for: "Download indicator style")), - SettingsSearchEntry(tab: .downloads, title: "Download icon style", keywords: ["icon", "style"], highlightID: SettingsTab.downloads.highlightID(for: "Download icon style")), - // HUDs SettingsSearchEntry(tab: .hud, title: "Show Bluetooth device connections", keywords: ["bluetooth", "hud"], highlightID: SettingsTab.hud.highlightID(for: "Show Bluetooth device connections")), SettingsSearchEntry(tab: .hud, title: "Use circular battery indicator", keywords: ["battery", "circular"], highlightID: SettingsTab.hud.highlightID(for: "Use circular battery indicator")), @@ -714,8 +711,6 @@ struct SettingsView: View { switch tab { case .timer, .stats, .clipboard, .screenAssistant, .colorPicker, .shelf: return !enableMinimalisticUI - case .downloads: - return extensionManager.installedExtensions.contains { $0.bundleIdentifier == downloadManagerExtension } default: return true } @@ -810,10 +805,6 @@ struct SettingsView: View { SettingsForm(tab: .shortcuts) { Shortcuts() } - case .extensions: - SettingsForm(tab: .extensions) { - Extensions() - } case .about: if let controller = updaterController { SettingsForm(tab: .about) { @@ -1150,6 +1141,8 @@ struct Downloads: View { } } .navigationTitle("Downloads") + .disabled(true) + .opacity(0.6) } } @@ -1928,125 +1921,6 @@ struct Shelf: View { } } -struct Extensions: View { - @EnvironmentObject var extensionManager: DynamicIslandExtensionManager - @State private var effectTrigger: Bool = false - var body: some View { - Form { - //warningBadge("We don't support extensions yet") // Uhhhh You do? <><><> Oori.S - Section { - List { - ForEach(extensionManager.installedExtensions.indices, id: \.self) { index in - let item = extensionManager.installedExtensions[index] - HStack { - AppIcon(for: item.bundleIdentifier) - .resizable() - .frame(width: 24, height: 24) - Text(item.name) - ListItemPopover { - Text("Description") - } - Spacer(minLength: 0) - HStack(spacing: 6) { - Circle() - .frame(width: 6, height: 6) - .foregroundColor(isExtensionRunning(item.bundleIdentifier) ? .green : item.status == .disabled ? .gray : .red) - .conditionalModifier(isExtensionRunning(item.bundleIdentifier)) { view in - view - .shadow(color: .green, radius: 3) - } - Text(isExtensionRunning(item.bundleIdentifier) ? "Running" : item.status == .disabled ? "Disabled" : "Stopped") - .contentTransition(.numericText()) - .foregroundStyle(.secondary) - .font(.footnote) - } - .frame(width: 60, alignment: .leading) - - Menu(content: { - Button("Restart") { - let ws = NSWorkspace.shared - - if let ext = ws.runningApplications.first(where: { $0.bundleIdentifier == item.bundleIdentifier }) { - ext.terminate() - } - - if let appURL = ws.urlForApplication(withBundleIdentifier: item.bundleIdentifier) { - ws.openApplication(at: appURL, configuration: .init(), completionHandler: nil) - } - } - .keyboardShortcut("R", modifiers: .command) - Button("Disable") { - if let ext = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == item.bundleIdentifier }) { - ext.terminate() - } - extensionManager.installedExtensions[index].status = .disabled - } - .keyboardShortcut("D", modifiers: .command) - Divider() - Button("Uninstall", role: .destructive) { - // - } - }, label: { - Image(systemName: "ellipsis.circle") - .foregroundStyle(.secondary) - }) - .controlSize(.regular) - } - .buttonStyle(PlainButtonStyle()) - .padding(.vertical, 5) - } - } - .frame(minHeight: 120) - .actionBar { - Button {} label: { - HStack(spacing: 3) { - Image(systemName: "plus") - Text("Add manually") - } - .foregroundStyle(.secondary) - } - .disabled(true) - Spacer() - Button { - withAnimation(.linear(duration: 1)) { - effectTrigger.toggle() - } completion: { - effectTrigger.toggle() - } - extensionManager.checkIfExtensionsAreInstalled() - } label: { - HStack(spacing: 3) { - Image(systemName: "arrow.triangle.2.circlepath") - .rotationEffect(effectTrigger ? .degrees(360) : .zero) - } - .foregroundStyle(.secondary) - } - } - .controlSize(.small) - .buttonStyle(PlainButtonStyle()) - .overlay { - if extensionManager.installedExtensions.isEmpty { - Text("No extension installed") - .foregroundStyle(Color(.secondaryLabelColor)) - .padding(.bottom, 22) - } - } - } header: { - HStack(spacing: 0) { - Text("Installed extensions") - if !extensionManager.installedExtensions.isEmpty { - Text(" – \(extensionManager.installedExtensions.count)") - .foregroundStyle(.secondary) - } - } - } - } - .navigationTitle("Extensions") - // TipsView() - // .padding(.horizontal, 19) - } -} - struct LiveActivitiesSettings: View { @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var recordingManager = ScreenRecordingManager.shared diff --git a/DynamicIsland/managers/DynamicIslandExtensionManager.swift b/DynamicIsland/managers/DynamicIslandExtensionManager.swift deleted file mode 100644 index 4c6e417..0000000 --- a/DynamicIsland/managers/DynamicIslandExtensionManager.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// DynamicIslandExtensionManager.swift -// DynamicIsland -// -// Created by Harsh Vardhan Goswami on 04/08/24. -// DynamicIslandExtensionManager.swift -// DynamicIsland -// -// Created by Harsh Vardhan Goswami on 07/09/24. -// - -import Foundation -import SwiftUI - -var clipboardExtension: String = "dynamicisland.TheDynamicClipboard" -var hudExtension: String = "dynamicisland.TheDynamicHUDs" -var downloadManagerExtension: String = "dynamicisland.TheDynamicDownloadManager" - -struct Extension: Identifiable, Hashable { - var id = UUID() - var name: String - var bundleIdentifier: String - var status: StatusModel = .enabled -} - -enum StatusModel { - case disabled - case enabled -} - -class DynamicIslandExtensionManager: ObservableObject { - - @Published var installedExtensions: [Extension] = [] { - didSet { - // Only log when there's an actual change in content, not just initialization - if oldValue != installedExtensions { - print("📦 Extensions installed: \(installedExtensions.map { $0.name })") - } - } - } - - var extensions = [ - clipboardExtension, - hudExtension - ] - - init() { - checkIfExtensionsAreInstalled() - - DistributedNotificationCenter.default().addObserver(self, selector: #selector(checkIfExtensionsAreInstalled), name: NSNotification.Name("NSWorkspaceDidLaunchApplicationNotification"), object: nil) - } - - @objc func checkIfExtensionsAreInstalled() { - installedExtensions = [] - for extensionName in extensions { - if NSWorkspace.shared.urlForApplication(withBundleIdentifier: extensionName) != nil { - let ext = Extension(name: extensionName.components(separatedBy: ".").last ?? extensionName, bundleIdentifier: extensionName) - installedExtensions.append(ext) - } - } - } -} From ff3bdde7d336c5ab8abfb46d8150cb47626bfd7d Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Thu, 4 Dec 2025 20:06:04 +0530 Subject: [PATCH 4/4] Volume control now mutes completely --- DynamicIsland/managers/SystemMediaControllers.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DynamicIsland/managers/SystemMediaControllers.swift b/DynamicIsland/managers/SystemMediaControllers.swift index fe43156..562907e 100644 --- a/DynamicIsland/managers/SystemMediaControllers.swift +++ b/DynamicIsland/managers/SystemMediaControllers.swift @@ -52,6 +52,7 @@ final class SystemVolumeController { private var listenersInstalled = false private var volumeElement: AudioObjectPropertyElement? private var muteElement: AudioObjectPropertyElement? + private let silenceThreshold: Float = 0.001 // Treat very low values as mute requests. private let candidateElements: [AudioObjectPropertyElement] = [ kAudioObjectPropertyElementMain, @@ -101,7 +102,13 @@ final class SystemVolumeController { func setVolume(_ value: Float) { let clamped = max(0, min(1, value)) - if isMuted && clamped > 0 { + let currentlyMuted = isMuted + + if clamped <= silenceThreshold { + if !currentlyMuted { + setMuted(true) + } + } else if currentlyMuted { setMuted(false) }