This commit is contained in:
Forrest 2026-07-29 14:14:18 -04:00
commit d205277cdd
182 changed files with 22978 additions and 0 deletions

98
lib/state/favorites.dart Normal file
View file

@ -0,0 +1,98 @@
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);
}
}
}