From f425b38a811a3ab15b6aa4930ab84ce2a2433713 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 28 Jan 2026 03:03:25 +0530 Subject: [PATCH] Feature: Media Panel Disable in standard mode --- DynamicIsland/ContentView.swift | 46 +++++++++ DynamicIsland/DynamicIslandApp.swift | 96 +++++++++++++++++++ DynamicIsland/Localizable.xcstrings | 15 +++ .../components/Notch/NotchHomeView.swift | 7 +- .../components/Settings/SettingsView.swift | 38 +++++++- .../components/Tabs/TabSelectionView.swift | 35 ++++++- DynamicIsland/managers/MusicManager.swift | 5 + DynamicIsland/models/Constants.swift | 4 + 8 files changed, 240 insertions(+), 6 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 389644e..12b845f 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -66,6 +66,7 @@ struct ContentView: View { @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.enableCapsLockIndicator) var enableCapsLockIndicator @Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities + @Default(.showStandardMediaControls) var showStandardMediaControls // Dynamic sizing based on view type and graph count with smooth transitions var dynamicNotchSize: CGSize { @@ -171,6 +172,14 @@ struct ContentView: View { private let statsAdditionalRowHeight: CGFloat = 110 private let musicControlPauseGrace: TimeInterval = 5 private let musicControlResumeDelay: TimeInterval = 0.24 + + private var standardMediaControlsActive: Bool { + showStandardMediaControls && !enableMinimalisticUI + } + + private var closedMusicContentEnabled: Bool { + enableMinimalisticUI || showStandardMediaControls + } // Use minimalistic corner radius ONLY when opened, keep normal when closed private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) { @@ -449,6 +458,12 @@ struct ContentView: View { enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) } } + .onChange(of: showStandardMediaControls) { _, _ in + handleStandardMediaControlsAvailabilityChange() + } + .onChange(of: enableMinimalisticUI) { _, _ in + handleStandardMediaControlsAvailabilityChange() + } .onChange(of: gestureProgress) { _, _ in if shouldShowMusicControlWindow() { enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) @@ -506,6 +521,7 @@ struct ContentView: View { let musicPairingEligible = vm.notchState == .closed && hasActiveMusicSnapshot && coordinator.musicLiveActivityEnabled + && closedMusicContentEnabled && !vm.hideOnClosed && !lockScreenManager.isLocked let musicSecondary = resolveMusicSecondaryLiveActivity(isMusicPairingEligible: musicPairingEligible) @@ -1181,6 +1197,15 @@ struct ContentView: View { return nil } + guard standardMediaControlsActive else { + ExtensionRoutingDiagnostics.shared.logSuppression( + .music, + reason: "standard media controls disabled", + pendingCount: candidates.count + ) + return nil + } + guard vm.notchState == .closed else { ExtensionRoutingDiagnostics.shared.logSuppression( .music, @@ -1706,6 +1731,26 @@ struct ContentView: View { } } + private func handleStandardMediaControlsAvailabilityChange() { + guard musicControlWindowEnabled else { + hideMusicControlWindow() + return + } + + if standardMediaControlsActive { + if musicManager.isPlaying || !musicManager.isPlayerIdle { + clearMusicControlVisibilityDeadline() + } + enqueueMusicControlWindowSync(forceRefresh: true) + } else { + cancelMusicControlWindowSync() + hideMusicControlWindow() + clearMusicControlVisibilityDeadline() + hasPendingMusicControlSync = false + pendingMusicControlForceRefresh = false + } + } + private func extendMusicControlVisibilityAfterPause() { let deadline = Date().addingTimeInterval(musicControlPauseGrace) musicControlVisibilityDeadline = deadline @@ -1817,6 +1862,7 @@ struct ContentView: View { private func shouldShowMusicControlWindow() -> Bool { guard musicControlWindowEnabled, coordinator.musicLiveActivityEnabled, + standardMediaControlsActive, vm.notchState == .closed, !vm.hideOnClosed, !lockScreenManager.isLocked, diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 953c982..118ccd2 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -90,6 +90,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let idleAnimationManager = IdleAnimationManager.shared // NEW: Custom idle animations let downloadManager = DownloadManager.shared // NEW: Chromium downloads detection let lockScreenPanelManager = LockScreenPanelManager.shared // NEW: Lock screen music panel + let mediaControlsStateCoordinator = MediaControlsStateCoordinator.shared let systemTimerBridge = SystemTimerBridge.shared let extensionXPCServiceHost = ExtensionXPCServiceHost.shared var closeNotchWorkItem: DispatchWorkItem? @@ -889,3 +890,98 @@ extension CGRect: @retroactive Hashable { return lhs.origin == rhs.origin && lhs.size == rhs.size } } + +@MainActor +final class MediaControlsStateCoordinator { + static let shared = MediaControlsStateCoordinator() + + private var cancellables = Set() + + private init() { + let masterPublisher = Defaults.publisher(.showStandardMediaControls) + let minimalisticPublisher = Defaults.publisher(.enableMinimalisticUI) + + Publishers.CombineLatest(masterPublisher, minimalisticPublisher) + .receive(on: RunLoop.main) + .sink { [weak self] masterChange, minimalisticChange in + self?.handleStateChange( + showStandard: masterChange.newValue, + minimalistic: minimalisticChange.newValue + ) + } + .store(in: &cancellables) + } + + private func handleStateChange(showStandard: Bool, minimalistic: Bool) { + if !showStandard && !minimalistic { + cacheAndDisableMusicLiveActivity() + } else { + restoreMusicLiveActivity(clearCache: showStandard) + } + + if showStandard { + restoreLockScreenPanelIfNeeded() + restoreMusicControlWindowIfNeeded() + } else { + cacheAndDisableLockScreenPanel() + cacheAndDisableMusicControlWindow() + } + } + + private func cacheAndDisableMusicLiveActivity() { + if Defaults[.cachedMusicLiveActivityPreference] == nil { + Defaults[.cachedMusicLiveActivityPreference] = DynamicIslandViewCoordinator.shared.musicLiveActivityEnabled + } + + if DynamicIslandViewCoordinator.shared.musicLiveActivityEnabled { + DynamicIslandViewCoordinator.shared.musicLiveActivityEnabled = false + } + } + + private func restoreMusicLiveActivity(clearCache: Bool) { + guard let cached = Defaults[.cachedMusicLiveActivityPreference] else { return } + + if DynamicIslandViewCoordinator.shared.musicLiveActivityEnabled != cached { + DynamicIslandViewCoordinator.shared.musicLiveActivityEnabled = cached + } + + if clearCache { + Defaults[.cachedMusicLiveActivityPreference] = nil + } + } + + private func cacheAndDisableLockScreenPanel() { + if Defaults[.cachedLockScreenMediaWidgetPreference] == nil { + Defaults[.cachedLockScreenMediaWidgetPreference] = Defaults[.enableLockScreenMediaWidget] + } + + if Defaults[.enableLockScreenMediaWidget] { + Defaults[.enableLockScreenMediaWidget] = false + LockScreenPanelManager.shared.hidePanel() + } + } + + private func restoreLockScreenPanelIfNeeded() { + guard let cached = Defaults[.cachedLockScreenMediaWidgetPreference] else { return } + Defaults[.enableLockScreenMediaWidget] = cached + Defaults[.cachedLockScreenMediaWidgetPreference] = nil + } + + private func cacheAndDisableMusicControlWindow() { + if Defaults[.cachedMusicControlWindowPreference] == nil { + Defaults[.cachedMusicControlWindowPreference] = Defaults[.musicControlWindowEnabled] + } + + if Defaults[.musicControlWindowEnabled] { + Defaults[.musicControlWindowEnabled] = false + } + + MusicControlWindowManager.shared.hide() + } + + private func restoreMusicControlWindowIfNeeded() { + guard let cached = Defaults[.cachedMusicControlWindowPreference] else { return } + Defaults[.musicControlWindowEnabled] = cached + Defaults[.cachedMusicControlWindowPreference] = nil + } +} diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 3b4f4db..ea6078e 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -7070,6 +7070,9 @@ } } } + }, + "Disable Minimalistic UI to configure the standard notch media controls." : { + }, "Disabled" : { "localizations" : { @@ -7604,6 +7607,9 @@ "Dynamic Island Progress Bars" : { "comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.", "isCommentAutoGenerated" : true + }, + "Dynamic Island Visibility" : { + }, "dynamic.island" : { "localizations" : { @@ -7990,6 +7996,9 @@ "Enable customizable controls" : { "comment" : "A toggle that allows users to enable or disable customizable controls for media playback.", "isCommentAutoGenerated" : true + }, + "Enable Dynamic Island media controls to manage the lock screen panel." : { + }, "Enable extensions to allow third-party apps to display live activities and lock screen widgets in Atoll." : { "comment" : "A footer text that explains the purpose of enabling extensions.", @@ -18463,6 +18472,12 @@ } } } + }, + "Standard notch media controls are hidden while this toggle is off." : { + + }, + "Standard notch media controls are hidden. Re-enable the toggle above to restore them." : { + }, "Start" : { "localizations" : { diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index b1843e7..ac02c68 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -383,6 +383,7 @@ struct NotchHomeView: View { @ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared + @Default(.showStandardMediaControls) private var showStandardMediaControls let albumArtNamespace: Namespace.ID var body: some View { @@ -407,8 +408,10 @@ struct NotchHomeView: View { } } else { // Normal mode: Show full music player with optional calendar and webcam - MusicPlayerView(albumArtNamespace: albumArtNamespace) - .frame(maxWidth: .infinity, alignment: .leading) + if showStandardMediaControls { + MusicPlayerView(albumArtNamespace: albumArtNamespace) + .frame(maxWidth: .infinity, alignment: .leading) + } if Defaults[.showCalendar] { CalendarView() diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 069d625..436710f 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1964,11 +1964,16 @@ struct Media: View { @Default(.lockScreenGlassStyle) private var lockScreenGlassStyle @Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode @Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenMusicAlbumParallaxEnabled + @Default(.showStandardMediaControls) private var showStandardMediaControls private func highlightID(_ title: String) -> String { SettingsTab.media.highlightID(for: title) } + private var standardControlsSuppressed: Bool { + !showStandardMediaControls && !enableMinimalisticUI + } + var body: some View { Form { Section { @@ -2002,6 +2007,23 @@ struct Media: View { .font(.caption) } } + Section { + Defaults.Toggle("Show media controls in Dynamic Island", key: .showStandardMediaControls) + .disabled(enableMinimalisticUI) + .settingsHighlight(id: highlightID("Show media controls in Dynamic Island")) + + if enableMinimalisticUI { + Text("Disable Minimalistic UI to configure the standard notch media controls.") + .font(.caption) + .foregroundStyle(.secondary) + } else if standardControlsSuppressed { + Text("Standard notch media controls are hidden. Re-enable the toggle above to restore them.") + .font(.caption) + .foregroundStyle(.secondary) + } + } header: { + Text("Dynamic Island Visibility") + } Section { Defaults.Toggle(key: .showShuffleAndRepeat) { HStack { @@ -2053,11 +2075,13 @@ struct Media: View { "Enable music live activity", isOn: $coordinator.musicLiveActivityEnabled.animation() ) + .disabled(standardControlsSuppressed) + .help(standardControlsSuppressed ? "Standard notch media controls are hidden while this toggle is off." : "") Defaults.Toggle( "Show floating media controls", key: .musicControlWindowEnabled ) - .disabled(!coordinator.musicLiveActivityEnabled) + .disabled(!coordinator.musicLiveActivityEnabled || standardControlsSuppressed) .help("Displays play/pause and skip buttons beside the notch while music is active. Disabled by default.") Toggle("Enable sneak peek", isOn: $enableSneakPeek) Toggle("Show sneak peek on playback changes", isOn: $showSneakPeekOnTrackChange) @@ -2124,6 +2148,8 @@ struct Media: View { } footer: { Text("These controls mirror the Lock Screen tab so you can tune the media overlay while focusing on playback settings.") } + .disabled(!showStandardMediaControls) + .opacity(showStandardMediaControls ? 1 : 0.5) Picker(selection: $hideNotchOption, label: HStack { @@ -3301,6 +3327,7 @@ struct LockScreenSettings: View { @Default(.lockScreenWeatherShowsAQI) private var lockScreenWeatherShowsAQI @Default(.lockScreenWeatherShowsSunrise) private var lockScreenWeatherShowsSunrise @Default(.lockScreenWeatherAQIScale) private var lockScreenWeatherAQIScale + @Default(.showStandardMediaControls) private var showStandardMediaControls private func highlightID(_ title: String) -> String { SettingsTab.lockScreen.highlightID(for: title) @@ -3422,11 +3449,20 @@ struct LockScreenSettings: View { .opacity(enableLockScreenMediaWidget ? 1 : 0.5) .settingsHighlight(id: highlightID("Enable media panel blur")) } + + if !showStandardMediaControls { + Text("Enable Dynamic Island media controls to manage the lock screen panel.") + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } } header: { Text("Media Panel") } footer: { Text("Enable and style the media controls that appear above the system clock when the screen is locked.") } + .disabled(!showStandardMediaControls) + .opacity(showStandardMediaControls ? 1 : 0.5) Section { Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget) diff --git a/DynamicIsland/components/Tabs/TabSelectionView.swift b/DynamicIsland/components/Tabs/TabSelectionView.swift index e88e826..5f4fa49 100644 --- a/DynamicIsland/components/Tabs/TabSelectionView.swift +++ b/DynamicIsland/components/Tabs/TabSelectionView.swift @@ -36,12 +36,18 @@ struct TabSelectionView: View { @Default(.enableThirdPartyExtensions) private var enableThirdPartyExtensions @Default(.enableExtensionNotchExperiences) private var enableExtensionNotchExperiences @Default(.enableExtensionNotchTabs) private var enableExtensionNotchTabs + @Default(.showCalendar) private var showCalendar + @Default(.showMirror) private var showMirror + @Default(.showStandardMediaControls) private var showStandardMediaControls + @Default(.enableMinimalisticUI) private var enableMinimalisticUI @Namespace var animation private var tabs: [TabModel] { var tabsArray: [TabModel] = [] - - tabsArray.append(TabModel(label: "Home", icon: "house.fill", view: .home)) + + if homeTabVisible { + tabsArray.append(TabModel(label: "Home", icon: "house.fill", view: .home)) + } if Defaults[.dynamicShelf] { tabsArray.append(TabModel(label: "Shelf", icon: "tray.fill", view: .shelf)) @@ -77,7 +83,9 @@ struct TabSelectionView: View { ) } } - + DispatchQueue.main.async { + ensureValidSelection(with: tabsArray) + } return tabsArray } var body: some View { @@ -121,6 +129,13 @@ struct TabSelectionView: View { extensionNotchExperienceManager.activeExperiences.filter { $0.descriptor.tab != nil } } + private var homeTabVisible: Bool { + if enableMinimalisticUI { + return true + } + return showStandardMediaControls || showCalendar || showMirror + } + private func isSelected(_ tab: TabModel) -> Bool { if tab.view == .extensionExperience { return coordinator.currentView == .extensionExperience @@ -128,6 +143,20 @@ struct TabSelectionView: View { } return coordinator.currentView == tab.view } + + private func ensureValidSelection(with tabs: [TabModel]) { + guard !tabs.isEmpty else { return } + if tabs.contains(where: { isSelected($0) }) { + return + } + guard let first = tabs.first else { return } + if first.view == .extensionExperience { + coordinator.selectedExtensionExperienceID = first.experienceID + } else { + coordinator.selectedExtensionExperienceID = nil + } + coordinator.currentView = first.view + } } #Preview { diff --git a/DynamicIsland/managers/MusicManager.swift b/DynamicIsland/managers/MusicManager.swift index 241ac7c..62da86f 100644 --- a/DynamicIsland/managers/MusicManager.swift +++ b/DynamicIsland/managers/MusicManager.swift @@ -477,6 +477,11 @@ class MusicManager: ObservableObject { } private func updateSneakPeek() { + let standardControlsEnabled = Defaults[.showStandardMediaControls] + let minimalisticEnabled = Defaults[.enableMinimalisticUI] + + guard standardControlsEnabled || minimalisticEnabled else { return } + if isPlaying && Defaults[.enableSneakPeek] { if Defaults[.sneakPeekStyles] == .standard { coordinator.toggleSneakPeek(status: true, type: .music) diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 1db0658..1202417 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -683,6 +683,10 @@ extension Defaults.Keys { static let didMigrateMusicControlSlots = Key("didMigrateMusicControlSlots", default: false) static let musicSkipBehavior = Key("musicSkipBehavior", default: .track) static let musicControlWindowEnabled = Key("musicControlWindowEnabled", default: false) + static let showStandardMediaControls = Key("showStandardMediaControls", default: true) + static let cachedMusicLiveActivityPreference = Key("cachedMusicLiveActivityPreference", default: nil) + static let cachedLockScreenMediaWidgetPreference = Key("cachedLockScreenMediaWidgetPreference", default: nil) + static let cachedMusicControlWindowPreference = Key("cachedMusicControlWindowPreference", default: nil) // 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)