Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
53a512c
Polish: album-art accent, liquid-glass clock, blurred lock-screen bac…
tirthfx Jul 10, 2026
8ca1e43
Polish: reference lock-screen look + custom wallpaper picker
tirthfx Jul 11, 2026
762df5f
Polish: clear Liquid Glass player card + softer glass clock
tirthfx Jul 11, 2026
0c6198c
Polish: expand zoom animation, blur adjuster, SF Pro clock, matched pill
tirthfx Jul 11, 2026
2f633d6
Revert "Polish: expand zoom animation, blur adjuster, SF Pro clock, m…
tirthfx Jul 11, 2026
2c4fb6b
Reapply "Polish: expand zoom animation, blur adjuster, SF Pro clock, …
tirthfx Jul 11, 2026
fbc93a8
Polish: match L2 reference — bigger cover, compact mini pill, neutral…
tirthfx Jul 11, 2026
2cf9ef1
Polish: clear-glass mini pill, 480pt cover, bare hour:minute clock
tirthfx Jul 11, 2026
601af0f
Polish: reference clock glyphs, squarer cover edges, working grow zoom
tirthfx Jul 11, 2026
90cf461
Polish: bigger compact clock (78 -> 96) to match reference proportions
tirthfx Jul 11, 2026
ba733c1
Phase 5 visual spec: persistent morphing cluster + LockScreenLayout c…
tirthfx Jul 11, 2026
0415ef2
Lock-screen fix: idle = Large Clock + corner chip, tap grows into card
tirthfx Jul 11, 2026
f355a78
Round 2: measured clock scale-ups, centered idle mini-player, tighter…
tirthfx Jul 11, 2026
e54d5d4
Round 3: bigger idle clock, minimal left-aligned idle chip, taller pill
tirthfx Jul 11, 2026
b02d699
ui: add regular LiquidGlassStyle and support macOS 26 glassEffect
tirthfx Jul 11, 2026
ce3a247
layout: support dynamic screen-relative idle chip sizing and adjust l…
tirthfx Jul 11, 2026
7e3270a
Round 4: absolute specs, real Liquid Glass pill, clock snaps not tweens
tirthfx Jul 11, 2026
ce2ce0d
Revert round-4 lock-screen changes
tirthfx Jul 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions NotchBeat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
1B21A56838FE8FD8C02B0907 /* PKCE.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDE8CB4F434738E3DA92CE4 /* PKCE.swift */; };
2D36EB8D1A9988C8C2B7B781 /* MediaMetadataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F19FAF1275C541AB173F1F7 /* MediaMetadataService.swift */; };
316A63FA9FCD92E88E0BD9A0 /* LyricsPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81D658A3FC6D7C63D43E2149 /* LyricsPanelView.swift */; };
437E1ACCA99EA5DC1F2AC1C6 /* LockScreenLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34E04AFF0EF337B09488D2F /* LockScreenLayout.swift */; };
57EAB3E0EA2FA7D3A9EFF38A /* NotchRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A862BCC79D94A263060B84F3 /* NotchRootView.swift */; };
5BB1E43ADDFAFED421D13ACF /* MenuBarContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE226CBE5A055D67B715329 /* MenuBarContentView.swift */; };
72522930F2F1674F82C61598 /* VolumeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EBE1A010C9C568DF7EF30B9 /* VolumeController.swift */; };
Expand All @@ -27,18 +28,21 @@
B543D6063513F9830EC5CE7A /* SpotifyAuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A3D07D6AF6BC0759ABB9C7 /* SpotifyAuthManager.swift */; };
B68F48D70CDCD0AA188976C7 /* CompactNotchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC64A71BE0F2587FD91FC2F8 /* CompactNotchView.swift */; };
BD741B1B99AB90F0372BE471 /* LyricsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E371951F75508989A077CFC5 /* LyricsService.swift */; };
C177B8C0317B675C7F35D539 /* LockScreenWallpaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16113F68F8AABB7034250A19 /* LockScreenWallpaper.swift */; };
C1A583D31E0BE32E48DD776A /* LockScreenUIState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474238E0460DBDA760586035 /* LockScreenUIState.swift */; };
C7236C71AD7D95EED5695CFD /* NotchLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87D141A7BB7819335674E9B /* NotchLayout.swift */; };
CF607D3364BA55B9E3E6AD90 /* NotchBeatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21253425E6FA10293E2FFD4A /* NotchBeatApp.swift */; };
DFA3175B057F2A2348AB9A1F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C75977B9D3E98AEEFDDE820 /* AppDelegate.swift */; };
E1403BD41DF3C6FEEE6AE730 /* AlbumArtColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A6B47FE1219D7765B6B11F6 /* AlbumArtColor.swift */; };
E7227127738431B9CA49DA60 /* NotchWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C99A7818B9692BE3A3237E75 /* NotchWindowController.swift */; };
ECBB61F19F9B55A9BB57631E /* LockScreenControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30FA4A6CC5E1F4A8DBCB4DAA /* LockScreenControls.swift */; };
EFA41F7AEDAB74B765C30B56 /* LockScreenExpandedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A614F3F6690B30F4FB4E602A /* LockScreenExpandedView.swift */; };
F1494A1F40A2D6FFCBC28F17 /* LockScreenNowPlayingWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8741427FCCF71EA5F049344D /* LockScreenNowPlayingWidget.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
0C0A326FFF2D90FCF3CA1C1C /* ExpandedPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandedPlayerView.swift; sourceTree = "<group>"; };
16113F68F8AABB7034250A19 /* LockScreenWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenWallpaper.swift; sourceTree = "<group>"; };
1A6B47FE1219D7765B6B11F6 /* AlbumArtColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumArtColor.swift; sourceTree = "<group>"; };
21253425E6FA10293E2FFD4A /* NotchBeatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchBeatApp.swift; sourceTree = "<group>"; };
30FA4A6CC5E1F4A8DBCB4DAA /* LockScreenControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenControls.swift; sourceTree = "<group>"; };
474238E0460DBDA760586035 /* LockScreenUIState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenUIState.swift; sourceTree = "<group>"; };
Expand All @@ -57,7 +61,6 @@
8741427FCCF71EA5F049344D /* LockScreenNowPlayingWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenNowPlayingWidget.swift; sourceTree = "<group>"; };
898EE9859801623F36E78015 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
9404B9EDA37A59B491EF92E1 /* LockScreenBackdropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenBackdropView.swift; sourceTree = "<group>"; };
A614F3F6690B30F4FB4E602A /* LockScreenExpandedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenExpandedView.swift; sourceTree = "<group>"; };
A862BCC79D94A263060B84F3 /* NotchRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchRootView.swift; sourceTree = "<group>"; };
C001B28DA5319FDF1493DCE6 /* LiquidGlassUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiquidGlassUI.swift; sourceTree = "<group>"; };
C6DDE1A937A6DE6D87F8066F /* LockScreenLyricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenLyricsView.swift; sourceTree = "<group>"; };
Expand All @@ -66,6 +69,7 @@
D463E53A2075B4EFF62193C8 /* GestureEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GestureEngine.swift; sourceTree = "<group>"; };
D87D141A7BB7819335674E9B /* NotchLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchLayout.swift; sourceTree = "<group>"; };
E114462CC31BFC8BFDB6571D /* GlobalHotKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalHotKey.swift; sourceTree = "<group>"; };
E34E04AFF0EF337B09488D2F /* LockScreenLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenLayout.swift; sourceTree = "<group>"; };
E371951F75508989A077CFC5 /* LyricsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LyricsService.swift; sourceTree = "<group>"; };
EC64A71BE0F2587FD91FC2F8 /* CompactNotchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactNotchView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand All @@ -77,10 +81,11 @@
E114462CC31BFC8BFDB6571D /* GlobalHotKey.swift */,
9404B9EDA37A59B491EF92E1 /* LockScreenBackdropView.swift */,
30FA4A6CC5E1F4A8DBCB4DAA /* LockScreenControls.swift */,
A614F3F6690B30F4FB4E602A /* LockScreenExpandedView.swift */,
E34E04AFF0EF337B09488D2F /* LockScreenLayout.swift */,
C6DDE1A937A6DE6D87F8066F /* LockScreenLyricsView.swift */,
8741427FCCF71EA5F049344D /* LockScreenNowPlayingWidget.swift */,
474238E0460DBDA760586035 /* LockScreenUIState.swift */,
16113F68F8AABB7034250A19 /* LockScreenWallpaper.swift */,
68EB8D95060ED9D416B0AD35 /* PseudoLockScreenController.swift */,
);
path = PseudoLockScreenController;
Expand All @@ -98,6 +103,7 @@
4B98F2266DC599916DFF0B2D /* MediaMetadataService */ = {
isa = PBXGroup;
children = (
1A6B47FE1219D7765B6B11F6 /* AlbumArtColor.swift */,
7F19FAF1275C541AB173F1F7 /* MediaMetadataService.swift */,
5DDE8CB4F434738E3DA92CE4 /* PKCE.swift */,
709D63BE29CA0D22B1F331FB /* SpotifyAPIClient.swift */,
Expand Down Expand Up @@ -245,6 +251,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E1403BD41DF3C6FEEE6AE730 /* AlbumArtColor.swift in Sources */,
DFA3175B057F2A2348AB9A1F /* AppDelegate.swift in Sources */,
B68F48D70CDCD0AA188976C7 /* CompactNotchView.swift in Sources */,
7CE2042F64E912316794D314 /* ExpandedPlayerView.swift in Sources */,
Expand All @@ -253,10 +260,11 @@
852BCE6356651B307827CFA9 /* LiquidGlassUI.swift in Sources */,
B03F55117440084F1A9EEC47 /* LockScreenBackdropView.swift in Sources */,
ECBB61F19F9B55A9BB57631E /* LockScreenControls.swift in Sources */,
EFA41F7AEDAB74B765C30B56 /* LockScreenExpandedView.swift in Sources */,
437E1ACCA99EA5DC1F2AC1C6 /* LockScreenLayout.swift in Sources */,
AE3D9D73EE09457F9FC9F512 /* LockScreenLyricsView.swift in Sources */,
F1494A1F40A2D6FFCBC28F17 /* LockScreenNowPlayingWidget.swift in Sources */,
C1A583D31E0BE32E48DD776A /* LockScreenUIState.swift in Sources */,
C177B8C0317B675C7F35D539 /* LockScreenWallpaper.swift in Sources */,
316A63FA9FCD92E88E0BD9A0 /* LyricsPanelView.swift in Sources */,
BD741B1B99AB90F0372BE471 /* LyricsService.swift in Sources */,
2D36EB8D1A9988C8C2B7B781 /* MediaMetadataService.swift in Sources */,
Expand Down
13 changes: 12 additions & 1 deletion NotchBeat/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
let authManager = SpotifyAuthManager()
let gestureEngine = GestureEngine()
let lockScreenWallpaper = LockScreenWallpaper()
private(set) lazy var metadataService = MediaMetadataService(auth: authManager)
private lazy var notchWindowController = NotchWindowController(metadata: metadataService)
private lazy var lockScreenController = PseudoLockScreenController(metadata: metadataService)
private lazy var lockScreenController = PseudoLockScreenController(
metadata: metadataService, wallpaper: lockScreenWallpaper
)

func applicationDidFinishLaunching(_ notification: Notification) {
notchWindowController.show()
Expand All @@ -18,4 +21,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
func toggleLockScreen() {
lockScreenController.toggle()
}

func chooseLockScreenWallpaper() {
lockScreenWallpaper.choose()
}

func resetLockScreenWallpaper() {
lockScreenWallpaper.resetToDesktop()
}
}
11 changes: 11 additions & 0 deletions NotchBeat/App/MenuBarContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import SwiftUI
struct MenuBarContentView: View {
@ObservedObject var auth: SpotifyAuthManager
@ObservedObject var gestureEngine: GestureEngine
@ObservedObject var wallpaper: LockScreenWallpaper
let onToggleLockScreen: () -> Void
let onChooseWallpaper: () -> Void
let onResetWallpaper: () -> Void

var body: some View {
if !auth.isConfigured {
Expand All @@ -30,6 +33,14 @@ struct MenuBarContentView: View {
Button("Toggle Lock Screen (⌃⌘L)") {
onToggleLockScreen()
}
Button("Set Lock Screen Wallpaper…") {
onChooseWallpaper()
}
if wallpaper.isCustom {
Button("Use Desktop Wallpaper") {
onResetWallpaper()
}
}
// Opt-in: enabling prompts for Input Monitoring the first time, since
// the left-edge volume slide reads raw trackpad data (PRD F4).
Toggle("Left-edge Volume Gesture", isOn: Binding(
Expand Down
5 changes: 4 additions & 1 deletion NotchBeat/App/NotchBeatApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ struct NotchBeatApp: App {
MenuBarContentView(
auth: appDelegate.authManager,
gestureEngine: appDelegate.gestureEngine,
onToggleLockScreen: { appDelegate.toggleLockScreen() }
wallpaper: appDelegate.lockScreenWallpaper,
onToggleLockScreen: { appDelegate.toggleLockScreen() },
onChooseWallpaper: { appDelegate.chooseLockScreenWallpaper() },
onResetWallpaper: { appDelegate.resetLockScreenWallpaper() }
)
}
}
Expand Down
19 changes: 17 additions & 2 deletions NotchBeat/LiquidGlassUI/LiquidGlassUI.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import SwiftUI

/// Visual weight of a `LiquidGlassPanel`.
enum LiquidGlassStyle {
/// Dark-tinted glass — legible container for dense UI (notch panels).
case dark
/// Untinted, fully transparent glass — the backdrop reads straight
/// through (lock-screen now-playing card).
case clear
}

/// Thin wrapper around the glass background material (ARCHITECTURE.md §3):
/// real Liquid Glass (`.glassEffect()`) when running on macOS 26+, an
/// `.ultraThinMaterial` + specular-gradient approximation otherwise. Keeping
/// the choice here means swapping implementations never ripples into the UI.
struct LiquidGlassPanel<PanelShape: InsettableShape, Content: View>: View {
let shape: PanelShape
var style: LiquidGlassStyle = .dark
@ViewBuilder let content: () -> Content

var body: some View {
if #available(macOS 26.0, *) {
content()
.glassEffect(.regular.tint(.black.opacity(0.35)), in: shape)
.glassEffect(
style == .clear ? .clear : .regular.tint(.black.opacity(0.35)),
in: shape
)
} else {
content()
.background {
ZStack {
shape.fill(.ultraThinMaterial)
shape.fill(.black.opacity(0.3))
if style == .dark {
shape.fill(.black.opacity(0.3))
}
// Soft specular sheen standing in for real glass.
shape.fill(
LinearGradient(
Expand Down
62 changes: 62 additions & 0 deletions NotchBeat/MediaMetadataService/AlbumArtColor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import AppKit
import SwiftUI

extension NSImage {
/// A vibrant accent color sampled from the album art, used to tint the glass
/// edge glow and the current lyric line on both surfaces (creative polish,
/// PRD "dynamic album-art accent color").
///
/// Downsamples to a small bitmap, averages the reasonably saturated,
/// mid-bright pixels (so near-black/near-white backgrounds don't dominate),
/// then normalizes saturation/brightness so the result always reads as an
/// accent rather than a muddy or washed-out tone. Falls back to white.
func accentColor() -> Color {
let side = 24
guard let rep = downsampledBitmap(side: side) else { return .white }

var rSum = 0.0, gSum = 0.0, bSum = 0.0, weight = 0.0
for y in 0..<side {
for x in 0..<side {
guard let pixel = rep.colorAt(x: x, y: y)?.usingColorSpace(.sRGB) else { continue }
let s = Double(pixel.saturationComponent)
let v = Double(pixel.brightnessComponent)
// Favor colorful, mid-bright pixels; heavily discount flat
// dark/bright areas (letterboxing, white sleeves, shadows).
let w = s * ((v > 0.12 && v < 0.97) ? 1.0 : 0.08)
rSum += Double(pixel.redComponent) * w
gSum += Double(pixel.greenComponent) * w
bSum += Double(pixel.blueComponent) * w
weight += w
}
}
Comment on lines +14 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Downsampling to a 24x24 grid requires 576 iterations of colorAt(x:y:) and usingColorSpace(.sRGB) on the main thread, which is highly inefficient and can cause noticeable UI micro-stutters during track changes.

By reducing the downsample size to 12 (144 pixels) and removing the redundant .usingColorSpace(.sRGB) call (since the bitmap is already initialized in deviceRGB), we can achieve a virtually identical accent color with a fraction of the CPU overhead.

Suggested change
let side = 24
guard let rep = downsampledBitmap(side: side) else { return .white }
var rSum = 0.0, gSum = 0.0, bSum = 0.0, weight = 0.0
for y in 0..<side {
for x in 0..<side {
guard let pixel = rep.colorAt(x: x, y: y)?.usingColorSpace(.sRGB) else { continue }
let s = Double(pixel.saturationComponent)
let v = Double(pixel.brightnessComponent)
// Favor colorful, mid-bright pixels; heavily discount flat
// dark/bright areas (letterboxing, white sleeves, shadows).
let w = s * ((v > 0.12 && v < 0.97) ? 1.0 : 0.08)
rSum += Double(pixel.redComponent) * w
gSum += Double(pixel.greenComponent) * w
bSum += Double(pixel.blueComponent) * w
weight += w
}
}
let side = 12
guard let rep = downsampledBitmap(side: side) else { return .white }
var rSum = 0.0, gSum = 0.0, bSum = 0.0, weight = 0.0
for y in 0..<side {
for x in 0..<side {
guard let pixel = rep.colorAt(x: x, y: y) else { continue }
let s = Double(pixel.saturationComponent)
let v = Double(pixel.brightnessComponent)
// Favor colorful, mid-bright pixels; heavily discount flat
// dark/bright areas (letterboxing, white sleeves, shadows).
let w = s * ((v > 0.12 && v < 0.97) ? 1.0 : 0.08)
rSum += Double(pixel.redComponent) * w
gSum += Double(pixel.greenComponent) * w
bSum += Double(pixel.blueComponent) * w
weight += w
}
}

guard weight > 0.0001 else { return .white }

let base = NSColor(
srgbRed: rSum / weight, green: gSum / weight, blue: bSum / weight, alpha: 1
)
// Normalize so the tint always pops, even from desaturated art.
let boosted = NSColor(
hue: base.hueComponent,
saturation: min(max(base.saturationComponent, 0.55), 0.9),
brightness: max(base.brightnessComponent, 0.78),
alpha: 1
)
return Color(nsColor: boosted)
}

private func downsampledBitmap(side: Int) -> NSBitmapImageRep? {
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: side, pixelsHigh: side,
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
) else { return nil }
rep.size = NSSize(width: side, height: side)

NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
draw(in: NSRect(x: 0, y: 0, width: side, height: side),
from: .zero, operation: .copy, fraction: 1)
NSGraphicsContext.restoreGraphicsState()
Comment on lines +55 to +59

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In AppKit, NSGraphicsContext(bitmapImageRep:) returns an optional context. Setting NSGraphicsContext.current directly without checking if the context is non-nil can lead to silent drawing failures or unexpected behavior if context creation fails.

Suggested change
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
draw(in: NSRect(x: 0, y: 0, width: side, height: side),
from: .zero, operation: .copy, fraction: 1)
NSGraphicsContext.restoreGraphicsState()
guard let context = NSGraphicsContext(bitmapImageRep: rep) else { return nil }
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = context
draw(in: NSRect(x: 0, y: 0, width: side, height: side),
from: .zero, operation: .copy, fraction: 1)
NSGraphicsContext.restoreGraphicsState()

return rep
}
}
9 changes: 8 additions & 1 deletion NotchBeat/MediaMetadataService/MediaMetadataService.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AppKit
import Combine
import Foundation
import SwiftUI

/// Abstraction boundary for the now-playing metadata source (ARCHITECTURE.md
/// §3). v1 polls the Spotify Web API every ~2.5s; the UI only observes
Expand All @@ -19,7 +20,13 @@ final class MediaMetadataService: ObservableObject {
}

@Published private(set) var state: State = .idle
@Published private(set) var albumArt: NSImage?
@Published private(set) var albumArt: NSImage? {
// Recompute the accent tint whenever the art actually changes (the
// load path early-returns when the URL is unchanged, so this is rare).
didSet { accentColor = albumArt?.accentColor() ?? .white }
}
/// Vibrant color sampled from the current album art; .white when idle.
@Published private(set) var accentColor: Color = .white

private let auth: SpotifyAuthManager
private let client: SpotifyAPIClient
Expand Down
11 changes: 7 additions & 4 deletions NotchBeat/NotchWindowController/ExpandedPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ struct ScrubberView: View {
let nowPlaying: NowPlaying
let onSeek: (TimeInterval) -> Void
let onActivity: () -> Void
/// Condensed variant for the lock screen's compact pill: thinner bar,
/// smaller timestamps.
var compact = false

@State private var dragFraction: Double?

Expand All @@ -133,15 +136,15 @@ struct ScrubberView: View {
let fraction = dragFraction
?? (nowPlaying.duration > 0 ? progress / nowPlaying.duration : 0)

VStack(spacing: 4) {
VStack(spacing: compact ? 2 : 4) {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule().fill(.white.opacity(0.18))
Capsule()
.fill(.white.opacity(0.85))
.frame(width: max(4, geo.size.width * fraction))
}
.frame(height: 4)
.frame(height: compact ? 3 : 4)
.frame(maxHeight: .infinity, alignment: .center)
.contentShape(Rectangle())
.gesture(
Expand All @@ -157,14 +160,14 @@ struct ScrubberView: View {
}
)
}
.frame(height: 14)
.frame(height: compact ? 8 : 14)

HStack {
Text(Self.timestamp(dragFraction.map { $0 * nowPlaying.duration } ?? progress))
Spacer()
Text(Self.timestamp(nowPlaying.duration))
}
.font(.system(size: 9, weight: .medium))
.font(.system(size: compact ? 7 : 9, weight: .medium))
.monospacedDigit()
.foregroundStyle(.white.opacity(0.45))
}
Expand Down
Loading