diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 4b14356..6d116e6 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -5157,6 +5157,9 @@ } } } + }, + "Choose the global material mode for lock screen widgets. Custom Liquid unlocks per-widget variant sliders while Standard sticks to the classic frosted/liquid options." : { + }, "Choose which service to use when sharing files from the shelf. Drag files onto the shelf or click the shelf button to pick files." : { "comment" : "A footer description under the Quick Share section of the Shelf settings.", @@ -5292,6 +5295,10 @@ } } }, + "Classic mode keeps the original translucent black background." : { + "comment" : "A description of the \"Classic mode\" option in the Lock Screen Integration settings.", + "isCommentAutoGenerated" : true + }, "Clear" : { "extractionState" : "stale", "localizations" : { @@ -5811,6 +5818,9 @@ }, "Configure how the timer looks inside the closed notch. Progress can render as a ring around the icon or as horizontal bars." : { + }, + "Configure lock screen materials from the Appearance tab. Custom Liquid unlocks variant sliders for both widgets whenever Liquid Glass is selected." : { + }, "Configure presets in Settings" : { "comment" : "A message displayed when there are no presets configured. It encourages the user to configure presets in the app settings.", @@ -5951,9 +5961,8 @@ "Controls how often system metrics refresh while monitoring is active." : { }, - "Controls the optional timer widget that floats above the media panel. Blur adds a frosted finish behind the compact view." : { - "comment" : "A footer for the \"Timer Widget\" section in the Lock Screen Settings view.", - "isCommentAutoGenerated" : true + "Controls the optional timer widget that floats above the media panel, including its classic, frosted, or liquid glass surface independent of the global material setting." : { + }, "Controls whether Dynamic Island mirrors lock/unlock events with its own live activity and audible chimes." : { @@ -6211,6 +6220,13 @@ } } } + }, + "Custom liquid glass already renders with Apple's liquid material, so this option is managed automatically." : { + "comment" : "A description of the custom liquid blur option in the media settings.", + "isCommentAutoGenerated" : true + }, + "Custom Liquid settings require the Liquid Glass material." : { + }, "Custom music live activity animation" : { "localizations" : { @@ -9555,6 +9571,9 @@ } } } + }, + "Glass mode" : { + }, "Global Settings" : { "comment" : "The header for the section that controls global settings related to extensions.", @@ -11137,6 +11156,12 @@ } } } + }, + "Lock Screen Glass" : { + + }, + "Lock screen glass mode" : { + }, "Lock Screen Integration" : { @@ -13244,6 +13269,9 @@ "Move Up" : { "comment" : "A button label that says \"Move Up\".", "isCommentAutoGenerated" : true + }, + "Music panel variant" : { + }, "Music Source" : { "localizations" : { @@ -15141,6 +15169,9 @@ }, "Pick Color" : { + }, + "Pick per-widget liquid-glass variants below. Changes mirror the Lock Screen tab." : { + }, "Picked %@" : { @@ -19166,6 +19197,14 @@ } } }, + "Timer glass material" : { + "comment" : "A label displayed above a picker for selecting the material of the timer glass.", + "isCommentAutoGenerated" : true + }, + "Timer liquid mode" : { + "comment" : "A segment control that lets the user choose between different customization modes for the timer glass.", + "isCommentAutoGenerated" : true + }, "Timer Presets" : { "comment" : "A section header in the timer settings view, describing timer presets.", "isCommentAutoGenerated" : true @@ -19180,12 +19219,19 @@ } } }, + "Timer surface" : { + "comment" : "A label displayed above the user's timer surface.", + "isCommentAutoGenerated" : true + }, "Timer tint" : { }, "Timer Widget" : { "comment" : "A section header for the timer widget settings.", "isCommentAutoGenerated" : true + }, + "Timer widget variant" : { + }, "Title" : { "comment" : "A label for the title input field in the note editor.", @@ -20126,6 +20172,9 @@ "Use the Media tab to configure sneak peek, lyrics, and floating media controls." : { "comment" : "A footer for the \"Media Live Activity\" section, explaining how to configure the media controls.", "isCommentAutoGenerated" : true + }, + "Use the sliders below to pick unique Apple liquid-glass variants for each widget." : { + }, "Used" : { "comment" : "A label describing the percentage of memory that is currently in use.", @@ -20153,6 +20202,10 @@ }, "Uses event-driven private API for real-time screen recording detection" : { + }, + "Uses the frosted blur treatment while glass mode is enabled." : { + "comment" : "A description of the frosted blur treatment when glass mode is enabled.", + "isCommentAutoGenerated" : true }, "Utilization" : { "comment" : "A label displayed alongside the utilization percentage in the GPU detail view.", @@ -20165,6 +20218,9 @@ } } } + }, + "v%lld" : { + }, "Version" : { "localizations" : { diff --git a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift index 10a3f1f..61b6cf0 100644 --- a/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift +++ b/DynamicIsland/components/LockScreen/LockScreenMusicPanel.swift @@ -11,6 +11,8 @@ import Defaults struct LockScreenMusicPanel: View { private struct GlassLogSnapshot: Equatable { let style: LockScreenGlassStyle + let customizationMode: LockScreenGlassCustomizationMode + let variantRawValue: Int let usesLiquidGlass: Bool } @@ -32,6 +34,8 @@ struct LockScreenMusicPanel: View { @State private var isParallaxSuspended = false @State private var lastLoggedGlassSnapshot: GlassLogSnapshot? @Default(.lockScreenGlassStyle) var lockScreenGlassStyle + @Default(.lockScreenGlassCustomizationMode) private var glassCustomizationMode + @Default(.lockScreenMusicLiquidGlassVariant) private var musicGlassVariant @Default(.lockScreenShowAppIcon) var showAppIcon @Default(.lockScreenPanelShowsBorder) var showPanelBorder @Default(.lockScreenPanelUsesBlur) var enableBlur @@ -70,12 +74,21 @@ struct LockScreenMusicPanel: View { isExpanded ? expandedPanelCornerRadius : collapsedPanelCornerRadius } - private var usesLiquidGlass: Bool { + private var usesCustomLiquidGlass: Bool { + glassCustomizationMode == .customLiquid + } + + private var usesStandardLiquidGlass: Bool { + guard glassCustomizationMode == .standard else { return false } if #available(macOS 26.0, *) { return lockScreenGlassStyle == .liquid } return false } + + private var usesLiquidGlass: Bool { + usesCustomLiquidGlass || usesStandardLiquidGlass + } var body: some View { if isActive { @@ -138,6 +151,14 @@ struct LockScreenMusicPanel: View { .onChange(of: lockScreenGlassStyle) { _, _ in logGlassState(reason: "Glass style updated") } + .onChange(of: glassCustomizationMode) { _, _ in + logGlassState(reason: "Glass mode updated") + } + .onChange(of: musicGlassVariant) { _, _ in + if usesCustomLiquidGlass { + logGlassState(reason: "Liquid variant updated") + } + } .scaleEffect(animator.isPresented ? 1 : 0.9, anchor: .center) .opacity(animator.isPresented ? 1 : 0) .animation(.spring(response: 0.52, dampingFraction: 0.8), value: animator.isPresented) @@ -723,8 +744,10 @@ struct LockScreenMusicPanel: View { @ViewBuilder private var panelBackgroundLayer: some View { - if usesLiquidGlass { - liquidPanelBackdrop + if usesCustomLiquidGlass { + customLiquidPanelBackdrop + } else if usesStandardLiquidGlass { + standardLiquidPanelBackdrop } else if shouldUseFrostedBlur { frostedPanelBackground } else { @@ -733,7 +756,7 @@ struct LockScreenMusicPanel: View { } @ViewBuilder - private var liquidPanelBackdrop: some View { + private var standardLiquidPanelBackdrop: some View { if #available(macOS 26.0, *) { GlassTextBackdrop(cornerRadius: panelCornerRadius) .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -742,6 +765,16 @@ struct LockScreenMusicPanel: View { } } + @ViewBuilder + private var customLiquidPanelBackdrop: some View { + LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: panelCornerRadius) { + Color.white.opacity(0.04) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .allowsHitTesting(false) + .accessibilityHidden(true) + } + private var frostedPanelBackground: some View { RoundedRectangle(cornerRadius: panelCornerRadius) .fill(.ultraThinMaterial) @@ -768,7 +801,9 @@ struct LockScreenMusicPanel: View { @ViewBuilder private func albumArtBackground(cornerRadius: CGFloat) -> some View { - if usesLiquidGlass { + if usesCustomLiquidGlass { + customLiquidAlbumArtBackground(cornerRadius: cornerRadius) + } else if usesStandardLiquidGlass { if #available(macOS 26.0, *) { clearLiquidGlassSurface(cornerRadius: cornerRadius) } @@ -809,8 +844,20 @@ struct LockScreenMusicPanel: View { ) } + @ViewBuilder + private func customLiquidAlbumArtBackground(cornerRadius: CGFloat) -> some View { + LiquidGlassBackground(variant: musicGlassVariant, cornerRadius: cornerRadius) { + Color.clear + } + } + private func logGlassState(reason: String) { - let snapshot = GlassLogSnapshot(style: lockScreenGlassStyle, usesLiquidGlass: usesLiquidGlass) + let snapshot = GlassLogSnapshot( + style: lockScreenGlassStyle, + customizationMode: glassCustomizationMode, + variantRawValue: musicGlassVariant.rawValue, + usesLiquidGlass: usesLiquidGlass + ) guard snapshot != lastLoggedGlassSnapshot else { return } lastLoggedGlassSnapshot = snapshot @@ -831,9 +878,18 @@ struct LockScreenMusicPanel: View { return "\(entry.name)=\(mode)" }.joined(separator: ", ") - print("[LockScreenMusicPanel] \(reason) – style=\(lockScreenGlassStyle.rawValue), usesLiquidGlass=\(usesLiquidGlass), components[\(componentSummary)], macOS \(currentOSVersionDescription())") + let modeDescription: String + if usesCustomLiquidGlass { + modeDescription = "Custom Liquid (variant \(musicGlassVariant.rawValue))" + } else if usesStandardLiquidGlass { + modeDescription = "Standard Liquid" + } else { + modeDescription = lockScreenGlassStyle.rawValue + } - if lockScreenGlassStyle == .liquid && !usesLiquidGlass { + print("[LockScreenMusicPanel] \(reason) – customization=\(glassCustomizationMode.rawValue), mode=\(modeDescription), components[\(componentSummary)], macOS \(currentOSVersionDescription())") + + if glassCustomizationMode == .standard && lockScreenGlassStyle == .liquid && !usesStandardLiquidGlass { print("[LockScreenMusicPanel] Liquid Glass requested but unavailable on this macOS build. Falling back to frosted visuals.") } } diff --git a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift index 21a9be6..54ee35e 100644 --- a/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift +++ b/DynamicIsland/components/LockScreen/LockScreenTimerWidget.swift @@ -7,8 +7,10 @@ struct LockScreenTimerWidget: View { @ObservedObject private var animator: LockScreenTimerWidgetAnimator @ObservedObject private var timerManager = TimerManager.shared - @Default(.lockScreenGlassStyle) private var glassStyle - @Default(.lockScreenTimerWidgetUsesBlur) private var enableTimerBlur + @Default(.lockScreenTimerGlassStyle) private var timerGlassStyle + @Default(.lockScreenTimerGlassCustomizationMode) private var timerGlassCustomizationMode + @Default(.lockScreenTimerLiquidGlassVariant) private var timerGlassVariant + @Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass @Default(.timerPresets) private var timerPresets @MainActor @@ -78,29 +80,45 @@ struct LockScreenTimerWidget: View { ) } - private var usesLiquidGlass: Bool { + private var timerSurfaceMode: LockScreenTimerSurfaceMode { + timerGlassModeIsGlass ? .glass : .classic + } + + private var usesGlassBackground: Bool { + timerSurfaceMode == .glass + } + + private var usesCustomLiquidGlass: Bool { + guard usesGlassBackground else { return false } + return timerGlassStyle == .liquid && timerGlassCustomizationMode == .customLiquid + } + + private var usesStandardLiquidGlass: Bool { + guard usesGlassBackground else { return false } + guard timerGlassStyle == .liquid else { return false } if #available(macOS 26.0, *) { - return glassStyle == .liquid + return timerGlassCustomizationMode == .standard } return false } @ViewBuilder private var widgetBackground: some View { - if enableTimerBlur { - if usesLiquidGlass { - liquidBackground + if usesGlassBackground { + if usesCustomLiquidGlass { + customLiquidBackground + } else if timerGlassStyle == .liquid { + standardLiquidBackground } else { frostedBackground } } else { - RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) - .fill(Color.black.opacity(0.65)) + classicBackground } } @ViewBuilder - private var liquidBackground: some View { + private var standardLiquidBackground: some View { if #available(macOS 26.0, *) { RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) .glassEffect( @@ -112,11 +130,22 @@ struct LockScreenTimerWidget: View { } } + private var customLiquidBackground: some View { + LiquidGlassBackground(variant: timerGlassVariant, cornerRadius: Self.cornerRadius) { + Color.black.opacity(0.12) + } + } + private var frostedBackground: some View { RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) .fill(.ultraThinMaterial) } + private var classicBackground: some View { + RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous) + .fill(Color.black.opacity(0.65)) + } + private var pauseIcon: String { timerManager.isPaused ? "play.fill" : "pause.fill" } diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index cf04058..1b8acfc 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -178,26 +178,34 @@ private struct SettingsHighlightModifier: ViewModifier { func body(content: Content) -> some View { content .id(id) - .background( - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(Color.accentColor.opacity(0.08)) - .padding(.horizontal, -8) - .opacity(isActive ? 1 : 0) - .animation(.easeInOut(duration: 0.25), value: isActive) - ) - .overlay { + .background(highlightBackground) + .onChange(of: isActive) { _, active in + animatePulse = active + } + .onAppear { if isActive { - RoundedRectangle(cornerRadius: 10, style: .continuous) - .stroke(Color.accentColor.opacity(0.85), lineWidth: 2) - .padding(.vertical, -4) - .padding(.horizontal, -8) - .opacity(animatePulse ? 0.25 : 0.9) - .animation(.easeInOut(duration: 0.6).repeatForever(autoreverses: true), value: animatePulse) - .onAppear { animatePulse = true } - .onDisappear { animatePulse = false } + animatePulse = true } } } + + private var highlightBackground: some View { + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke( + Color.accentColor.opacity(isActive ? (animatePulse ? 0.95 : 0.4) : 0), + lineWidth: 2 + ) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color.accentColor.opacity(isActive ? 0.08 : 0)) + ) + .padding(-4) + .shadow(color: Color.accentColor.opacity(isActive ? 0.25 : 0), radius: animatePulse ? 8 : 2) + .animation( + isActive ? .easeInOut(duration: 0.8).repeatForever(autoreverses: true) : .default, + value: animatePulse + ) + } } extension View { @@ -679,7 +687,10 @@ struct SettingsView: View { SettingsSearchEntry(tab: .lockScreen, title: "Show panel border", keywords: ["panel border"], highlightID: SettingsTab.lockScreen.highlightID(for: "Show panel border")), SettingsSearchEntry(tab: .lockScreen, title: "Enable media panel blur", keywords: ["blur", "media panel"], highlightID: SettingsTab.lockScreen.highlightID(for: "Enable media panel blur")), SettingsSearchEntry(tab: .lockScreen, title: "Show lock screen timer", keywords: ["timer widget", "lock screen timer"], highlightID: SettingsTab.lockScreen.highlightID(for: "Show lock screen timer")), - SettingsSearchEntry(tab: .lockScreen, title: "Enable timer blur", keywords: ["timer blur"], highlightID: SettingsTab.lockScreen.highlightID(for: "Enable timer blur")), + SettingsSearchEntry(tab: .lockScreen, title: "Timer surface", keywords: ["timer glass", "classic", "blur"], highlightID: SettingsTab.lockScreen.highlightID(for: "Timer surface")), + SettingsSearchEntry(tab: .lockScreen, title: "Timer glass material", keywords: ["frosted", "liquid", "timer material"], highlightID: SettingsTab.lockScreen.highlightID(for: "Timer glass material")), + SettingsSearchEntry(tab: .lockScreen, title: "Timer liquid mode", keywords: ["timer", "standard", "custom"], highlightID: SettingsTab.lockScreen.highlightID(for: "Timer liquid mode")), + SettingsSearchEntry(tab: .lockScreen, title: "Timer widget variant", keywords: ["timer variant", "liquid"], highlightID: SettingsTab.lockScreen.highlightID(for: "Timer widget variant")), SettingsSearchEntry(tab: .lockScreen, title: "Show lock screen weather", keywords: ["weather widget"], highlightID: SettingsTab.lockScreen.highlightID(for: "Show lock screen weather")), SettingsSearchEntry(tab: .lockScreen, title: "Layout", keywords: ["inline", "circular", "weather layout"], highlightID: SettingsTab.lockScreen.highlightID(for: "Layout")), SettingsSearchEntry(tab: .lockScreen, title: "Weather data provider", keywords: ["wttr", "open meteo"], highlightID: SettingsTab.lockScreen.highlightID(for: "Weather data provider")), @@ -708,7 +719,10 @@ struct SettingsView: View { SettingsSearchEntry(tab: .timer, title: "Enable timer feature", keywords: ["timer", "enable"], highlightID: SettingsTab.timer.highlightID(for: "Enable timer feature")), SettingsSearchEntry(tab: .timer, title: "Mirror macOS Clock timers", keywords: ["system timer", "clock app"], highlightID: SettingsTab.timer.highlightID(for: "Mirror macOS Clock timers")), SettingsSearchEntry(tab: .timer, title: "Show lock screen timer widget", keywords: ["lock screen", "timer widget"], highlightID: SettingsTab.timer.highlightID(for: "Show lock screen timer widget")), - SettingsSearchEntry(tab: .timer, title: "Enable timer blur", keywords: ["timer blur", "lock screen"], highlightID: SettingsTab.timer.highlightID(for: "Enable timer blur")), + SettingsSearchEntry(tab: .timer, title: "Timer surface", keywords: ["timer glass", "classic", "blur"], highlightID: SettingsTab.timer.highlightID(for: "Timer surface")), + SettingsSearchEntry(tab: .timer, title: "Timer glass material", keywords: ["frosted", "liquid", "timer material"], highlightID: SettingsTab.timer.highlightID(for: "Timer glass material")), + SettingsSearchEntry(tab: .timer, title: "Timer liquid mode", keywords: ["timer", "standard", "custom"], highlightID: SettingsTab.timer.highlightID(for: "Timer liquid mode")), + SettingsSearchEntry(tab: .timer, title: "Timer widget variant", keywords: ["timer variant", "liquid"], highlightID: SettingsTab.timer.highlightID(for: "Timer widget variant")), SettingsSearchEntry(tab: .timer, title: "Timer tint", keywords: ["timer colour", "preset"], highlightID: SettingsTab.timer.highlightID(for: "Timer tint")), SettingsSearchEntry(tab: .timer, title: "Solid colour", keywords: ["timer colour", "custom"], highlightID: SettingsTab.timer.highlightID(for: "Solid colour")), SettingsSearchEntry(tab: .timer, title: "Progress style", keywords: ["progress", "bar", "ring"], highlightID: SettingsTab.timer.highlightID(for: "Progress style")), @@ -1936,6 +1950,7 @@ struct Media: View { @Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget @Default(.showSneakPeekOnTrackChange) private var showSneakPeekOnTrackChange @Default(.lockScreenGlassStyle) private var lockScreenGlassStyle + @Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode @Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenMusicAlbumParallaxEnabled private func highlightID(_ title: String) -> String { @@ -2079,7 +2094,11 @@ struct Media: View { .disabled(!enableLockScreenMediaWidget) Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder) .disabled(!enableLockScreenMediaWidget) - if lockScreenGlassStyle == .frosted { + if lockScreenGlassCustomizationMode == .customLiquid { + customLiquidBlurRow + .opacity(enableLockScreenMediaWidget ? 1 : 0.5) + .settingsHighlight(id: highlightID("Enable media panel blur")) + } else if lockScreenGlassStyle == .frosted { Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur) .disabled(!enableLockScreenMediaWidget) .settingsHighlight(id: highlightID("Enable media panel blur")) @@ -2129,6 +2148,16 @@ struct Media: View { } .frame(maxWidth: .infinity, alignment: .leading) } + private var customLiquidBlurRow: some View { + VStack(alignment: .leading, spacing: 4) { + Text("Enable media panel blur") + .foregroundStyle(.secondary) + Text("Custom liquid glass already renders with Apple's liquid material, so this option is managed automatically.") + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } } struct CalendarSettings: View { @@ -2762,6 +2791,15 @@ struct Appearance: View { @Default(.selectedVisualizer) var selectedVisualizer @Default(.openNotchWidth) var openNotchWidth @Default(.enableMinimalisticUI) var enableMinimalisticUI + @Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode + @Default(.lockScreenGlassStyle) private var lockScreenGlassStyle + @Default(.lockScreenMusicLiquidGlassVariant) private var lockScreenMusicLiquidGlassVariant + @Default(.lockScreenTimerLiquidGlassVariant) private var lockScreenTimerLiquidGlassVariant + @Default(.lockScreenTimerGlassStyle) private var lockScreenTimerGlassStyle + @Default(.lockScreenTimerGlassCustomizationMode) private var lockScreenTimerGlassCustomizationMode + @Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass + @Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget + @Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget let icons: [String] = ["logo2"] @State private var selectedIcon: String = "logo2" @State private var selectedListVisualizer: CustomVisualizer? = nil @@ -2778,6 +2816,37 @@ struct Appearance: View { SettingsTab.appearance.highlightID(for: title) } + private var liquidVariantRange: ClosedRange { + Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound) + } + + private var appearanceMusicVariantBinding: Binding { + Binding( + get: { Double(lockScreenMusicLiquidGlassVariant.rawValue) }, + set: { newValue in + let raw = Int(newValue.rounded()) + lockScreenMusicLiquidGlassVariant = LiquidGlassVariant.clamped(raw) + } + ) + } + + private var appearanceTimerVariantBinding: Binding { + Binding( + get: { Double(lockScreenTimerLiquidGlassVariant.rawValue) }, + set: { newValue in + let raw = Int(newValue.rounded()) + lockScreenTimerLiquidGlassVariant = LiquidGlassVariant.clamped(raw) + } + ) + } + + private var timerSurfaceBinding: Binding { + Binding( + get: { timerGlassModeIsGlass ? .glass : .classic }, + set: { mode in timerGlassModeIsGlass = (mode == .glass) } + ) + } + var body: some View { Form { Section { @@ -2796,6 +2865,80 @@ struct Appearance: View { notchWidthControls() + Section { + if #available(macOS 26.0, *) { + Picker("Material", selection: $lockScreenGlassStyle) { + ForEach(LockScreenGlassStyle.allCases) { style in + Text(style.rawValue).tag(style) + } + } + .settingsHighlight(id: highlightID("Lock screen material")) + } else { + Picker("Material", selection: $lockScreenGlassStyle) { + ForEach(LockScreenGlassStyle.allCases) { style in + Text(style.rawValue).tag(style) + } + } + .disabled(true) + .settingsHighlight(id: highlightID("Lock screen material")) + Text("Liquid Glass requires macOS 26 or later.") + .font(.caption) + .foregroundStyle(.secondary) + } + + if lockScreenGlassStyle == .liquid { + Picker("Lock screen glass mode", selection: $lockScreenGlassCustomizationMode) { + ForEach(LockScreenGlassCustomizationMode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + .settingsHighlight(id: highlightID("Lock screen glass mode")) + + if lockScreenGlassCustomizationMode == .customLiquid { + Text("Pick per-widget liquid-glass variants below. Changes mirror the Lock Screen tab.") + .font(.caption) + .foregroundStyle(.secondary) + + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Music panel variant") + Spacer() + Text("v\(lockScreenMusicLiquidGlassVariant.rawValue)") + .font(.caption) + .foregroundStyle(.secondary) + } + Slider(value: appearanceMusicVariantBinding, in: liquidVariantRange, step: 1) + } + .settingsHighlight(id: highlightID("Music panel variant (appearance)")) + .disabled(!enableLockScreenMediaWidget) + .opacity(enableLockScreenMediaWidget ? 1 : 0.4) + + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Timer widget variant") + Spacer() + Text("v\(lockScreenTimerLiquidGlassVariant.rawValue)") + .font(.caption) + .foregroundStyle(.secondary) + } + Slider(value: appearanceTimerVariantBinding, in: liquidVariantRange, step: 1) + } + .settingsHighlight(id: highlightID("Timer widget variant (appearance)")) + .disabled(!enableLockScreenTimerWidget) + .opacity(enableLockScreenTimerWidget ? 1 : 0.4) + } + } else { + Text("Custom Liquid settings require the Liquid Glass material.") + .font(.caption) + .foregroundStyle(.secondary) + } + } header: { + Text("Lock Screen Glass") + } footer: { + Text("Configure lock screen materials from the Appearance tab. Custom Liquid unlocks variant sliders for both widgets whenever Liquid Glass is selected.") + } + Section { Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram) .settingsHighlight(id: highlightID("Enable colored spectrograms")) @@ -3047,6 +3190,9 @@ struct Appearance: View { } } } + .onAppear(perform: enforceLockScreenGlassConsistency) + .onChange(of: lockScreenGlassStyle) { _, _ in enforceLockScreenGlassConsistency() } + .onChange(of: lockScreenGlassCustomizationMode) { _, _ in enforceLockScreenGlassConsistency() } .navigationTitle("Appearance") } @@ -3112,10 +3258,25 @@ struct Appearance: View { } } } + + private func enforceLockScreenGlassConsistency() { + if lockScreenGlassStyle == .frosted && lockScreenGlassCustomizationMode != .standard { + lockScreenGlassCustomizationMode = .standard + } + if lockScreenGlassCustomizationMode == .customLiquid && lockScreenGlassStyle != .liquid { + lockScreenGlassStyle = .liquid + } + } } struct LockScreenSettings: View { @Default(.lockScreenGlassStyle) private var lockScreenGlassStyle + @Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode + @Default(.lockScreenMusicLiquidGlassVariant) private var lockScreenMusicLiquidGlassVariant + @Default(.lockScreenTimerLiquidGlassVariant) private var lockScreenTimerLiquidGlassVariant + @Default(.lockScreenTimerGlassStyle) private var lockScreenTimerGlassStyle + @Default(.lockScreenTimerGlassCustomizationMode) private var lockScreenTimerGlassCustomizationMode + @Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass @Default(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget @Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget @Default(.enableLockScreenWeatherWidget) private var enableLockScreenWeatherWidget @@ -3133,6 +3294,37 @@ struct LockScreenSettings: View { SettingsTab.lockScreen.highlightID(for: title) } + private var liquidVariantRange: ClosedRange { + Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound) + } + + private var musicVariantBinding: Binding { + Binding( + get: { Double(lockScreenMusicLiquidGlassVariant.rawValue) }, + set: { newValue in + let raw = Int(newValue.rounded()) + lockScreenMusicLiquidGlassVariant = LiquidGlassVariant.clamped(raw) + } + ) + } + + private var timerVariantBinding: Binding { + Binding( + get: { Double(lockScreenTimerLiquidGlassVariant.rawValue) }, + set: { newValue in + let raw = Int(newValue.rounded()) + lockScreenTimerLiquidGlassVariant = LiquidGlassVariant.clamped(raw) + } + ) + } + + private var timerSurfaceBinding: Binding { + Binding( + get: { timerGlassModeIsGlass ? .glass : .classic }, + set: { mode in timerGlassModeIsGlass = (mode == .glass) } + ) + } + var body: some View { Form { Section { @@ -3166,6 +3358,33 @@ struct LockScreenSettings: View { .font(.caption) .foregroundStyle(.secondary) } + + if lockScreenGlassStyle == .liquid { + Picker("Glass mode", selection: $lockScreenGlassCustomizationMode) { + ForEach(LockScreenGlassCustomizationMode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + .settingsHighlight(id: highlightID("Glass mode")) + + if lockScreenGlassCustomizationMode == .customLiquid { + Text("Use the sliders below to pick unique Apple liquid-glass variants for each widget.") + .font(.caption) + .foregroundStyle(.secondary) + } + } else { + Text("Custom Liquid settings require the Liquid Glass material.") + .font(.caption) + .foregroundStyle(.secondary) + } + } header: { + Text("Lock Screen Glass") + } footer: { + Text("Choose the global material mode for lock screen widgets. Custom Liquid unlocks per-widget variant sliders while Standard sticks to the classic frosted/liquid options.") + } + + Section { Defaults.Toggle("Show lock screen media panel", key: .enableLockScreenMediaWidget) .settingsHighlight(id: highlightID("Show lock screen media panel")) Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon) @@ -3174,7 +3393,15 @@ struct LockScreenSettings: View { Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder) .disabled(!enableLockScreenMediaWidget) .settingsHighlight(id: highlightID("Show panel border")) - if lockScreenGlassStyle == .frosted { + if lockScreenGlassCustomizationMode == .customLiquid { + variantSlider( + title: "Music panel variant", + value: musicVariantBinding, + currentValue: lockScreenMusicLiquidGlassVariant.rawValue, + isEnabled: enableLockScreenMediaWidget, + highlight: highlightID("Music panel variant") + ) + } else if lockScreenGlassStyle == .frosted { Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur) .disabled(!enableLockScreenMediaWidget) .settingsHighlight(id: highlightID("Enable media panel blur")) @@ -3192,13 +3419,63 @@ struct LockScreenSettings: View { Section { Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget) .settingsHighlight(id: highlightID("Show lock screen timer")) - Defaults.Toggle("Enable timer blur", key: .lockScreenTimerWidgetUsesBlur) + Picker("Timer surface", selection: timerSurfaceBinding) { + ForEach(LockScreenTimerSurfaceMode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + .disabled(!enableLockScreenTimerWidget) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + .settingsHighlight(id: highlightID("Timer surface")) + + if timerGlassModeIsGlass { + Picker("Timer glass material", selection: $lockScreenTimerGlassStyle) { + ForEach(LockScreenGlassStyle.allCases) { style in + Text(style.rawValue).tag(style) + } + } .disabled(!enableLockScreenTimerWidget) - .settingsHighlight(id: highlightID("Enable timer blur")) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + .settingsHighlight(id: highlightID("Timer glass material")) + + if lockScreenTimerGlassStyle == .liquid { + Picker("Timer liquid mode", selection: $lockScreenTimerGlassCustomizationMode) { + ForEach(LockScreenGlassCustomizationMode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + .disabled(!enableLockScreenTimerWidget) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + .settingsHighlight(id: highlightID("Timer liquid mode")) + + if lockScreenTimerGlassCustomizationMode == .customLiquid { + variantSlider( + title: "Timer widget variant", + value: timerVariantBinding, + currentValue: lockScreenTimerLiquidGlassVariant.rawValue, + isEnabled: enableLockScreenTimerWidget, + highlight: highlightID("Timer widget variant") + ) + } + } else { + Text("Uses the frosted blur treatment while glass mode is enabled.") + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } else { + Text("Classic mode keeps the original translucent black background.") + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + } } header: { Text("Timer Widget") } footer: { - Text("Controls the optional timer widget that floats above the media panel. Blur adds a frosted finish behind the compact view.") + Text("Controls the optional timer widget that floats above the media panel, including its classic, frosted, or liquid glass surface independent of the global material setting.") } Section { @@ -3315,11 +3592,23 @@ struct LockScreenSettings: View { Text("Collect the latest crash report to share with the developer when reporting lock screen or overlay issues.") } } + .onAppear(perform: enforceLockScreenGlassConsistency) + .onChange(of: lockScreenGlassStyle) { _, _ in enforceLockScreenGlassConsistency() } + .onChange(of: lockScreenGlassCustomizationMode) { _, _ in enforceLockScreenGlassConsistency() } .navigationTitle("Lock Screen") } } extension LockScreenSettings { + private func enforceLockScreenGlassConsistency() { + if lockScreenGlassStyle == .frosted && lockScreenGlassCustomizationMode != .standard { + lockScreenGlassCustomizationMode = .standard + } + if lockScreenGlassCustomizationMode == .customLiquid && lockScreenGlassStyle != .liquid { + lockScreenGlassStyle = .liquid + } + } + private var blurSettingUnavailableRow: some View { VStack(alignment: .leading, spacing: 4) { Text("Enable media panel blur") @@ -3330,6 +3619,29 @@ extension LockScreenSettings { } .frame(maxWidth: .infinity, alignment: .leading) } + + @ViewBuilder + private func variantSlider( + title: String, + value: Binding, + currentValue: Int, + isEnabled: Bool, + highlight: String + ) -> some View { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text(title) + Spacer() + Text("v\(currentValue)") + .font(.caption) + .foregroundStyle(.secondary) + } + Slider(value: value, in: liquidVariantRange, step: 1) + } + .settingsHighlight(id: highlight) + .disabled(!isEnabled) + .opacity(isEnabled ? 1 : 0.4) + } } private struct LockScreenPositioningControls: View { @@ -3954,6 +4266,10 @@ struct TimerSettings: View { @Default(.mirrorSystemTimer) private var mirrorSystemTimer @Default(.timerDisplayMode) private var timerDisplayMode @Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget + @Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass + @Default(.lockScreenTimerGlassStyle) private var lockScreenTimerGlassStyle + @Default(.lockScreenTimerGlassCustomizationMode) private var lockScreenTimerGlassCustomizationMode + @Default(.lockScreenTimerLiquidGlassVariant) private var lockScreenTimerLiquidGlassVariant @AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 @State private var customHours: Int = 0 @State private var customMinutes: Int = 10 @@ -3963,6 +4279,27 @@ struct TimerSettings: View { private func highlightID(_ title: String) -> String { SettingsTab.timer.highlightID(for: title) } + + private var liquidVariantRange: ClosedRange { + Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound) + } + + private var timerVariantBinding: Binding { + Binding( + get: { Double(lockScreenTimerLiquidGlassVariant.rawValue) }, + set: { newValue in + let raw = Int(newValue.rounded()) + lockScreenTimerLiquidGlassVariant = LiquidGlassVariant.clamped(raw) + } + ) + } + + private var timerSurfaceBinding: Binding { + Binding( + get: { timerGlassModeIsGlass ? .glass : .classic }, + set: { mode in timerGlassModeIsGlass = (mode == .glass) } + ) + } var body: some View { Form { @@ -4037,9 +4374,65 @@ struct TimerSettings: View { Section { Defaults.Toggle("Show lock screen timer widget", key: .enableLockScreenTimerWidget) .settingsHighlight(id: highlightID("Show lock screen timer widget")) - Defaults.Toggle("Enable timer blur", key: .lockScreenTimerWidgetUsesBlur) + Picker("Timer surface", selection: timerSurfaceBinding) { + ForEach(LockScreenTimerSurfaceMode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + .disabled(!enableLockScreenTimerWidget) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + .settingsHighlight(id: highlightID("Timer surface")) + + if timerGlassModeIsGlass { + Picker("Timer glass material", selection: $lockScreenTimerGlassStyle) { + ForEach(LockScreenGlassStyle.allCases) { style in + Text(style.rawValue).tag(style) + } + } .disabled(!enableLockScreenTimerWidget) - .settingsHighlight(id: highlightID("Enable timer blur")) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + .settingsHighlight(id: highlightID("Timer glass material")) + + if lockScreenTimerGlassStyle == .liquid { + Picker("Timer liquid mode", selection: $lockScreenTimerGlassCustomizationMode) { + ForEach(LockScreenGlassCustomizationMode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + .disabled(!enableLockScreenTimerWidget) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + .settingsHighlight(id: highlightID("Timer liquid mode")) + + if lockScreenTimerGlassCustomizationMode == .customLiquid { + VStack(alignment: .leading, spacing: 6) { + HStack { + Text("Timer widget variant") + Spacer() + Text("v\(lockScreenTimerLiquidGlassVariant.rawValue)") + .font(.caption) + .foregroundStyle(.secondary) + } + Slider(value: timerVariantBinding, in: liquidVariantRange, step: 1) + } + .settingsHighlight(id: highlightID("Timer widget variant")) + .disabled(!enableLockScreenTimerWidget) + .opacity(enableLockScreenTimerWidget ? 1 : 0.4) + } + } else { + Text("Uses the frosted blur treatment while glass mode is enabled.") + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + } else { + Text("Classic mode keeps the original translucent black background.") + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .opacity(enableLockScreenTimerWidget ? 1 : 0.5) + } } header: { Text("Lock Screen Integration") } footer: { diff --git a/DynamicIsland/enums/generic.swift b/DynamicIsland/enums/generic.swift index ab33e05..c3ffeb7 100644 --- a/DynamicIsland/enums/generic.swift +++ b/DynamicIsland/enums/generic.swift @@ -100,6 +100,24 @@ enum LockScreenGlassStyle: String, CaseIterable, Defaults.Serializable, Identifi var id: String { rawValue } } +enum LockScreenGlassCustomizationMode: String, CaseIterable, Defaults.Serializable, Identifiable { + case standard = "Standard" + case customLiquid = "Custom Liquid" + + var id: String { rawValue } + + var allowsVariantSelection: Bool { + self == .customLiquid + } +} + +enum LockScreenTimerSurfaceMode: String, CaseIterable, Defaults.Serializable, Identifiable { + case classic = "Classic" + case glass = "Glass" + + var id: String { rawValue } +} + enum LockScreenWeatherWidgetStyle: String, CaseIterable, Defaults.Serializable, Identifiable { case inline = "Inline" case circular = "Circular" diff --git a/DynamicIsland/helpers/LiquidGlassBackground.swift b/DynamicIsland/helpers/LiquidGlassBackground.swift new file mode 100644 index 0000000..b8d8f7f --- /dev/null +++ b/DynamicIsland/helpers/LiquidGlassBackground.swift @@ -0,0 +1,134 @@ +// LiquidGlassBackground.swift +// A SwiftUI wrapper around Apple’s private NSGlassEffectView. +// Provides easy access to every liquid‑glass variant (0–19). + + +import SwiftUI +import AppKit +import Defaults + +/// All 20 available liquid‑glass variants. +/// Apple does not publicly describe how each value looks so experiment and pick the one you like! +public enum LiquidGlassVariant: Int, CaseIterable, Identifiable, Defaults.Serializable, Sendable { + case v0 = 0, v1 = 1, v2 = 2, v3 = 3, v4 = 4 + case v5 = 5, v6 = 6, v7 = 7, v8 = 8, v9 = 9 + case v10 = 10, v11 = 11, v12 = 12, v13 = 13, v14 = 14 + case v15 = 15, v16 = 16, v17 = 17, v18 = 18, v19 = 19 + + public var id: Int { rawValue } + + public static let supportedRange = 0...19 + + public static var defaultVariant: LiquidGlassVariant { .v11 } + + public static func clamped(_ rawValue: Int) -> LiquidGlassVariant { + let clamped = min(max(rawValue, supportedRange.lowerBound), supportedRange.upperBound) + return LiquidGlassVariant(rawValue: clamped) ?? .defaultVariant + } +} + +/// A SwiftUI view that embeds its content inside Apple’s private liquid‑glass material. +/// +/// ```swift +/// GlassBackground(variant: .v11, cornerRadius: 12) { +/// VStack(spacing: 12) { +/// Image(systemName: "sparkles") +/// .font(.largeTitle) +/// Text("Hello, glass!") +/// .font(.title2) +/// } +/// .padding() +/// } +/// ``` +public struct LiquidGlassBackground: NSViewRepresentable { + private let content: Content + private let cornerRadius: CGFloat + private let variant: LiquidGlassVariant + + /// Creates a new liquid‑glass container. + /// - Parameters: + /// - variant: Any ``LiquidGlassVariant`` (0–19). Defaults to `.v11`, which is visually super pleasing + /// - cornerRadius: Corner radius in points. Defaults to `10`. + /// - content: Your SwiftUI hierarchy. + public init( + variant: LiquidGlassVariant = .defaultVariant, + cornerRadius: CGFloat = 10, + @ViewBuilder content: () -> Content + ) { + self.variant = variant + self.cornerRadius = cornerRadius + self.content = content() + } + + + @inline(__always) + private func setterSelector(for key: String, privateVariant: Bool = true) -> Selector? { + guard !key.isEmpty else { return nil } + let name: String + if privateVariant { + let cleaned = key.hasPrefix("_") ? key : "_" + key + name = "set" + cleaned + } else { + let first = String(key.prefix(1)).uppercased() + let rest = String(key.dropFirst()) + name = "set" + first + rest + } + return NSSelectorFromString(name + ":") + } + + private typealias VariantSetterIMP = @convention(c) (AnyObject, Selector, Int) -> Void + + private func callPrivateVariantSetter(on object: AnyObject, value: Int) { + guard + let sel = setterSelector(for: "variant", privateVariant: true), + let m = class_getInstanceMethod(object_getClass(object), sel) + else { + #if DEBUG + print("✗ LiquidGlassBackground: selector set_variant: not found. falling back to default") + #endif + return + } + let imp = method_getImplementation(m) + let f = unsafeBitCast(imp, to: VariantSetterIMP.self) + f(object, sel, value) + } + + + + public func makeNSView(context: Context) -> NSView { + // `NSGlassEffectView` is private. Look it up dynamically to avoid compile‑time coupling. + if let glassType = NSClassFromString("NSGlassEffectView") as? NSView.Type { + let glass = glassType.init(frame: .zero) + glass.setValue(cornerRadius, forKey: "cornerRadius") + callPrivateVariantSetter(on: glass, value: variant.rawValue) + + let hosting = NSHostingView(rootView: content) + hosting.translatesAutoresizingMaskIntoConstraints = false + glass.setValue(hosting, forKey: "contentView") + return glass + } + + // Fallback for earlier macOS – use an ordinary blur. + let fallback = NSVisualEffectView() + fallback.material = .underWindowBackground + + let hosting = NSHostingView(rootView: content) + hosting.translatesAutoresizingMaskIntoConstraints = false + fallback.addSubview(hosting) + NSLayoutConstraint.activate([ + hosting.leadingAnchor.constraint(equalTo: fallback.leadingAnchor), + hosting.trailingAnchor.constraint(equalTo: fallback.trailingAnchor), + hosting.topAnchor.constraint(equalTo: fallback.topAnchor), + hosting.bottomAnchor.constraint(equalTo: fallback.bottomAnchor) + ]) + return fallback + } + + public func updateNSView(_ nsView: NSView, context: Context) { + if let hosting = nsView.value(forKey: "contentView") as? NSHostingView { + hosting.rootView = content + } + nsView.setValue(cornerRadius, forKey: "cornerRadius") + callPrivateVariantSetter(on: nsView, value: variant.rawValue) + } +} diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 85c2859..b1aaf96 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -703,6 +703,23 @@ extension Defaults.Keys { static let lockScreenMusicAlbumParallaxEnabled = Key("lockScreenMusicAlbumParallaxEnabled", default: false) static let lockScreenTimerVerticalOffset = Key("lockScreenTimerVerticalOffset", default: 0) static let lockScreenGlassStyle = Key("lockScreenGlassStyle", default: .liquid) + static let lockScreenGlassCustomizationMode = Key( + "lockScreenGlassCustomizationMode", + default: .standard + ) + static let lockScreenTimerGlassStyle = Key("lockScreenTimerGlassStyle", default: .frosted) + static let lockScreenTimerGlassCustomizationMode = Key( + "lockScreenTimerGlassCustomizationMode", + default: .standard + ) + static let lockScreenMusicLiquidGlassVariant = Key( + "lockScreenMusicLiquidGlassVariant", + default: .defaultVariant + ) + static let lockScreenTimerLiquidGlassVariant = Key( + "lockScreenTimerLiquidGlassVariant", + default: .defaultVariant + ) static let lockScreenShowAppIcon = Key("lockScreenShowAppIcon", default: false) static let lockScreenPanelShowsBorder = Key("lockScreenPanelShowsBorder", default: false) static let lockScreenPanelUsesBlur = Key("lockScreenPanelUsesBlur", default: true)