From c555aa40a604a6fc926338b040999fe53bf35088 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 26 Nov 2025 13:12:32 +0530 Subject: [PATCH 1/6] BugFix: Bluetooth Battery Percentage Report --- .../managers/BluetoothAudioManager.swift | 168 +++++++++++++++++- 1 file changed, 163 insertions(+), 5 deletions(-) diff --git a/DynamicIsland/managers/BluetoothAudioManager.swift b/DynamicIsland/managers/BluetoothAudioManager.swift index ba03a9d..ae2fa61 100644 --- a/DynamicIsland/managers/BluetoothAudioManager.swift +++ b/DynamicIsland/managers/BluetoothAudioManager.swift @@ -40,6 +40,10 @@ class BluetoothAudioManager: ObservableObject { private var missingBatteryLog: Set = [] private var lastBatteryStatusUpdate: Date? private let batteryStatusUpdateInterval: TimeInterval = 20 + private let pmsetFetchQueue = DispatchQueue(label: "com.dynamicisland.bluetooth.pmset", qos: .utility) + private var isPmsetRefreshInFlight = false + private var lastPmsetRefreshDate: Date? + private let pmsetRefreshCooldown: TimeInterval = 5 // MARK: - Initialization private init() { @@ -423,7 +427,7 @@ class BluetoothAudioManager: ObservableObject { triggerLiveBatteryRefreshIfNeeded() } - private func applyConnectedDeviceBatteryLevels() { + private func applyConnectedDeviceBatteryLevels(triggerPmsetFallback: Bool = true) { guard !connectedDevices.isEmpty else { lastConnectedDevice = nil return @@ -446,6 +450,11 @@ class BluetoothAudioManager: ObservableObject { if let last = updatedDevices.last { lastConnectedDevice = last } + + if triggerPmsetFallback, + updatedDevices.contains(where: { $0.batteryLevel == nil }) { + requestPmsetFallback(reason: "missing battery after refresh") + } } private func bestBatteryLevel(for device: BluetoothAudioDevice) -> Int? { @@ -456,6 +465,44 @@ class BluetoothAudioManager: ObservableObject { ?? device.batteryLevel } + private func requestPmsetFallback(reason: String) { + guard connectedDevices.contains(where: { $0.batteryLevel == nil }) else { return } + guard !isPmsetRefreshInFlight else { return } + + let now = Date() + if let lastPmsetRefreshDate, + now.timeIntervalSince(lastPmsetRefreshDate) < pmsetRefreshCooldown { + return + } + + isPmsetRefreshInFlight = true + print("🎧 [BluetoothAudioManager] 🔄 Triggering pmset fallback (\(reason))") + pmsetFetchQueue.async { [weak self] in + guard let self else { return } + let entries = self.collectPmsetAccessoryBatteryEntries() + DispatchQueue.main.async { + self.handlePmsetFallbackResults(entries) + } + } + } + + private func handlePmsetFallbackResults(_ entries: [PmsetAccessoryBatteryEntry]) { + isPmsetRefreshInFlight = false + lastPmsetRefreshDate = Date() + guard !entries.isEmpty else { return } + + var updatedNames = batteryStatusByName + let newlyFilled = mergePmsetEntries(entries, into: &updatedNames, logNewEntries: true) + guard !newlyFilled.isEmpty else { return } + + batteryStatusByName = updatedNames + applyConnectedDeviceBatteryLevels(triggerPmsetFallback: false) + + if let level = hudBatteryLevelCandidate() { + updateActiveBluetoothHUDBattery(with: level) + } + } + private func triggerLiveBatteryRefreshIfNeeded() { guard !connectedDevices.isEmpty else { return } guard connectedDevices.contains(where: { $0.batteryLevel == nil }) else { return } @@ -534,10 +581,8 @@ class BluetoothAudioManager: ObservableObject { guard didUpdate else { return } applyConnectedDeviceBatteryLevels() - if let lastLevel = lastConnectedDevice?.batteryLevel { - updateActiveBluetoothHUDBattery(with: lastLevel) - } else if let fallbackLevel = connectedDevices.last(where: { $0.batteryLevel != nil })?.batteryLevel { - updateActiveBluetoothHUDBattery(with: fallbackLevel) + if let level = hudBatteryLevelCandidate() { + updateActiveBluetoothHUDBattery(with: level) } } @@ -550,6 +595,11 @@ class BluetoothAudioManager: ObservableObject { } } + private func hudBatteryLevelCandidate() -> Int? { + lastConnectedDevice?.batteryLevel + ?? connectedDevices.last(where: { $0.batteryLevel != nil })?.batteryLevel + } + private func coreBluetoothCacheSnapshot() -> CoreBluetoothCacheSnapshot { guard let preferences = UserDefaults(suiteName: bluetoothPreferencesSuite), let coreCache = preferences.object(forKey: "CoreBluetoothCache") as? [String: [String: Any]] else { @@ -626,6 +676,46 @@ class BluetoothAudioManager: ObservableObject { static let empty = CoreBluetoothCacheSnapshot(byAddress: [:], byName: [:], namesByUUID: [:]) } + private struct PmsetAccessoryBatteryEntry { + let displayName: String + let normalizedName: String + let level: Int + } + + @discardableResult + private func mergePmsetEntries( + _ entries: [PmsetAccessoryBatteryEntry], + into names: inout [String: Int], + logNewEntries: Bool + ) -> [PmsetAccessoryBatteryEntry] { + guard !entries.isEmpty else { return [] } + + var newlyFilled: [PmsetAccessoryBatteryEntry] = [] + + for entry in entries { + let clamped = clampBatteryPercentage(entry.level) + let previous = names[entry.normalizedName] + + if previous == nil { + newlyFilled.append(entry) + names[entry.normalizedName] = clamped + continue + } + + if let previous, clamped > previous { + names[entry.normalizedName] = clamped + } + } + + if logNewEntries { + for entry in newlyFilled { + print("🎧 [BluetoothAudioManager] â„šī¸ pmset reported \(entry.level)% for \(entry.displayName)") + } + } + + return newlyFilled + } + private func batteryLevelFromDefaults(forAddress address: String?) -> Int? { guard let address, !address.isEmpty else { return nil } guard let preferences = UserDefaults(suiteName: bluetoothPreferencesSuite) else { return nil } @@ -701,6 +791,9 @@ class BluetoothAudioManager: ObservableObject { mergeBatteryLevels(into: &combinedAddressPercentages, from: profiler.addresses) mergeBatteryLevels(into: &combinedNamePercentages, from: profiler.names) + let pmsetEntries = collectPmsetAccessoryBatteryEntries() + mergePmsetEntries(pmsetEntries, into: &combinedNamePercentages, logNewEntries: true) + var statuses: [String: String] = [:] for (key, value) in combinedAddressPercentages { statuses[key] = String(clampBatteryPercentage(value)) @@ -867,6 +960,71 @@ class BluetoothAudioManager: ObservableObject { return (addressPercentages, namePercentages) } + private func collectPmsetAccessoryBatteryEntries() -> [PmsetAccessoryBatteryEntry] { + let process = Process() + process.launchPath = "/usr/bin/pmset" + process.arguments = ["-g", "accps"] + + let outputPipe = Pipe() + process.standardOutput = outputPipe + process.standardError = Pipe() + + do { + try process.run() + } catch { + return [] + } + + let data = outputPipe.fileHandleForReading.readDataToEndOfFile() + process.waitUntilExit() + + guard process.terminationStatus == 0 else { + return [] + } + + guard !data.isEmpty, let output = String(data: data, encoding: .utf8) else { + return [] + } + + guard let regex = try? NSRegularExpression( + pattern: #"^\s*-\s*(.+?)\s*(?:\(.+?\))?\s+(\d+)\s*%"#, + options: [.anchorsMatchLines] + ) else { + return [] + } + + var entries: [PmsetAccessoryBatteryEntry] = [] + let nsOutput = output as NSString + let range = NSRange(location: 0, length: nsOutput.length) + + regex.enumerateMatches(in: output, options: [], range: range) { match, _, _ in + guard let match, match.numberOfRanges >= 3 else { return } + + let rawName = nsOutput + .substring(with: match.range(at: 1)) + .trimmingCharacters(in: .whitespacesAndNewlines) + let percentString = nsOutput.substring(with: match.range(at: 2)) + + guard !rawName.isEmpty, let level = Int(percentString) else { return } + + let normalizedName = normalizeProductName(rawName) + guard !normalizedName.isEmpty else { return } + if normalizedName.hasPrefix("internalbattery") { + return + } + + entries.append( + PmsetAccessoryBatteryEntry( + displayName: rawName, + normalizedName: normalizedName, + level: level + ) + ) + } + + return entries + } + private func identifiersFromDeviceCachePayload(_ payload: [String: Any]) -> [String] { var identifiers: Set = [] let candidateKeys = ["DeviceAddress", "Address", "BD_ADDR", "SerialNumber"] From 18f68f56eab018c45f6c4eda34b5423dba4a3b53 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 26 Nov 2025 15:48:07 +0530 Subject: [PATCH 2/6] BugFix: Hide Notch from Screen Recordings --- DynamicIsland/DynamicIslandApp.swift | 2 + .../components/Clipboard/ClipboardPanel.swift | 29 +----- .../ColorPicker/ColorPickerPanel.swift | 27 +----- .../Notch/DynamicIslandWindow.swift | 2 + .../ScreenAssistant/ChatPanels.swift | 88 +++---------------- .../ScreenAssistant/ModelSelectionPanel.swift | 27 +----- .../ScreenAssistantPanel.swift | 27 +----- .../components/Settings/SettingsView.swift | 2 +- .../Settings/SettingsWindowController.swift | 28 +----- .../ScreenCaptureVisibilityManager.swift | 51 +++++++++++ .../managers/ClipboardWindowManager.swift | 19 ++-- .../LockScreenLiveActivityWindowManager.swift | 2 + .../managers/LockScreenPanelManager.swift | 2 + ...LockScreenReminderWidgetPanelManager.swift | 2 + .../LockScreenWeatherPanelManager.swift | 2 + .../managers/MusicControlWindowManager.swift | 2 + .../managers/TimerControlWindowManager.swift | 2 + DynamicIsland/models/Constants.swift | 2 +- 18 files changed, 105 insertions(+), 211 deletions(-) create mode 100644 DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 04417c5..7d37ba9 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -781,6 +781,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.isRestorable = false window.identifier = NSUserInterfaceItemIdentifier("OnboardingWindow") + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) + onboardingWindowController = NSWindowController(window: window) } diff --git a/DynamicIsland/components/Clipboard/ClipboardPanel.swift b/DynamicIsland/components/Clipboard/ClipboardPanel.swift index dec51fb..b79f6ba 100644 --- a/DynamicIsland/components/Clipboard/ClipboardPanel.swift +++ b/DynamicIsland/components/Clipboard/ClipboardPanel.swift @@ -7,7 +7,6 @@ import AppKit import SwiftUI -import Defaults class ClipboardPanel: NSPanel { @@ -51,10 +50,8 @@ class ClipboardPanel: NSPanel { .stationary, .fullScreenAuxiliary // Float above full-screen apps ] - - // Apply screenshot protection - updateScreenshotProtection() - setupScreenshotProtectionObserver() + + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) // Accept mouse moved events for proper hover behavior acceptsMouseMovedEvents = true @@ -140,28 +137,6 @@ class ClipboardPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenshotProtectionObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenshotProtection() - } - } - } - - private func updateScreenshotProtection() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Exclude from screenshots and screen recordings - self.sharingType = .none - print("🙈 ClipboardPanel: Protected from screenshots and screen recordings") - } else { - // Allow normal screenshot inclusion - self.sharingType = .readOnly - print("đŸ‘ī¸ ClipboardPanel: Visible in screenshots and screen recordings") - } - } } struct ClipboardPanelView: View { diff --git a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift index ad39a0c..1f2d609 100644 --- a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift +++ b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift @@ -93,9 +93,7 @@ class ColorPickerPanel: NSPanel { .fullScreenAuxiliary // Float above full-screen apps ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) // Accept mouse moved events for proper hover behavior acceptsMouseMovedEvents = true @@ -161,27 +159,8 @@ class ColorPickerPanel: NSPanel { saveCurrentPosition() } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ColorPickerPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("đŸ‘ī¸ ColorPickerPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } diff --git a/DynamicIsland/components/Notch/DynamicIslandWindow.swift b/DynamicIsland/components/Notch/DynamicIslandWindow.swift index 47e9bc9..5118ae7 100644 --- a/DynamicIsland/components/Notch/DynamicIslandWindow.swift +++ b/DynamicIsland/components/Notch/DynamicIslandWindow.swift @@ -38,6 +38,8 @@ class DynamicIslandWindow: NSPanel { isReleasedWhenClosed = false level = .mainMenu + 3 hasShadow = false + + ScreenCaptureVisibilityManager.shared.register(self, scope: .entireInterface) } override var canBecomeKey: Bool { diff --git a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift index 3932db5..41dffde 100644 --- a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift +++ b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift @@ -47,9 +47,7 @@ class ChatMessagesPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) acceptsMouseMovedEvents = true } @@ -78,27 +76,8 @@ class ChatMessagesPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ChatMessagesPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("đŸ‘ī¸ ChatMessagesPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } @@ -152,9 +131,7 @@ class ChatInputPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) acceptsMouseMovedEvents = true } @@ -183,27 +160,8 @@ class ChatInputPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ChatInputPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("đŸ‘ī¸ ChatInputPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } @@ -786,45 +744,23 @@ struct ScreenshotPopoverBackground: NSViewRepresentable { func makeNSView(context: Context) -> NSView { let view = NSView() - // Apply screen capture protection like our other panels DispatchQueue.main.async { - if let window = view.window { - self.updateScreenCaptureVisibility(window: window) - self.setupScreenCaptureObserver(window: window) - } + guard let window = view.window else { return } + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) } return view } func updateNSView(_ nsView: NSView, context: Context) { - // Update screen capture protection when view updates if let window = nsView.window { - updateScreenCaptureVisibility(window: window) + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) } } - private func updateScreenCaptureVisibility(window: NSWindow) { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - window.sharingType = .none - print("🙈 ScreenshotPopover: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - window.sharingType = .readOnly - print("đŸ‘ī¸ ScreenshotPopover: Visible in screen capture and recordings") - } - } - - private func setupScreenCaptureObserver(window: NSWindow) { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak window] change in - DispatchQueue.main.async { - guard let window = window else { return } - self.updateScreenCaptureVisibility(window: window) - } + static func dismantleNSView(_ nsView: NSView, coordinator: ()) { + if let window = nsView.window { + ScreenCaptureVisibilityManager.shared.unregister(window) } } } \ No newline at end of file diff --git a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift index 65e82ff..5995251 100644 --- a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift +++ b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift @@ -58,9 +58,7 @@ class ModelSelectionPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) acceptsMouseMovedEvents = true } @@ -89,27 +87,8 @@ class ModelSelectionPanel: NSPanel { setFrameOrigin(NSPoint(x: xPosition, y: yPosition)) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ModelSelectionPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("đŸ‘ī¸ ModelSelectionPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } diff --git a/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift b/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift index 5c36558..064b153 100644 --- a/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift +++ b/DynamicIsland/components/ScreenAssistant/ScreenAssistantPanel.swift @@ -57,9 +57,7 @@ class ScreenAssistantPanel: NSPanel { .fullScreenAuxiliary ] - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(self, scope: .panelsOnly) // Set minimal size since this is just a wrapper setContentSize(CGSize(width: 1, height: 1)) @@ -77,27 +75,8 @@ class ScreenAssistantPanel: NSPanel { super.close() } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - self.sharingType = .none - print("🙈 ScreenAssistantPanel: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - self.sharingType = .readOnly - print("đŸ‘ī¸ ScreenAssistantPanel: Visible in screen capture and recordings") - } + deinit { + ScreenCaptureVisibilityManager.shared.unregister(self) } } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 805b8f0..4e55b25 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -232,7 +232,7 @@ struct GeneralSettings: View { NotificationCenter.default.post(name: Notification.Name.automaticallySwitchDisplayChanged, object: nil) } .disabled(showOnAllDisplays) - Defaults.Toggle("Hide panels from screenshots & screen recordings", key: .hidePanelsFromScreenCapture) + Defaults.Toggle("Hide Dynamic Island during screenshots & recordings", key: .hideDynamicIslandFromScreenCapture) } header: { Text("System features") } diff --git a/DynamicIsland/components/Settings/SettingsWindowController.swift b/DynamicIsland/components/Settings/SettingsWindowController.swift index 4d17eec..2a40ddc 100644 --- a/DynamicIsland/components/Settings/SettingsWindowController.swift +++ b/DynamicIsland/components/Settings/SettingsWindowController.swift @@ -7,7 +7,6 @@ import AppKit import SwiftUI -import Defaults import Sparkle class SettingsWindowController: NSWindowController { @@ -65,9 +64,7 @@ class SettingsWindowController: NSWindowController { // Handle window closing window.delegate = self - // Apply screen capture hiding setting - updateScreenCaptureVisibility() - setupScreenCaptureObserver() + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) } func showWindow() { @@ -108,26 +105,9 @@ class SettingsWindowController: NSWindowController { NSApp.setActivationPolicy(.accessory) } - private func setupScreenCaptureObserver() { - // Observe changes to hidePanelsFromScreenCapture setting - Defaults.observe(.hidePanelsFromScreenCapture) { [weak self] change in - DispatchQueue.main.async { - self?.updateScreenCaptureVisibility() - } - } - } - - private func updateScreenCaptureVisibility() { - let shouldHide = Defaults[.hidePanelsFromScreenCapture] - - if shouldHide { - // Hide from screen capture and recording - window?.sharingType = .none - print("🙈 SettingsWindow: Hidden from screen capture and recordings") - } else { - // Allow normal screen capture - window?.sharingType = .readOnly - print("đŸ‘ī¸ SettingsWindow: Visible in screen capture and recordings") + deinit { + if let window = window { + ScreenCaptureVisibilityManager.shared.unregister(window) } } } diff --git a/DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift b/DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift new file mode 100644 index 0000000..71993ca --- /dev/null +++ b/DynamicIsland/helpers/ScreenCaptureVisibilityManager.swift @@ -0,0 +1,51 @@ +import AppKit +import Combine +import Defaults + +enum ScreenCaptureScope: Int { + case panelsOnly + case entireInterface +} + +final class ScreenCaptureVisibilityManager { + static let shared = ScreenCaptureVisibilityManager() + + private let scopedWindows = NSMapTable(keyOptions: .weakMemory, valueOptions: .strongMemory) + private var cancellables = Set() + + private init() { + let interfacePublisher = Defaults.publisher(.hideDynamicIslandFromScreenCapture) + .map { _ in () } + .eraseToAnyPublisher() + + interfacePublisher + .receive(on: RunLoop.main) + .sink { [weak self] in + self?.updateAllWindows() + } + .store(in: &cancellables) + } + + func register(_ window: NSWindow, scope: ScreenCaptureScope) { + scopedWindows.setObject(NSNumber(value: scope.rawValue), forKey: window) + applyVisibility(to: window, scope: scope) + } + + func unregister(_ window: NSWindow) { + scopedWindows.removeObject(forKey: window) + } + + private func updateAllWindows() { + guard let windows = scopedWindows.keyEnumerator().allObjects as? [NSWindow] else { return } + for window in windows { + guard let raw = scopedWindows.object(forKey: window)?.intValue, + let scope = ScreenCaptureScope(rawValue: raw) else { continue } + applyVisibility(to: window, scope: scope) + } + } + + private func applyVisibility(to window: NSWindow, scope _: ScreenCaptureScope) { + let shouldHide = Defaults[.hideDynamicIslandFromScreenCapture] + window.sharingType = shouldHide ? .none : .readOnly + } +} diff --git a/DynamicIsland/managers/ClipboardWindowManager.swift b/DynamicIsland/managers/ClipboardWindowManager.swift index b76941e..727f663 100644 --- a/DynamicIsland/managers/ClipboardWindowManager.swift +++ b/DynamicIsland/managers/ClipboardWindowManager.swift @@ -60,9 +60,12 @@ class ClipboardWindowManager: ObservableObject { window.contentView = hostingView // Handle window closing - window.delegate = WindowDelegate { [weak self] in + window.delegate = WindowDelegate { [weak self] window in + ScreenCaptureVisibilityManager.shared.unregister(window) self?.clipboardWindow = nil } + + ScreenCaptureVisibilityManager.shared.register(window, scope: .panelsOnly) self.clipboardWindow = window window.makeKeyAndOrderFront(nil) @@ -88,21 +91,17 @@ class ClipboardWindowManager: ObservableObject { } private class WindowDelegate: NSObject, NSWindowDelegate { - private let onClose: () -> Void - - init(onClose: @escaping () -> Void) { + private let onClose: (NSWindow) -> Void + + init(onClose: @escaping (NSWindow) -> Void) { self.onClose = onClose super.init() } - - func windowWillClose(_ notification: Notification) { - onClose() - } - + func windowShouldClose(_ sender: NSWindow) -> Bool { // Hide instead of close when user clicks close button sender.orderOut(nil) - onClose() + onClose(sender) return false } } diff --git a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift index 2a50070..ef35ac4 100644 --- a/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift +++ b/DynamicIsland/managers/LockScreenLiveActivityWindowManager.swift @@ -70,6 +70,8 @@ class LockScreenLiveActivityWindowManager { window.alphaValue = 0 window.animationBehavior = .none + ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface) + self.window = window self.hasDelegated = false return window diff --git a/DynamicIsland/managers/LockScreenPanelManager.swift b/DynamicIsland/managers/LockScreenPanelManager.swift index 3e6eda8..e30d8bd 100644 --- a/DynamicIsland/managers/LockScreenPanelManager.swift +++ b/DynamicIsland/managers/LockScreenPanelManager.swift @@ -81,6 +81,8 @@ class LockScreenPanelManager { newWindow.isMovable = false newWindow.hasShadow = false + ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) + panelWindow = newWindow window = newWindow hasDelegated = false diff --git a/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift b/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift index 5c66f75..4a82398 100644 --- a/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift +++ b/DynamicIsland/managers/LockScreenReminderWidgetPanelManager.swift @@ -70,6 +70,8 @@ final class LockScreenReminderWidgetPanelManager { newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] + ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) + window = newWindow if !hasDelegated { SkyLightOperator.shared.delegateWindow(newWindow) diff --git a/DynamicIsland/managers/LockScreenWeatherPanelManager.swift b/DynamicIsland/managers/LockScreenWeatherPanelManager.swift index f95df89..f4112f9 100644 --- a/DynamicIsland/managers/LockScreenWeatherPanelManager.swift +++ b/DynamicIsland/managers/LockScreenWeatherPanelManager.swift @@ -78,6 +78,8 @@ final class LockScreenWeatherPanelManager { newWindow.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) newWindow.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] + ScreenCaptureVisibilityManager.shared.register(newWindow, scope: .entireInterface) + window = newWindow if !hasDelegated { SkyLightOperator.shared.delegateWindow(newWindow) diff --git a/DynamicIsland/managers/MusicControlWindowManager.swift b/DynamicIsland/managers/MusicControlWindowManager.swift index a7857b7..f72d59e 100644 --- a/DynamicIsland/managers/MusicControlWindowManager.swift +++ b/DynamicIsland/managers/MusicControlWindowManager.swift @@ -166,6 +166,8 @@ final class MusicControlWindowManager { window.isMovable = false window.alphaValue = 0 + ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface) + self.window = window self.hasDelegated = false return window diff --git a/DynamicIsland/managers/TimerControlWindowManager.swift b/DynamicIsland/managers/TimerControlWindowManager.swift index 5eed75f..eeb2378 100644 --- a/DynamicIsland/managers/TimerControlWindowManager.swift +++ b/DynamicIsland/managers/TimerControlWindowManager.swift @@ -145,6 +145,8 @@ final class TimerControlWindowManager { window.isMovable = false window.alphaValue = 0 + ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface) + self.window = window self.hasDelegated = false return window diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 5ca47b7..c2677ef 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -383,7 +383,7 @@ extension Defaults.Keys { static let showOnAllDisplays = Key("showOnAllDisplays", default: false) static let automaticallySwitchDisplay = Key("automaticallySwitchDisplay", default: true) static let releaseName = Key("releaseName", default: "alpha 0.0.1") - static let hidePanelsFromScreenCapture = Key("hideFromScreenCapture", default: false) + static let hideDynamicIslandFromScreenCapture = Key("hideDynamicIslandFromScreenCapture", default: false) // MARK: Behavior static let minimumHoverDuration = Key("minimumHoverDuration", default: 0.3) From 3eb9122b9fd02ad55beab38371e2f971edaa89dc Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 26 Nov 2025 16:08:34 +0530 Subject: [PATCH 3/6] BugFix: Corners in borders for panels --- .../components/Clipboard/ClipboardPanel.swift | 11 +++++++++++ .../components/ColorPicker/ColorPickerPanel.swift | 11 +++++++++++ .../components/ScreenAssistant/ChatPanels.swift | 12 ++++++++++++ .../ScreenAssistant/ModelSelectionPanel.swift | 11 +++++++++++ 4 files changed, 45 insertions(+) diff --git a/DynamicIsland/components/Clipboard/ClipboardPanel.swift b/DynamicIsland/components/Clipboard/ClipboardPanel.swift index b79f6ba..3952fb7 100644 --- a/DynamicIsland/components/Clipboard/ClipboardPanel.swift +++ b/DynamicIsland/components/Clipboard/ClipboardPanel.swift @@ -8,6 +8,16 @@ import AppKit import SwiftUI +private func applyClipboardCornerMask(_ view: NSView, radius: CGFloat) { + view.wantsLayer = true + view.layer?.masksToBounds = true + view.layer?.cornerRadius = radius + view.layer?.backgroundColor = NSColor.clear.cgColor + if #available(macOS 13.0, *) { + view.layer?.cornerCurve = .continuous + } +} + class ClipboardPanel: NSPanel { init() { @@ -63,6 +73,7 @@ class ClipboardPanel: NSPanel { } let hostingView = NSHostingView(rootView: contentView) + applyClipboardCornerMask(hostingView, radius: 12) self.contentView = hostingView // Set initial size diff --git a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift index 1f2d609..305c2a5 100644 --- a/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift +++ b/DynamicIsland/components/ColorPicker/ColorPickerPanel.swift @@ -9,6 +9,16 @@ import SwiftUI import Cocoa import Defaults +private func applyColorPickerCornerMask(_ view: NSView, radius: CGFloat) { + view.wantsLayer = true + view.layer?.masksToBounds = true + view.layer?.cornerRadius = radius + view.layer?.backgroundColor = NSColor.clear.cgColor + if #available(macOS 13.0, *) { + view.layer?.cornerCurve = .continuous + } +} + class ColorPickerPanelManager: ObservableObject { static let shared = ColorPickerPanelManager() @@ -103,6 +113,7 @@ class ColorPickerPanel: NSPanel { } let hostingView = NSHostingView(rootView: panelView) + applyColorPickerCornerMask(hostingView, radius: 12) contentView = hostingView // Set initial size diff --git a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift index 41dffde..f8115cb 100644 --- a/DynamicIsland/components/ScreenAssistant/ChatPanels.swift +++ b/DynamicIsland/components/ScreenAssistant/ChatPanels.swift @@ -8,6 +8,16 @@ import AppKit import SwiftUI import Defaults +private func applyChatPanelCornerMask(_ view: NSView, radius: CGFloat) { + view.wantsLayer = true + view.layer?.masksToBounds = true + view.layer?.cornerRadius = radius + view.layer?.backgroundColor = NSColor.clear.cgColor + if #available(macOS 13.0, *) { + view.layer?.cornerCurve = .continuous + } +} + // MARK: - Chat Messages Panel (Left Side) class ChatMessagesPanel: NSPanel { @@ -55,6 +65,7 @@ class ChatMessagesPanel: NSPanel { private func setupContentView() { let contentView = ChatMessagesView() let hostingView = NSHostingView(rootView: contentView) + applyChatPanelCornerMask(hostingView, radius: 16) self.contentView = hostingView // Set size for chat messages panel (wider and taller) @@ -139,6 +150,7 @@ class ChatInputPanel: NSPanel { private func setupContentView() { let contentView = ChatInputView() let hostingView = NSHostingView(rootView: contentView) + applyChatPanelCornerMask(hostingView, radius: 16) self.contentView = hostingView // Set compact size for single-line input panel diff --git a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift index 5995251..19f07df 100644 --- a/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift +++ b/DynamicIsland/components/ScreenAssistant/ModelSelectionPanel.swift @@ -8,6 +8,16 @@ import AppKit import SwiftUI import Defaults +private func applyModelSelectionCornerMask(_ view: NSView, radius: CGFloat) { + view.wantsLayer = true + view.layer?.masksToBounds = true + view.layer?.cornerRadius = radius + view.layer?.backgroundColor = NSColor.clear.cgColor + if #available(macOS 13.0, *) { + view.layer?.cornerCurve = .continuous + } +} + // MARK: - Model Selection Panel class ModelSelectionPanel: NSPanel { @@ -66,6 +76,7 @@ class ModelSelectionPanel: NSPanel { private func setupContentView() { let contentView = ModelSelectionView() let hostingView = NSHostingView(rootView: contentView) + applyModelSelectionCornerMask(hostingView, radius: 16) self.contentView = hostingView // Set size for model selection panel From 80d132a85fbc810a29603689c6e9235d58187153 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 26 Nov 2025 22:40:23 +0530 Subject: [PATCH 4/6] Temporary fix for glitch, TODO: remove push up animation --- DynamicIsland/DynamicIslandApp.swift | 34 ++----- .../DynamicIslandViewCoordinator.swift | 32 ++++++- .../components/Notch/NotchStatsView.swift | 96 ++++++++++++++++--- .../models/DynamicIslandViewModel.swift | 45 ++++----- DynamicIsland/sizing/matters.swift | 34 +++++++ 5 files changed, 177 insertions(+), 64 deletions(-) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 7d37ba9..3beb5b5 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -276,33 +276,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { return CGSize(width: inlineSneakPeekWidth, height: vm.effectiveClosedNotchHeight) } - // Use minimalistic or normal size based on settings - var baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize - - // Only apply dynamic sizing when on stats tab and stats are enabled - guard coordinator.currentView == .stats && Defaults[.enableStatsFeature] else { - return baseSize - } - - let enabledGraphsCount = [ - Defaults[.showCpuGraph], - Defaults[.showMemoryGraph], - Defaults[.showGpuGraph], - Defaults[.showNetworkGraph], - Defaults[.showDiskGraph] - ].filter { $0 }.count - - // Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows - var requiredHeight = baseSize.height - - if enabledGraphsCount >= 4 { - // Two rows needed - add height for second row plus spacing - let extraHeight: CGFloat = 120 + 12 // Graph height + spacing - requiredHeight = baseSize.height + extraHeight - } - - // Width stays constant - no horizontal expansion - return CGSize(width: baseSize.width, height: requiredHeight) + // Use minimalistic or normal size based on settings, then adjust for stats layout + let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize + return statsAdjustedNotchSize( + from: baseSize, + isStatsTabActive: coordinator.currentView == .stats, + secondRowProgress: coordinator.statsSecondRowExpansion + ) } func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) { diff --git a/DynamicIsland/DynamicIslandViewCoordinator.swift b/DynamicIsland/DynamicIslandViewCoordinator.swift index d2ed741..68c1c9f 100644 --- a/DynamicIsland/DynamicIslandViewCoordinator.swift +++ b/DynamicIsland/DynamicIslandViewCoordinator.swift @@ -48,7 +48,16 @@ struct ExpandedItem { class DynamicIslandViewCoordinator: ObservableObject { static let shared = DynamicIslandViewCoordinator() - @Published var currentView: NotchViews = .home + @Published var currentView: NotchViews = .home { + didSet { + handleStatsTabTransition(from: oldValue, to: currentView) + } + } + + @Published var statsSecondRowExpansion: CGFloat = 1 + private var statsSecondRowWorkItem: DispatchWorkItem? + private let statsSecondRowRevealDelay: TimeInterval = 0.5 + private let statsSecondRowAnimationDuration: TimeInterval = 0.3 @AppStorage("firstLaunch") var firstLaunch: Bool = true @@ -91,6 +100,27 @@ class DynamicIslandViewCoordinator: ObservableObject { private init() { selectedScreen = preferredScreen } + + private func handleStatsTabTransition(from oldValue: NotchViews, to newValue: NotchViews) { + guard oldValue != newValue else { return } + statsSecondRowWorkItem?.cancel() + if newValue == .stats && Defaults[.enableStatsFeature] { + statsSecondRowExpansion = 0 + let workItem = DispatchWorkItem { [weak self] in + guard let self else { return } + withAnimation(.easeInOut(duration: self.statsSecondRowAnimationDuration)) { + self.statsSecondRowExpansion = 1 + } + self.statsSecondRowWorkItem = nil + } + statsSecondRowWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + statsSecondRowRevealDelay, execute: workItem) + } else { + withAnimation(.easeInOut(duration: 0.2)) { + statsSecondRowExpansion = 0 + } + } + } func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") { let resolvedDuration: TimeInterval diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index 9bb3dd7..02ee976 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -67,6 +67,7 @@ struct NotchStatsView: View { @State private var statsHoverGraceActive = false @State private var statsHoverGraceWorkItem: DispatchWorkItem? @EnvironmentObject var vm: DynamicIslandViewModel + @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared var availableGraphs: [GraphData] { var graphs: [GraphData] = [] @@ -148,18 +149,35 @@ struct NotchStatsView: View { } } } else if graphCount == 4 { - // 4 graphs: 2x2 grid - LazyVGrid( - columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2), - spacing: 12 - ) { - ForEach(0..= 4 && newValue < 0.999 + if shouldHold { + isResizingForStats = true + } else { + DispatchQueue.main.async { + isResizingForStats = false + } + } + } } + +private struct StatsCollapsibleRow: View { + let expansion: CGFloat + let height: CGFloat + let content: () -> Content + private let minVisibleHeight: CGFloat = 0.5 + + init(expansion: CGFloat, height: CGFloat, @ViewBuilder content: @escaping () -> Content) { + self.expansion = expansion + self.height = height + self.content = content + } + + var body: some View { + let clamped = max(0, min(expansion, 1)) + let targetHeight = clamped == 0 ? 0 : max(height * clamped, minVisibleHeight) + + return Group { + if targetHeight == 0 { + Color.clear.frame(height: 0) + } else { + content() + .frame(height: height, alignment: .top) + .clipped() + .mask( + Rectangle() + .frame(height: targetHeight) + .frame(maxWidth: .infinity, alignment: .top) + ) + .frame(height: targetHeight, alignment: .top) + } + } + .animation(.easeInOut(duration: 0.3), value: clamped) + } +} private func activateStatsHoverGrace(duration: TimeInterval = 0.45) { statsHoverGraceWorkItem?.cancel() statsHoverGraceActive = true diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index 8a346ab..8dd164d 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -110,6 +110,23 @@ class DynamicIslandViewModel: NSObject, ObservableObject { } } .store(in: &cancellables) + + coordinator.$statsSecondRowExpansion + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak self] _ in + guard let self else { return } + guard self.notchState == .open else { return } + let updatedTarget = self.calculateDynamicNotchSize() + guard self.notchSize != updatedTarget else { return } + withAnimation(.easeInOut(duration: 0.3)) { + self.notchSize = updatedTarget + } + if let delegate = AppDelegate.shared { + delegate.ensureWindowSize(updatedTarget, animated: false, force: false) + } + } + .store(in: &cancellables) } private func setupDetectorObserver() { @@ -187,28 +204,12 @@ class DynamicIslandViewModel: NSObject, ObservableObject { private func calculateDynamicNotchSize() -> CGSize { // Use minimalistic size if minimalistic UI is enabled - var baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize - - // Only apply dynamic sizing when on stats tab and stats are enabled - guard DynamicIslandViewCoordinator.shared.currentView == .stats && Defaults[.enableStatsFeature] else { - return baseSize - } - - let enabledGraphsCount = [ - Defaults[.showCpuGraph], - Defaults[.showMemoryGraph], - Defaults[.showGpuGraph], - Defaults[.showNetworkGraph], - Defaults[.showDiskGraph] - ].filter { $0 }.count - - // If 4+ graphs are enabled, increase width - if enabledGraphsCount >= 4 { - let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 - return CGSize(width: baseSize.width + extraWidth, height: baseSize.height) - } - - return baseSize + let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize + return statsAdjustedNotchSize( + from: baseSize, + isStatsTabActive: DynamicIslandViewCoordinator.shared.currentView == .stats, + secondRowProgress: coordinator.statsSecondRowExpansion + ) } func close() { diff --git a/DynamicIsland/sizing/matters.swift b/DynamicIsland/sizing/matters.swift index 2e1ff7b..64f2723 100644 --- a/DynamicIsland/sizing/matters.swift +++ b/DynamicIsland/sizing/matters.swift @@ -17,6 +17,8 @@ let batterySneakSize: CGSize = .init(width: 160, height: 1) let openNotchSize: CGSize = .init(width: 640, height: 190) private let minimalisticBaseOpenNotchSize: CGSize = .init(width: 420, height: 180) private let minimalisticLyricsExtraHeight: CGFloat = 40 +let statsSecondRowContentHeight: CGFloat = 120 +let statsGridSpacingHeight: CGFloat = 12 @MainActor var minimalisticOpenNotchSize: CGSize { @@ -37,6 +39,38 @@ var minimalisticOpenNotchSize: CGSize { let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14)) let minimalisticCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 35, bottom: 35), closed: cornerRadiusInsets.closed) +func statsAdjustedNotchSize( + from baseSize: CGSize, + isStatsTabActive: Bool, + secondRowProgress: CGFloat +) -> CGSize { + guard isStatsTabActive, Defaults[.enableStatsFeature] else { + return baseSize + } + + let enabledGraphsCount = [ + Defaults[.showCpuGraph], + Defaults[.showMemoryGraph], + Defaults[.showGpuGraph], + Defaults[.showNetworkGraph], + Defaults[.showDiskGraph] + ].filter { $0 }.count + + guard enabledGraphsCount >= 4 else { + return baseSize + } + + let clampedProgress = max(0, min(secondRowProgress, 1)) + guard clampedProgress > 0 else { + return baseSize + } + + var adjustedSize = baseSize + let extraHeight = (statsSecondRowContentHeight + statsGridSpacingHeight) * clampedProgress + adjustedSize.height += extraHeight + return adjustedSize +} + enum MusicPlayerImageSizes { static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0) static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20)) From 3bba53c58327f872dc351d2597e70da3b2f9962b Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 26 Nov 2025 23:23:49 +0530 Subject: [PATCH 5/6] UIFix: Window Shadows and Notch Shadows --- DynamicIsland/ContentView.swift | 45 ++++++++++++++----- DynamicIsland/DynamicIslandApp.swift | 6 ++- .../models/DynamicIslandViewModel.swift | 24 ++++++++-- DynamicIsland/sizing/matters.swift | 10 +++++ 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index d554847..e070a19 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -126,6 +126,20 @@ struct ContentView: View { } return cornerRadiusInsets } + + private var currentShadowPadding: CGFloat { + notchShadowPaddingValue(isMinimalistic: enableMinimalisticUI) + } + + private var currentNotchShape: NotchShape { + let topRadius = (vm.notchState == .open && Defaults[.cornerRadiusScaling]) + ? activeCornerRadiusInsets.opened.top + : activeCornerRadiusInsets.closed.top + let bottomRadius = (vm.notchState == .open && Defaults[.cornerRadiusScaling]) + ? activeCornerRadiusInsets.opened.bottom + : activeCornerRadiusInsets.closed.bottom + return NotchShape(topCornerRadius: topRadius, bottomCornerRadius: bottomRadius) + } var body: some View { let interactionsEnabled = !lockScreenManager.isLocked @@ -139,16 +153,20 @@ struct ContentView: View { ) .padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0) .background(.black) - .mask { - ((vm.notchState == .open) && Defaults[.cornerRadiusScaling]) - ? NotchShape(topCornerRadius: activeCornerRadiusInsets.opened.top, bottomCornerRadius: activeCornerRadiusInsets.opened.bottom) - .drawingGroup() - : NotchShape(topCornerRadius: activeCornerRadiusInsets.closed.top, bottomCornerRadius: activeCornerRadiusInsets.closed.bottom) - .drawingGroup() - } - .padding(.bottom, vm.notchState == .open && Defaults[.extendHoverArea] ? 0 : (vm.effectiveClosedNotchHeight == 0) - ? zeroHeightHoverPadding - : 0 + .clipShape(currentNotchShape) + .compositingGroup() + .shadow( + color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) + ? .black.opacity(0.6) + : .clear, + radius: Defaults[.cornerRadiusScaling] ? 10 : 5 + ) + .padding(.bottom, + currentShadowPadding + ( + vm.notchState == .open && Defaults[.extendHoverArea] + ? 0 + : (vm.effectiveClosedNotchHeight == 0 ? zeroHeightHoverPadding : 0) + ) ) mainLayout @@ -280,7 +298,11 @@ struct ContentView: View { // .keyboardShortcut("E", modifiers: .command) } } - .frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top) + .frame( + maxWidth: dynamicNotchSize.width, + maxHeight: dynamicNotchSize.height + currentShadowPadding, + alignment: .top + ) .animation(dynamicNotchResizeAnimation, value: dynamicNotchSize) .animation(.easeInOut(duration: 0.4), value: coordinator.currentView) .environmentObject(privacyManager) @@ -291,7 +313,6 @@ struct ContentView: View { vm.shouldRecheckHover.toggle() } } - .shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5) .background(dragDetector) .environmentObject(vm) .environmentObject(webcamManager) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 3beb5b5..80944c4 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -278,11 +278,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Use minimalistic or normal size based on settings, then adjust for stats layout let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize - return statsAdjustedNotchSize( + let adjustedContentSize = statsAdjustedNotchSize( from: baseSize, isStatsTabActive: coordinator.currentView == .stats, secondRowProgress: coordinator.statsSecondRowExpansion ) + return addShadowPadding( + to: adjustedContentSize, + isMinimalistic: Defaults[.enableMinimalisticUI] + ) } func ensureWindowSize(_ size: CGSize, animated: Bool, force: Bool = false) { diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index 8dd164d..7d7937c 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -84,7 +84,11 @@ class DynamicIslandViewModel: NSObject, ObservableObject { self.notchSize = updatedTarget } if let delegate = AppDelegate.shared { - delegate.ensureWindowSize(updatedTarget, animated: true, force: false) + delegate.ensureWindowSize( + addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]), + animated: true, + force: false + ) } } .store(in: &cancellables) @@ -106,7 +110,11 @@ class DynamicIslandViewModel: NSObject, ObservableObject { self.notchSize = updatedTarget } if let delegate = AppDelegate.shared { - delegate.ensureWindowSize(updatedTarget, animated: true, force: false) + delegate.ensureWindowSize( + addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]), + animated: true, + force: false + ) } } .store(in: &cancellables) @@ -123,7 +131,11 @@ class DynamicIslandViewModel: NSObject, ObservableObject { self.notchSize = updatedTarget } if let delegate = AppDelegate.shared { - delegate.ensureWindowSize(updatedTarget, animated: false, force: false) + delegate.ensureWindowSize( + addShadowPadding(to: updatedTarget, isMinimalistic: Defaults[.enableMinimalisticUI]), + animated: false, + force: false + ) } } .store(in: &cancellables) @@ -184,7 +196,11 @@ class DynamicIslandViewModel: NSObject, ObservableObject { let applyWindowResize: () -> Void = { guard let delegate = AppDelegate.shared else { return } - delegate.ensureWindowSize(targetSize, animated: false, force: true) + delegate.ensureWindowSize( + addShadowPadding(to: targetSize, isMinimalistic: Defaults[.enableMinimalisticUI]), + animated: false, + force: true + ) } if Thread.isMainThread { diff --git a/DynamicIsland/sizing/matters.swift b/DynamicIsland/sizing/matters.swift index 64f2723..9166a96 100644 --- a/DynamicIsland/sizing/matters.swift +++ b/DynamicIsland/sizing/matters.swift @@ -19,6 +19,8 @@ private let minimalisticBaseOpenNotchSize: CGSize = .init(width: 420, height: 18 private let minimalisticLyricsExtraHeight: CGFloat = 40 let statsSecondRowContentHeight: CGFloat = 120 let statsGridSpacingHeight: CGFloat = 12 +let notchShadowPaddingStandard: CGFloat = 18 +let notchShadowPaddingMinimalistic: CGFloat = 12 @MainActor var minimalisticOpenNotchSize: CGSize { @@ -71,6 +73,14 @@ func statsAdjustedNotchSize( return adjustedSize } +func notchShadowPaddingValue(isMinimalistic: Bool) -> CGFloat { + isMinimalistic ? notchShadowPaddingMinimalistic : notchShadowPaddingStandard +} + +func addShadowPadding(to size: CGSize, isMinimalistic: Bool) -> CGSize { + CGSize(width: size.width, height: size.height + notchShadowPaddingValue(isMinimalistic: isMinimalistic)) +} + enum MusicPlayerImageSizes { static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0) static let size = (opened: CGSize(width: 90, height: 90), closed: CGSize(width: 20, height: 20)) From 149a022765479a0591b29b9bab1e90807c029adb Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 26 Nov 2025 23:48:32 +0530 Subject: [PATCH 6/6] Add Lock/Unlock sounds --- .../components/Settings/SettingsView.swift | 1 + DynamicIsland/lock.mp3 | Bin 0 -> 8403 bytes .../managers/LockScreenManager.swift | 66 ++++++++++++++++++ DynamicIsland/models/Constants.swift | 1 + DynamicIsland/unlock.mp3 | Bin 0 -> 8776 bytes 5 files changed, 68 insertions(+) create mode 100644 DynamicIsland/lock.mp3 create mode 100644 DynamicIsland/unlock.mp3 diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 4e55b25..c51894e 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -356,6 +356,7 @@ struct GeneralSettings: View { Section { Defaults.Toggle("Enable Lock Screen Live Activity", key: .enableLockScreenLiveActivity) + Defaults.Toggle("Play lock/unlock sounds", key: .enableLockSounds) if #available(macOS 26.0, *) { Picker("Lock screen material", selection: $lockScreenGlassStyle) { ForEach(LockScreenGlassStyle.allCases) { style in diff --git a/DynamicIsland/lock.mp3 b/DynamicIsland/lock.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..5d873fe1f2cc4cfaa0b7ae9b4af8c81df2aba654 GIT binary patch literal 8403 zcmds-XEa=2_^6LTj6PxXHhS-(L>r76!RWnrLJ(z=VDv5wqJ-!nx)2g1L?>FbXwiF+ zXc2<>^ZS3fAMRTB`+Yv_v-jF(o%62soc->#-hDI?;&{L*+P9e9;(UwW ze}vwWcuV#zZ~mj~`C7fZ(^6+c~3^4)fJ##OsNG9!VP z5BuTEcl&v2k>Qb)G!Tbeo5CYkg;G!l8~}v-vaV=n0Mf!ac470=hU;$7AF$Kx*(w{! z=up?h0og0JNXUpisSVM|a`H;)7Km6?^Te5vE0Txe!XaE{5Sj;Y*f@dF}F6bP6>mWQ7*~ zU^w<4#!(eVBE_RYgiHH(ieR)qbRhA8Q{ovA=|A9C_j(zUbdJ{Rf%SYFNC4mfBwr1C z60boO@gZDb zTI`&7bZ`@bl`khJ`HnUY)3&(K!HEDYxlPu`9vS5J0^fjvtCG?CF*}+ir*rhYVef z-+|>X^N~RSfUf44$rIn5_u}cZ&a)>`;8a4tSaue_NS}kiSf#PFH-}b>6b3`TT1RdT zQAhd-c*5adX!RVbrNLl!1?@*PWWD(K1D3X7bjyx`MZ^*@Tvk_+J(@3b{Mozm4z#g6~iFo ze&0bj$EB&<+_mVSmzvDbB2nPHaHDnhce+^O#5rGViYma^sBPE zZ`T_9e#Hm&{D1QKv42s?0kwg@W=8(V_A-UROnxETIz&XoQUWvPT$zUk-v?@k=mZ6r zNTC#eFgLi6>956#dzIQ%a0c8jH<#)>3 zTVj=dQ-s%>qNuB7WyCb21D|sk$?O3i7U&b(^4Ur_J{0KXreX-o^XCa;7)sd^efH

Ci<_?b{&vdz`x9j_Orfr&@k&v+3Q5Fi_f~E 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 Bool { + guard let last = lastPlaybackDates[type] else { return true } + return Date().timeIntervalSince(last) >= throttleInterval + } + + private func resolvePlayer(for type: SoundType) -> AVAudioPlayer? { + if let cached = players[type] { + return cached + } + + guard let url = Bundle.main.url(forResource: type.resourceName, withExtension: "mp3") else { + Logger.log("Missing \(type.resourceName).mp3 in bundle", category: .warning) + return nil + } + + do { + let player = try AVAudioPlayer(contentsOf: url) + player.prepareToPlay() + players[type] = player + return player + } catch { + Logger.log("Failed to initialize lock sound player for \(type.resourceName): \(error.localizedDescription)", category: .error) + return nil + } + } + + private enum SoundType: String { + case lock + case unlock + + var resourceName: String { rawValue } + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index c2677ef..9d7dc3c 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -601,6 +601,7 @@ extension Defaults.Keys { // MARK: Lock Screen Features static let enableLockScreenLiveActivity = Key("enableLockScreenLiveActivity", default: true) + static let enableLockSounds = Key("enableLockSounds", default: true) // MARK: ImageService static let didClearLegacyURLCacheV1 = Key("didClearLegacyURLCacheV1", default: false) diff --git a/DynamicIsland/unlock.mp3 b/DynamicIsland/unlock.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..3b1b06d71d47a2102e08f2398c539a624bf84fe1 GIT binary patch literal 8776 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 X`)^(N|H?`KBMf1m|G%OCxdHWWdyEO} literal 0 HcmV?d00001