98 lines
3.1 KiB
Dart
98 lines
3.1 KiB
Dart
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<String> songIds;
|
|
final Set<String> albumIds;
|
|
final Set<String> artistIds;
|
|
final Map<String, int> ratings;
|
|
|
|
bool isSongStarred(String id) => songIds.contains(id);
|
|
int ratingFor(String id) => ratings[id] ?? 0;
|
|
|
|
FavoritesState copyWith({
|
|
Set<String>? songIds,
|
|
Set<String>? albumIds,
|
|
Set<String>? artistIds,
|
|
Map<String, int>? ratings,
|
|
}) =>
|
|
FavoritesState(
|
|
songIds: songIds ?? this.songIds,
|
|
albumIds: albumIds ?? this.albumIds,
|
|
artistIds: artistIds ?? this.artistIds,
|
|
ratings: ratings ?? this.ratings,
|
|
);
|
|
}
|
|
|
|
class FavoritesController extends StateNotifier<FavoritesState> {
|
|
FavoritesController(this._clientGetter) : super(const FavoritesState()) {
|
|
if (_clientGetter() != null) hydrate();
|
|
}
|
|
|
|
final SubsonicClient? Function() _clientGetter;
|
|
|
|
Future<void> hydrate() async {
|
|
final client = _clientGetter();
|
|
if (client == null) return;
|
|
try {
|
|
final starred = await client.getStarred2();
|
|
final ratings = <String, int>{};
|
|
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<void> toggleSong(Song song) async {
|
|
final client = _clientGetter();
|
|
if (client == null) return;
|
|
final wasStarred = state.isSongStarred(song.id);
|
|
final next = Set<String>.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<String>.from(state.songIds);
|
|
wasStarred ? reverted.add(song.id) : reverted.remove(song.id);
|
|
state = state.copyWith(songIds: reverted);
|
|
}
|
|
}
|
|
|
|
Future<void> rateSong(String songId, int rating) async {
|
|
final client = _clientGetter();
|
|
if (client == null) return;
|
|
final previous = state.ratings[songId] ?? 0;
|
|
final next = Map<String, int>.from(state.ratings)..[songId] = rating;
|
|
state = state.copyWith(ratings: next); // optimistic
|
|
try {
|
|
await client.setRating(songId, rating);
|
|
} catch (_) {
|
|
final reverted = Map<String, int>.from(state.ratings)
|
|
..[songId] = previous;
|
|
state = state.copyWith(ratings: reverted);
|
|
}
|
|
}
|
|
}
|