diff --git a/flo/AlbumViewModel.swift b/flo/AlbumViewModel.swift index eb174f3..dd7b5cb 100644 --- a/flo/AlbumViewModel.swift +++ b/flo/AlbumViewModel.swift @@ -217,6 +217,11 @@ class AlbumViewModel: ObservableObject { return AlbumService.shared.getAlbumCover( artistName: artistName, albumName: albumName, albumId: id, albumCover: albumCover) } + + func getPlaylistCoverArt(id: String, coverArtId: String? = nil) -> String { + let artId = coverArtId ?? id + return AlbumService.shared.getPlaylistCover(playlistId: artId) + } func shareAlbum(description: String, completion: @escaping (String) -> Void) { AlbumService.shared.share(albumId: self.album.id, description: description, downloadable: false) diff --git a/flo/PlaylistDetailView.swift b/flo/PlaylistDetailView.swift index ed6290a..2791626 100644 --- a/flo/PlaylistDetailView.swift +++ b/flo/PlaylistDetailView.swift @@ -7,6 +7,39 @@ import SwiftUI +struct PlaylistCoverImageView: View { + let pathOrUrlString: String + + var body: some View { + if pathOrUrlString.hasPrefix("http://") || pathOrUrlString.hasPrefix("https://") { + AsyncImage(url: URL(string: pathOrUrlString)) { phase in + switch phase { + case .success(let image): + image + .resizable() + .aspectRatio(contentMode: .fit) + case .failure, .empty: + placeholderImage + @unknown default: + placeholderImage + } + } + } else if let uiImage = UIImage(contentsOfFile: pathOrUrlString) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + } else { + placeholderImage + } + } + + private var placeholderImage: some View { + Image(uiImage: UIImage(named: "placeholder") ?? UIImage()) + .resizable() + .aspectRatio(contentMode: .fit) + } +} + struct PlaylistDetailView: View { @EnvironmentObject private var viewModel: AlbumViewModel @EnvironmentObject private var playerViewModel: PlayerViewModel @@ -18,17 +51,18 @@ struct PlaylistDetailView: View { var body: some View { ScrollView { VStack { - if let image = UIImage(named: "placeholder") { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 300, height: 300) - .clipShape( - RoundedRectangle(cornerRadius: 10, style: .continuous) + PlaylistCoverImageView( + pathOrUrlString: viewModel.getPlaylistCoverArt( + id: viewModel.playlist.id, + coverArtId: viewModel.playlist.coverArtId ) - .shadow(radius: 5) - .padding(.top, 10) - } + ) + .frame(width: 300, height: 300) + .clipShape( + RoundedRectangle(cornerRadius: 10, style: .continuous) + ) + .shadow(radius: 5) + .padding(.top, 10) Text(viewModel.playlist.name) .customFont(.title) diff --git a/flo/Shared/Models/Playlist.swift b/flo/Shared/Models/Playlist.swift index 73d4385..441ff92 100644 --- a/flo/Shared/Models/Playlist.swift +++ b/flo/Shared/Models/Playlist.swift @@ -14,6 +14,7 @@ struct Playlist: Codable, Identifiable, Hashable, Playable { let isPublic: Bool let ownerName: String let artist: String + let coverArtId: String? var songs: [Song] = [] enum CodingKeys: String, CodingKey { @@ -22,18 +23,25 @@ struct Playlist: Codable, Identifiable, Hashable, Playable { case comment case isPublic = "public" case ownerName + case coverArtId case songs } init( - id: String = "", name: String = "", comment: String = "", isPublic: Bool = false, - ownerName: String = "", songs: [Song] = [] + id: String = "", + name: String = "", + comment: String = "", + isPublic: Bool = false, + ownerName: String = "", + coverArtId: String? = nil, + songs: [Song] = [] ) { self.id = id self.name = name self.comment = comment self.isPublic = isPublic self.ownerName = ownerName + self.coverArtId = coverArtId self.songs = songs self.artist = ownerName } @@ -46,6 +54,7 @@ struct Playlist: Codable, Identifiable, Hashable, Playable { self.comment = try container.decode(String.self, forKey: .comment) self.isPublic = try container.decode(Bool.self, forKey: .isPublic) self.ownerName = try container.decode(String.self, forKey: .ownerName) + self.coverArtId = try container.decodeIfPresent(String.self, forKey: .coverArtId) self.songs = try container.decodeIfPresent([Song].self, forKey: .songs) ?? [] self.artist = try container.decode(String.self, forKey: .ownerName) } diff --git a/flo/Shared/Services/AlbumService.swift b/flo/Shared/Services/AlbumService.swift index 41c347b..fc44911 100644 --- a/flo/Shared/Services/AlbumService.swift +++ b/flo/Shared/Services/AlbumService.swift @@ -302,6 +302,19 @@ class AlbumService { "\(UserDefaultsManager.serverBaseURL)\(API.SubsonicEndpoint.coverArt)\(AuthService.shared.getCreds(key: "subsonicToken"))&id=al-\(albumId)&size=300" } } + + func getPlaylistCover(playlistId: String) -> String { + let target = "Media/Various Artists/\(playlistId)/cover.png" + + if LocalFileManager.shared.fileExists(fileName: target) { + return LocalFileManager.shared.fileURL(for: target)?.path ?? "" + } else if let cached = CoverArtCacheManager.shared.cachedFilePath(albumId: playlistId) { + return cached + } else { + let artId = playlistId.hasPrefix("pl-") ? playlistId : "pl-\(playlistId)" + return "\(UserDefaultsManager.serverBaseURL)\(API.SubsonicEndpoint.coverArt)\(AuthService.shared.getCreds(key: "subsonicToken"))&id=\(artId)&size=300" + } + } func downloadAlbumCover( artistName: String, albumId: String, albumName: String,