Queue, Favorites, Providers, Now playing
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user