import SwiftUI struct WatchNowPlayingView: View { @EnvironmentObject var audioPlayer: WatchAudioPlayer @EnvironmentObject var watchManager: WatchSessionManager @State private var crownVolume: Double = 0.8 @State private var showRouteInfo = false @FocusState private var isCrownFocused: Bool var body: some View { if let song = audioPlayer.currentSong { localNowPlaying(song) } else if let phone = watchManager.phoneNowPlaying { remoteNowPlaying(phone) } else { emptyState } } // MARK: - Local Playback (watch is playing) private func localNowPlaying(_ song: Song) -> some View { VStack(spacing: 4) { // Audio route indicator routeIndicator // Song info VStack(spacing: 2) { Text(song.title) .font(.system(size: 14, weight: .semibold)) .lineLimit(1) .foregroundColor(.white) Text(song.artist ?? "") .font(.system(size: 11)) .foregroundColor(.pink) .lineLimit(1) } .padding(.top, 2) // Visualizer WatchVisualizerView( levels: audioPlayer.audioLevels, isPlaying: audioPlayer.isPlaying ) .frame(height: 28) .padding(.horizontal, 4) // Progress bar GeometryReader { geo in ZStack(alignment: .leading) { Capsule() .fill(Color.white.opacity(0.2)) .frame(height: 3) Capsule() .fill(Color.pink) .frame(width: geo.size.width * audioPlayer.progressPercent, height: 3) } } .frame(height: 3) .padding(.horizontal, 8) // Time labels HStack { Text(audioPlayer.currentTimeFormatted) .font(.system(size: 9)) .foregroundColor(.gray) Spacer() Text(audioPlayer.remainingTimeFormatted) .font(.system(size: 9)) .foregroundColor(.gray) } .padding(.horizontal, 8) // Transport controls HStack(spacing: 0) { Spacer() Button(action: { audioPlayer.previous() }) { Image(systemName: "backward.fill") .font(.system(size: 16)) .foregroundColor(.white) } .buttonStyle(.plain) .frame(width: 40, height: 36) Spacer() Button(action: { audioPlayer.togglePlayPause() }) { Image(systemName: audioPlayer.isPlaying ? "pause.fill" : "play.fill") .font(.system(size: 24)) .foregroundColor(.white) } .buttonStyle(.plain) .frame(width: 50, height: 44) Spacer() Button(action: { audioPlayer.next() }) { Image(systemName: "forward.fill") .font(.system(size: 16)) .foregroundColor(.white) } .buttonStyle(.plain) .frame(width: 40, height: 36) Spacer() } // Bottom row HStack(spacing: 14) { Button(action: { audioPlayer.toggleShuffle() }) { Image(systemName: "shuffle") .font(.system(size: 11)) .foregroundColor(audioPlayer.shuffleEnabled ? .pink : .gray) } .buttonStyle(.plain) Button(action: { audioPlayer.cycleRepeat() }) { Image(systemName: audioPlayer.repeatMode == .one ? "repeat.1" : "repeat") .font(.system(size: 11)) .foregroundColor(audioPlayer.repeatMode != .off ? .pink : .gray) } .buttonStyle(.plain) // Volume HStack(spacing: 2) { Image(systemName: "speaker.fill") .font(.system(size: 7)) .foregroundColor(.gray) GeometryReader { geo in ZStack(alignment: .leading) { Capsule().fill(Color.white.opacity(0.15)).frame(height: 2) Capsule().fill(Color.white.opacity(0.6)) .frame(width: geo.size.width * CGFloat(audioPlayer.volume), height: 2) } } .frame(height: 8) Image(systemName: "speaker.wave.2.fill") .font(.system(size: 7)) .foregroundColor(.gray) } .frame(width: 60) } } .padding(.horizontal, 4) .focused($isCrownFocused) .digitalCrownRotation( $crownVolume, from: 0, through: 1, by: 0.05, sensitivity: .medium, isContinuous: false, isHapticFeedbackEnabled: true ) .onChange(of: crownVolume) { _, newValue in audioPlayer.setVolume(Float(newValue)) } .onAppear { crownVolume = Double(audioPlayer.volume) isCrownFocused = true } .sheet(isPresented: $showRouteInfo) { audioRouteSheet } } // MARK: - Audio Route Indicator private var routeIndicator: some View { Button(action: { showRouteInfo = true }) { HStack(spacing: 4) { Image(systemName: audioPlayer.useSpeakerMode ? "speaker.wave.2.fill" : (audioPlayer.isBluetoothConnected ? "airpodspro" : "airpodspro")) .font(.system(size: 9)) .foregroundColor(routeColor) Text(routeLabel) .font(.system(size: 9, weight: .medium)) .foregroundColor(routeColor) .lineLimit(1) } .padding(.horizontal, 8) .padding(.vertical, 3) .background(Capsule().fill(routeColor.opacity(0.15))) } .buttonStyle(.plain) } private var routeColor: Color { if audioPlayer.useSpeakerMode { return .cyan } return audioPlayer.isBluetoothConnected ? .green : .orange } private var routeLabel: String { if audioPlayer.useSpeakerMode { return "Speaker" } return audioPlayer.isBluetoothConnected ? audioPlayer.currentRouteName : "Connect Audio" } // MARK: - Audio Route Sheet private var audioRouteSheet: some View { ScrollView { VStack(spacing: 14) { Text("Audio Output") .font(.system(size: 16, weight: .bold)) .padding(.top, 8) // Speaker option if audioPlayer.isSpeakerAvailable { Button(action: { audioPlayer.useSpeakerMode = true showRouteInfo = false }) { HStack(spacing: 10) { Image(systemName: "speaker.wave.2.fill") .font(.system(size: 16)) .foregroundColor(.cyan) .frame(width: 28) VStack(alignment: .leading, spacing: 2) { Text("Built-in Speaker") .font(.system(size: 14, weight: .medium)) .foregroundColor(.white) Text("Plays through watch speaker") .font(.system(size: 10)) .foregroundColor(.gray) } Spacer() if audioPlayer.useSpeakerMode { Image(systemName: "checkmark.circle.fill") .foregroundColor(.cyan) } } .padding(.horizontal, 12) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 10) .fill(audioPlayer.useSpeakerMode ? Color.cyan.opacity(0.12) : Color.white.opacity(0.06)) ) } .buttonStyle(.plain) } // Bluetooth option Button(action: { audioPlayer.useSpeakerMode = false showRouteInfo = false Task { await audioPlayer.activateSession() } }) { HStack(spacing: 10) { Image(systemName: "airpodspro") .font(.system(size: 16)) .foregroundColor(.green) .frame(width: 28) VStack(alignment: .leading, spacing: 2) { Text("Bluetooth / AirPlay") .font(.system(size: 14, weight: .medium)) .foregroundColor(.white) Text(audioPlayer.isBluetoothConnected ? audioPlayer.currentRouteName : "Tap to connect") .font(.system(size: 10)) .foregroundColor(.gray) } Spacer() if !audioPlayer.useSpeakerMode { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) } } .padding(.horizontal, 12) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 10) .fill(!audioPlayer.useSpeakerMode ? Color.green.opacity(0.12) : Color.white.opacity(0.06)) ) } .buttonStyle(.plain) if audioPlayer.useSpeakerMode { Text("Speaker mode uses a background workout session to keep the app alive. A green ring indicator will appear on your watch face.") .font(.system(size: 10)) .foregroundColor(.gray) .multilineTextAlignment(.center) .padding(.horizontal, 8) } } .padding(.horizontal, 4) } } // MARK: - Remote Control (iPhone is playing) private func remoteNowPlaying(_ nowPlaying: WatchSessionManager.PhoneNowPlaying) -> some View { VStack(spacing: 8) { HStack(spacing: 4) { Image(systemName: "iphone") .font(.system(size: 10)) .foregroundColor(.blue) Text("Playing on iPhone") .font(.system(size: 10)) .foregroundColor(.blue) } VStack(spacing: 2) { Text(nowPlaying.title) .font(.system(size: 14, weight: .semibold)) .lineLimit(1) Text(nowPlaying.artist) .font(.system(size: 11)) .foregroundColor(.pink) .lineLimit(1) } GeometryReader { geo in let pct = nowPlaying.duration > 0 ? nowPlaying.currentTime / nowPlaying.duration : 0 ZStack(alignment: .leading) { Capsule().fill(Color.white.opacity(0.2)).frame(height: 3) Capsule().fill(Color.pink) .frame(width: geo.size.width * pct, height: 3) } } .frame(height: 3) .padding(.horizontal, 8) HStack(spacing: 20) { Button(action: { watchManager.sendPreviousCommand() }) { Image(systemName: "backward.fill") .font(.system(size: 16)) } .buttonStyle(.plain) Button(action: { watchManager.sendPlayCommand() }) { Image(systemName: nowPlaying.isPlaying ? "pause.fill" : "play.fill") .font(.system(size: 24)) } .buttonStyle(.plain) Button(action: { watchManager.sendNextCommand() }) { Image(systemName: "forward.fill") .font(.system(size: 16)) } .buttonStyle(.plain) } } .padding() } // MARK: - Empty State private var emptyState: some View { VStack(spacing: 12) { Image(systemName: "music.note") .font(.title2) .foregroundColor(.gray) Text("Not Playing") .font(.caption) .foregroundColor(.gray) // Show connect button if no Bluetooth if !audioPlayer.isBluetoothConnected { Button(action: { Task { await audioPlayer.activateSession() } }) { HStack(spacing: 4) { Image(systemName: "airpodspro") .font(.system(size: 11)) Text("Connect AirPods") .font(.system(size: 12)) } .foregroundColor(.pink) .padding(.horizontal, 12) .padding(.vertical, 6) .background(Capsule().stroke(Color.pink, lineWidth: 1)) } .buttonStyle(.plain) .padding(.top, 4) } if let error = audioPlayer.activationError { Text(error) .font(.system(size: 9)) .foregroundColor(.red) .multilineTextAlignment(.center) .padding(.horizontal) } } } }