mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
Refined UI
This commit is contained in:
parent
7af1d1bc27
commit
1eb3d3d968
3 changed files with 128 additions and 31 deletions
|
|
@ -10,7 +10,7 @@ import Defaults
|
||||||
|
|
||||||
struct LockScreenMusicPanel: View {
|
struct LockScreenMusicPanel: View {
|
||||||
static let collapsedSize = CGSize(width: 420, height: 180)
|
static let collapsedSize = CGSize(width: 420, height: 180)
|
||||||
static let expandedSize = CGSize(width: 660, height: 310)
|
static let expandedSize = CGSize(width: 720, height: 340)
|
||||||
|
|
||||||
@ObservedObject var musicManager = MusicManager.shared
|
@ObservedObject var musicManager = MusicManager.shared
|
||||||
@State private var sliderValue: Double = 0
|
@State private var sliderValue: Double = 0
|
||||||
|
|
@ -19,14 +19,24 @@ struct LockScreenMusicPanel: View {
|
||||||
@State private var isExpanded = false
|
@State private var isExpanded = false
|
||||||
@State private var collapseWorkItem: DispatchWorkItem?
|
@State private var collapseWorkItem: DispatchWorkItem?
|
||||||
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||||
|
@Default(.lockScreenShowAppIcon) var showAppIcon
|
||||||
|
@Default(.lockScreenPanelShowsBorder) var showPanelBorder
|
||||||
|
@Default(.lockScreenPanelUsesBlur) var enableBlur
|
||||||
|
|
||||||
private let cornerRadius: CGFloat = 28
|
private let collapsedPanelCornerRadius: CGFloat = 28
|
||||||
|
private let expandedPanelCornerRadius: CGFloat = 44
|
||||||
|
private let collapsedAlbumArtCornerRadius: CGFloat = 16
|
||||||
|
private let expandedAlbumArtCornerRadius: CGFloat = 52
|
||||||
private let collapseTimeout: TimeInterval = 5
|
private let collapseTimeout: TimeInterval = 5
|
||||||
|
|
||||||
private var currentSize: CGSize {
|
private var currentSize: CGSize {
|
||||||
isExpanded ? Self.expandedSize : Self.collapsedSize
|
isExpanded ? Self.expandedSize : Self.collapsedSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var panelCornerRadius: CGFloat {
|
||||||
|
isExpanded ? expandedPanelCornerRadius : collapsedPanelCornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
private var usesLiquidGlass: Bool {
|
private var usesLiquidGlass: Bool {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
return lockScreenGlassStyle == .liquid
|
return lockScreenGlassStyle == .liquid
|
||||||
|
|
@ -57,7 +67,7 @@ struct LockScreenMusicPanel: View {
|
||||||
isActive = false
|
isActive = false
|
||||||
cancelCollapseTimer()
|
cancelCollapseTimer()
|
||||||
}
|
}
|
||||||
.onChange(of: isExpanded) { expanded in
|
.onChange(of: isExpanded) { _, expanded in
|
||||||
LockScreenPanelManager.shared.updatePanelSize(expanded: expanded)
|
LockScreenPanelManager.shared.updatePanelSize(expanded: expanded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,13 +85,15 @@ struct LockScreenMusicPanel: View {
|
||||||
.padding(.vertical, isExpanded ? 22 : 16)
|
.padding(.vertical, isExpanded ? 22 : 16)
|
||||||
.frame(width: currentSize.width, height: currentSize.height, alignment: .topLeading)
|
.frame(width: currentSize.width, height: currentSize.height, alignment: .topLeading)
|
||||||
.background(panelBackground)
|
.background(panelBackground)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
|
||||||
.overlay {
|
.overlay {
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
if showPanelBorder {
|
||||||
.stroke(Color.white.opacity(0.35), lineWidth: 1.4)
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
|
.stroke(Color.white.opacity(0.35), lineWidth: 1.4)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 10)
|
.shadow(color: Color.black.opacity(0.3), radius: 20, x: 0, y: 10)
|
||||||
.contentShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
.contentShape(RoundedRectangle(cornerRadius: panelCornerRadius, style: .continuous))
|
||||||
}
|
}
|
||||||
|
|
||||||
private var collapsedLayout: some View {
|
private var collapsedLayout: some View {
|
||||||
|
|
@ -96,9 +108,9 @@ struct LockScreenMusicPanel: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var expandedLayout: some View {
|
private var expandedLayout: some View {
|
||||||
HStack(alignment: .center, spacing: 28) {
|
HStack(alignment: .center, spacing: 32) {
|
||||||
albumArtButton(size: 220, cornerRadius: cornerRadius)
|
albumArtButton(size: 230, cornerRadius: expandedAlbumArtCornerRadius)
|
||||||
.frame(width: 220, height: 220)
|
.frame(width: 230, height: 230)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
expandedHeader
|
expandedHeader
|
||||||
|
|
@ -114,7 +126,7 @@ struct LockScreenMusicPanel: View {
|
||||||
|
|
||||||
private var collapsedHeader: some View {
|
private var collapsedHeader: some View {
|
||||||
HStack(alignment: .center, spacing: 16) {
|
HStack(alignment: .center, spacing: 16) {
|
||||||
albumArtButton(size: 60, cornerRadius: 16)
|
albumArtButton(size: 60, cornerRadius: collapsedAlbumArtCornerRadius)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 1) {
|
VStack(alignment: .leading, spacing: 1) {
|
||||||
Text(musicManager.songTitle.isEmpty ? "No Music Playing" : musicManager.songTitle)
|
Text(musicManager.songTitle.isEmpty ? "No Music Playing" : musicManager.songTitle)
|
||||||
|
|
@ -157,11 +169,29 @@ struct LockScreenMusicPanel: View {
|
||||||
|
|
||||||
private func albumArtButton(size: CGFloat, cornerRadius: CGFloat) -> some View {
|
private func albumArtButton(size: CGFloat, cornerRadius: CGFloat) -> some View {
|
||||||
Button(action: toggleExpanded) {
|
Button(action: toggleExpanded) {
|
||||||
Image(nsImage: musicManager.albumArt)
|
ZStack(alignment: .bottomTrailing) {
|
||||||
.resizable()
|
albumArtImage(size: size, cornerRadius: cornerRadius)
|
||||||
.aspectRatio(contentMode: .fill)
|
if showAppIcon, let icon = lockScreenAppIcon {
|
||||||
.frame(width: size, height: size)
|
RoundedRectangle(cornerRadius: appIconCornerRadius, style: .continuous)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
.fill(Color.black.opacity(0.45))
|
||||||
|
.frame(width: appIconSize, height: appIconSize)
|
||||||
|
.overlay {
|
||||||
|
icon
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: appIconCornerRadius, style: .continuous))
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: appIconCornerRadius, style: .continuous)
|
||||||
|
.stroke(Color.white.opacity(0.4), lineWidth: 1)
|
||||||
|
}
|
||||||
|
.shadow(color: Color.black.opacity(0.35), radius: 6, x: 0, y: 4)
|
||||||
|
.offset(x: appIconOffset, y: appIconOffset)
|
||||||
|
.transition(.scale.combined(with: .opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.background(albumArtBackground(cornerRadius: cornerRadius))
|
||||||
}
|
}
|
||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
.opacity(musicManager.isPlaying ? 1 : 0.4)
|
||||||
|
|
@ -311,22 +341,22 @@ struct LockScreenMusicPanel: View {
|
||||||
return HStack(spacing: spacing) {
|
return HStack(spacing: spacing) {
|
||||||
// Always show shuffle on lock screen
|
// Always show shuffle on lock screen
|
||||||
controlButton(icon: "shuffle", isActive: musicManager.isShuffled) {
|
controlButton(icon: "shuffle", isActive: musicManager.isShuffled) {
|
||||||
Task { await musicManager.toggleShuffle() }
|
musicManager.toggleShuffle()
|
||||||
}
|
}
|
||||||
|
|
||||||
controlButton(icon: "backward.fill", size: 18) {
|
controlButton(icon: "backward.fill", size: 18) {
|
||||||
Task { await musicManager.previousTrack() }
|
musicManager.previousTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
playPauseButton
|
playPauseButton
|
||||||
|
|
||||||
controlButton(icon: "forward.fill", size: 18) {
|
controlButton(icon: "forward.fill", size: 18) {
|
||||||
Task { await musicManager.nextTrack() }
|
musicManager.nextTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always show repeat on lock screen
|
// Always show repeat on lock screen
|
||||||
controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) {
|
controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) {
|
||||||
Task { await musicManager.toggleRepeat() }
|
musicManager.toggleRepeat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: alignment)
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
|
|
@ -334,14 +364,15 @@ struct LockScreenMusicPanel: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var playPauseButton: some View {
|
private var playPauseButton: some View {
|
||||||
let frameSize: CGFloat = isExpanded ? 60 : 48
|
let frameSize: CGFloat = isExpanded ? 72 : 48
|
||||||
|
let symbolSize: CGFloat = isExpanded ? 30 : 24
|
||||||
|
|
||||||
return Button(action: {
|
return Button(action: {
|
||||||
registerInteraction()
|
registerInteraction()
|
||||||
Task { await musicManager.togglePlay() }
|
musicManager.togglePlay()
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill")
|
||||||
.font(.system(size: 24, weight: .semibold))
|
.font(.system(size: symbolSize, weight: .semibold))
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.frame(width: frameSize, height: frameSize)
|
.frame(width: frameSize, height: frameSize)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
|
|
@ -350,8 +381,8 @@ struct LockScreenMusicPanel: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View {
|
private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View {
|
||||||
let frameSize: CGFloat = isExpanded ? 44 : 32
|
let frameSize: CGFloat = isExpanded ? 56 : 32
|
||||||
let iconSize: CGFloat = isExpanded ? max(size, 20) : size
|
let iconSize: CGFloat = isExpanded ? max(size, 24) : size
|
||||||
|
|
||||||
return Button(action: {
|
return Button(action: {
|
||||||
registerInteraction()
|
registerInteraction()
|
||||||
|
|
@ -376,31 +407,88 @@ struct LockScreenMusicPanel: View {
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var panelBackground: some View {
|
private var panelBackground: some View {
|
||||||
if usesLiquidGlass {
|
if enableBlur {
|
||||||
liquidPanelBackground
|
if usesLiquidGlass {
|
||||||
|
liquidPanelBackground
|
||||||
|
} else {
|
||||||
|
frostedPanelBackground
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
frostedPanelBackground
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
|
.fill(Color.black.opacity(0.45))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var liquidPanelBackground: some View {
|
private var liquidPanelBackground: some View {
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
.glassEffect(
|
.glassEffect(
|
||||||
.regular
|
.regular
|
||||||
.tint(Color.white.opacity(0.12))
|
.tint(Color.white.opacity(0.12))
|
||||||
.interactive(),
|
.interactive(),
|
||||||
in: .rect(cornerRadius: cornerRadius)
|
in: .rect(cornerRadius: panelCornerRadius)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var frostedPanelBackground: some View {
|
private var frostedPanelBackground: some View {
|
||||||
RoundedRectangle(cornerRadius: cornerRadius)
|
RoundedRectangle(cornerRadius: panelCornerRadius)
|
||||||
.fill(.ultraThinMaterial)
|
.fill(.ultraThinMaterial)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func albumArtImage(size: CGFloat, cornerRadius: CGFloat) -> some View {
|
||||||
|
Image(nsImage: musicManager.albumArt)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private func albumArtBackground(cornerRadius: CGFloat) -> some View {
|
||||||
|
if enableBlur {
|
||||||
|
if usesLiquidGlass {
|
||||||
|
if #available(macOS 26.0, *) {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.glassEffect(
|
||||||
|
.regular
|
||||||
|
.tint(Color.white.opacity(0.16))
|
||||||
|
.interactive(),
|
||||||
|
in: .rect(cornerRadius: cornerRadius)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.fill(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.fill(.ultraThinMaterial)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius)
|
||||||
|
.fill(Color.black.opacity(0.35))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var lockScreenAppIcon: Image? {
|
||||||
|
guard showAppIcon, !musicManager.usingAppIconForArtwork else { return nil }
|
||||||
|
let bundleIdentifier = musicManager.bundleIdentifier ?? "com.apple.Music"
|
||||||
|
return AppIcon(for: bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appIconSize: CGFloat {
|
||||||
|
isExpanded ? 54 : 32
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appIconCornerRadius: CGFloat {
|
||||||
|
isExpanded ? 18 : 10
|
||||||
|
}
|
||||||
|
|
||||||
|
private var appIconOffset: CGFloat {
|
||||||
|
isExpanded ? 16 : 10
|
||||||
|
}
|
||||||
|
|
||||||
private func logPanelAppearance(event: String = "✅ View appeared") {
|
private func logPanelAppearance(event: String = "✅ View appeared") {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "HH:mm:ss.SSS"
|
formatter.dateFormat = "HH:mm:ss.SSS"
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,9 @@ struct GeneralSettings: View {
|
||||||
@Default(.showRecordingIndicator) var showRecordingIndicator
|
@Default(.showRecordingIndicator) var showRecordingIndicator
|
||||||
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
@Default(.enableMinimalisticUI) var enableMinimalisticUI
|
||||||
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
@Default(.lockScreenGlassStyle) var lockScreenGlassStyle
|
||||||
|
@Default(.lockScreenShowAppIcon) var lockScreenShowAppIcon
|
||||||
|
@Default(.lockScreenPanelShowsBorder) var lockScreenPanelShowsBorder
|
||||||
|
@Default(.lockScreenPanelUsesBlur) var lockScreenPanelUsesBlur
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
|
|
@ -316,6 +319,9 @@ struct GeneralSettings: View {
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
Defaults.Toggle("Show media app icon", key: .lockScreenShowAppIcon)
|
||||||
|
Defaults.Toggle("Show panel border", key: .lockScreenPanelShowsBorder)
|
||||||
|
Defaults.Toggle("Enable blur", key: .lockScreenPanelUsesBlur)
|
||||||
|
|
||||||
Button("Copy Latest Crash Report") {
|
Button("Copy Latest Crash Report") {
|
||||||
copyLatestCrashReport()
|
copyLatestCrashReport()
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,9 @@ extension Defaults.Keys {
|
||||||
// Enable lock screen media widget (shows the standalone panel when screen is locked)
|
// Enable lock screen media widget (shows the standalone panel when screen is locked)
|
||||||
static let enableLockScreenMediaWidget = Key<Bool>("enableLockScreenMediaWidget", default: true)
|
static let enableLockScreenMediaWidget = Key<Bool>("enableLockScreenMediaWidget", default: true)
|
||||||
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
static let lockScreenGlassStyle = Key<LockScreenGlassStyle>("lockScreenGlassStyle", default: .liquid)
|
||||||
|
static let lockScreenShowAppIcon = Key<Bool>("lockScreenShowAppIcon", default: false)
|
||||||
|
static let lockScreenPanelShowsBorder = Key<Bool>("lockScreenPanelShowsBorder", default: true)
|
||||||
|
static let lockScreenPanelUsesBlur = Key<Bool>("lockScreenPanelUsesBlur", default: true)
|
||||||
|
|
||||||
// MARK: Battery
|
// MARK: Battery
|
||||||
static let showPowerStatusNotifications = Key<Bool>("showPowerStatusNotifications", default: true)
|
static let showPowerStatusNotifications = Key<Bool>("showPowerStatusNotifications", default: true)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue