From e6fcddae67e87538dbb756f827433d4e89bb848c Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 21 Nov 2025 01:51:01 +0530 Subject: [PATCH 1/5] Working on Media controls --- DynamicIsland/ContentView.swift | 245 +++++++++++++++- DynamicIsland/Localizable.xcstrings | 3 + .../LockScreen/LockScreenMusicPanel.swift | 8 +- .../Music/MusicControlOverlay.swift | 273 ++++++++++++++++++ .../components/Settings/SettingsView.swift | 7 + .../managers/MusicControlWindowManager.swift | 199 +++++++++++++ DynamicIsland/managers/MusicManager.swift | 14 +- DynamicIsland/models/Constants.swift | 1 + 8 files changed, 744 insertions(+), 6 deletions(-) create mode 100644 DynamicIsland/components/Music/MusicControlOverlay.swift create mode 100644 DynamicIsland/managers/MusicControlWindowManager.swift diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 08a65c4..c49aa7a 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -71,13 +71,17 @@ struct ContentView: View { @State private var lastHapticTime: Date = Date() @State private var gestureProgress: CGFloat = .zero + @State private var isMusicControlWindowVisible = false + @State private var pendingMusicControlSync: DispatchWorkItem? + @State private var musicControlHideWorkItem: DispatchWorkItem? + @State private var musicControlVisibilityDeadline: Date? @State private var haptics: Bool = false @Namespace var albumArtNamespace @Default(.useMusicVisualizer) var useMusicVisualizer - + @Default(.musicControlWindowEnabled) var musicControlWindowEnabled @Default(.showNotHumanFace) var showNotHumanFace @Default(.useModernCloseAnimation) var useModernCloseAnimation @Default(.enableMinimalisticUI) var enableMinimalisticUI @@ -91,6 +95,7 @@ struct ContentView: View { private let extendedHoverPadding: CGFloat = 30 private let zeroHeightHoverPadding: CGFloat = 10 private let statsAdditionalRowHeight: CGFloat = 110 + private let musicControlPauseGrace: TimeInterval = 10 // Use minimalistic corner radius ONLY when opened, keep normal when closed private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) { @@ -368,6 +373,86 @@ struct ContentView: View { .background(dragDetector) .environmentObject(vm) .environmentObject(webcamManager) + .onAppear { + if musicManager.isPlaying || !musicManager.isPlayerIdle { + clearMusicControlVisibilityDeadline() + } + if let deadline = musicControlVisibilityDeadline, Date() > deadline { + clearMusicControlVisibilityDeadline() + } + scheduleMusicControlWindowSync(forceRefresh: true) + } + .onChange(of: vm.notchState) { _, state in + if state == .open { + cancelMusicControlWindowSync() + hideMusicControlWindow() + } else { + scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + } + } + .onChange(of: musicControlWindowEnabled) { _, enabled in + if enabled { + if musicManager.isPlaying || !musicManager.isPlayerIdle { + clearMusicControlVisibilityDeadline() + } + scheduleMusicControlWindowSync(forceRefresh: true) + } else { + cancelMusicControlWindowSync() + hideMusicControlWindow() + clearMusicControlVisibilityDeadline() + } + } + .onChange(of: coordinator.musicLiveActivityEnabled) { _, enabled in + if enabled { + scheduleMusicControlWindowSync(forceRefresh: true) + } else { + cancelMusicControlWindowSync() + hideMusicControlWindow() + clearMusicControlVisibilityDeadline() + } + } + .onChange(of: vm.hideOnClosed) { _, hidden in + if hidden { + cancelMusicControlWindowSync() + hideMusicControlWindow() + } else { + scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + } + } + .onChange(of: lockScreenManager.isLocked) { _, locked in + if locked { + cancelMusicControlWindowSync() + hideMusicControlWindow() + } else { + scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + } + } + .onChange(of: gestureProgress) { _, _ in + if shouldShowMusicControlWindow() { + scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + } + } + .onChange(of: isHovering) { _, hovering in + if shouldShowMusicControlWindow() { + scheduleMusicControlWindowSync(forceRefresh: true, delay: hovering ? 0.05 : 0.12) + } + } + .onChange(of: musicManager.isPlaying) { _, isPlaying in + handleMusicControlPlaybackChange(isPlaying: isPlaying) + } + .onChange(of: musicManager.isPlayerIdle) { _, isIdle in + handleMusicControlIdleChange(isIdle: isIdle) + } + .onChange(of: vm.closedNotchSize) { _, _ in + if shouldShowMusicControlWindow() { + scheduleMusicControlWindowSync(forceRefresh: true) + } + } + .onChange(of: vm.effectiveClosedNotchHeight) { _, _ in + if shouldShowMusicControlWindow() { + scheduleMusicControlWindowSync(forceRefresh: true) + } + } .onDisappear { // Clean up all timer work items to prevent memory leaks and conflicts hoverWorkItem?.cancel() @@ -375,6 +460,10 @@ struct ContentView: View { statsTransitionWorkItem?.cancel() viewTransitionWorkItem?.cancel() sizeChangeWorkItem?.cancel() + cancelMusicControlWindowSync() + hideMusicControlWindow() + cancelMusicControlVisibilityTimer() + clearMusicControlVisibilityDeadline() } } @@ -910,6 +999,160 @@ struct ContentView: View { } } } + + private func handleMusicControlPlaybackChange(isPlaying: Bool) { + guard musicControlWindowEnabled else { return } + + if isPlaying { + clearMusicControlVisibilityDeadline() + scheduleMusicControlWindowSync(forceRefresh: true) + } else { + extendMusicControlVisibilityAfterPause() + } + } + + private func handleMusicControlIdleChange(isIdle: Bool) { + guard musicControlWindowEnabled else { return } + + if isIdle { + if musicControlVisibilityDeadline == nil { + extendMusicControlVisibilityAfterPause() + } + } else if musicManager.isPlaying { + clearMusicControlVisibilityDeadline() + } + } + + private func extendMusicControlVisibilityAfterPause() { + let deadline = Date().addingTimeInterval(musicControlPauseGrace) + musicControlVisibilityDeadline = deadline + scheduleMusicControlVisibilityCheck(deadline: deadline) + scheduleMusicControlWindowSync(forceRefresh: true) + } + + private func clearMusicControlVisibilityDeadline() { + musicControlVisibilityDeadline = nil + cancelMusicControlVisibilityTimer() + } + + private func scheduleMusicControlVisibilityCheck(deadline: Date) { + cancelMusicControlVisibilityTimer() + + let workItem = DispatchWorkItem { + musicControlHideWorkItem = nil + + if let currentDeadline = musicControlVisibilityDeadline, currentDeadline <= Date() { + musicControlVisibilityDeadline = nil + } + + scheduleMusicControlWindowSync(forceRefresh: true) + } + + musicControlHideWorkItem = workItem + let interval = max(0, deadline.timeIntervalSinceNow) + DispatchQueue.main.asyncAfter(deadline: .now() + interval, execute: workItem) + } + + private func cancelMusicControlVisibilityTimer() { + musicControlHideWorkItem?.cancel() + musicControlHideWorkItem = nil + } + + private func musicControlVisibilityIsActive() -> Bool { + if musicManager.isPlaying { + return true + } + + guard let deadline = musicControlVisibilityDeadline else { return false } + return Date() <= deadline + } + + private func shouldShowMusicControlWindow() -> Bool { + guard musicControlWindowEnabled, + coordinator.musicLiveActivityEnabled, + vm.notchState == .closed, + !vm.hideOnClosed, + !lockScreenManager.isLocked else { + return false + } + + return musicControlVisibilityIsActive() + } + + private func scheduleMusicControlWindowSync(forceRefresh: Bool, delay: TimeInterval = 0) { + #if os(macOS) + cancelMusicControlWindowSync() + + guard shouldShowMusicControlWindow() else { + hideMusicControlWindow() + return + } + + let syncDelay = max(0, delay) + let workItem = DispatchWorkItem { [forceRefresh] in + pendingMusicControlSync = nil + + if shouldShowMusicControlWindow() { + syncMusicControlWindow(forceRefresh: forceRefresh) + } else { + hideMusicControlWindow() + } + } + + pendingMusicControlSync = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + syncDelay, execute: workItem) + #endif + } + + private func cancelMusicControlWindowSync() { + pendingMusicControlSync?.cancel() + pendingMusicControlSync = nil + } + + #if os(macOS) + private func currentMusicControlWindowMetrics() -> MusicControlWindowMetrics { + MusicControlWindowMetrics( + notchHeight: max(vm.closedNotchSize.height, vm.effectiveClosedNotchHeight), + notchWidth: vm.closedNotchSize.width + (isHovering ? 8 : 0), + rightWingWidth: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12) + gestureProgress / 2), + cornerRadius: activeCornerRadiusInsets.closed.bottom, + spacing: 36 + ) + } + + private func syncMusicControlWindow(forceRefresh: Bool = false) { + let notchAvailable = vm.effectiveClosedNotchHeight > 0 && vm.closedNotchSize.width > 0 + let targetVisible = shouldShowMusicControlWindow() && notchAvailable + + if targetVisible { + let metrics = currentMusicControlWindowMetrics() + if !isMusicControlWindowVisible { + let didPresent = MusicControlWindowManager.shared.present(using: vm, metrics: metrics) + isMusicControlWindowVisible = didPresent + } else if forceRefresh { + let didRefresh = MusicControlWindowManager.shared.refresh(using: vm, metrics: metrics) + if !didRefresh { + MusicControlWindowManager.shared.hide() + isMusicControlWindowVisible = false + } + } + } else if isMusicControlWindowVisible { + MusicControlWindowManager.shared.hide() + isMusicControlWindowVisible = false + } + } + + private func hideMusicControlWindow() { + if isMusicControlWindowVisible { + MusicControlWindowManager.shared.hide() + isMusicControlWindowVisible = false + } + } + #else + private func syncMusicControlWindow(forceRefresh: Bool = false) {} + + private func hideMusicControlWindow() {} + #endif } struct FullScreenDropDelegate: DropDelegate { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index b757544..ecd459f 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -6816,6 +6816,9 @@ } } } + }, + "Displays play/pause and skip buttons beside the notch while music is active. Disabled by default." : { + }, "Do Not Disturb" : { diff --git a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift index adf3d70..6299566 100644 --- a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift +++ b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift @@ -711,8 +711,8 @@ struct LockScreenMusicPanel: View { if #available(macOS 26.0, *) { RoundedRectangle(cornerRadius: panelCornerRadius) .glassEffect( - .regular - .tint(Color.white.opacity(0.12)) + .clear + //.tint(Color.white.opacity(0.12)) .interactive(), in: .rect(cornerRadius: panelCornerRadius) ) @@ -739,8 +739,8 @@ struct LockScreenMusicPanel: View { if #available(macOS 26.0, *) { RoundedRectangle(cornerRadius: cornerRadius) .glassEffect( - .regular - .tint(Color.white.opacity(0.16)) + .clear + //.tint(Color.white.opacity(0.16)) .interactive(), in: .rect(cornerRadius: cornerRadius) ) diff --git a/DynamicIsland/components/Music/MusicControlOverlay.swift b/DynamicIsland/components/Music/MusicControlOverlay.swift new file mode 100644 index 0000000..1dd431c --- /dev/null +++ b/DynamicIsland/components/Music/MusicControlOverlay.swift @@ -0,0 +1,273 @@ +import SwiftUI +import Defaults + +struct MusicControlOverlay: View { + let notchHeight: CGFloat + let cornerRadius: CGFloat + + @ObservedObject private var musicManager = MusicManager.shared + @Default(.musicSkipBehavior) private var musicSkipBehavior + + private let seekInterval: TimeInterval = 10 + private let skipPressMagnitude: CGFloat = 8 + + private var controlsEnabled: Bool { + !musicManager.isPlayerIdle || musicManager.bundleIdentifier != nil + } + + private var buttonSide: CGFloat { + let preferred = notchHeight * 0.94 + let lowerBound = max(30, notchHeight * 0.8) + let upperBound = max(notchHeight + 2, 32) + return min(max(preferred, lowerBound), upperBound) + } + + private var playPauseSide: CGFloat { + min(buttonSide + 8, notchHeight + 4) + } + + private var buttonCornerRadius: CGFloat { + max(cornerRadius - 8, buttonSide * 0.28) + } + + private var windowCornerRadius: CGFloat { + max(cornerRadius - 6, 12) + } + + private var backwardConfig: ButtonConfig { + switch musicSkipBehavior { + case .track: + return ButtonConfig( + icon: "backward.fill", + pressEffect: .nudge(-skipPressMagnitude), + symbolEffect: .replace, + action: { MusicManager.shared.previousTrack() } + ) + case .tenSecond: + return ButtonConfig( + icon: "gobackward.10", + pressEffect: .wiggle(.counterClockwise), + symbolEffect: .wiggle, + action: { MusicManager.shared.seek(by: -seekInterval) } + ) + } + } + + private var forwardConfig: ButtonConfig { + switch musicSkipBehavior { + case .track: + return ButtonConfig( + icon: "forward.fill", + pressEffect: .nudge(skipPressMagnitude), + symbolEffect: .replace, + action: { MusicManager.shared.nextTrack() } + ) + case .tenSecond: + return ButtonConfig( + icon: "goforward.10", + pressEffect: .wiggle(.clockwise), + symbolEffect: .wiggle, + action: { MusicManager.shared.seek(by: seekInterval) } + ) + } + } + + private var playPauseConfig: ButtonConfig { + ButtonConfig( + icon: musicManager.isPlaying ? "pause.fill" : "play.fill", + pressEffect: .none, + symbolEffect: .replace, + action: { MusicManager.shared.togglePlay() } + ) + } + + var body: some View { + HStack(spacing: 18) { + FloatingMediaButton( + icon: backwardConfig.icon, + frameSize: CGSize(width: buttonSide, height: buttonSide), + cornerRadius: buttonCornerRadius, + foregroundColor: .white.opacity(controlsEnabled ? 0.9 : 0.35), + pressEffect: backwardConfig.pressEffect, + symbolEffectStyle: backwardConfig.symbolEffect, + isEnabled: controlsEnabled, + action: backwardConfig.action + ) + + FloatingMediaButton( + icon: playPauseConfig.icon, + frameSize: CGSize(width: playPauseSide, height: playPauseSide), + cornerRadius: max(buttonCornerRadius + 2, playPauseSide * 0.32), + foregroundColor: .white, + pressEffect: playPauseConfig.pressEffect, + symbolEffectStyle: playPauseConfig.symbolEffect, + isEnabled: controlsEnabled, + action: playPauseConfig.action + ) + + FloatingMediaButton( + icon: forwardConfig.icon, + frameSize: CGSize(width: buttonSide, height: buttonSide), + cornerRadius: buttonCornerRadius, + foregroundColor: .white.opacity(controlsEnabled ? 0.9 : 0.35), + pressEffect: forwardConfig.pressEffect, + symbolEffectStyle: forwardConfig.symbolEffect, + isEnabled: controlsEnabled, + action: forwardConfig.action + ) + } + .padding(.horizontal, 18) + .frame(height: notchHeight) + .frame(minWidth: buttonSide * 3.2) + .background { + RoundedRectangle(cornerRadius: windowCornerRadius, style: .continuous) + .fill(Color.black.opacity(0.88)) + } + .compositingGroup() + .animation(.smooth(duration: 0.2), value: musicManager.isPlaying) + .animation(.smooth(duration: 0.2), value: musicSkipBehavior) + .animation(.smooth(duration: 0.2), value: controlsEnabled) + } + + private struct ButtonConfig { + let icon: String + let pressEffect: FloatingMediaButton.PressEffect + let symbolEffect: FloatingMediaButton.SymbolEffectStyle + let action: () -> Void + } +} + +private struct FloatingMediaButton: View { + let icon: String + let frameSize: CGSize + let cornerRadius: CGFloat + let foregroundColor: Color + let pressEffect: PressEffect + let symbolEffectStyle: SymbolEffectStyle + let isEnabled: Bool + let action: () -> Void + + @State private var isHovering = false + @State private var pressOffset: CGFloat = 0 + @State private var rotationAngle: Double = 0 + @State private var wiggleToken: Int = 0 + + var body: some View { + Button { + guard isEnabled else { return } + triggerPressEffect() + action() + } label: { + iconView + .frame(width: frameSize.width, height: frameSize.height) + .background( + RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) + .fill(isHovering && isEnabled ? Color.white.opacity(0.18) : .clear) + ) + .contentShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) + } + .buttonStyle(PlainButtonStyle()) + .offset(x: pressOffset) + .rotationEffect(.degrees(rotationAngle)) + .disabled(!isEnabled) + .onHover { hovering in + withAnimation(.easeOut(duration: 0.18)) { + isHovering = hovering + } + } + } + + private var iconView: some View { + let image = Image(systemName: icon) + .font(.system(size: min(frameSize.width, frameSize.height) * 0.42, weight: .semibold)) + .foregroundColor(foregroundColor) + + switch symbolEffectStyle { + case .none: + return AnyView(image) + case .replace: + if #available(macOS 14.0, *) { + return AnyView(image.contentTransition(.symbolEffect(.replace))) + } else { + return AnyView(image) + } + case .replaceAndBounce: + if #available(macOS 14.0, *) { + return AnyView( + image + .contentTransition(.symbolEffect(.replace)) + .symbolEffect(.bounce, value: icon) + ) + } else { + return AnyView(image) + } + case .wiggle: + if #available(macOS 14.0, *) { + return AnyView( + image.symbolEffect( + .wiggle.byLayer, + options: .nonRepeating, + value: wiggleToken + ) + ) + } else { + return AnyView(image) + } + } + } + + private func triggerPressEffect() { + switch pressEffect { + case .none: + return + case .nudge(let amount): + withAnimation(.spring(response: 0.16, dampingFraction: 0.72)) { + pressOffset = amount + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { + withAnimation(.spring(response: 0.26, dampingFraction: 0.8)) { + pressOffset = 0 + } + } + case .wiggle(let direction): + guard #available(macOS 14.0, *) else { return } + wiggleToken += 1 + let angle: Double = direction == .clockwise ? 11 : -11 + + withAnimation(.spring(response: 0.18, dampingFraction: 0.52)) { + rotationAngle = angle + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { + withAnimation(.spring(response: 0.32, dampingFraction: 0.76)) { + rotationAngle = 0 + } + } + } + } + + enum PressEffect { + case none + case nudge(CGFloat) + case wiggle(WiggleDirection) + } + + enum SymbolEffectStyle { + case none + case replace + case replaceAndBounce + case wiggle + } + + enum WiggleDirection { + case clockwise + case counterClockwise + } +} + +#Preview { + MusicControlOverlay(notchHeight: 34, cornerRadius: 14) + .padding() + .background(Color.gray.opacity(0.2)) +} diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index aef213f..4f375fa 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -858,6 +858,7 @@ struct Media: View { @Default(.musicAuxLeftControl) private var musicAuxLeftControl @Default(.musicAuxRightControl) private var musicAuxRightControl @Default(.musicSkipBehavior) private var musicSkipBehavior + @Default(.musicControlWindowEnabled) private var musicControlWindowEnabled @State private var previousLeftAuxControl: MusicAuxiliaryControl = Defaults[.musicAuxLeftControl] @State private var previousRightAuxControl: MusicAuxiliaryControl = Defaults[.musicAuxRightControl] @@ -938,6 +939,12 @@ struct Media: View { "Enable music live activity", isOn: $coordinator.musicLiveActivityEnabled.animation() ) + Defaults.Toggle( + "Show floating media controls", + key: .musicControlWindowEnabled + ) + .disabled(!coordinator.musicLiveActivityEnabled) + .help("Displays play/pause and skip buttons beside the notch while music is active. Disabled by default.") Toggle("Enable sneak peek", isOn: $enableSneakPeek) Defaults.Toggle("Enable lyrics", key: .enableLyrics) Picker("Sneak Peek Style", selection: $sneakPeekStyles){ diff --git a/DynamicIsland/managers/MusicControlWindowManager.swift b/DynamicIsland/managers/MusicControlWindowManager.swift new file mode 100644 index 0000000..115cc14 --- /dev/null +++ b/DynamicIsland/managers/MusicControlWindowManager.swift @@ -0,0 +1,199 @@ +#if os(macOS) +import AppKit +import SwiftUI +import SkyLightWindow +import QuartzCore + +struct MusicControlWindowMetrics { + let notchHeight: CGFloat + let notchWidth: CGFloat + let rightWingWidth: CGFloat + let cornerRadius: CGFloat + let spacing: CGFloat +} + +@MainActor +final class MusicControlWindowManager { + static let shared = MusicControlWindowManager() + + private var window: NSWindow? + private var hostingView: NSHostingView? + private var hasDelegated = false + private var lastMetrics: MusicControlWindowMetrics? + + private init() {} + + @discardableResult + func present(using viewModel: DynamicIslandViewModel, metrics: MusicControlWindowMetrics) -> Bool { + guard let screen = resolveScreen(from: viewModel) else { return false } + guard viewModel.effectiveClosedNotchHeight > 0, viewModel.closedNotchSize.width > 0 else { + hide() + return false + } + + let overlay = MusicControlOverlay( + notchHeight: metrics.notchHeight, + cornerRadius: metrics.cornerRadius + ) + let hosting = ensureHostingView(with: overlay) + let fittingSize = measuredSize(for: hosting) + hosting.frame = NSRect(origin: .zero, size: fittingSize) + + let window = ensureWindow(on: screen) + if window.contentView !== hosting { + window.contentView = hosting + } + + let targetFrame = frame(for: fittingSize, viewModel: viewModel, screen: screen, metrics: metrics) + lastMetrics = metrics + + if !hasDelegated { + SkyLightOperator.shared.delegateWindow(window) + hasDelegated = true + } + + if window.alphaValue <= 0.01 { + let startFrame = initialFrame(for: targetFrame, metrics: metrics) + window.setFrame(startFrame, display: true) + window.alphaValue = 0 + window.orderFrontRegardless() + + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.24 + context.timingFunction = CAMediaTimingFunction(name: .easeOut) + window.animator().setFrame(targetFrame, display: true) + window.animator().alphaValue = 1 + } completionHandler: { + window.setFrame(targetFrame, display: false) + } + } else { + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.16 + context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + window.animator().setFrame(targetFrame, display: true) + } completionHandler: { + window.setFrame(targetFrame, display: false) + } + window.orderFrontRegardless() + window.alphaValue = 1 + } + + return true + } + + @discardableResult + func refresh(using viewModel: DynamicIslandViewModel, metrics: MusicControlWindowMetrics) -> Bool { + guard window != nil else { + return present(using: viewModel, metrics: metrics) + } + return present(using: viewModel, metrics: metrics) + } + + func hide(animated: Bool = true) { + guard let window else { return } + + guard animated, window.alphaValue > 0.01 else { + window.orderOut(nil) + window.alphaValue = 0 + return + } + + let retreatFrame = notchRetreatFrame(from: window.frame) + let originalFrame = window.frame + + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.18 + context.timingFunction = CAMediaTimingFunction(name: .easeIn) + window.animator().setFrame(retreatFrame, display: true) + window.animator().alphaValue = 0 + } completionHandler: { + window.setFrame(originalFrame, display: false) + window.orderOut(nil) + window.alphaValue = 0 + } + } + + private func ensureHostingView(with overlay: MusicControlOverlay) -> NSHostingView { + if let hostingView { + hostingView.rootView = overlay + return hostingView + } + let view = NSHostingView(rootView: overlay) + hostingView = view + return view + } + + private func ensureWindow(on screen: NSScreen) -> NSWindow { + if let window { + return window + } + + let window = NSWindow( + contentRect: NSRect(x: screen.frame.midX, y: screen.frame.midY, width: 240, height: 68), + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: false + ) + + window.isReleasedWhenClosed = false + window.isOpaque = false + window.backgroundColor = .clear + window.hasShadow = false + window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel())) + window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] + window.ignoresMouseEvents = false + window.isMovable = false + window.alphaValue = 0 + + self.window = window + self.hasDelegated = false + return window + } + + private func measuredSize(for hosting: NSHostingView) -> CGSize { + hosting.layoutSubtreeIfNeeded() + let size = hosting.fittingSize + return CGSize(width: ceil(size.width), height: ceil(size.height)) + } + + private func resolveScreen(from viewModel: DynamicIslandViewModel) -> NSScreen? { + if let screenName = viewModel.screen, + let targetScreen = NSScreen.screens.first(where: { $0.localizedName == screenName }) { + return targetScreen + } + return NSScreen.main + } + + private func frame(for size: CGSize, viewModel: DynamicIslandViewModel, screen: NSScreen, metrics: MusicControlWindowMetrics) -> NSRect { + let screenFrame = screen.frame + let notchOriginX = screenFrame.midX - (metrics.notchWidth / 2) + let originY = screenFrame.maxY - size.height + + let rightEdge = notchOriginX + metrics.notchWidth + metrics.rightWingWidth + let rawOriginX = rightEdge + metrics.spacing + + let clampedOriginX = max(screenFrame.minX + 8, min(rawOriginX, screenFrame.maxX - size.width - 8)) + + return NSRect(x: clampedOriginX, y: originY, width: size.width, height: size.height) + } + + private func initialFrame(for targetFrame: NSRect, metrics: MusicControlWindowMetrics) -> NSRect { + var frame = targetFrame + let notchRightEdge = targetFrame.minX - metrics.spacing + frame.origin.x = notchRightEdge - targetFrame.width + return frame + } + + private func notchRetreatFrame(from currentFrame: NSRect) -> NSRect { + guard let metrics = lastMetrics else { + return currentFrame.offsetBy(dx: -currentFrame.width * 0.75, dy: 0) + } + + let notchRightEdge = currentFrame.minX - metrics.spacing + var frame = currentFrame + frame.origin.x = notchRightEdge - currentFrame.width + return frame + } +} + +#endif diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index 4aad931..b5dbb3a 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -486,8 +486,20 @@ class MusicManager: ObservableObject { } func togglePlay() { + let controller = activeController + Task { - await activeController?.togglePlay() + await MainActor.run { + let newState = !isPlaying + withAnimation(.smooth(duration: 0.18)) { + isPlaying = newState + if newState { + isPlayerIdle = false + } + } + } + + await controller?.togglePlay() } } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 4f7607b..5ca47b7 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -447,6 +447,7 @@ extension Defaults.Keys { static let musicAuxRightControl = Key("musicAuxRightControl", default: .repeatMode) static let didMigrateMusicAuxControls = Key("didMigrateMusicAuxControls", default: false) static let musicSkipBehavior = Key("musicSkipBehavior", default: .track) + static let musicControlWindowEnabled = Key("musicControlWindowEnabled", default: false) // Enable lock screen media widget (shows the standalone panel when screen is locked) static let enableLockScreenMediaWidget = Key("enableLockScreenMediaWidget", default: true) static let enableLockScreenWeatherWidget = Key("enableLockScreenWeatherWidget", default: true) From 8fb0731ebb55a85fe0be11cba297a26848a2c686 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 21 Nov 2025 10:39:26 +0530 Subject: [PATCH 2/5] Lagging as Hell --- DynamicIsland/ContentView.swift | 56 +++++++++++++++-------- DynamicIsland/managers/MusicManager.swift | 49 +++++++++++++------- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index c49aa7a..62b8b0c 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -72,8 +72,8 @@ struct ContentView: View { @State private var gestureProgress: CGFloat = .zero @State private var isMusicControlWindowVisible = false - @State private var pendingMusicControlSync: DispatchWorkItem? - @State private var musicControlHideWorkItem: DispatchWorkItem? + @State private var pendingMusicControlTask: Task? + @State private var musicControlHideTask: Task? @State private var musicControlVisibilityDeadline: Date? @State private var haptics: Bool = false @@ -1005,7 +1005,9 @@ struct ContentView: View { if isPlaying { clearMusicControlVisibilityDeadline() - scheduleMusicControlWindowSync(forceRefresh: true) + if vm.notchState == .closed { + scheduleMusicControlWindowSync(forceRefresh: false) + } } else { extendMusicControlVisibilityAfterPause() } @@ -1027,7 +1029,9 @@ struct ContentView: View { let deadline = Date().addingTimeInterval(musicControlPauseGrace) musicControlVisibilityDeadline = deadline scheduleMusicControlVisibilityCheck(deadline: deadline) - scheduleMusicControlWindowSync(forceRefresh: true) + if vm.notchState == .closed { + scheduleMusicControlWindowSync(forceRefresh: false) + } } private func clearMusicControlVisibilityDeadline() { @@ -1038,24 +1042,31 @@ struct ContentView: View { private func scheduleMusicControlVisibilityCheck(deadline: Date) { cancelMusicControlVisibilityTimer() - let workItem = DispatchWorkItem { - musicControlHideWorkItem = nil + let interval = max(0, deadline.timeIntervalSinceNow) + + musicControlHideTask = Task { @MainActor [interval] in + defer { musicControlHideTask = nil } + + if interval > 0 { + let nanoseconds = UInt64(interval * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanoseconds) + } + + guard !Task.isCancelled else { return } if let currentDeadline = musicControlVisibilityDeadline, currentDeadline <= Date() { musicControlVisibilityDeadline = nil } - scheduleMusicControlWindowSync(forceRefresh: true) + if vm.notchState == .closed { + scheduleMusicControlWindowSync(forceRefresh: false) + } } - - musicControlHideWorkItem = workItem - let interval = max(0, deadline.timeIntervalSinceNow) - DispatchQueue.main.asyncAfter(deadline: .now() + interval, execute: workItem) } private func cancelMusicControlVisibilityTimer() { - musicControlHideWorkItem?.cancel() - musicControlHideWorkItem = nil + musicControlHideTask?.cancel() + musicControlHideTask = nil } private func musicControlVisibilityIsActive() -> Bool { @@ -1089,8 +1100,16 @@ struct ContentView: View { } let syncDelay = max(0, delay) - let workItem = DispatchWorkItem { [forceRefresh] in - pendingMusicControlSync = nil + + pendingMusicControlTask = Task { @MainActor [forceRefresh, syncDelay] in + defer { pendingMusicControlTask = nil } + + if syncDelay > 0 { + let nanoseconds = UInt64(syncDelay * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanoseconds) + } + + guard !Task.isCancelled else { return } if shouldShowMusicControlWindow() { syncMusicControlWindow(forceRefresh: forceRefresh) @@ -1098,15 +1117,12 @@ struct ContentView: View { hideMusicControlWindow() } } - - pendingMusicControlSync = workItem - DispatchQueue.main.asyncAfter(deadline: .now() + syncDelay, execute: workItem) #endif } private func cancelMusicControlWindowSync() { - pendingMusicControlSync?.cancel() - pendingMusicControlSync = nil + pendingMusicControlTask?.cancel() + pendingMusicControlTask = nil } #if os(macOS) diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index b5dbb3a..1419319 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -33,6 +33,7 @@ class MusicManager: ObservableObject { private var cancellables = Set() private var controllerCancellables = Set() private var debounceIdleTask: Task? + @MainActor private var pendingOptimisticPlayState: Bool? // Helper to check if macOS has removed support for NowPlayingController public private(set) var isNowPlayingDeprecated: Bool = false @@ -193,20 +194,38 @@ class MusicManager: ObservableObject { forceUpdate() } + @MainActor + private func applyPlayState(_ state: Bool, animation: Animation?) { + if let animation { + var transaction = Transaction() + transaction.animation = animation + withTransaction(transaction) { + self.isPlaying = state + } + } else { + self.isPlaying = state + } + + self.updateIdleState(state: state) + } + // MARK: - Update Methods @MainActor private func updateFromPlaybackState(_ state: PlaybackState) { // Check for playback state changes (playing/paused) - if state.isPlaying != self.isPlaying { - NSLog("Playback state changed: \(state.isPlaying ? "Playing" : "Paused")") - withAnimation(.smooth) { - self.isPlaying = state.isPlaying - self.updateIdleState(state: state.isPlaying) - } + let eventIsPlaying = state.isPlaying + let expectedState = pendingOptimisticPlayState + pendingOptimisticPlayState = nil - if state.isPlaying && !state.title.isEmpty && !state.artist.isEmpty { - self.updateSneakPeek() - } + if eventIsPlaying != self.isPlaying { + let animation: Animation? = (expectedState == eventIsPlaying) ? .smooth(duration: 0.18) : .smooth + applyPlayState(eventIsPlaying, animation: animation) + } else { + self.updateIdleState(state: eventIsPlaying) + } + + if eventIsPlaying && !state.title.isEmpty && !state.artist.isEmpty { + self.updateSneakPeek() } // Check for changes in track metadata using last artwork change values @@ -486,20 +505,16 @@ class MusicManager: ObservableObject { } func togglePlay() { - let controller = activeController + guard let controller = activeController else { return } Task { await MainActor.run { let newState = !isPlaying - withAnimation(.smooth(duration: 0.18)) { - isPlaying = newState - if newState { - isPlayerIdle = false - } - } + pendingOptimisticPlayState = newState + applyPlayState(newState, animation: .smooth(duration: 0.18)) } - await controller?.togglePlay() + await controller.togglePlay() } } From 14114f5c8efabf0c409b2b522195d587d4e2ab8d Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sat, 22 Nov 2025 12:13:30 +0530 Subject: [PATCH 3/5] Still needs some working --- DynamicIsland/ContentView.swift | 34 ++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 62b8b0c..ea9321a 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -1044,9 +1044,7 @@ struct ContentView: View { let interval = max(0, deadline.timeIntervalSinceNow) - musicControlHideTask = Task { @MainActor [interval] in - defer { musicControlHideTask = nil } - + musicControlHideTask = Task { [interval] in if interval > 0 { let nanoseconds = UInt64(interval * 1_000_000_000) try? await Task.sleep(nanoseconds: nanoseconds) @@ -1054,12 +1052,16 @@ struct ContentView: View { guard !Task.isCancelled else { return } - if let currentDeadline = musicControlVisibilityDeadline, currentDeadline <= Date() { - musicControlVisibilityDeadline = nil - } + await MainActor.run { + if let currentDeadline = musicControlVisibilityDeadline, currentDeadline <= Date() { + musicControlVisibilityDeadline = nil + } - if vm.notchState == .closed { - scheduleMusicControlWindowSync(forceRefresh: false) + if vm.notchState == .closed { + scheduleMusicControlWindowSync(forceRefresh: false) + } + + musicControlHideTask = nil } } } @@ -1101,9 +1103,7 @@ struct ContentView: View { let syncDelay = max(0, delay) - pendingMusicControlTask = Task { @MainActor [forceRefresh, syncDelay] in - defer { pendingMusicControlTask = nil } - + pendingMusicControlTask = Task { [forceRefresh, syncDelay] in if syncDelay > 0 { let nanoseconds = UInt64(syncDelay * 1_000_000_000) try? await Task.sleep(nanoseconds: nanoseconds) @@ -1111,10 +1111,14 @@ struct ContentView: View { guard !Task.isCancelled else { return } - if shouldShowMusicControlWindow() { - syncMusicControlWindow(forceRefresh: forceRefresh) - } else { - hideMusicControlWindow() + await MainActor.run { + if shouldShowMusicControlWindow() { + syncMusicControlWindow(forceRefresh: forceRefresh) + } else { + hideMusicControlWindow() + } + + pendingMusicControlTask = nil } } #endif From 574a64ef34ed3164a359e30b61d5e88f4b4b9cfc Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 23 Nov 2025 00:08:43 +0530 Subject: [PATCH 4/5] Fully working --- DynamicIsland/ContentView.swift | 146 +++++++++++++++--- .../managers/MusicControlWindowManager.swift | 25 ++- 2 files changed, 145 insertions(+), 26 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index ea9321a..93360f4 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -75,6 +75,10 @@ struct ContentView: View { @State private var pendingMusicControlTask: Task? @State private var musicControlHideTask: Task? @State private var musicControlVisibilityDeadline: Date? + @State private var isMusicControlWindowSuppressed = false + @State private var hasPendingMusicControlSync = false + @State private var pendingMusicControlForceRefresh = false + @State private var musicControlSuppressionTask: Task? @State private var haptics: Bool = false @@ -85,6 +89,24 @@ struct ContentView: View { @Default(.showNotHumanFace) var showNotHumanFace @Default(.useModernCloseAnimation) var useModernCloseAnimation @Default(.enableMinimalisticUI) var enableMinimalisticUI + + private static let musicControlLogFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss.SSS" + return formatter + }() + + private func logMusicControlEvent(_ message: String) { +#if DEBUG + let timestamp = Self.musicControlLogFormatter.string(from: Date()) + print("[MusicControl] \(timestamp): \(message)") +#endif + } + + private func requestMusicControlWindowSyncIfHidden(forceRefresh: Bool = false, delay: TimeInterval = 0) { + guard !isMusicControlWindowVisible else { return } + enqueueMusicControlWindowSync(forceRefresh: forceRefresh, delay: delay) + } private var dynamicNotchResizeAnimation: Animation? { if enableMinimalisticUI && reminderManager.activeWindowReminders.isEmpty == false { return nil @@ -96,6 +118,7 @@ struct ContentView: View { private let zeroHeightHoverPadding: CGFloat = 10 private let statsAdditionalRowHeight: CGFloat = 110 private let musicControlPauseGrace: TimeInterval = 10 + private let musicControlResumeDelay: TimeInterval = 0.24 // Use minimalistic corner radius ONLY when opened, keep normal when closed private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) { @@ -374,20 +397,23 @@ struct ContentView: View { .environmentObject(vm) .environmentObject(webcamManager) .onAppear { + isMusicControlWindowSuppressed = vm.notchState != .closed || lockScreenManager.isLocked if musicManager.isPlaying || !musicManager.isPlayerIdle { clearMusicControlVisibilityDeadline() } if let deadline = musicControlVisibilityDeadline, Date() > deadline { clearMusicControlVisibilityDeadline() } - scheduleMusicControlWindowSync(forceRefresh: true) + enqueueMusicControlWindowSync(forceRefresh: true) } .onChange(of: vm.notchState) { _, state in if state == .open { + suppressMusicControlWindowUpdates() cancelMusicControlWindowSync() hideMusicControlWindow() } else { - scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + releaseMusicControlWindowUpdates(after: musicControlResumeDelay) + enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) } } .onChange(of: musicControlWindowEnabled) { _, enabled in @@ -395,20 +421,24 @@ struct ContentView: View { if musicManager.isPlaying || !musicManager.isPlayerIdle { clearMusicControlVisibilityDeadline() } - scheduleMusicControlWindowSync(forceRefresh: true) + enqueueMusicControlWindowSync(forceRefresh: true) } else { cancelMusicControlWindowSync() hideMusicControlWindow() clearMusicControlVisibilityDeadline() + hasPendingMusicControlSync = false + pendingMusicControlForceRefresh = false } } .onChange(of: coordinator.musicLiveActivityEnabled) { _, enabled in if enabled { - scheduleMusicControlWindowSync(forceRefresh: true) + enqueueMusicControlWindowSync(forceRefresh: true) } else { cancelMusicControlWindowSync() hideMusicControlWindow() clearMusicControlVisibilityDeadline() + hasPendingMusicControlSync = false + pendingMusicControlForceRefresh = false } } .onChange(of: vm.hideOnClosed) { _, hidden in @@ -416,25 +446,27 @@ struct ContentView: View { cancelMusicControlWindowSync() hideMusicControlWindow() } else { - scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) } } .onChange(of: lockScreenManager.isLocked) { _, locked in if locked { + suppressMusicControlWindowUpdates() cancelMusicControlWindowSync() hideMusicControlWindow() } else { - scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + releaseMusicControlWindowUpdates(after: musicControlResumeDelay) + enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) } } .onChange(of: gestureProgress) { _, _ in if shouldShowMusicControlWindow() { - scheduleMusicControlWindowSync(forceRefresh: true, delay: 0.05) + enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) } } .onChange(of: isHovering) { _, hovering in if shouldShowMusicControlWindow() { - scheduleMusicControlWindowSync(forceRefresh: true, delay: hovering ? 0.05 : 0.12) + enqueueMusicControlWindowSync(forceRefresh: true, delay: hovering ? 0.05 : 0.12) } } .onChange(of: musicManager.isPlaying) { _, isPlaying in @@ -445,12 +477,12 @@ struct ContentView: View { } .onChange(of: vm.closedNotchSize) { _, _ in if shouldShowMusicControlWindow() { - scheduleMusicControlWindowSync(forceRefresh: true) + enqueueMusicControlWindowSync(forceRefresh: true) } } .onChange(of: vm.effectiveClosedNotchHeight) { _, _ in if shouldShowMusicControlWindow() { - scheduleMusicControlWindowSync(forceRefresh: true) + enqueueMusicControlWindowSync(forceRefresh: true) } } .onDisappear { @@ -464,6 +496,7 @@ struct ContentView: View { hideMusicControlWindow() cancelMusicControlVisibilityTimer() clearMusicControlVisibilityDeadline() + musicControlSuppressionTask?.cancel() } } @@ -1005,9 +1038,7 @@ struct ContentView: View { if isPlaying { clearMusicControlVisibilityDeadline() - if vm.notchState == .closed { - scheduleMusicControlWindowSync(forceRefresh: false) - } + requestMusicControlWindowSyncIfHidden() } else { extendMusicControlVisibilityAfterPause() } @@ -1029,9 +1060,7 @@ struct ContentView: View { let deadline = Date().addingTimeInterval(musicControlPauseGrace) musicControlVisibilityDeadline = deadline scheduleMusicControlVisibilityCheck(deadline: deadline) - if vm.notchState == .closed { - scheduleMusicControlWindowSync(forceRefresh: false) - } + requestMusicControlWindowSyncIfHidden() } private func clearMusicControlVisibilityDeadline() { @@ -1044,7 +1073,7 @@ struct ContentView: View { let interval = max(0, deadline.timeIntervalSinceNow) - musicControlHideTask = Task { [interval] in + musicControlHideTask = Task.detached(priority: .background) { [interval] in if interval > 0 { let nanoseconds = UInt64(interval * 1_000_000_000) try? await Task.sleep(nanoseconds: nanoseconds) @@ -1057,9 +1086,7 @@ struct ContentView: View { musicControlVisibilityDeadline = nil } - if vm.notchState == .closed { - scheduleMusicControlWindowSync(forceRefresh: false) - } + enqueueMusicControlWindowSync(forceRefresh: false) musicControlHideTask = nil } @@ -1080,30 +1107,101 @@ struct ContentView: View { return Date() <= deadline } + private func suppressMusicControlWindowUpdates() { + isMusicControlWindowSuppressed = true + musicControlSuppressionTask?.cancel() + musicControlSuppressionTask = nil + } + + private func releaseMusicControlWindowUpdates(after delay: TimeInterval) { + musicControlSuppressionTask?.cancel() + musicControlSuppressionTask = Task { [delay] in + if delay > 0 { + let nanoseconds = UInt64(delay * 1_000_000_000) + try? await Task.sleep(nanoseconds: nanoseconds) + } + + guard !Task.isCancelled else { return } + + await MainActor.run { + if vm.notchState == .closed && !lockScreenManager.isLocked { + isMusicControlWindowSuppressed = false + triggerPendingMusicControlSyncIfNeeded() + } else { + isMusicControlWindowSuppressed = true + } + musicControlSuppressionTask = nil + } + } + } + + private func triggerPendingMusicControlSyncIfNeeded() { + guard hasPendingMusicControlSync else { return } + + let shouldForce = pendingMusicControlForceRefresh + hasPendingMusicControlSync = false + pendingMusicControlForceRefresh = false + + logMusicControlEvent("Flushing pending floating window sync (force: \(shouldForce))") + scheduleMusicControlWindowSync(forceRefresh: shouldForce, bypassSuppression: true) + } + + private func shouldDeferMusicControlSync() -> Bool { + vm.notchState != .closed || lockScreenManager.isLocked || isMusicControlWindowSuppressed + } + + private func enqueueMusicControlWindowSync(forceRefresh: Bool, delay: TimeInterval = 0) { + if shouldDeferMusicControlSync() { + hasPendingMusicControlSync = true + if forceRefresh { + pendingMusicControlForceRefresh = true + } + logMusicControlEvent("Queued floating window sync (force: \(forceRefresh)) while deferred") + return + } + + logMusicControlEvent("Scheduling floating window sync (force: \(forceRefresh), delay: \(delay))") + scheduleMusicControlWindowSync(forceRefresh: forceRefresh, delay: delay) + } + private func shouldShowMusicControlWindow() -> Bool { guard musicControlWindowEnabled, coordinator.musicLiveActivityEnabled, vm.notchState == .closed, !vm.hideOnClosed, - !lockScreenManager.isLocked else { + !lockScreenManager.isLocked, + !isMusicControlWindowSuppressed else { return false } return musicControlVisibilityIsActive() } - private func scheduleMusicControlWindowSync(forceRefresh: Bool, delay: TimeInterval = 0) { + private func scheduleMusicControlWindowSync(forceRefresh: Bool, delay: TimeInterval = 0, bypassSuppression: Bool = false) { #if os(macOS) cancelMusicControlWindowSync() guard shouldShowMusicControlWindow() else { + hasPendingMusicControlSync = false + pendingMusicControlForceRefresh = false hideMusicControlWindow() return } + if !bypassSuppression && (isMusicControlWindowSuppressed || lockScreenManager.isLocked) { + hasPendingMusicControlSync = true + if forceRefresh { + pendingMusicControlForceRefresh = true + } + return + } + + hasPendingMusicControlSync = false + pendingMusicControlForceRefresh = false + let syncDelay = max(0, delay) - pendingMusicControlTask = Task { [forceRefresh, syncDelay] in + pendingMusicControlTask = Task.detached(priority: .userInitiated) { [forceRefresh, syncDelay] in if syncDelay > 0 { let nanoseconds = UInt64(syncDelay * 1_000_000_000) try? await Task.sleep(nanoseconds: nanoseconds) @@ -1113,8 +1211,10 @@ struct ContentView: View { await MainActor.run { if shouldShowMusicControlWindow() { + logMusicControlEvent("Running floating window sync (force: \(forceRefresh))") syncMusicControlWindow(forceRefresh: forceRefresh) } else { + logMusicControlEvent("Skipping floating window sync (conditions changed)") hideMusicControlWindow() } diff --git a/DynamicIsland/managers/MusicControlWindowManager.swift b/DynamicIsland/managers/MusicControlWindowManager.swift index 115cc14..b68aad7 100644 --- a/DynamicIsland/managers/MusicControlWindowManager.swift +++ b/DynamicIsland/managers/MusicControlWindowManager.swift @@ -4,7 +4,7 @@ import SwiftUI import SkyLightWindow import QuartzCore -struct MusicControlWindowMetrics { +struct MusicControlWindowMetrics: Equatable { let notchHeight: CGFloat let notchWidth: CGFloat let rightWingWidth: CGFloat @@ -86,15 +86,23 @@ final class MusicControlWindowManager { guard window != nil else { return present(using: viewModel, metrics: metrics) } + + if let lastMetrics, lastMetrics == metrics { + return true + } + return present(using: viewModel, metrics: metrics) } - func hide(animated: Bool = true) { + func hide(animated: Bool = true, tearDown: Bool = true) { guard let window else { return } guard animated, window.alphaValue > 0.01 else { window.orderOut(nil) window.alphaValue = 0 + if tearDown { + tearDownHostingView() + } return } @@ -106,13 +114,24 @@ final class MusicControlWindowManager { context.timingFunction = CAMediaTimingFunction(name: .easeIn) window.animator().setFrame(retreatFrame, display: true) window.animator().alphaValue = 0 - } completionHandler: { + } completionHandler: { [weak self] in window.setFrame(originalFrame, display: false) window.orderOut(nil) window.alphaValue = 0 + if tearDown { + self?.tearDownHostingView() + } } } + private func tearDownHostingView() { + if let window { + window.contentView = nil + } + hostingView = nil + lastMetrics = nil + } + private func ensureHostingView(with overlay: MusicControlOverlay) -> NSHostingView { if let hostingView { hostingView.rootView = overlay From afda162c1ff67cf877998d410110d0349c88f536 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sun, 23 Nov 2025 00:42:48 +0530 Subject: [PATCH 5/5] Icon Size Parity --- DynamicIsland/ContentView.swift | 2 +- DynamicIsland/components/Music/MusicControlOverlay.swift | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 93360f4..4786404 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -117,7 +117,7 @@ struct ContentView: View { private let extendedHoverPadding: CGFloat = 30 private let zeroHeightHoverPadding: CGFloat = 10 private let statsAdditionalRowHeight: CGFloat = 110 - private let musicControlPauseGrace: TimeInterval = 10 + private let musicControlPauseGrace: TimeInterval = 5 private let musicControlResumeDelay: TimeInterval = 0.24 // Use minimalistic corner radius ONLY when opened, keep normal when closed diff --git a/DynamicIsland/components/Music/MusicControlOverlay.swift b/DynamicIsland/components/Music/MusicControlOverlay.swift index 1dd431c..ff38af3 100644 --- a/DynamicIsland/components/Music/MusicControlOverlay.swift +++ b/DynamicIsland/components/Music/MusicControlOverlay.swift @@ -82,6 +82,8 @@ struct MusicControlOverlay: View { } var body: some View { + let verticalPadding = max(8, notchHeight * 0.12) + HStack(spacing: 18) { FloatingMediaButton( icon: backwardConfig.icon, @@ -116,12 +118,13 @@ struct MusicControlOverlay: View { action: forwardConfig.action ) } + .padding(.vertical, verticalPadding) .padding(.horizontal, 18) .frame(height: notchHeight) .frame(minWidth: buttonSide * 3.2) .background { RoundedRectangle(cornerRadius: windowCornerRadius, style: .continuous) - .fill(Color.black.opacity(0.88)) + .fill(Color.black) } .compositingGroup() .animation(.smooth(duration: 0.2), value: musicManager.isPlaying) @@ -179,7 +182,7 @@ private struct FloatingMediaButton: View { private var iconView: some View { let image = Image(systemName: icon) - .font(.system(size: min(frameSize.width, frameSize.height) * 0.42, weight: .semibold)) + .font(.system(size: min(frameSize.width, frameSize.height) * 0.48, weight: .semibold)) .foregroundColor(foregroundColor) switch symbolEffectStyle {