Update Lock Screen Customization

This commit is contained in:
Hariharan Mudaliar 2026-01-22 13:13:55 +05:30
commit 8720de3cf7
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
7 changed files with 749 additions and 46 deletions

View file

@ -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" : {

View file

@ -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,13 +74,22 @@ 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 {
panelContent
@ -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.")
}
}

View file

@ -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"
}

View file

@ -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> {
Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound)
}
private var appearanceMusicVariantBinding: Binding<Double> {
Binding(
get: { Double(lockScreenMusicLiquidGlassVariant.rawValue) },
set: { newValue in
let raw = Int(newValue.rounded())
lockScreenMusicLiquidGlassVariant = LiquidGlassVariant.clamped(raw)
}
)
}
private var appearanceTimerVariantBinding: Binding<Double> {
Binding(
get: { Double(lockScreenTimerLiquidGlassVariant.rawValue) },
set: { newValue in
let raw = Int(newValue.rounded())
lockScreenTimerLiquidGlassVariant = LiquidGlassVariant.clamped(raw)
}
)
}
private var timerSurfaceBinding: Binding<LockScreenTimerSurfaceMode> {
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> {
Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound)
}
private var musicVariantBinding: Binding<Double> {
Binding(
get: { Double(lockScreenMusicLiquidGlassVariant.rawValue) },
set: { newValue in
let raw = Int(newValue.rounded())
lockScreenMusicLiquidGlassVariant = LiquidGlassVariant.clamped(raw)
}
)
}
private var timerVariantBinding: Binding<Double> {
Binding(
get: { Double(lockScreenTimerLiquidGlassVariant.rawValue) },
set: { newValue in
let raw = Int(newValue.rounded())
lockScreenTimerLiquidGlassVariant = LiquidGlassVariant.clamped(raw)
}
)
}
private var timerSurfaceBinding: Binding<LockScreenTimerSurfaceMode> {
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<Double>,
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
@ -3964,6 +4280,27 @@ struct TimerSettings: View {
SettingsTab.timer.highlightID(for: title)
}
private var liquidVariantRange: ClosedRange<Double> {
Double(LiquidGlassVariant.supportedRange.lowerBound)...Double(LiquidGlassVariant.supportedRange.upperBound)
}
private var timerVariantBinding: Binding<Double> {
Binding(
get: { Double(lockScreenTimerLiquidGlassVariant.rawValue) },
set: { newValue in
let raw = Int(newValue.rounded())
lockScreenTimerLiquidGlassVariant = LiquidGlassVariant.clamped(raw)
}
)
}
private var timerSurfaceBinding: Binding<LockScreenTimerSurfaceMode> {
Binding(
get: { timerGlassModeIsGlass ? .glass : .classic },
set: { mode in timerGlassModeIsGlass = (mode == .glass) }
)
}
var body: some View {
Form {
timerFeatureSection
@ -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: {

View file

@ -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"

View file

@ -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<Content: View>: 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<Content> {
hosting.rootView = content
}
nsView.setValue(cornerRadius, forKey: "cornerRadius")
callPrivateVariantSetter(on: nsView, value: variant.rawValue)
}
}

View file

@ -703,6 +703,23 @@ extension Defaults.Keys {
static let lockScreenMusicAlbumParallaxEnabled = Key<Bool>("lockScreenMusicAlbumParallaxEnabled", default: false)
static let lockScreenTimerVerticalOffset = Key<Double>("lockScreenTimerVerticalOffset", default: 0)
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
static let lockScreenGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>(
"lockScreenGlassCustomizationMode",
default: .standard
)
static let lockScreenTimerGlassStyle = Key<LockScreenGlassStyle>("lockScreenTimerGlassStyle", default: .frosted)
static let lockScreenTimerGlassCustomizationMode = Key<LockScreenGlassCustomizationMode>(
"lockScreenTimerGlassCustomizationMode",
default: .standard
)
static let lockScreenMusicLiquidGlassVariant = Key<LiquidGlassVariant>(
"lockScreenMusicLiquidGlassVariant",
default: .defaultVariant
)
static let lockScreenTimerLiquidGlassVariant = Key<LiquidGlassVariant>(
"lockScreenTimerLiquidGlassVariant",
default: .defaultVariant
)
static let lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false)
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: false)
static let lockScreenPanelUsesBlur = Key<Bool>("lockScreenPanelUsesBlur", default: true)