Skip to content

Phase 8: lock-screen lyrics (state L3)#8

Open
tirthfx wants to merge 1 commit into
phase-7-lockscreen-expandfrom
phase-8-lockscreen-lyrics
Open

Phase 8: lock-screen lyrics (state L3)#8
tirthfx wants to merge 1 commit into
phase-7-lockscreen-expandfrom
phase-8-lockscreen-lyrics

Conversation

@tirthfx

@tirthfx tirthfx commented Jul 10, 2026

Copy link
Copy Markdown
Owner

What changed

Phase 8 — the final piece of the lock-screen surface (PRD §3.2 state L3, reference image 3). The lock screen now matches your reference flow end to end.

  • From the expanded state, a two-finger right-to-left swipe reveals synced lyrics: the album art + control pill shift to the left column (the art carries the same hero morph, sliding left and shrinking) while the lyrics fill the right, current line bold/bright, neighbors dimmed, auto-scrolling with playback.
  • Left-to-right swipe or click-away / Esc returns to the expanded view.
  • LockScreenLyricsView reuses LyricsService and the notch's SyncedLyricsView / PlainLyricsView (now internal + font-size parameterized) at larger lock-screen sizes. Loading / plain-text / "No lyrics found" states all handled.
  • Extracted a shared LockScreenControlPill used by both L2 and L3.
  • Swipe detection via a LockScreenHostingView — same trackpad-scroll logic and SwipeDirection as the notch; vertical scrolling passes through so the lyrics list still scrolls.

Why

Brings Surface B to full parity with the notch: idle widget (L1) → expand (L2) → lyrics (L3), reusing the shared engine throughout.

Base

