Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion App/Continuity/Views/MiniPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion App/Continuity/Views/NowPlayingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
18 changes: 18 additions & 0 deletions Packages/ContinuityKit/Sources/Playback/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
Loading