203 lines
6.6 KiB
Swift
203 lines
6.6 KiB
Swift
//
|
|
// 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
|
|
|
|
// Deduplicated, non-empty genres — populated by MALibraryManager after load + filter pass.
|
|
private var genres: [MAGenre] { service.libraryManager.displayGenres }
|
|
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 service.libraryManager.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
|
|
defer { isLoading = false }
|
|
do {
|
|
items = try await service.libraryManager.browseGenresByName(genre.name)
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
showError = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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())
|
|
}
|
|
}
|