Implement artwork download and cache
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import SwiftUI
|
||||
import VelodyDomain
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
struct iPhoneLibraryView: View {
|
||||
@State private var viewModel = iPhoneLibraryViewModel()
|
||||
@@ -37,22 +40,28 @@ struct iPhoneLibraryView: View {
|
||||
Section("Remote Library: \(viewModel.remoteTracks.count)") {
|
||||
ForEach(viewModel.remoteTracks) { track in
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .top) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ArtworkThumbnailView(localFilePath: track.artworkLocalFilePath)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(track.title)
|
||||
.font(.headline)
|
||||
Text(track.artist)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(track.title)
|
||||
.font(.headline)
|
||||
Text(track.artist)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(track.statusText)
|
||||
.font(.caption.weight(.semibold))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(statusColor(for: track.status), in: Capsule())
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(track.statusText)
|
||||
.font(.caption.weight(.semibold))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 4)
|
||||
.background(statusColor(for: track.status), in: Capsule())
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Text("Duration: \(track.durationText)")
|
||||
.font(.subheadline)
|
||||
@@ -103,20 +112,26 @@ struct iPhoneLibraryView: View {
|
||||
} else {
|
||||
ForEach(viewModel.availableOfflineTracks) { track in
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .top) {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ArtworkThumbnailView(localFilePath: track.artworkLocalFilePath)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(track.title)
|
||||
.font(.headline)
|
||||
Text(track.artist)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(track.title)
|
||||
.font(.headline)
|
||||
Text(track.artist)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
|
||||
Button(track.playButtonTitle) {
|
||||
viewModel.togglePlayback(trackID: track.id)
|
||||
Button(track.playButtonTitle) {
|
||||
viewModel.togglePlayback(trackID: track.id)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
|
||||
Text("Duration: \(track.durationText)")
|
||||
@@ -219,3 +234,39 @@ struct iPhoneLibraryView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ArtworkThumbnailView: View {
|
||||
let localFilePath: String?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let artworkImage {
|
||||
Image(uiImage: artworkImage)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
} else {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color.gray.opacity(0.14))
|
||||
Image(systemName: "music.note")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 52, height: 52)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.stroke(Color.secondary.opacity(0.12), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
private var artworkImage: UIImage? {
|
||||
guard let localFilePath, !localFilePath.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIImage(contentsOfFile: localFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,6 +211,7 @@ final class iPhoneLibraryViewModel {
|
||||
let store = Self.makeRemoteLibraryStore()
|
||||
let downloadStateStore = Self.makeRemoteTrackDownloadStateStore()
|
||||
let audioFileStore = Self.makeOfflineAudioFileStore()
|
||||
let artworkStore = Self.makeArtworkStore()
|
||||
let repository = DefaultRemoteLibraryRepository(
|
||||
apiClient: apiClient,
|
||||
store: store
|
||||
@@ -218,7 +219,8 @@ final class iPhoneLibraryViewModel {
|
||||
let syncService = RemoteLibrarySyncService(
|
||||
repository: repository,
|
||||
downloadStateStore: downloadStateStore,
|
||||
audioFileStore: audioFileStore
|
||||
audioFileStore: audioFileStore,
|
||||
artworkStore: artworkStore
|
||||
)
|
||||
|
||||
self.environment = environment
|
||||
@@ -228,7 +230,8 @@ final class iPhoneLibraryViewModel {
|
||||
self.syncService = syncService
|
||||
self.offlineLibraryService = OfflineLibraryService(
|
||||
syncService: syncService,
|
||||
audioFileStore: audioFileStore
|
||||
audioFileStore: audioFileStore,
|
||||
artworkStore: artworkStore
|
||||
)
|
||||
self.player.onStateChange = { [weak self] state in
|
||||
self?.handleNowPlayingStateChange(state)
|
||||
@@ -402,6 +405,14 @@ final class iPhoneLibraryViewModel {
|
||||
return InMemoryOfflineAudioFileStore()
|
||||
}
|
||||
|
||||
private static func makeArtworkStore() -> any ArtworkStore {
|
||||
if let store = try? FileArtworkStore() {
|
||||
return store
|
||||
}
|
||||
|
||||
return InMemoryArtworkStore()
|
||||
}
|
||||
|
||||
private func reloadOfflineLibrary() async throws -> OfflineLibrarySnapshot {
|
||||
let snapshot = try await offlineLibraryService.loadSnapshot()
|
||||
|
||||
@@ -484,6 +495,7 @@ struct RemoteTrackRowViewData: Identifiable, Equatable {
|
||||
let canPlay: Bool
|
||||
let playButtonTitle: String
|
||||
let lastDownloadError: String?
|
||||
let artworkLocalFilePath: String?
|
||||
|
||||
init(
|
||||
track: OfflineLibraryRemoteTrack,
|
||||
@@ -504,6 +516,7 @@ struct RemoteTrackRowViewData: Identifiable, Equatable {
|
||||
? "Pause"
|
||||
: "Play"
|
||||
lastDownloadError = track.lastDownloadError
|
||||
artworkLocalFilePath = track.localArtworkFilePath
|
||||
}
|
||||
|
||||
static func formatDuration(seconds: Int) -> String {
|
||||
@@ -549,6 +562,7 @@ struct AvailableOfflineTrackRowViewData: Identifiable, Equatable {
|
||||
let remoteTrackID: String
|
||||
let assetID: String
|
||||
let playButtonTitle: String
|
||||
let artworkLocalFilePath: String?
|
||||
|
||||
init(
|
||||
track: OfflineLibraryTrack,
|
||||
@@ -563,5 +577,6 @@ struct AvailableOfflineTrackRowViewData: Identifiable, Equatable {
|
||||
playButtonTitle = nowPlaying.trackID == track.remoteTrackId && nowPlaying.isPlaying
|
||||
? "Pause"
|
||||
: "Play"
|
||||
artworkLocalFilePath = track.localArtworkFilePath
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user