| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- import SwiftUI
- /// Compact bottom bar showing current track with play/pause and next.
- /// Tapping opens the full Now Playing view.
- struct MiniPlayerView: View {
- @Environment(PlayerViewModel.self) private var playerVM
- @EnvironmentObject private var theme: AppTheme
- var body: some View {
- VStack(spacing: 0) {
- // Progress bar
- GeometryReader { geo in
- ZStack(alignment: .leading) {
- Rectangle()
- .fill(theme.seekbarBackground)
- Rectangle()
- .fill(theme.seekbarForeground)
- .frame(width: max(0, playerVM.progress * geo.size.width))
- }
- }
- .frame(height: 3)
- HStack(spacing: 12) {
- // Artwork or icon
- if let track = playerVM.currentTrack {
- ArtworkThumbnail(track: track)
- .frame(width: 40, height: 40)
- .overlay(alignment: .bottomTrailing) {
- if track.isCloud {
- Image(systemName: "cloud.fill")
- .font(.system(size: 10))
- .foregroundStyle(.white)
- .padding(2)
- .background(theme.accent.opacity(0.8))
- .clipShape(Circle())
- .offset(x: 2, y: 2)
- }
- }
- } else if playerVM.isCloudPlayback {
- // Cloud track without SwiftData Track (direct play from browser)
- Image(systemName: "cloud.fill")
- .font(.title2)
- .foregroundStyle(theme.accent)
- .frame(width: 40, height: 40)
- .background(theme.accent.opacity(0.1))
- .clipShape(RoundedRectangle(cornerRadius: 8))
- }
- // Track info
- VStack(alignment: .leading, spacing: 1) {
- Text(playerVM.currentTrack?.title ?? playerVM.currentCloudTrack?.title ?? "Not Playing")
- .font(.system(size: 14, weight: .medium))
- .foregroundStyle(theme.primaryText)
- .lineLimit(1)
- if let artist = playerVM.currentTrack?.artist ?? playerVM.currentCloudTrack?.artist, !artist.isEmpty {
- Text(artist)
- .font(.system(size: 12))
- .foregroundStyle(theme.secondaryText)
- .lineLimit(1)
- }
- }
- Spacer()
- // Buffering indicator
- if playerVM.isBuffering {
- ProgressView()
- .scaleEffect(0.7)
- }
- // Transport
- HStack(spacing: 16) {
- Button {
- playerVM.togglePlayPause()
- } label: {
- Image(systemName: playerVM.isPlaying ? "pause.fill" : "play.fill")
- .font(.system(size: 22))
- .foregroundStyle(theme.accent)
- .frame(width: 44, height: 44)
- .contentShape(Rectangle())
- }
- .buttonStyle(.plain)
- .accessibilityIdentifier("miniPlayerPlayPause")
- Button {
- playerVM.playNext()
- } label: {
- Image(systemName: "forward.fill")
- .font(.system(size: 18))
- .foregroundStyle(theme.secondaryText)
- .frame(width: 44, height: 44)
- .contentShape(Rectangle())
- }
- .buttonStyle(.plain)
- .accessibilityIdentifier("miniPlayerNext")
- Button {
- playerVM.showQueue = true
- } label: {
- Image(systemName: "list.bullet")
- .font(.system(size: 16))
- .foregroundStyle(theme.secondaryText)
- .frame(width: 36, height: 44)
- .contentShape(Rectangle())
- }
- .buttonStyle(.plain)
- .accessibilityIdentifier("miniPlayerQueue")
- }
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 8)
- .background(theme.playerBarBackground)
- }
- .background(theme.playerBarBackground)
- .accessibilityIdentifier("miniPlayer")
- .onTapGesture {
- playerVM.showNowPlaying = true
- }
- }
- }
|