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 } } }