172 lines
5.1 KiB
Swift
172 lines
5.1 KiB
Swift
import Foundation
|
|
import Observation
|
|
import VelodyDomain
|
|
import VelodyNetworking
|
|
import VelodyPersistence
|
|
import VelodySync
|
|
import VelodyUtilities
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#endif
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class iPhoneLibraryViewModel {
|
|
enum ViewState: Equatable {
|
|
case idle
|
|
case loading
|
|
case success
|
|
case empty
|
|
case networkError(String)
|
|
}
|
|
|
|
var remoteTracks: [RemoteTrackRowViewData] = []
|
|
var syncStatus = "Remote library not synced yet."
|
|
var state: ViewState = .idle
|
|
|
|
private let environment: ServerEnvironment
|
|
private let apiClient: any VelodyAPIClient
|
|
private let syncService: RemoteLibrarySyncService
|
|
private let keychainService: any KeychainService
|
|
private var hasLoaded = false
|
|
|
|
init(
|
|
keychainService: any KeychainService = SystemKeychainService(
|
|
service: "de.diyaa.velody.iphone"
|
|
)
|
|
) {
|
|
let environment = ServerEnvironment(
|
|
baseURL: ServerEnvironment.defaultLocalBaseURL,
|
|
appVersion: "0.1.0"
|
|
)
|
|
let apiClient = URLSessionVelodyAPIClient(environment: environment)
|
|
let store = Self.makeRemoteLibraryStore()
|
|
|
|
self.environment = environment
|
|
self.apiClient = apiClient
|
|
self.keychainService = keychainService
|
|
self.syncService = RemoteLibrarySyncService(
|
|
repository: DefaultRemoteLibraryRepository(
|
|
apiClient: apiClient,
|
|
store: store
|
|
)
|
|
)
|
|
}
|
|
|
|
func loadIfNeeded() async {
|
|
guard !hasLoaded else { return }
|
|
hasLoaded = true
|
|
|
|
do {
|
|
let persistedTracks = try await syncService.loadCachedRemoteTracks()
|
|
applyRestoredTracks(persistedTracks)
|
|
} catch {
|
|
state = .idle
|
|
syncStatus = "Failed to load cached remote library: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
|
|
func refreshSync() async {
|
|
state = .loading
|
|
syncStatus = "Syncing remote library..."
|
|
|
|
do {
|
|
let deviceId = try await currentOrRegisterDeviceID()
|
|
let tracks = try await syncService.syncRemoteLibrary(deviceId: deviceId)
|
|
applySyncedTracks(tracks)
|
|
} catch {
|
|
state = .networkError("Remote library sync failed.")
|
|
syncStatus = "Remote library sync failed: \(error.localizedDescription)"
|
|
}
|
|
}
|
|
|
|
private func currentOrRegisterDeviceID() async throws -> String {
|
|
if let existingDeviceID = try await keychainService.loadValue(
|
|
forKey: Self.deviceIDKey
|
|
), !existingDeviceID.isEmpty {
|
|
return existingDeviceID
|
|
}
|
|
|
|
let response = try await apiClient.registerDevice(
|
|
DeviceRegistrationPayload(
|
|
platform: .iphone,
|
|
deviceName: Self.currentDeviceName,
|
|
appVersion: environment.appVersion
|
|
)
|
|
)
|
|
|
|
try await keychainService.save(response.deviceId, forKey: Self.deviceIDKey)
|
|
try await keychainService.save(
|
|
response.bootstrapToken,
|
|
forKey: Self.bootstrapTokenKey
|
|
)
|
|
|
|
return response.deviceId
|
|
}
|
|
|
|
private func applyRestoredTracks(_ tracks: [RemoteTrack]) {
|
|
remoteTracks = tracks.map(RemoteTrackRowViewData.init(track:))
|
|
|
|
if tracks.isEmpty {
|
|
state = .idle
|
|
syncStatus = "Tap Sync Remote Library to load remote metadata."
|
|
} else {
|
|
state = .success
|
|
syncStatus = "Restored \(tracks.count) cached remote track(s)."
|
|
}
|
|
}
|
|
|
|
private func applySyncedTracks(_ tracks: [RemoteTrack]) {
|
|
remoteTracks = tracks.map(RemoteTrackRowViewData.init(track:))
|
|
|
|
if tracks.isEmpty {
|
|
state = .empty
|
|
syncStatus = "Remote library is empty."
|
|
} else {
|
|
state = .success
|
|
syncStatus = "Sync Remote Library completed. Remote tracks: \(tracks.count)."
|
|
}
|
|
}
|
|
|
|
private static func makeRemoteLibraryStore() -> any RemoteLibraryStore {
|
|
if let store = try? FileRemoteLibraryStore() {
|
|
return store
|
|
}
|
|
|
|
return InMemoryRemoteLibraryStore()
|
|
}
|
|
|
|
#if canImport(UIKit)
|
|
private static var currentDeviceName: String {
|
|
UIDevice.current.name
|
|
}
|
|
#else
|
|
private static let currentDeviceName = "Velody iPhone"
|
|
#endif
|
|
|
|
private static let deviceIDKey = "velody.iphone.device-id"
|
|
private static let bootstrapTokenKey = "velody.iphone.bootstrap-token"
|
|
}
|
|
|
|
struct RemoteTrackRowViewData: Identifiable, Equatable {
|
|
let id: String
|
|
let title: String
|
|
let artist: String
|
|
let durationText: String
|
|
let remoteTrackID: String
|
|
|
|
init(track: RemoteTrack) {
|
|
id = track.trackId
|
|
title = track.title
|
|
artist = track.artist
|
|
durationText = Self.formatDuration(seconds: track.durationSeconds)
|
|
remoteTrackID = track.trackId
|
|
}
|
|
|
|
private static func formatDuration(seconds: Int) -> String {
|
|
let minutes = seconds / 60
|
|
let remainingSeconds = seconds % 60
|
|
return "\(minutes):\(String(format: "%02d", remainingSeconds))"
|
|
}
|
|
}
|