// // AlbumsView.swift // Mobile Music Assistant // // Created by Sven Hanold on 26.03.26. // import SwiftUI struct AlbumsView: View { @Environment(MAService.self) private var service @State private var errorMessage: String? @State private var showError = false private var albums: [MAAlbum] { service.libraryManager.albums } private var isLoading: Bool { service.libraryManager.isLoadingAlbums } private let columns = [ GridItem(.adaptive(minimum: 160), spacing: 16) ] var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: 16) { ForEach(albums) { album in NavigationLink(value: album) { AlbumGridItem(album: album) } .buttonStyle(.plain) .task { await loadMoreIfNeeded(currentItem: album) } } if isLoading { ProgressView() .gridCellColumns(columns.count) .padding() } } .padding() } .navigationDestination(for: MAAlbum.self) { album in AlbumDetailView(album: album) } .refreshable { await loadAlbums(refresh: true) } .task { if albums.isEmpty { await loadAlbums(refresh: false) } } .alert("Error", isPresented: $showError) { Button("OK", role: .cancel) { } } message: { if let errorMessage { Text(errorMessage) } } .overlay { if albums.isEmpty && !isLoading { ContentUnavailableView( "No Albums", systemImage: "square.stack", description: Text("Your library doesn't contain any albums yet") ) } } } private func loadAlbums(refresh: Bool) async { do { try await service.libraryManager.loadAlbums(refresh: refresh) } catch { errorMessage = error.localizedDescription showError = true } } private func loadMoreIfNeeded(currentItem: MAAlbum) async { do { try await service.libraryManager.loadMoreAlbumsIfNeeded(currentItem: currentItem) } catch { errorMessage = error.localizedDescription showError = true } } } // MARK: - Album Grid Item struct AlbumGridItem: View { @Environment(MAService.self) private var service let album: MAAlbum var body: some View { VStack(alignment: .leading, spacing: 8) { // Album Cover if let imageUrl = album.imageUrl { let coverURL = service.imageProxyURL(path: imageUrl, size: 256) CachedAsyncImage(url: coverURL) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { Rectangle() .fill(Color.gray.opacity(0.2)) } .frame(width: 160, height: 160) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { RoundedRectangle(cornerRadius: 8) .fill(Color.gray.opacity(0.2)) .frame(width: 160, height: 160) .overlay { Image(systemName: "opticaldisc") .font(.system(size: 40)) .foregroundStyle(.secondary) } } // Album Info VStack(alignment: .leading, spacing: 2) { Text(album.name) .font(.subheadline) .fontWeight(.medium) .lineLimit(2) .foregroundStyle(.primary) if let artists = album.artists, !artists.isEmpty { Text(artists.map { $0.name }.joined(separator: ", ")) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } if let year = album.year { Text(String(year)) .font(.caption2) .foregroundStyle(.tertiary) } } .frame(width: 160, alignment: .leading) } } } #Preview { NavigationStack { AlbumsView() .environment(MAService()) } }