From 5b8cdf603c70b04284166f996b39306ec81777f6 Mon Sep 17 00:00:00 2001 From: Laurent Date: Sun, 24 May 2026 09:38:06 +0200 Subject: [PATCH] feat: add PlaylistViewModel with reactive playlist observation --- Music/ViewModels/PlaylistViewModel.swift | 122 +++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 Music/ViewModels/PlaylistViewModel.swift diff --git a/Music/ViewModels/PlaylistViewModel.swift b/Music/ViewModels/PlaylistViewModel.swift new file mode 100644 index 0000000..e58541a --- /dev/null +++ b/Music/ViewModels/PlaylistViewModel.swift @@ -0,0 +1,122 @@ +import Foundation +import Observation +import GRDB + +@Observable +final class PlaylistViewModel { + var playlists: [Playlist] = [] + var selectedPlaylist: Playlist? + var playlistTracks: [Track] = [] + + var lastUsedPlaylistId: Int64? { + get { UserDefaults.standard.object(forKey: "lastUsedPlaylistId") as? Int64 } + set { UserDefaults.standard.set(newValue, forKey: "lastUsedPlaylistId") } + } + + var lastUsedPlaylistName: String? { + guard let id = lastUsedPlaylistId else { return nil } + return playlists.first(where: { $0.id == id })?.name + } + + private let db: DatabaseService + private var playlistsCancellable: AnyDatabaseCancellable? + private var tracksCancellable: AnyDatabaseCancellable? + private var searchTask: Task? + private var searchText = "" + + init(db: DatabaseService) { + self.db = db + observePlaylists() + } + + func createPlaylist(name: String) throws { + _ = try db.createPlaylist(name: name) + } + + func renamePlaylist(_ playlist: Playlist, to name: String) throws { + guard let id = playlist.id else { return } + try db.renamePlaylist(id: id, name: name) + } + + func deletePlaylist(_ playlist: Playlist) throws { + guard let id = playlist.id else { return } + if selectedPlaylist?.id == id { + deselectPlaylist() + } + if lastUsedPlaylistId == id { + lastUsedPlaylistId = nil + } + try db.deletePlaylist(id: id) + } + + func addTrack(_ track: Track, to playlist: Playlist) throws { + guard let trackId = track.id, let playlistId = playlist.id else { return } + try db.addTrackToPlaylist(trackId: trackId, playlistId: playlistId) + lastUsedPlaylistId = playlistId + } + + func addTrackToLastUsedPlaylist(_ track: Track) throws { + guard let playlistId = lastUsedPlaylistId, + let playlist = playlists.first(where: { $0.id == playlistId }) else { return } + try addTrack(track, to: playlist) + } + + func removeTrack(_ track: Track, from playlist: Playlist) throws { + guard let trackId = track.id, let playlistId = playlist.id else { return } + try db.removeTrackFromPlaylist(trackId: trackId, playlistId: playlistId) + } + + func moveTrack(in playlist: Playlist, from source: Int, to destination: Int) throws { + guard let playlistId = playlist.id else { return } + try db.reorderPlaylistTrack(playlistId: playlistId, fromPosition: source, toPosition: destination) + } + + func search(_ text: String) { + searchText = text + searchTask?.cancel() + searchTask = Task { @MainActor [weak self] in + try? await Task.sleep(for: .milliseconds(150)) + guard !Task.isCancelled else { return } + self?.observePlaylistTracks() + } + } + + func selectPlaylist(_ playlist: Playlist) { + selectedPlaylist = playlist + observePlaylistTracks() + } + + func deselectPlaylist() { + selectedPlaylist = nil + tracksCancellable?.cancel() + tracksCancellable = nil + playlistTracks = [] + searchText = "" + } + + private func observePlaylists() { + let observation = ValueObservation.tracking { [db] dbAccess in + try db.fetchPlaylists(db: dbAccess) + } + playlistsCancellable = observation.start( + in: db.dbPool, + onError: { error in print("Playlists observation error: \(error)") }, + onChange: { [weak self] playlists in self?.playlists = playlists } + ) + } + + private func observePlaylistTracks() { + tracksCancellable?.cancel() + guard let playlistId = selectedPlaylist?.id else { return } + let search = searchText + + let observation = ValueObservation.tracking { [db] dbAccess in + try db.fetchPlaylistTracks(db: dbAccess, playlistId: playlistId, search: search) + } + tracksCancellable = observation.start( + in: db.dbPool, + onError: { error in print("Playlist tracks observation error: \(error)") }, + onChange: { [weak self] tracks in self?.playlistTracks = tracks } + ) + } +}