Feature: Media Panel Disable in standard mode

This commit is contained in:
Hariharan Mudaliar 2026-01-28 03:03:25 +05:30
commit f425b38a81
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
8 changed files with 240 additions and 6 deletions

View file

@ -66,6 +66,7 @@ struct ContentView: View {
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
@Default(.enableCapsLockIndicator) var enableCapsLockIndicator @Default(.enableCapsLockIndicator) var enableCapsLockIndicator
@Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities @Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities
@Default(.showStandardMediaControls) var showStandardMediaControls
// Dynamic sizing based on view type and graph count with smooth transitions // Dynamic sizing based on view type and graph count with smooth transitions
var dynamicNotchSize: CGSize { var dynamicNotchSize: CGSize {
@ -172,6 +173,14 @@ struct ContentView: View {
private let musicControlPauseGrace: TimeInterval = 5 private let musicControlPauseGrace: TimeInterval = 5
private let musicControlResumeDelay: TimeInterval = 0.24 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 // Use minimalistic corner radius ONLY when opened, keep normal when closed
private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) { private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) {
if enableMinimalisticUI { if enableMinimalisticUI {
@ -449,6 +458,12 @@ struct ContentView: View {
enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05)
} }
} }
.onChange(of: showStandardMediaControls) { _, _ in
handleStandardMediaControlsAvailabilityChange()
}
.onChange(of: enableMinimalisticUI) { _, _ in
handleStandardMediaControlsAvailabilityChange()
}
.onChange(of: gestureProgress) { _, _ in .onChange(of: gestureProgress) { _, _ in
if shouldShowMusicControlWindow() { if shouldShowMusicControlWindow() {
enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05) enqueueMusicControlWindowSync(forceRefresh: true, delay: 0.05)
@ -506,6 +521,7 @@ struct ContentView: View {
let musicPairingEligible = vm.notchState == .closed let musicPairingEligible = vm.notchState == .closed
&& hasActiveMusicSnapshot && hasActiveMusicSnapshot
&& coordinator.musicLiveActivityEnabled && coordinator.musicLiveActivityEnabled
&& closedMusicContentEnabled
&& !vm.hideOnClosed && !vm.hideOnClosed
&& !lockScreenManager.isLocked && !lockScreenManager.isLocked
let musicSecondary = resolveMusicSecondaryLiveActivity(isMusicPairingEligible: musicPairingEligible) let musicSecondary = resolveMusicSecondaryLiveActivity(isMusicPairingEligible: musicPairingEligible)
@ -1181,6 +1197,15 @@ struct ContentView: View {
return nil return nil
} }
guard standardMediaControlsActive else {
ExtensionRoutingDiagnostics.shared.logSuppression(
.music,
reason: "standard media controls disabled",
pendingCount: candidates.count
)
return nil
}
guard vm.notchState == .closed else { guard vm.notchState == .closed else {
ExtensionRoutingDiagnostics.shared.logSuppression( ExtensionRoutingDiagnostics.shared.logSuppression(
.music, .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() { private func extendMusicControlVisibilityAfterPause() {
let deadline = Date().addingTimeInterval(musicControlPauseGrace) let deadline = Date().addingTimeInterval(musicControlPauseGrace)
musicControlVisibilityDeadline = deadline musicControlVisibilityDeadline = deadline
@ -1817,6 +1862,7 @@ struct ContentView: View {
private func shouldShowMusicControlWindow() -> Bool { private func shouldShowMusicControlWindow() -> Bool {
guard musicControlWindowEnabled, guard musicControlWindowEnabled,
coordinator.musicLiveActivityEnabled, coordinator.musicLiveActivityEnabled,
standardMediaControlsActive,
vm.notchState == .closed, vm.notchState == .closed,
!vm.hideOnClosed, !vm.hideOnClosed,
!lockScreenManager.isLocked, !lockScreenManager.isLocked,

View file

@ -90,6 +90,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
let idleAnimationManager = IdleAnimationManager.shared // NEW: Custom idle animations let idleAnimationManager = IdleAnimationManager.shared // NEW: Custom idle animations
let downloadManager = DownloadManager.shared // NEW: Chromium downloads detection let downloadManager = DownloadManager.shared // NEW: Chromium downloads detection
let lockScreenPanelManager = LockScreenPanelManager.shared // NEW: Lock screen music panel let lockScreenPanelManager = LockScreenPanelManager.shared // NEW: Lock screen music panel
let mediaControlsStateCoordinator = MediaControlsStateCoordinator.shared
let systemTimerBridge = SystemTimerBridge.shared let systemTimerBridge = SystemTimerBridge.shared
let extensionXPCServiceHost = ExtensionXPCServiceHost.shared let extensionXPCServiceHost = ExtensionXPCServiceHost.shared
var closeNotchWorkItem: DispatchWorkItem? var closeNotchWorkItem: DispatchWorkItem?
@ -889,3 +890,98 @@ extension CGRect: @retroactive Hashable {
return lhs.origin == rhs.origin && lhs.size == rhs.size return lhs.origin == rhs.origin && lhs.size == rhs.size
} }
} }
@MainActor
final class MediaControlsStateCoordinator {
static let shared = MediaControlsStateCoordinator()
private var cancellables = Set<AnyCancellable>()
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
}
}

View file

@ -7070,6 +7070,9 @@
} }
} }
} }
},
"Disable Minimalistic UI to configure the standard notch media controls." : {
}, },
"Disabled" : { "Disabled" : {
"localizations" : { "localizations" : {
@ -7604,6 +7607,9 @@
"Dynamic Island Progress Bars" : { "Dynamic Island Progress Bars" : {
"comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.", "comment" : "A section header that lists the options for customizing the appearance of the progress bars displayed by Dynamic Island.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
},
"Dynamic Island Visibility" : {
}, },
"dynamic.island" : { "dynamic.island" : {
"localizations" : { "localizations" : {
@ -7990,6 +7996,9 @@
"Enable customizable controls" : { "Enable customizable controls" : {
"comment" : "A toggle that allows users to enable or disable customizable controls for media playback.", "comment" : "A toggle that allows users to enable or disable customizable controls for media playback.",
"isCommentAutoGenerated" : true "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." : { "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.", "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" : { "Start" : {
"localizations" : { "localizations" : {

View file

@ -383,6 +383,7 @@ struct NotchHomeView: View {
@ObservedObject var batteryModel = BatteryStatusViewModel.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared @ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
@ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared @ObservedObject private var extensionNotchExperienceManager = ExtensionNotchExperienceManager.shared
@Default(.showStandardMediaControls) private var showStandardMediaControls
let albumArtNamespace: Namespace.ID let albumArtNamespace: Namespace.ID
var body: some View { var body: some View {
@ -407,8 +408,10 @@ struct NotchHomeView: View {
} }
} else { } else {
// Normal mode: Show full music player with optional calendar and webcam // Normal mode: Show full music player with optional calendar and webcam
if showStandardMediaControls {
MusicPlayerView(albumArtNamespace: albumArtNamespace) MusicPlayerView(albumArtNamespace: albumArtNamespace)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
}
if Defaults[.showCalendar] { if Defaults[.showCalendar] {
CalendarView() CalendarView()

View file

@ -1964,11 +1964,16 @@ struct Media: View {
@Default(.lockScreenGlassStyle) private var lockScreenGlassStyle @Default(.lockScreenGlassStyle) private var lockScreenGlassStyle
@Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode @Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode
@Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenMusicAlbumParallaxEnabled @Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenMusicAlbumParallaxEnabled
@Default(.showStandardMediaControls) private var showStandardMediaControls
private func highlightID(_ title: String) -> String { private func highlightID(_ title: String) -> String {
SettingsTab.media.highlightID(for: title) SettingsTab.media.highlightID(for: title)
} }
private var standardControlsSuppressed: Bool {
!showStandardMediaControls && !enableMinimalisticUI
}
var body: some View { var body: some View {
Form { Form {
Section { Section {
@ -2002,6 +2007,23 @@ struct Media: View {
.font(.caption) .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 { Section {
Defaults.Toggle(key: .showShuffleAndRepeat) { Defaults.Toggle(key: .showShuffleAndRepeat) {
HStack { HStack {
@ -2053,11 +2075,13 @@ struct Media: View {
"Enable music live activity", "Enable music live activity",
isOn: $coordinator.musicLiveActivityEnabled.animation() isOn: $coordinator.musicLiveActivityEnabled.animation()
) )
.disabled(standardControlsSuppressed)
.help(standardControlsSuppressed ? "Standard notch media controls are hidden while this toggle is off." : "")
Defaults.Toggle( Defaults.Toggle(
"Show floating media controls", "Show floating media controls",
key: .musicControlWindowEnabled 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.") .help("Displays play/pause and skip buttons beside the notch while music is active. Disabled by default.")
Toggle("Enable sneak peek", isOn: $enableSneakPeek) Toggle("Enable sneak peek", isOn: $enableSneakPeek)
Toggle("Show sneak peek on playback changes", isOn: $showSneakPeekOnTrackChange) Toggle("Show sneak peek on playback changes", isOn: $showSneakPeekOnTrackChange)
@ -2124,6 +2148,8 @@ struct Media: View {
} footer: { } footer: {
Text("These controls mirror the Lock Screen tab so you can tune the media overlay while focusing on playback settings.") 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: Picker(selection: $hideNotchOption, label:
HStack { HStack {
@ -3301,6 +3327,7 @@ struct LockScreenSettings: View {
@Default(.lockScreenWeatherShowsAQI) private var lockScreenWeatherShowsAQI @Default(.lockScreenWeatherShowsAQI) private var lockScreenWeatherShowsAQI
@Default(.lockScreenWeatherShowsSunrise) private var lockScreenWeatherShowsSunrise @Default(.lockScreenWeatherShowsSunrise) private var lockScreenWeatherShowsSunrise
@Default(.lockScreenWeatherAQIScale) private var lockScreenWeatherAQIScale @Default(.lockScreenWeatherAQIScale) private var lockScreenWeatherAQIScale
@Default(.showStandardMediaControls) private var showStandardMediaControls
private func highlightID(_ title: String) -> String { private func highlightID(_ title: String) -> String {
SettingsTab.lockScreen.highlightID(for: title) SettingsTab.lockScreen.highlightID(for: title)
@ -3422,11 +3449,20 @@ struct LockScreenSettings: View {
.opacity(enableLockScreenMediaWidget ? 1 : 0.5) .opacity(enableLockScreenMediaWidget ? 1 : 0.5)
.settingsHighlight(id: highlightID("Enable media panel blur")) .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: { } header: {
Text("Media Panel") Text("Media Panel")
} footer: { } footer: {
Text("Enable and style the media controls that appear above the system clock when the screen is locked.") 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 { Section {
Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget) Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget)

View file

@ -36,12 +36,18 @@ struct TabSelectionView: View {
@Default(.enableThirdPartyExtensions) private var enableThirdPartyExtensions @Default(.enableThirdPartyExtensions) private var enableThirdPartyExtensions
@Default(.enableExtensionNotchExperiences) private var enableExtensionNotchExperiences @Default(.enableExtensionNotchExperiences) private var enableExtensionNotchExperiences
@Default(.enableExtensionNotchTabs) private var enableExtensionNotchTabs @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 @Namespace var animation
private var tabs: [TabModel] { private var tabs: [TabModel] {
var tabsArray: [TabModel] = [] var tabsArray: [TabModel] = []
if homeTabVisible {
tabsArray.append(TabModel(label: "Home", icon: "house.fill", view: .home)) tabsArray.append(TabModel(label: "Home", icon: "house.fill", view: .home))
}
if Defaults[.dynamicShelf] { if Defaults[.dynamicShelf] {
tabsArray.append(TabModel(label: "Shelf", icon: "tray.fill", view: .shelf)) 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 return tabsArray
} }
var body: some View { var body: some View {
@ -121,6 +129,13 @@ struct TabSelectionView: View {
extensionNotchExperienceManager.activeExperiences.filter { $0.descriptor.tab != nil } 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 { private func isSelected(_ tab: TabModel) -> Bool {
if tab.view == .extensionExperience { if tab.view == .extensionExperience {
return coordinator.currentView == .extensionExperience return coordinator.currentView == .extensionExperience
@ -128,6 +143,20 @@ struct TabSelectionView: View {
} }
return coordinator.currentView == tab.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 { #Preview {

View file

@ -477,6 +477,11 @@ class MusicManager: ObservableObject {
} }
private func updateSneakPeek() { private func updateSneakPeek() {
let standardControlsEnabled = Defaults[.showStandardMediaControls]
let minimalisticEnabled = Defaults[.enableMinimalisticUI]
guard standardControlsEnabled || minimalisticEnabled else { return }
if isPlaying && Defaults[.enableSneakPeek] { if isPlaying && Defaults[.enableSneakPeek] {
if Defaults[.sneakPeekStyles] == .standard { if Defaults[.sneakPeekStyles] == .standard {
coordinator.toggleSneakPeek(status: true, type: .music) coordinator.toggleSneakPeek(status: true, type: .music)

View file

@ -683,6 +683,10 @@ extension Defaults.Keys {
static let didMigrateMusicControlSlots = Key<Bool>("didMigrateMusicControlSlots", default: false) static let didMigrateMusicControlSlots = Key<Bool>("didMigrateMusicControlSlots", default: false)
static let musicSkipBehavior = Key<MusicSkipBehavior>("musicSkipBehavior", default: .track) static let musicSkipBehavior = Key<MusicSkipBehavior>("musicSkipBehavior", default: .track)
static let musicControlWindowEnabled = Key<Bool>("musicControlWindowEnabled", default: false) static let musicControlWindowEnabled = Key<Bool>("musicControlWindowEnabled", default: false)
static let showStandardMediaControls = Key<Bool>("showStandardMediaControls", default: true)
static let cachedMusicLiveActivityPreference = Key<Bool?>("cachedMusicLiveActivityPreference", default: nil)
static let cachedLockScreenMediaWidgetPreference = Key<Bool?>("cachedLockScreenMediaWidgetPreference", default: nil)
static let cachedMusicControlWindowPreference = Key<Bool?>("cachedMusicControlWindowPreference", default: nil)
// Enable lock screen media widget (shows the standalone panel when screen is locked) // Enable lock screen media widget (shows the standalone panel when screen is locked)
static let enableLockScreenMediaWidget = Key<Bool>("enableLockScreenMediaWidget", default: true) static let enableLockScreenMediaWidget = Key<Bool>("enableLockScreenMediaWidget", default: true)
static let enableLockScreenWeatherWidget = Key<Bool>("enableLockScreenWeatherWidget", default: true) static let enableLockScreenWeatherWidget = Key<Bool>("enableLockScreenWeatherWidget", default: true)