// // ArtistDetailView.swift // Mobile Music Assistant // // Created by Sven Hanold on 26.03.26. // import SwiftUI struct ArtistDetailView: View { @Environment(MAService.self) private var service let artist: MAArtist @State private var albums: [MAAlbum] = [] @State private var biography: String? @State private var isLoading = true @State private var errorMessage: String? @State private var showError = false @State private var kenBurnsScale: CGFloat = 1.0 @State private var isBiographyExpanded = false var body: some View { ZStack { // Blurred Background with Ken Burns Effect backgroundArtwork // Content ScrollView { VStack(spacing: 24) { // Artist Header artistHeader // Biography Section if let biography { biographySection(biography) } Divider() .background(Color.white.opacity(0.3)) // Albums Section if isLoading { ProgressView() .padding() .tint(.white) } else if albums.isEmpty { Text("No albums found") .foregroundStyle(.white.opacity(0.7)) .padding() } else { albumGrid } } } } .navigationTitle(artist.name) .navigationBarTitleDisplayMode(.inline) .toolbarColorScheme(.dark, for: .navigationBar) .task { async let albumsLoad: () = loadAlbums() async let detailLoad: () = loadArtistDetail() _ = await (albumsLoad, detailLoad) // Start Ken Burns animation withAnimation(.linear(duration: 20).repeatForever(autoreverses: true)) { kenBurnsScale = 1.15 } } .alert("Error", isPresented: $showError) { Button("OK", role: .cancel) { } } message: { if let errorMessage { Text(errorMessage) } } } // MARK: - Background Artwork @ViewBuilder private var backgroundArtwork: some View { GeometryReader { geometry in CachedAsyncImage(url: service.imageProxyURL(path: artist.imageUrl, provider: artist.imageProvider, size: 512)) { image in image .resizable() .aspectRatio(contentMode: .fill) .frame(width: geometry.size.width, height: geometry.size.height) .scaleEffect(kenBurnsScale) .blur(radius: 50) .overlay { // Dark gradient overlay for better text contrast LinearGradient( colors: [ Color.black.opacity(0.7), Color.black.opacity(0.5), Color.black.opacity(0.7) ], startPoint: .top, endPoint: .bottom ) } .clipped() } placeholder: { Rectangle() .fill( LinearGradient( colors: [Color(.systemGray6), Color(.systemGray5)], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .overlay { Color.black.opacity(0.6) } } } .ignoresSafeArea() } // MARK: - Artist Header @ViewBuilder private var artistHeader: some View { VStack(spacing: 16) { CachedAsyncImage(url: service.imageProxyURL(path: artist.imageUrl, provider: artist.imageProvider, size: 512)) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Circle() .fill(Color.white.opacity(0.1)) .overlay { Image(systemName: "music.mic") .font(.system(size: 60)) .foregroundStyle(.white.opacity(0.5)) } } .frame(width: 200, height: 200) .clipShape(Circle()) .shadow(color: .black.opacity(0.5), radius: 20, y: 10) if !albums.isEmpty { Text("\(albums.count) albums") .font(.subheadline) .fontWeight(.semibold) .foregroundStyle(.white.opacity(0.8)) .shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1) } } .padding(.top) } // MARK: - Album Grid @ViewBuilder private var albumGrid: some View { VStack(alignment: .leading, spacing: 12) { Text("Albums") .font(.title2.bold()) .foregroundStyle(.white) .shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1) .padding(.horizontal) LazyVGrid( columns: [GridItem(.adaptive(minimum: 160), spacing: 16)], spacing: 16 ) { ForEach(albums) { album in NavigationLink(value: album) { ArtistAlbumCard(album: album, service: service) } .buttonStyle(.plain) } } .padding(.horizontal) .padding(.bottom, 24) } } // MARK: - Biography Section @ViewBuilder private func biographySection(_ text: String) -> some View { VStack(alignment: .leading, spacing: 10) { Text("About") .font(.title2.bold()) .foregroundStyle(.white) .shadow(color: .black.opacity(0.3), radius: 2, x: 0, y: 1) Text(verbatim: text) .font(.body) .foregroundStyle(.white.opacity(0.85)) .lineLimit(isBiographyExpanded ? nil : 4) .lineSpacing(3) Button { withAnimation(.easeInOut(duration: 0.25)) { isBiographyExpanded.toggle() } } label: { Text(isBiographyExpanded ? "Show less" : "Show more") .font(.subheadline.bold()) .foregroundStyle(.white.opacity(0.6)) } } .padding(.horizontal) .frame(maxWidth: .infinity, alignment: .leading) } // MARK: - Actions private func loadAlbums() async { print("đŸ”ĩ ArtistDetailView: Loading albums for artist: \(artist.name)") print("đŸ”ĩ ArtistDetailView: Artist URI: \(artist.uri)") isLoading = true errorMessage = nil do { albums = try await service.libraryManager.getArtistAlbums(artistUri: artist.uri) print("✅ ArtistDetailView: Loaded \(albums.count) albums") isLoading = false } catch { print("❌ ArtistDetailView: Failed to load albums: \(error)") errorMessage = error.localizedDescription showError = true isLoading = false } } private func loadArtistDetail() async { do { let detail = try await service.getArtistDetail(artistUri: artist.uri) if let desc = detail.metadata?.description, !desc.isEmpty { biography = desc } } catch { // Biography is optional — silently ignore if unavailable print("â„šī¸ ArtistDetailView: Could not load artist detail: \(error)") } } } // MARK: - Artist Album Card private struct ArtistAlbumCard: View { let album: MAAlbum let service: MAService var body: some View { VStack(alignment: .leading, spacing: 8) { CachedAsyncImage(url: service.imageProxyURL(path: album.imageUrl, provider: album.imageProvider, size: 256)) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { RoundedRectangle(cornerRadius: 10) .fill(Color.white.opacity(0.1)) .overlay { Image(systemName: "opticaldisc") .font(.system(size: 36)) .foregroundStyle(.white.opacity(0.5)) } } .aspectRatio(1, contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 10)) .shadow(color: .black.opacity(0.4), radius: 8, y: 4) Text(album.name) .font(.caption.bold()) .lineLimit(2) .foregroundStyle(.white) .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1) if let year = album.year { Text(String(year)) .font(.caption2) .foregroundStyle(.white.opacity(0.7)) .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1) } } .padding(8) .background( RoundedRectangle(cornerRadius: 12) .fill(Color.black.opacity(0.3)) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.white.opacity(0.1), lineWidth: 1) ) ) } } #Preview { NavigationStack { ArtistDetailView( artist: MAArtist( uri: "library://artist/1", name: "Test Artist", imageUrl: nil, sortName: nil, musicbrainzId: nil ) ) .environment(MAService()) } }