Queue, Favorites, Providers, Now playing

This commit is contained in:
2026-04-06 11:46:04 +02:00
parent e7e9a59e70
commit 56199db301
12 changed files with 462 additions and 58 deletions
+117 -3
View File
@@ -17,20 +17,30 @@ struct PlayerNowPlayingView: View {
@State private var isMuted = false
@State private var preMuteVolume: Double = 50
@State private var showQueue = false
@State private var displayedElapsed: Double = 0
@State private var progressTimer: Timer?
// Auto-tracks live updates via @Observable
private var player: MAPlayer? {
service.playerManager.players[playerId]
}
private var playerQueue: MAPlayerQueue? {
service.playerManager.playerQueues[playerId]
}
private var currentItem: MAQueueItem? {
service.playerManager.playerQueues[playerId]?.currentItem
playerQueue?.currentItem
}
private var mediaItem: MAMediaItem? {
currentItem?.mediaItem
}
private var trackDuration: Double {
Double(currentItem?.duration ?? 0)
}
var body: some View {
VStack(spacing: 0) {
// Header
@@ -47,6 +57,11 @@ struct PlayerNowPlayingView: View {
Spacer(minLength: 0)
// Progress bar (only in player mode, not queue)
if !showQueue {
progressView
}
// Transport + volume (always visible)
controlsView
}
@@ -80,6 +95,25 @@ struct PlayerNowPlayingView: View {
}
.onAppear {
localVolume = Double(player?.volume ?? 50)
syncElapsedTime()
startProgressTimer()
}
.onDisappear {
progressTimer?.invalidate()
progressTimer = nil
}
.onChange(of: playerQueue?.elapsedTime) {
syncElapsedTime()
}
.onChange(of: player?.state) {
// Restart timer when play state changes
syncElapsedTime()
if player?.state == .playing {
startProgressTimer()
} else {
progressTimer?.invalidate()
progressTimer = nil
}
}
.presentationDetents([.large])
.presentationDragIndicator(.hidden)
@@ -129,12 +163,12 @@ struct PlayerNowPlayingView: View {
Image(systemName: "list.bullet")
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(showQueue ? .accent : .primary)
.foregroundStyle(showQueue ? Color.accentColor : .primary)
.frame(width: 44, height: 44)
.contentShape(Rectangle())
}
if let uri = mediaItem?.uri {
if !showQueue, let uri = mediaItem?.uri {
FavoriteButton(uri: uri, size: 22)
.frame(width: 44, height: 44)
}
@@ -281,6 +315,86 @@ struct PlayerNowPlayingView: View {
}
}
// MARK: - Progress View
@ViewBuilder
private var progressView: some View {
let duration = trackDuration
let progress = duration > 0 ? min(displayedElapsed / duration, 1.0) : 0
VStack(spacing: 4) {
// Progress bar
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule()
.fill(.primary.opacity(0.15))
.frame(height: 4)
Capsule()
.fill(.primary)
.frame(width: geo.size.width * progress, height: 4)
}
}
.frame(height: 4)
// Time labels
HStack {
Text(formatTime(displayedElapsed))
.font(.caption2)
.foregroundStyle(.secondary)
.monospacedDigit()
Spacer()
Text("-\(formatTime(max(0, duration - displayedElapsed)))")
.font(.caption2)
.foregroundStyle(.secondary)
.monospacedDigit()
}
}
.padding(.horizontal, 32)
.padding(.bottom, 4)
}
// MARK: - Progress Helpers
private func syncElapsedTime() {
guard let queue = playerQueue,
let elapsed = queue.elapsedTime else {
displayedElapsed = 0
return
}
if player?.state == .playing,
let lastUpdated = queue.elapsedTimeLastUpdated {
let serverNow = Date().timeIntervalSince1970
let delta = serverNow - lastUpdated
displayedElapsed = elapsed + max(0, delta)
} else {
displayedElapsed = elapsed
}
}
private func startProgressTimer() {
progressTimer?.invalidate()
guard player?.state == .playing else { return }
progressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in
Task { @MainActor in
guard player?.state == .playing else { return }
displayedElapsed += 0.5
// Clamp to duration
if trackDuration > 0 {
displayedElapsed = min(displayedElapsed, trackDuration)
}
}
}
}
private func formatTime(_ seconds: Double) -> String {
let totalSeconds = Int(max(0, seconds))
let m = totalSeconds / 60
let s = totalSeconds % 60
return String(format: "%d:%02d", m, s)
}
// MARK: - Volume Helpers
private func adjustVolume(by delta: Int) {