d0a12847-494a-4347-ae88-d87.../Sources/MusicPlayerView.swift

117 lines
3.8 KiB
Swift
Raw Permalink Normal View History

//
// MusicPlayerView.swift
// MioIsland Music Plugin
//
// Compact Now Playing UI designed for the notch panel.
//
import SwiftUI
struct MusicPlayerView: View {
@ObservedObject var bridge = NowPlayingBridge.shared
var body: some View {
if bridge.info.title.isEmpty {
emptyState
} else {
nowPlayingView
}
}
// MARK: - Now Playing
private var nowPlayingView: some View {
HStack(spacing: 12) {
// Album art
if let artwork = bridge.info.artwork {
Image(nsImage: artwork)
.resizable()
.scaledToFill()
.frame(width: 48, height: 48)
.clipShape(RoundedRectangle(cornerRadius: 8))
} else {
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.1))
.frame(width: 48, height: 48)
.overlay(
Image(systemName: "music.note")
.font(.system(size: 18))
.foregroundColor(.white.opacity(0.3))
)
}
// Info + controls
VStack(alignment: .leading, spacing: 4) {
// Title
Text(bridge.info.title)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.white.opacity(0.95))
.lineLimit(1)
// Artist
Text(bridge.info.artist)
.font(.system(size: 10))
.foregroundColor(.white.opacity(0.5))
.lineLimit(1)
// Progress bar
if bridge.info.duration > 0 {
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 1.5)
.fill(Color.white.opacity(0.1))
.frame(height: 3)
RoundedRectangle(cornerRadius: 1.5)
.fill(Color.white.opacity(0.6))
.frame(width: geo.size.width * progress, height: 3)
}
}
.frame(height: 3)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
// Playback controls
HStack(spacing: 14) {
controlButton("backward.fill") { bridge.previousTrack() }
controlButton(bridge.info.isPlaying ? "pause.fill" : "play.fill") { bridge.togglePlayPause() }
.font(.system(size: 14))
controlButton("forward.fill") { bridge.nextTrack() }
}
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
}
// MARK: - Empty State
private var emptyState: some View {
HStack(spacing: 8) {
Image(systemName: "music.note.list")
.font(.system(size: 16))
.foregroundColor(.white.opacity(0.25))
Text("Nothing playing")
.font(.system(size: 12))
.foregroundColor(.white.opacity(0.3))
}
.frame(maxWidth: .infinity)
.padding(.vertical, 16)
}
// MARK: - Helpers
private var progress: CGFloat {
guard bridge.info.duration > 0 else { return 0 }
return CGFloat(bridge.info.elapsedTime / bridge.info.duration)
}
private func controlButton(_ symbol: String, action: @escaping () -> Void) -> some View {
Button(action: action) {
Image(systemName: symbol)
.font(.system(size: 11))
.foregroundColor(.white.opacity(0.7))
}
.buttonStyle(.plain)
}
}