// // 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 @State private var scrollPosition: String? private var albums: [MAAlbum] { service.libraryManager.albums } private var isLoading: Bool { service.libraryManager.isLoadingAlbums } private let columns = [ GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8), GridItem(.flexible(), spacing: 8) ] var body: some View { ScrollView { LazyVGrid(columns: columns, spacing: 8) { ForEach(albums) { album in NavigationLink(value: album) { AlbumGridItem(album: album) } .buttonStyle(.plain) .id(album.uri) .task { await loadMoreIfNeeded(currentItem: album) } } if isLoading { ProgressView() .gridCellColumns(columns.count) .padding(.horizontal, 12) .padding(.vertical, 8) } } .padding() } .scrollPosition(id: $scrollPosition) .refreshable { await loadAlbums(refresh: true) } .task { if albums.isEmpty { await loadAlbums(refresh: true) } } .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: 4) { // Album Cover CachedAsyncImage(url: service.imageProxyURL(path: album.imageUrl, provider: album.imageProvider, size: 256)) { image in image .resizable() .aspectRatio(contentMode: .fill) } placeholder: { RoundedRectangle(cornerRadius: 8) .fill(Color.gray.opacity(0.2)) .overlay { Image(systemName: "opticaldisc") .font(.system(size: 40)) .foregroundStyle(.secondary) } } .aspectRatio(1, contentMode: .fit) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay(alignment: .bottomTrailing) { if service.libraryManager.isFavorite(uri: album.uri) { Image(systemName: "heart.fill") .font(.system(size: 12)) .foregroundStyle(.red) .padding(6) } } // Album Info VStack(alignment: .leading, spacing: 2) { Text(album.name) .font(.caption) .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(maxWidth: .infinity, alignment: .leading) } } } #Preview { NavigationStack { AlbumsView() .environment(MAService()) } }