Top of the lock-screen stack: 5 → 6 → 7 → 8, based on phase-7-lockscreen-expand (open PR #7). Diff is Phase 8 only. Merge the stack in order, or rebase onto main.

Notes for review — needs your eyes in Xcode

  • Visuals unverifiable headlessly. Confirmed: builds clean, launches without crashing, notch unaffected. Please run it, play a track with known lyrics, ⌃⌘L → click art to expand → two-finger swipe right-to-left: expect art+controls to slide left and synced lyrics to appear on the right, scrolling in time; swipe back or Esc returns.
  • Swipe direction assumes natural scrolling, same as the notch. If it feels inverted, it's the same one-line flip in LockScreenHostingView.scrollWheel.
  • Refactor touches one notch file (LyricsPanelView.swift) only to make SyncedLyricsView/PlainLyricsView reusable with default params — the notch's appearance is unchanged.

🤖 Generated with Claude Code

Completes the lock-screen surface (PRD §3.2 L3, reference image 3). From the
expanded state, a two-finger right-to-left swipe reveals synced lyrics: the
album art + control pill shift to the left column (art carries the hero morph,
sliding left and shrinking) while the lyrics fill the right. Left-to-right swipe
or click-away/Esc returns to the expanded view.

- LockScreenLyricsView reuses LyricsService and the notch's SyncedLyricsView /
  PlainLyricsView (now internal + font-size parameterized) at larger lock-screen
  sizes; loading / plain / 'No lyrics found' states handled.
- Extracted a shared LockScreenControlPill used by both the expanded and lyrics
  states.
- Swipe detection via a LockScreenHostingView (same trackpad-scroll logic and
  SwipeDirection as the notch); vertical scrolling passes through to the lyrics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a lock-screen lyrics view (LockScreenLyricsView) to the pseudo lock-screen interface, allowing users to view synced or plain lyrics alongside media controls. It refactors existing views to promote reuse, extracting LockScreenControlPill and parameterizing SyncedLyricsView and PlainLyricsView for different font sizes and paddings. Additionally, it implements trackpad swipe detection in a custom LockScreenHostingView to transition between the expanded and lyrics states. The review feedback suggests two key improvements: first, refactoring the trackpad swipe detection to support both natural and traditional scrolling while preventing event leakage that could cause AppKit layout crashes; second, changing the lyrics dependency in LockScreenBackdropView from @ObservedObject to a simple let constant to avoid redundant and expensive re-renders of the backdrop, clock, and wallpaper views.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +136 to +161
override func scrollWheel(with event: NSEvent) {
switch event.phase {
case .began:
swipeAccumX = 0
swipeAccumY = 0
swipeFired = false
case .changed:
swipeAccumX += event.scrollingDeltaX
swipeAccumY += event.scrollingDeltaY
if !swipeFired,
abs(swipeAccumX) > swipeThreshold,
abs(swipeAccumX) > abs(swipeAccumY) * 1.6 {
swipeFired = true
// Assumes natural scrolling: right-to-left produces a negative
// cumulative deltaX. Same convention as the notch.
onSwipe(swipeAccumX < 0 ? .left : .right)
}
case .ended, .cancelled:
swipeAccumX = 0
swipeAccumY = 0
swipeFired = false
default:
break
}
super.scrollWheel(with: event)
}

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

Support Both Natural and Traditional Scrolling & Prevent Jitter/Crashes

This refactors the trackpad swipe detection to address two key issues:

  1. Natural Scrolling Dependency: By utilizing event.isDirectionInvertedFromDevice, we can normalize the scroll deltas. This ensures that a right-to-left swipe always maps to .left and a left-to-right swipe always maps to .right, regardless of whether the user has "Natural Scrolling" enabled in macOS System Settings.
  2. Event Leakage & UI Transition Safety: Currently, calling super.scrollWheel(with: event) after triggering onSwipe can propagate the event to a view hierarchy that is transitioning or being dismantled, which can cause AppKit layout crashes. Additionally, leaking horizontal swipe events to the underlying vertical scroll view can cause jittery vertical scrolling. Consuming the event when a swipe is active and dispatching the state transition asynchronously via DispatchQueue.main.async resolves both issues.
    override func scrollWheel(with event: NSEvent) {
        var handled = false
        switch event.phase {
        case .began:
            swipeAccumX = 0
            swipeAccumY = 0
            swipeFired = false
        case .changed:
            // Normalize deltas to support both natural and traditional scrolling directions.
            let isNatural = event.isDirectionInvertedFromDevice
            let dx = isNatural ? event.scrollingDeltaX : -event.scrollingDeltaX
            let dy = isNatural ? event.scrollingDeltaY : -event.scrollingDeltaY
            
            swipeAccumX += dx
            swipeAccumY += dy
            
            if !swipeFired,
               abs(swipeAccumX) > swipeThreshold,
               abs(swipeAccumX) > abs(swipeAccumY) * 1.6 {
                swipeFired = true
                let direction: SwipeDirection = swipeAccumX < 0 ? .left : .right
                DispatchQueue.main.async { [weak self] in
                    self?.onSwipe(direction)
                }
            }
            if swipeFired {
                handled = true
            }
        case .ended, .cancelled:
            if swipeFired {
                handled = true
            }
            swipeAccumX = 0
            swipeAccumY = 0
            swipeFired = false
        default:
            break
        }
        
        if !handled {
            super.scrollWheel(with: event)
        }
    }

/// (L2, image 2). The wallpaper dims toward black as it expands.
struct LockScreenBackdropView: View {
@ObservedObject var metadata: MediaMetadataService
@ObservedObject var lyrics: LyricsService

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

Optimize Performance by Avoiding Redundant Observations

LockScreenBackdropView does not read any properties of lyrics directly; it only passes it down to LockScreenLyricsView (which already observes it via @ObservedObject). Marking lyrics as @ObservedObject here causes the entire backdrop view—including the heavy WallpaperView and the ClockView—to re-evaluate and re-render whenever the lyrics loading state updates. Changing this to a simple let dependency avoids these redundant renders.

Suggested change
@ObservedObject var lyrics: LyricsService
let lyrics: LyricsService

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant