mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update Lock Screen Customization
This commit is contained in:
parent
5b41f2c364
commit
8720de3cf7
7 changed files with 749 additions and 46 deletions
|
|
@ -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." : {
|
"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.",
|
"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" : {
|
"Clear" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"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 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" : {
|
"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.",
|
"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 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." : {
|
"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." : {
|
||||||
"comment" : "A footer for the \"Timer Widget\" section in the Lock Screen Settings view.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
},
|
||||||
"Controls whether Dynamic Island mirrors lock/unlock events with its own live activity and audible chimes." : {
|
"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" : {
|
"Custom music live activity animation" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -9555,6 +9571,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Glass mode" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Global Settings" : {
|
"Global Settings" : {
|
||||||
"comment" : "The header for the section that controls global settings related to extensions.",
|
"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" : {
|
"Lock Screen Integration" : {
|
||||||
|
|
||||||
|
|
@ -13244,6 +13269,9 @@
|
||||||
"Move Up" : {
|
"Move Up" : {
|
||||||
"comment" : "A button label that says \"Move Up\".",
|
"comment" : "A button label that says \"Move Up\".",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Music panel variant" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Music Source" : {
|
"Music Source" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
@ -15141,6 +15169,9 @@
|
||||||
},
|
},
|
||||||
"Pick Color" : {
|
"Pick Color" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Pick per-widget liquid-glass variants below. Changes mirror the Lock Screen tab." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Picked %@" : {
|
"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" : {
|
"Timer Presets" : {
|
||||||
"comment" : "A section header in the timer settings view, describing timer presets.",
|
"comment" : "A section header in the timer settings view, describing timer presets.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|
@ -19180,12 +19219,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Timer surface" : {
|
||||||
|
"comment" : "A label displayed above the user's timer surface.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Timer tint" : {
|
"Timer tint" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Timer Widget" : {
|
"Timer Widget" : {
|
||||||
"comment" : "A section header for the timer widget settings.",
|
"comment" : "A section header for the timer widget settings.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Timer widget variant" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Title" : {
|
"Title" : {
|
||||||
"comment" : "A label for the title input field in the note editor.",
|
"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." : {
|
"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.",
|
"comment" : "A footer for the \"Media Live Activity\" section, explaining how to configure the media controls.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Use the sliders below to pick unique Apple liquid-glass variants for each widget." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Used" : {
|
"Used" : {
|
||||||
"comment" : "A label describing the percentage of memory that is currently in use.",
|
"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 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" : {
|
"Utilization" : {
|
||||||
"comment" : "A label displayed alongside the utilization percentage in the GPU detail view.",
|
"comment" : "A label displayed alongside the utilization percentage in the GPU detail view.",
|
||||||
|
|
@ -20165,6 +20218,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"v%lld" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Version" : {
|
"Version" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import Defaults
|
||||||
struct LockScreenMusicPanel: View {
|
struct LockScreenMusicPanel: View {
|
||||||
private struct GlassLogSnapshot: Equatable {
|
private struct GlassLogSnapshot: Equatable {
|
||||||
let style: LockScreenGlassStyle
|
let style: LockScreenGlassStyle
|
||||||
|
let customizationMode: LockScreenGlassCustomizationMode
|
||||||
|
let variantRawValue: Int
|
||||||
let usesLiquidGlass: Bool
|
let usesLiquidGlass: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +34,8 @@ struct LockScreenMusicPanel: View {
|
||||||
@State private var isParallaxSuspended = false
|
@State private var isParallaxSuspended = false
|
||||||
@State private var lastLoggedGlassSnapshot: GlassLogSnapshot?
|
@State private var lastLoggedGlassSnapshot: GlassLogSnapshot?
|
||||||
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||||
|
@Default(.lockScreenGlassCustomizationMode) private var glassCustomizationMode
|
||||||
|
@Default(.lockScreenMusicLiquidGlassVariant) private var musicGlassVariant
|
||||||
@Default(.lockScreenShowAppIcon) var showAppIcon
|
@Default(.lockScreenShowAppIcon) var showAppIcon
|
||||||
@Default(.lockScreenPanelShowsBorder) var showPanelBorder
|
@Default(.lockScreenPanelShowsBorder) var showPanelBorder
|
||||||
@Default(.lockScreenPanelUsesBlur) var enableBlur
|
@Default(.lockScreenPanelUsesBlur) var enableBlur
|
||||||
|
|
@ -70,13 +74,22 @@ struct LockScreenMusicPanel: View {
|
||||||
isExpanded ? expandedPanelCornerRadius : collapsedPanelCornerRadius
|
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, *) {
|
if #available(macOS 26.0, *) {
|
||||||
return lockScreenGlassStyle == .liquid
|
return lockScreenGlassStyle == .liquid
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var usesLiquidGlass: Bool {
|
||||||
|
usesCustomLiquidGlass || usesStandardLiquidGlass
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if isActive {
|
if isActive {
|
||||||
panelContent
|
panelContent
|
||||||
|
|
@ -138,6 +151,14 @@ struct LockScreenMusicPanel: View {
|
||||||
.onChange(of: lockScreenGlassStyle) { _, _ in
|
.onChange(of: lockScreenGlassStyle) { _, _ in
|
||||||
logGlassState(reason: "Glass style updated")
|
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)
|
.scaleEffect(animator.isPresented ? 1 : 0.9, anchor: .center)
|
||||||
.opacity(animator.isPresented ? 1 : 0)
|
.opacity(animator.isPresented ? 1 : 0)
|
||||||
.animation(.spring(response: 0.52, dampingFraction: 0.8), value: animator.isPresented)
|
.animation(.spring(response: 0.52, dampingFraction: 0.8), value: animator.isPresented)
|
||||||
|
|
@ -723,8 +744,10 @@ struct LockScreenMusicPanel: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var panelBackgroundLayer: some View {
|
private var panelBackgroundLayer: some View {
|
||||||
if usesLiquidGlass {
|
if usesCustomLiquidGlass {
|
||||||
liquidPanelBackdrop
|
customLiquidPanelBackdrop
|
||||||
|
} else if usesStandardLiquidGlass {
|
||||||
|
standardLiquidPanelBackdrop
|
||||||
} else if shouldUseFrostedBlur {
|
} else if shouldUseFrostedBlur {
|
||||||
frostedPanelBackground
|
frostedPanelBackground
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -733,7 +756,7 @@ struct LockScreenMusicPanel: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var liquidPanelBackdrop: some View {
|
private var standardLiquidPanelBackdrop: some View {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
GlassTextBackdrop(cornerRadius: panelCornerRadius)
|
GlassTextBackdrop(cornerRadius: panelCornerRadius)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.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 {
|
private var frostedPanelBackground: some View {
|
||||||
RoundedRectangle(cornerRadius: panelCornerRadius)
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
.fill(.ultraThinMaterial)
|
.fill(.ultraThinMaterial)
|
||||||
|
|
@ -768,7 +801,9 @@ struct LockScreenMusicPanel: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func albumArtBackground(cornerRadius: CGFloat) -> some View {
|
private func albumArtBackground(cornerRadius: CGFloat) -> some View {
|
||||||
if usesLiquidGlass {
|
if usesCustomLiquidGlass {
|
||||||
|
customLiquidAlbumArtBackground(cornerRadius: cornerRadius)
|
||||||
|
} else if usesStandardLiquidGlass {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
clearLiquidGlassSurface(cornerRadius: cornerRadius)
|
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) {
|
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 }
|
guard snapshot != lastLoggedGlassSnapshot else { return }
|
||||||
lastLoggedGlassSnapshot = snapshot
|
lastLoggedGlassSnapshot = snapshot
|
||||||
|
|
||||||
|
|
@ -831,9 +878,18 @@ struct LockScreenMusicPanel: View {
|
||||||
return "\(entry.name)=\(mode)"
|
return "\(entry.name)=\(mode)"
|
||||||
}.joined(separator: ", ")
|
}.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.")
|
print("[LockScreenMusicPanel] Liquid Glass requested but unavailable on this macOS build. Falling back to frosted visuals.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ struct LockScreenTimerWidget: View {
|
||||||
|
|
||||||
@ObservedObject private var animator: LockScreenTimerWidgetAnimator
|
@ObservedObject private var animator: LockScreenTimerWidgetAnimator
|
||||||
@ObservedObject private var timerManager = TimerManager.shared
|
@ObservedObject private var timerManager = TimerManager.shared
|
||||||
@Default(.lockScreenGlassStyle) private var glassStyle
|
@Default(.lockScreenTimerGlassStyle) private var timerGlassStyle
|
||||||
@Default(.lockScreenTimerWidgetUsesBlur) private var enableTimerBlur
|
@Default(.lockScreenTimerGlassCustomizationMode) private var timerGlassCustomizationMode
|
||||||
|
@Default(.lockScreenTimerLiquidGlassVariant) private var timerGlassVariant
|
||||||
|
@Default(.lockScreenTimerWidgetUsesBlur) private var timerGlassModeIsGlass
|
||||||
@Default(.timerPresets) private var timerPresets
|
@Default(.timerPresets) private var timerPresets
|
||||||
|
|
||||||
@MainActor
|
@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, *) {
|
if #available(macOS 26.0, *) {
|
||||||
return glassStyle == .liquid
|
return timerGlassCustomizationMode == .standard
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var widgetBackground: some View {
|
private var widgetBackground: some View {
|
||||||
if enableTimerBlur {
|
if usesGlassBackground {
|
||||||
if usesLiquidGlass {
|
if usesCustomLiquidGlass {
|
||||||
liquidBackground
|
customLiquidBackground
|
||||||
|
} else if timerGlassStyle == .liquid {
|
||||||
|
standardLiquidBackground
|
||||||
} else {
|
} else {
|
||||||
frostedBackground
|
frostedBackground
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
classicBackground
|
||||||
.fill(Color.black.opacity(0.65))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var liquidBackground: some View {
|
private var standardLiquidBackground: some View {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
||||||
.glassEffect(
|
.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 {
|
private var frostedBackground: some View {
|
||||||
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
||||||
.fill(.ultraThinMaterial)
|
.fill(.ultraThinMaterial)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var classicBackground: some View {
|
||||||
|
RoundedRectangle(cornerRadius: Self.cornerRadius, style: .continuous)
|
||||||
|
.fill(Color.black.opacity(0.65))
|
||||||
|
}
|
||||||
|
|
||||||
private var pauseIcon: String {
|
private var pauseIcon: String {
|
||||||
timerManager.isPaused ? "play.fill" : "pause.fill"
|
timerManager.isPaused ? "play.fill" : "pause.fill"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,26 +178,34 @@ private struct SettingsHighlightModifier: ViewModifier {
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.id(id)
|
.id(id)
|
||||||
.background(
|
.background(highlightBackground)
|
||||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
.onChange(of: isActive) { _, active in
|
||||||
.fill(Color.accentColor.opacity(0.08))
|
animatePulse = active
|
||||||
.padding(.horizontal, -8)
|
}
|
||||||
.opacity(isActive ? 1 : 0)
|
.onAppear {
|
||||||
.animation(.easeInOut(duration: 0.25), value: isActive)
|
|
||||||
)
|
|
||||||
.overlay {
|
|
||||||
if isActive {
|
if isActive {
|
||||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
animatePulse = true
|
||||||
.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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
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: "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: "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: "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: "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: "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")),
|
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: "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: "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: "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: "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: "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")),
|
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(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget
|
||||||
@Default(.showSneakPeekOnTrackChange) private var showSneakPeekOnTrackChange
|
@Default(.showSneakPeekOnTrackChange) private var showSneakPeekOnTrackChange
|
||||||
@Default(.lockScreenGlassStyle) private var lockScreenGlassStyle
|
@Default(.lockScreenGlassStyle) private var lockScreenGlassStyle
|
||||||
|
@Default(.lockScreenGlassCustomizationMode) private var lockScreenGlassCustomizationMode
|
||||||
@Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenMusicAlbumParallaxEnabled
|
@Default(.lockScreenMusicAlbumParallaxEnabled) private var lockScreenMusicAlbumParallaxEnabled
|
||||||
|
|
||||||
private func highlightID(_ title: String) -> String {
|
private func highlightID(_ title: String) -> String {
|
||||||
|
|
@ -2079,7 +2094,11 @@ struct Media: View {
|
||||||
.disabled(!enableLockScreenMediaWidget)
|
.disabled(!enableLockScreenMediaWidget)
|
||||||
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
||||||
.disabled(!enableLockScreenMediaWidget)
|
.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)
|
Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur)
|
||||||
.disabled(!enableLockScreenMediaWidget)
|
.disabled(!enableLockScreenMediaWidget)
|
||||||
.settingsHighlight(id: highlightID("Enable media panel blur"))
|
.settingsHighlight(id: highlightID("Enable media panel blur"))
|
||||||
|
|
@ -2129,6 +2148,16 @@ struct Media: View {
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.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 {
|
struct CalendarSettings: View {
|
||||||
|
|
@ -2762,6 +2791,15 @@ struct Appearance: View {
|
||||||
@Default(.selectedVisualizer) var selectedVisualizer
|
@Default(.selectedVisualizer) var selectedVisualizer
|
||||||
@Default(.openNotchWidth) var openNotchWidth
|
@Default(.openNotchWidth) var openNotchWidth
|
||||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
@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"]
|
let icons: [String] = ["logo2"]
|
||||||
@State private var selectedIcon: String = "logo2"
|
@State private var selectedIcon: String = "logo2"
|
||||||
@State private var selectedListVisualizer: CustomVisualizer? = nil
|
@State private var selectedListVisualizer: CustomVisualizer? = nil
|
||||||
|
|
@ -2778,6 +2816,37 @@ struct Appearance: View {
|
||||||
SettingsTab.appearance.highlightID(for: title)
|
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 {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
|
@ -2796,6 +2865,80 @@ struct Appearance: View {
|
||||||
|
|
||||||
notchWidthControls()
|
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 {
|
Section {
|
||||||
Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram)
|
Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram)
|
||||||
.settingsHighlight(id: highlightID("Enable colored spectrograms"))
|
.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")
|
.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 {
|
struct LockScreenSettings: View {
|
||||||
@Default(.lockScreenGlassStyle) private var lockScreenGlassStyle
|
@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(.enableLockScreenMediaWidget) private var enableLockScreenMediaWidget
|
||||||
@Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget
|
@Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget
|
||||||
@Default(.enableLockScreenWeatherWidget) private var enableLockScreenWeatherWidget
|
@Default(.enableLockScreenWeatherWidget) private var enableLockScreenWeatherWidget
|
||||||
|
|
@ -3133,6 +3294,37 @@ struct LockScreenSettings: View {
|
||||||
SettingsTab.lockScreen.highlightID(for: title)
|
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 {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
Section {
|
Section {
|
||||||
|
|
@ -3166,6 +3358,33 @@ struct LockScreenSettings: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.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)
|
Defaults.Toggle("Show lock screen media panel", key: .enableLockScreenMediaWidget)
|
||||||
.settingsHighlight(id: highlightID("Show lock screen media panel"))
|
.settingsHighlight(id: highlightID("Show lock screen media panel"))
|
||||||
Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon)
|
Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon)
|
||||||
|
|
@ -3174,7 +3393,15 @@ struct LockScreenSettings: View {
|
||||||
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
||||||
.disabled(!enableLockScreenMediaWidget)
|
.disabled(!enableLockScreenMediaWidget)
|
||||||
.settingsHighlight(id: highlightID("Show panel border"))
|
.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)
|
Defaults.Toggle("Enable media panel blur", key: .lockScreenPanelUsesBlur)
|
||||||
.disabled(!enableLockScreenMediaWidget)
|
.disabled(!enableLockScreenMediaWidget)
|
||||||
.settingsHighlight(id: highlightID("Enable media panel blur"))
|
.settingsHighlight(id: highlightID("Enable media panel blur"))
|
||||||
|
|
@ -3192,13 +3419,63 @@ struct LockScreenSettings: View {
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget)
|
Defaults.Toggle("Show lock screen timer", key: .enableLockScreenTimerWidget)
|
||||||
.settingsHighlight(id: highlightID("Show lock screen timer"))
|
.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)
|
.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: {
|
} header: {
|
||||||
Text("Timer Widget")
|
Text("Timer Widget")
|
||||||
} footer: {
|
} 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 {
|
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.")
|
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")
|
.navigationTitle("Lock Screen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LockScreenSettings {
|
extension LockScreenSettings {
|
||||||
|
private func enforceLockScreenGlassConsistency() {
|
||||||
|
if lockScreenGlassStyle == .frosted && lockScreenGlassCustomizationMode != .standard {
|
||||||
|
lockScreenGlassCustomizationMode = .standard
|
||||||
|
}
|
||||||
|
if lockScreenGlassCustomizationMode == .customLiquid && lockScreenGlassStyle != .liquid {
|
||||||
|
lockScreenGlassStyle = .liquid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var blurSettingUnavailableRow: some View {
|
private var blurSettingUnavailableRow: some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Enable media panel blur")
|
Text("Enable media panel blur")
|
||||||
|
|
@ -3330,6 +3619,29 @@ extension LockScreenSettings {
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.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 {
|
private struct LockScreenPositioningControls: View {
|
||||||
|
|
@ -3954,6 +4266,10 @@ struct TimerSettings: View {
|
||||||
@Default(.mirrorSystemTimer) private var mirrorSystemTimer
|
@Default(.mirrorSystemTimer) private var mirrorSystemTimer
|
||||||
@Default(.timerDisplayMode) private var timerDisplayMode
|
@Default(.timerDisplayMode) private var timerDisplayMode
|
||||||
@Default(.enableLockScreenTimerWidget) private var enableLockScreenTimerWidget
|
@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
|
@AppStorage("customTimerDuration") private var customTimerDuration: Double = 600
|
||||||
@State private var customHours: Int = 0
|
@State private var customHours: Int = 0
|
||||||
@State private var customMinutes: Int = 10
|
@State private var customMinutes: Int = 10
|
||||||
|
|
@ -3964,6 +4280,27 @@ struct TimerSettings: View {
|
||||||
SettingsTab.timer.highlightID(for: title)
|
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 {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
timerFeatureSection
|
timerFeatureSection
|
||||||
|
|
@ -4037,9 +4374,65 @@ struct TimerSettings: View {
|
||||||
Section {
|
Section {
|
||||||
Defaults.Toggle("Show lock screen timer widget", key: .enableLockScreenTimerWidget)
|
Defaults.Toggle("Show lock screen timer widget", key: .enableLockScreenTimerWidget)
|
||||||
.settingsHighlight(id: highlightID("Show lock screen timer widget"))
|
.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)
|
.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: {
|
} header: {
|
||||||
Text("Lock Screen Integration")
|
Text("Lock Screen Integration")
|
||||||
} footer: {
|
} footer: {
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,24 @@ enum LockScreenGlassStyle: String, CaseIterable, Defaults.Serializable, Identifi
|
||||||
var id: String { rawValue }
|
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 {
|
enum LockScreenWeatherWidgetStyle: String, CaseIterable, Defaults.Serializable, Identifiable {
|
||||||
case inline = "Inline"
|
case inline = "Inline"
|
||||||
case circular = "Circular"
|
case circular = "Circular"
|
||||||
|
|
|
||||||
134
DynamicIsland/helpers/LiquidGlassBackground.swift
Normal file
134
DynamicIsland/helpers/LiquidGlassBackground.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -703,6 +703,23 @@ extension Defaults.Keys {
|
||||||
static let lockScreenMusicAlbumParallaxEnabled = Key<Bool>("lockScreenMusicAlbumParallaxEnabled", default: false)
|
static let lockScreenMusicAlbumParallaxEnabled = Key<Bool>("lockScreenMusicAlbumParallaxEnabled", default: false)
|
||||||
static let lockScreenTimerVerticalOffset = Key<Double>("lockScreenTimerVerticalOffset", default: 0)
|
static let lockScreenTimerVerticalOffset = Key<Double>("lockScreenTimerVerticalOffset", default: 0)
|
||||||
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
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 lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false)
|
||||||
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: false)
|
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: false)
|
||||||
static let lockScreenPanelUsesBlur = Key<Bool>("lockScreenPanelUsesBlur", default: true)
|
static let lockScreenPanelUsesBlur = Key<Bool>("lockScreenPanelUsesBlur", default: true)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue