Refined UI

This commit is contained in:
Hariharan Mudaliar 2025-10-17 22:58:34 +05:30
commit 1eb3d3d968
3 changed files with 128 additions and 31 deletions

View file

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

View file

@ -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()

View file

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