Files
MobileMusicAssistant/ViewsPlayerNowPlayingView.swift
T
2026-04-05 19:44:05 +02:00

218 lines
8.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// PlayerNowPlayingView.swift
// Mobile Music Assistant
//
// Created by Sven Hanold on 05.04.26.
//
import SwiftUI
struct PlayerNowPlayingView: View {
@Environment(MAService.self) private var service
@Environment(\.dismiss) private var dismiss
let playerId: String
// Auto-tracks live updates via @Observable
private var player: MAPlayer? {
service.playerManager.players[playerId]
}
private var mediaItem: MAMediaItem? {
player?.currentItem?.mediaItem
}
var body: some View {
ZStack {
// Blurred artwork background
CachedAsyncImage(url: service.imageProxyURL(
path: mediaItem?.imageUrl,
provider: mediaItem?.imageProvider,
size: 64
)) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
Color.clear
}
.ignoresSafeArea()
.blur(radius: 80)
.scaleEffect(1.4)
.opacity(0.5)
Rectangle()
.fill(.ultraThinMaterial)
.ignoresSafeArea()
// Content
VStack(spacing: 0) {
// Drag indicator
Capsule()
.fill(.secondary.opacity(0.4))
.frame(width: 36, height: 4)
.padding(.top, 12)
.padding(.bottom, 8)
// Player name
HStack {
Button { dismiss() } label: {
Image(systemName: "chevron.down")
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(.primary)
.frame(width: 44, height: 44)
.contentShape(Rectangle())
}
Spacer()
VStack(spacing: 2) {
Text("Now Playing")
.font(.caption)
.foregroundStyle(.secondary)
Text(player?.name ?? "")
.font(.subheadline)
.fontWeight(.semibold)
.lineLimit(1)
}
Spacer()
// Balance chevron button
Color.clear
.frame(width: 44, height: 44)
}
.padding(.horizontal, 20)
.padding(.bottom, 8)
// Album art
GeometryReader { geo in
let size = min(geo.size.width - 64, geo.size.height)
CachedAsyncImage(url: service.imageProxyURL(
path: mediaItem?.imageUrl,
provider: mediaItem?.imageProvider,
size: 512
)) { image in
image
.resizable()
.aspectRatio(1, contentMode: .fill)
} placeholder: {
RoundedRectangle(cornerRadius: 16)
.fill(Color.gray.opacity(0.2))
.overlay {
Image(systemName: "music.note")
.font(.system(size: 56))
.foregroundStyle(.secondary)
}
}
.frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(color: .black.opacity(0.35), radius: 24, y: 12)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(.horizontal, 32)
.padding(.vertical, 24)
// Track info
VStack(spacing: 6) {
Text(player?.currentItem?.name ?? "")
.font(.title2)
.fontWeight(.bold)
.lineLimit(2)
.multilineTextAlignment(.center)
if let artists = mediaItem?.artists, !artists.isEmpty {
Text(artists.map { $0.name }.joined(separator: ", "))
.font(.body)
.foregroundStyle(.secondary)
.lineLimit(1)
} else if let album = mediaItem?.album {
Text(album.name)
.font(.body)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
.padding(.horizontal, 32)
Spacer(minLength: 24)
// Transport controls
if let player {
HStack(spacing: 48) {
Button {
Task { try? await service.playerManager.previousTrack(playerId: playerId) }
} label: {
Image(systemName: "backward.fill")
.font(.system(size: 30))
.foregroundStyle(.primary)
}
Button {
Task {
if player.state == .playing {
try? await service.playerManager.pause(playerId: playerId)
} else {
try? await service.playerManager.play(playerId: playerId)
}
}
} label: {
Image(systemName: player.state == .playing ? "pause.circle.fill" : "play.circle.fill")
.font(.system(size: 72))
.foregroundStyle(.primary)
.symbolEffect(.bounce, value: player.state == .playing)
}
Button {
Task { try? await service.playerManager.nextTrack(playerId: playerId) }
} label: {
Image(systemName: "forward.fill")
.font(.system(size: 30))
.foregroundStyle(.primary)
}
}
.buttonStyle(.plain)
}
Spacer(minLength: 24)
// Volume
if let volume = player?.volume {
HStack(spacing: 10) {
Image(systemName: "speaker.fill")
.font(.caption)
.foregroundStyle(.secondary)
.frame(width: 20)
Slider(
value: Binding(
get: { Double(volume) },
set: { newValue in
Task {
try? await service.playerManager.setVolume(
playerId: playerId,
level: Int(newValue)
)
}
}
),
in: 0...100,
step: 1
)
Image(systemName: "speaker.wave.3.fill")
.font(.caption)
.foregroundStyle(.secondary)
.frame(width: 20)
}
.padding(.horizontal, 32)
}
Spacer(minLength: 32)
}
}
.presentationDetents([.large])
.presentationDragIndicator(.hidden) // using custom indicator
}
}