import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../subsonic/models.dart'; import '../subsonic/subsonic_client.dart'; /// Client-side mirror of the server's stars + ratings, so toggles feel instant /// (optimistic update, reverted on failure). Hydrated from `getStarred2`. class FavoritesState { const FavoritesState({ this.songIds = const {}, this.albumIds = const {}, this.artistIds = const {}, this.ratings = const {}, }); final Set songIds; final Set albumIds; final Set artistIds; final Map ratings; bool isSongStarred(String id) => songIds.contains(id); int ratingFor(String id) => ratings[id] ?? 0; FavoritesState copyWith({ Set? songIds, Set? albumIds, Set? artistIds, Map? ratings, }) => FavoritesState( songIds: songIds ?? this.songIds, albumIds: albumIds ?? this.albumIds, artistIds: artistIds ?? this.artistIds, ratings: ratings ?? this.ratings, ); } class FavoritesController extends StateNotifier { FavoritesController(this._clientGetter) : super(const FavoritesState()) { if (_clientGetter() != null) hydrate(); } final SubsonicClient? Function() _clientGetter; Future hydrate() async { final client = _clientGetter(); if (client == null) return; try { final starred = await client.getStarred2(); final ratings = {}; for (final s in starred.songs) { if (s.userRating != null) ratings[s.id] = s.userRating!; } state = FavoritesState( songIds: starred.songs.map((s) => s.id).toSet(), albumIds: starred.albums.map((a) => a.id).toSet(), artistIds: starred.artists.map((a) => a.id).toSet(), ratings: ratings, ); } catch (_) { // Leave current state on failure. } } void clear() => state = const FavoritesState(); Future toggleSong(Song song) async { final client = _clientGetter(); if (client == null) return; final wasStarred = state.isSongStarred(song.id); final next = Set.from(state.songIds); wasStarred ? next.remove(song.id) : next.add(song.id); state = state.copyWith(songIds: next); // optimistic try { await client.setStarred(starred: !wasStarred, songId: song.id); } catch (_) { // Revert on failure. final reverted = Set.from(state.songIds); wasStarred ? reverted.add(song.id) : reverted.remove(song.id); state = state.copyWith(songIds: reverted); } } Future rateSong(String songId, int rating) async { final client = _clientGetter(); if (client == null) return; final previous = state.ratings[songId] ?? 0; final next = Map.from(state.ratings)..[songId] = rating; state = state.copyWith(ratings: next); // optimistic try { await client.setRating(songId, rating); } catch (_) { final reverted = Map.from(state.ratings) ..[songId] = previous; state = state.copyWith(ratings: reverted); } } }