NavidromeApp/iOS/Views/NowPlaying/SiriSeekBar.swift
Dallas Groot d8041c0019 NavidromePlayer: iOS + watchOS Navidrome/Subsonic music player
Features:
- Dual-AVPlayer Smart DJ crossfade with LUFS normalization
- Mitsuha-style FFT visualizer (real-time + offline pre-computed)
- Companion API integration (Smart DJ, tag editing, vis frames)
- Offline-first SyncEngine with delta sync and album detail pre-caching
- Audio pre-fetcher for gapless queue playback
- Optimistic action queue (star/unstar with background retry)
- ShazamKit recognition with MusicKit preview playback
- Radio streaming with HLS/PLS/M3U support and buffer seek
- Watch app with Crown Sequencer and Ultra speaker support
- Batch metadata editing with album_artist fix for split albums
- Cache-first UI pattern across all views
- NWPathMonitor offline detection with reactive song greying
2026-03-28 20:49:47 +00:00

60 lines
2.4 KiB
Swift

import SwiftUI
/// A custom seek bar with independent drag state to prevent timer/thumb fighting during scrubs.
struct SiriSeekBar: View {
@Binding var value: Double // 0.0 to 1.0, bound to playback progress
var onSeek: (Double) -> Void // Called when drag ends with final position
var onDragChanged: ((Double) -> Void)? = nil // Called continuously during drag
@State private var isDragging: Bool = false
@State private var dragValue: Double = 0.0
var body: some View {
GeometryReader { geo in
let width = geo.size.width
let currentVal = isDragging ? dragValue : value
let fillWidth = max(0, min(width, CGFloat(currentVal) * width))
ZStack(alignment: .leading) {
// 1. Background track
Capsule()
.fill(Color.white.opacity(0.2))
.frame(height: isDragging ? 8 : 4)
// 2. Fill track
Capsule()
.fill(Color.white)
.frame(width: fillWidth, height: isDragging ? 8 : 4)
// 3. Scrubber thumb (grows when dragging)
Circle()
.fill(Color.white)
.frame(width: isDragging ? 18 : 8, height: isDragging ? 18 : 8)
.offset(x: fillWidth - (isDragging ? 9 : 4))
.shadow(color: .black.opacity(0.4), radius: 3, x: 0, y: 2)
}
.frame(height: 30)
.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
let pct = max(0, min(1, Double(gesture.location.x / width)))
if !isDragging {
isDragging = true
dragValue = value
}
dragValue = pct
onDragChanged?(pct)
}
.onEnded { gesture in
let pct = max(0, min(1, Double(gesture.location.x / width)))
dragValue = pct
isDragging = false
onSeek(pct)
}
)
.animation(.easeInOut(duration: 0.15), value: isDragging)
}
.frame(height: 30)
}
}