import SwiftUI // MARK: - Artist Detail View struct ArtistDetailView: View { @EnvironmentObject var serverManager: ServerManager let artistId: String @State private var artist: ArtistWithAlbums? @State private var isLoading = true @State private var loadFailed = false private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333) private let cache = LibraryCache.shared var body: some View { ScrollView { if let artist = artist { VStack(spacing: 0) { // Artist header VStack(spacing: 12) { AsyncCoverArt( coverArtId: artist.coverArt, size: 200 ) .frame(width: 160, height: 160) .clipShape(Circle()) .shadow(color: .black.opacity(0.4), radius: 12) .padding(.top, 20) Text(artist.name) .font(.system(size: 24, weight: .bold)) .foregroundColor(.white) Text("\(artist.albumCount ?? artist.album?.count ?? 0) Albums") .font(.system(size: 14)) .foregroundColor(.gray) } .padding(.bottom, 24) // Albums LazyVStack(spacing: 0) { ForEach(artist.album ?? []) { album in NavigationLink(destination: AlbumDetailView(albumId: album.id)) { HStack(spacing: 14) { AsyncCoverArt( coverArtId: album.coverArt, size: 60 ) .frame(width: 56, height: 56) .cornerRadius(4) VStack(alignment: .leading, spacing: 3) { Text(album.name) .font(.system(size: 16, weight: .medium)) .foregroundColor(.white) .lineLimit(1) HStack(spacing: 4) { if let year = album.year { Text(String(year)) } if let count = album.songCount { Text("• \(count) songs") } } .font(.system(size: 12)) .foregroundColor(.gray) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 12)) .foregroundColor(.gray) } .padding(.horizontal, 16) .padding(.vertical, 10) } Divider() .background(Color.white.opacity(0.08)) .padding(.leading, 86) } } Color.clear.frame(height: 120) } } else if loadFailed { // Connection failed — show retry VStack(spacing: 16) { Image(systemName: "wifi.slash") .font(.system(size: 32)) .foregroundColor(.gray) Text("Couldn't load artist") .font(.system(size: 15)) .foregroundColor(.gray) Button(action: { Task { await loadArtist() } }) { Text("Retry") .font(.system(size: 14, weight: .medium)) .foregroundColor(accentPink) } } .padding(.top, 100) } else if isLoading { ProgressView() .tint(accentPink) .padding(.top, 100) } } .background(Color(white: 0.06)) .navigationBarTitleDisplayMode(.inline) .task { await loadArtist() } } private func loadArtist() async { loadFailed = false // 1. Load from cache instantly — no spinner if we have data if artist == nil, let cached = cache.loadArtistDetail(id: artistId) { artist = cached isLoading = false } // 2. Fetch from server in background do { if let fetched = try await serverManager.client.getArtist(id: artistId) { cache.cacheArtistDetail(fetched) await MainActor.run { self.artist = fetched self.isLoading = false } } else if artist == nil { // Server returned nil and no cache — wait for connection and retry await retryAfterConnection() } else { isLoading = false } } catch { if artist == nil { // No cache, fetch failed — try waiting for connection await retryAfterConnection() } else { // We have cache, just stop loading isLoading = false } } } /// Wait for server connection to establish, then retry once private func retryAfterConnection() async { // If server is still connecting, wait up to 5 seconds for _ in 0..<10 { if serverManager.connectionState == .connected { break } try? await Task.sleep(for: .milliseconds(500)) } // One more attempt do { if let fetched = try await serverManager.client.getArtist(id: artistId) { cache.cacheArtistDetail(fetched) await MainActor.run { self.artist = fetched self.isLoading = false } return } } catch { } // Truly failed await MainActor.run { self.isLoading = false self.loadFailed = true } } } // MARK: - Genre Detail View struct GenreDetailView: View { @EnvironmentObject var serverManager: ServerManager let genre: Genre @State private var albums: [Album] = [] @State private var isLoading = true @State private var loadFailed = false private let columns = [ GridItem(.adaptive(minimum: 160), spacing: 14) ] var body: some View { ScrollView { if !albums.isEmpty { LazyVGrid(columns: columns, spacing: 18) { ForEach(albums) { album in NavigationLink(destination: AlbumDetailView(albumId: album.id)) { VStack(alignment: .leading, spacing: 6) { AsyncCoverArt(coverArtId: album.coverArt, size: 180) .frame(height: 160) .cornerRadius(4) Text(album.name) .font(.system(size: 13, weight: .medium)) .foregroundColor(.white) .lineLimit(1) Text(album.artist ?? "") .font(.system(size: 11)) .foregroundColor(.gray) .lineLimit(1) } } } } .padding(16) Color.clear.frame(height: 120) } else if loadFailed { VStack(spacing: 16) { Image(systemName: "wifi.slash") .font(.system(size: 32)) .foregroundColor(.gray) Text("Couldn't load genre") .font(.system(size: 15)) .foregroundColor(.gray) Button(action: { Task { await loadGenre() } }) { Text("Retry") .font(.system(size: 14, weight: .medium)) .foregroundColor(Color(red: 1.0, green: 0.176, blue: 0.333)) } } .padding(.top, 100) } else if isLoading { ProgressView() .tint(Color(red: 1.0, green: 0.176, blue: 0.333)) .padding(.top, 100) } } .background(Color(white: 0.06)) .navigationTitle(genre.value) .task { await loadGenre() } } private func loadGenre() async { let cache = LibraryCache.shared loadFailed = false // Cache first if albums.isEmpty, let cached = cache.load([Album].self, key: "genre_\(genre.value)") { albums = cached isLoading = false } // Server fetch do { let result = try await serverManager.client.getAlbumList2( type: "byGenre", size: 100, genre: genre.value ) cache.save(result, key: "genre_\(genre.value)") await MainActor.run { self.albums = result self.isLoading = false } } catch { if albums.isEmpty { // Wait for connection for _ in 0..<10 { if serverManager.connectionState == .connected { break } try? await Task.sleep(for: .milliseconds(500)) } do { let result = try await serverManager.client.getAlbumList2( type: "byGenre", size: 100, genre: genre.value ) cache.save(result, key: "genre_\(genre.value)") await MainActor.run { self.albums = result self.isLoading = false } } catch { await MainActor.run { self.isLoading = false self.loadFailed = true } } } else { await MainActor.run { self.isLoading = false } } } } }