// // GenresView.swift // Mobile Music Assistant // // Created by Sven Hanold on 17.04.26. // import SwiftUI // MARK: - Genres List struct GenresView: View { @Environment(MAService.self) private var service @State private var errorMessage: String? @State private var showError = false private var genres: [MAGenre] { service.libraryManager.genres } private var isLoading: Bool { service.libraryManager.isLoadingGenres } var body: some View { List(genres) { genre in NavigationLink(value: genre) { HStack(spacing: 12) { Image(systemName: "guitars") .font(.title3) .foregroundStyle(.tint) .frame(width: 36, height: 36) .background(.tint.opacity(0.12), in: RoundedRectangle(cornerRadius: 8)) Text(genre.name.capitalized) .font(.body) } .padding(.vertical, 2) } } .listStyle(.plain) .overlay { if genres.isEmpty && isLoading { ProgressView() } else if genres.isEmpty && !isLoading { ContentUnavailableView( "No Genres", systemImage: "guitars", description: Text("Your library doesn't contain any genres yet") ) } } .refreshable { await loadGenres(refresh: true) } .task { if genres.isEmpty { await loadGenres(refresh: true) } } .alert("Error", isPresented: $showError) { Button("OK", role: .cancel) { } } message: { if let errorMessage { Text(errorMessage) } } } private func loadGenres(refresh: Bool) async { do { try await service.libraryManager.loadGenres(refresh: refresh) } catch { errorMessage = error.localizedDescription showError = true } } } // MARK: - Genre Detail struct GenreDetailView: View { @Environment(MAService.self) private var service let genre: MAGenre @State private var items: [MAMediaItem] = [] @State private var isLoading = false @State private var errorMessage: String? @State private var showError = false private var artists: [MAMediaItem] { items.filter { $0.mediaType == .artist }.sorted { $0.name < $1.name } } private var albums: [MAMediaItem] { items.filter { $0.mediaType == .album }.sorted { $0.name < $1.name } } private var others: [MAMediaItem] { items.filter { $0.mediaType != .artist && $0.mediaType != .album } } var body: some View { List { if !artists.isEmpty { Section("Artists") { ForEach(artists) { item in let artist = MAArtist(uri: item.uri, name: item.name, imageUrl: item.imageUrl, imageProvider: item.imageProvider) NavigationLink(value: artist) { GenreItemRow(item: item, icon: "music.mic") } } } } if !albums.isEmpty { Section("Albums") { ForEach(albums) { item in let album = MAAlbum(uri: item.uri, name: item.name, artists: item.artists, imageUrl: item.imageUrl, imageProvider: item.imageProvider) NavigationLink(value: album) { GenreItemRow(item: item, icon: "square.stack") } } } } if !others.isEmpty { Section("Other") { ForEach(others) { item in GenreItemRow(item: item, icon: "music.note") } } } } .navigationTitle(genre.name.capitalized) .navigationBarTitleDisplayMode(.large) .overlay { if isLoading { ProgressView() } else if items.isEmpty && !isLoading { ContentUnavailableView( "No Items", systemImage: "guitars", description: Text("Nothing found for this genre") ) } } .task { await loadItems() } .refreshable { await loadItems() } .alert("Error", isPresented: $showError) { Button("OK", role: .cancel) { } } message: { if let errorMessage { Text(errorMessage) } } } private func loadItems() async { isLoading = true do { items = try await service.libraryManager.browseGenre(genreUri: genre.uri) } catch { errorMessage = error.localizedDescription showError = true } isLoading = false } } // MARK: - Genre Item Row private struct GenreItemRow: View { @Environment(MAService.self) private var service let item: MAMediaItem let icon: String var body: some View { HStack(spacing: 12) { CachedAsyncImage(url: service.imageProxyURL( path: item.imageUrl, provider: item.imageProvider, size: 64 )) { image in image.resizable().aspectRatio(contentMode: .fill) } placeholder: { RoundedRectangle(cornerRadius: 6) .fill(Color.gray.opacity(0.2)) .overlay { Image(systemName: icon) .foregroundStyle(.secondary) } } .frame(width: 44, height: 44) .clipShape(RoundedRectangle(cornerRadius: 6)) VStack(alignment: .leading, spacing: 2) { Text(item.name) .font(.body) .lineLimit(1) if let artists = item.artists, !artists.isEmpty { Text(artists.map(\.name).joined(separator: ", ")) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } } .padding(.vertical, 2) } } #Preview { NavigationStack { GenresView() .environment(MAService()) } }