From c6878258a9e6e3552481089eab5e480d1099c2fd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 06:07:35 +0000 Subject: [PATCH] Glide the progress indicator through blends instead of snapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At deck promotion the displayed progress jumped from the outgoing track's fraction to the incoming track's — jarring on skips and natural transitions alike. New Player.displayProgress morphs between the two fractions weighted by the blend progress, so by the moment the decks swap the shown value already equals the incoming fraction; the Now Playing ring and mini-player line both use it. --- App/Continuity/Views/MiniPlayerView.swift | 3 ++- App/Continuity/Views/NowPlayingView.swift | 3 ++- .../Sources/Playback/Player.swift | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/App/Continuity/Views/MiniPlayerView.swift b/App/Continuity/Views/MiniPlayerView.swift index 630d2f8..0dd31cd 100644 --- a/App/Continuity/Views/MiniPlayerView.swift +++ b/App/Continuity/Views/MiniPlayerView.swift @@ -52,7 +52,8 @@ private struct MiniProgressLine: View { @Environment(Player.self) private var player var body: some View { - let progress = player.duration > 0 ? min(max(player.position / player.duration, 0), 1) : 0 + // displayProgress morphs across blends, so the line glides into the next song. + let progress = player.displayProgress GeometryReader { geo in Capsule() .fill(Color.accentColor) diff --git a/App/Continuity/Views/NowPlayingView.swift b/App/Continuity/Views/NowPlayingView.swift index 6a348cc..9a900a0 100644 --- a/App/Continuity/Views/NowPlayingView.swift +++ b/App/Continuity/Views/NowPlayingView.swift @@ -427,7 +427,8 @@ private struct TrackProgressRing: View { @Environment(Player.self) private var player var body: some View { - let progress = player.duration > 0 ? min(max(player.position / player.duration, 0), 1) : 0 + // displayProgress morphs across blends, so the ring glides into the next song. + let progress = player.displayProgress Circle() .trim(from: 0, to: progress) .stroke(.white, style: StrokeStyle(lineWidth: 3, lineCap: .round)) diff --git a/Packages/ContinuityKit/Sources/Playback/Player.swift b/Packages/ContinuityKit/Sources/Playback/Player.swift index 8e2fe55..6bc4c7d 100644 --- a/Packages/ContinuityKit/Sources/Playback/Player.swift +++ b/Packages/ContinuityKit/Sources/Playback/Player.swift @@ -79,6 +79,24 @@ public final class Player { return currentTrack?.durationSeconds ?? 0 } + /// Progress fraction (0…1) for UI rings/bars. During a blend it MORPHS from the outgoing + /// track's fraction toward the incoming track's, weighted by the blend progress — by the + /// moment the decks swap, the displayed value already equals the incoming fraction, so the + /// indicator glides into the next song instead of snapping to it. + public var displayProgress: Double { + let current = duration > 0 ? min(max(position / duration, 0), 1) : 0 + guard isTransitioning, let audio, + queue.indices.contains(transitionTargetIndex) else { return current } + let incoming = queue[transitionTargetIndex] + let incomingDuration = audio.idle.loadedDuration > 0 + ? audio.idle.loadedDuration : incoming.durationSeconds + guard incomingDuration > 0 else { return current } + let incomingPosition = incomingStartOffset + audio.idle.elapsed + let incomingFraction = min(max(incomingPosition / incomingDuration, 0), 1) + let weight = min(max(transitionProgress, 0), 1) + return current + (incomingFraction - current) * weight + } + // MARK: Engine / decks /// All CoreAudio state, or nil until the first real audio need (see `ensureAudioStack()`). var audio: AudioStack?