import SwiftUI struct WatchLibraryView: View { @EnvironmentObject var watchManager: WatchSessionManager @EnvironmentObject var audioPlayer: WatchAudioPlayer @EnvironmentObject var offlineStore: WatchOfflineStore var body: some View { TabView { // Now Playing WatchNowPlayingView() // Offline Library WatchOfflineLibraryView() // Browse Server WatchBrowseView() // Settings WatchSettingsView() } .tabViewStyle(.verticalPage) } } // MARK: - Offline Library struct WatchOfflineLibraryView: View { @EnvironmentObject var offlineStore: WatchOfflineStore @EnvironmentObject var audioPlayer: WatchAudioPlayer @State private var showDeleteAll = false var body: some View { NavigationView { if offlineStore.songs.isEmpty { VStack(spacing: 8) { Image(systemName: "arrow.down.circle") .font(.title2) .foregroundColor(.gray) Text("No Offline Songs") .font(.caption) .foregroundColor(.gray) Text("Send songs from your iPhone or download from Browse") .font(.caption2) .foregroundColor(.gray) .multilineTextAlignment(.center) } } else { List { // Play controls Section { Button(action: playAllOffline) { HStack { Image(systemName: "play.fill") Text("Play All") } .foregroundColor(.pink) } Button(action: shuffleOffline) { HStack { Image(systemName: "shuffle") Text("Shuffle All") } .foregroundColor(.pink) } } // Songs by album (swipe to delete) ForEach(Array(offlineStore.songsByAlbum.keys.sorted()), id: \.self) { album in Section(album) { ForEach(offlineStore.songsByAlbum[album] ?? []) { offline in WatchSongRow( song: offline.song, isPlaying: audioPlayer.currentSong?.id == offline.id ) { let albumSongs = (offlineStore.songsByAlbum[album] ?? []).map { $0.song } let idx = albumSongs.firstIndex(where: { $0.id == offline.id }) ?? 0 audioPlayer.play(song: offline.song, fromQueue: albumSongs, at: idx) } } .onDelete { indexSet in let albumSongs = offlineStore.songsByAlbum[album] ?? [] for idx in indexSet { if idx < albumSongs.count { offlineStore.removeSong(albumSongs[idx].id) } } } } } // Storage info + delete all Section { HStack { Text("\(offlineStore.songs.count) songs") .font(.caption) .foregroundColor(.gray) Spacer() Text(offlineStore.formattedSize) .font(.caption) .foregroundColor(.gray) } Button(role: .destructive, action: { showDeleteAll = true }) { HStack { Image(systemName: "trash") Text("Remove All") } .font(.caption) } } } .navigationTitle("Library") .alert("Remove All Songs?", isPresented: $showDeleteAll) { Button("Remove", role: .destructive) { offlineStore.removeAll() } Button("Cancel", role: .cancel) { } } message: { Text("This will delete \(offlineStore.songs.count) songs (\(offlineStore.formattedSize)) from your watch.") } } } } private func playAllOffline() { let songs = offlineStore.songs.map { $0.song } guard let first = songs.first else { return } audioPlayer.play(song: first, fromQueue: songs) } private func shuffleOffline() { let songs = offlineStore.songs.map { $0.song }.shuffled() guard let first = songs.first else { return } audioPlayer.shuffleEnabled = true audioPlayer.play(song: first, fromQueue: songs) } } // MARK: - Browse Server struct WatchBrowseView: View { @EnvironmentObject var watchManager: WatchSessionManager var body: some View { NavigationView { List { NavigationLink(destination: WatchRecentAlbumsView()) { Label("Recent Albums", systemImage: "clock") } NavigationLink(destination: WatchPlaylistsListView()) { Label("Playlists", systemImage: "list.bullet") } NavigationLink(destination: WatchSearchView()) { Label("Search", systemImage: "magnifyingglass") } // Server info if let server = watchManager.activeServer { Section { VStack(alignment: .leading, spacing: 2) { Text(server.name) .font(.caption) Text(server.url) .font(.caption2) .foregroundColor(.gray) .lineLimit(1) } } } } .navigationTitle("Browse") } } } // MARK: - Recent Albums on Watch struct WatchRecentAlbumsView: View { @EnvironmentObject var watchManager: WatchSessionManager @State private var albums: [Album] = [] @State private var isLoading = true var body: some View { Group { if isLoading { ProgressView() } else { List(albums) { album in NavigationLink(destination: WatchAlbumDetailView(albumId: album.id)) { VStack(alignment: .leading, spacing: 2) { Text(album.name) .font(.caption) .lineLimit(1) Text(album.artist ?? "") .font(.caption2) .foregroundColor(.gray) .lineLimit(1) } } } } } .navigationTitle("Recent") .task { do { albums = try await watchManager.getAlbumList(type: "newest", size: 30) isLoading = false } catch { isLoading = false } } } } // MARK: - Album Detail on Watch struct WatchAlbumDetailView: View { @EnvironmentObject var watchManager: WatchSessionManager @EnvironmentObject var audioPlayer: WatchAudioPlayer @EnvironmentObject var offlineStore: WatchOfflineStore let albumId: String @State private var album: AlbumWithSongs? @State private var isLoading = true @State private var isDownloading = false var body: some View { Group { if isLoading { ProgressView() } else if let album = album { List { // Album header VStack(spacing: 4) { Text(album.name) .font(.caption) .fontWeight(.bold) .multilineTextAlignment(.center) Text(album.artist ?? "") .font(.caption2) .foregroundColor(.pink) } .frame(maxWidth: .infinity) .listRowBackground(Color.clear) // Play / Download buttons Button(action: { playAlbum(shuffle: false) }) { Label("Play", systemImage: "play.fill") .foregroundColor(.pink) } Button(action: { playAlbum(shuffle: true) }) { Label("Shuffle", systemImage: "shuffle") .foregroundColor(.pink) } Button(action: downloadAll) { HStack { if isDownloading { ProgressView().scaleEffect(0.6) } Label( isDownloading ? "Downloading..." : "Download for Offline", systemImage: "arrow.down.circle" ) } .foregroundColor(.blue) } .disabled(isDownloading) // Songs ForEach(album.song ?? []) { song in WatchSongRow( song: song, isPlaying: audioPlayer.currentSong?.id == song.id, isOffline: offlineStore.isSongAvailable(song.id) ) { let songs = album.song ?? [] let idx = songs.firstIndex(where: { $0.id == song.id }) ?? 0 audioPlayer.play(song: song, fromQueue: songs, at: idx) } } } } } .task { do { album = try await watchManager.getAlbum(id: albumId) isLoading = false } catch { isLoading = false } } } private func playAlbum(shuffle: Bool) { guard let songs = album?.song, let first = songs.first else { return } if shuffle { audioPlayer.shuffleEnabled = true } audioPlayer.play(song: first, fromQueue: songs) } private func downloadAll() { guard let album = album else { return } isDownloading = true Task { await offlineStore.downloadAlbum(album) await MainActor.run { isDownloading = false } } } } // MARK: - Playlists on Watch struct WatchPlaylistsListView: View { @EnvironmentObject var watchManager: WatchSessionManager @State private var playlists: [Playlist] = [] @State private var isLoading = true var body: some View { Group { if isLoading { ProgressView() } else if playlists.isEmpty { Text("No Playlists") .font(.caption) .foregroundColor(.gray) } else { List(playlists) { playlist in NavigationLink(destination: WatchPlaylistDetailView(playlistId: playlist.id)) { VStack(alignment: .leading, spacing: 2) { Text(playlist.name) .font(.caption) .lineLimit(1) Text("\(playlist.songCount ?? 0) songs") .font(.caption2) .foregroundColor(.gray) } } } } } .navigationTitle("Playlists") .task { do { playlists = try await watchManager.getPlaylists() isLoading = false } catch { isLoading = false } } } } // MARK: - Playlist Detail on Watch struct WatchPlaylistDetailView: View { @EnvironmentObject var watchManager: WatchSessionManager @EnvironmentObject var audioPlayer: WatchAudioPlayer @EnvironmentObject var offlineStore: WatchOfflineStore let playlistId: String @State private var playlist: PlaylistWithSongs? @State private var isLoading = true @State private var isDownloading = false var body: some View { Group { if isLoading { ProgressView() } else if let playlist = playlist { List { Button(action: { let songs = playlist.entry ?? [] guard let first = songs.first else { return } audioPlayer.play(song: first, fromQueue: songs) }) { Label("Play All", systemImage: "play.fill") .foregroundColor(.pink) } Button(action: downloadPlaylist) { HStack { if isDownloading { ProgressView().scaleEffect(0.6) } Label(isDownloading ? "Downloading..." : "Download All", systemImage: "arrow.down.circle") } .foregroundColor(.blue) } .disabled(isDownloading) ForEach(playlist.entry ?? []) { song in WatchSongRow( song: song, isPlaying: audioPlayer.currentSong?.id == song.id, isOffline: offlineStore.isSongAvailable(song.id) ) { let songs = playlist.entry ?? [] let idx = songs.firstIndex(where: { $0.id == song.id }) ?? 0 audioPlayer.play(song: song, fromQueue: songs, at: idx) } } } .navigationTitle(playlist.name) } } .task { do { playlist = try await watchManager.getPlaylist(id: playlistId) isLoading = false } catch { isLoading = false } } } private func downloadPlaylist() { guard let entries = playlist?.entry else { return } isDownloading = true Task { for song in entries { await offlineStore.downloadFromServer(song: song) } await MainActor.run { isDownloading = false } } } } // MARK: - Search on Watch struct WatchSearchView: View { @EnvironmentObject var watchManager: WatchSessionManager @EnvironmentObject var audioPlayer: WatchAudioPlayer @State private var query = "" @State private var results: SearchResult3? @State private var isSearching = false var body: some View { VStack { TextField("Search", text: $query) .onSubmit { performSearch() } if isSearching { ProgressView() } else if let results = results { List { if let songs = results.song, !songs.isEmpty { Section("Songs") { ForEach(songs.prefix(10)) { song in WatchSongRow( song: song, isPlaying: audioPlayer.currentSong?.id == song.id ) { audioPlayer.play(song: song, fromQueue: songs) } } } } if let albums = results.album, !albums.isEmpty { Section("Albums") { ForEach(albums.prefix(5)) { album in NavigationLink(destination: WatchAlbumDetailView(albumId: album.id)) { VStack(alignment: .leading) { Text(album.name).font(.caption).lineLimit(1) Text(album.artist ?? "").font(.caption2).foregroundColor(.gray) } } } } } } } } .navigationTitle("Search") } private func performSearch() { guard !query.isEmpty else { return } isSearching = true Task { do { results = try await watchManager.search(query: query) isSearching = false } catch { isSearching = false } } } } // MARK: - Reusable Song Row struct WatchSongRow: View { let song: Song let isPlaying: Bool var isOffline: Bool = false let onTap: () -> Void var body: some View { Button(action: onTap) { HStack(spacing: 6) { if isPlaying { Image(systemName: "waveform") .font(.caption2) .foregroundColor(.pink) .frame(width: 14) } else if isOffline { Image(systemName: "checkmark.circle.fill") .font(.caption2) .foregroundColor(.green) .frame(width: 14) } VStack(alignment: .leading, spacing: 1) { Text(song.title) .font(.caption) .foregroundColor(isPlaying ? .pink : .white) .lineLimit(1) Text(song.artist ?? "") .font(.caption2) .foregroundColor(.gray) .lineLimit(1) } Spacer() Text(song.durationFormatted) .font(.caption2) .foregroundColor(.gray) } } } } // MARK: - Watch Settings struct WatchSettingsView: View { @EnvironmentObject var watchManager: WatchSessionManager @EnvironmentObject var offlineStore: WatchOfflineStore @State private var showAddServer = false var body: some View { NavigationView { List { Section("Server") { ForEach(watchManager.servers) { server in VStack(alignment: .leading, spacing: 2) { HStack { Text(server.name) .font(.caption) if watchManager.activeServer?.id == server.id { Image(systemName: "checkmark") .font(.caption2) .foregroundColor(.pink) } } Text(server.url) .font(.caption2) .foregroundColor(.gray) .lineLimit(1) } .contentShape(Rectangle()) .onTapGesture { watchManager.setActive(server) } } Button("Add Server") { showAddServer = true } .font(.caption) .foregroundColor(.pink) Button("Sync from iPhone") { watchManager.requestServersFromPhone() } .font(.caption) } Section("Storage") { HStack { Text("Offline Songs") .font(.caption) Spacer() Text("\(offlineStore.songs.count)") .font(.caption) .foregroundColor(.gray) } HStack { Text("Used") .font(.caption) Spacer() Text(offlineStore.formattedSize) .font(.caption) .foregroundColor(.gray) } if !offlineStore.songs.isEmpty { Button("Remove All Downloads", role: .destructive) { offlineStore.removeAll() } .font(.caption) } } Section("Phone") { HStack { Circle() .fill(watchManager.isPhoneReachable ? .green : .orange) .frame(width: 6, height: 6) Text(watchManager.isPhoneReachable ? "Connected" : "Not Reachable") .font(.caption) .foregroundColor(.gray) } } } .navigationTitle("Settings") } .sheet(isPresented: $showAddServer) { WatchAddServerView() } } }