feat(flutter/likes): LikeButton with optimistic toggle + rollback
LikedIdsController loads /api/likes/ids once and mutates the local set on toggle. Failed mutations roll the set back so the icon never lies. Wired into ArtistDetail header, AlbumDetail header, and per-track in the album track list.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../api/endpoints/likes.dart';
|
||||
import '../library/library_providers.dart';
|
||||
|
||||
final likesApiProvider = FutureProvider<LikesApi>((ref) async {
|
||||
return LikesApi(await ref.watch(dioProvider.future));
|
||||
});
|
||||
|
||||
class LikedIds {
|
||||
const LikedIds({
|
||||
required this.artists,
|
||||
required this.albums,
|
||||
required this.tracks,
|
||||
});
|
||||
final Set<String> artists;
|
||||
final Set<String> albums;
|
||||
final Set<String> tracks;
|
||||
|
||||
bool has(LikeKind kind, String id) => switch (kind) {
|
||||
LikeKind.artist => artists.contains(id),
|
||||
LikeKind.album => albums.contains(id),
|
||||
LikeKind.track => tracks.contains(id),
|
||||
};
|
||||
}
|
||||
|
||||
class LikedIdsController extends AsyncNotifier<LikedIds> {
|
||||
@override
|
||||
Future<LikedIds> build() async {
|
||||
final api = await ref.watch(likesApiProvider.future);
|
||||
final r = await api.ids();
|
||||
return LikedIds(artists: r.artists, albums: r.albums, tracks: r.tracks);
|
||||
}
|
||||
|
||||
Future<void> toggle(LikeKind kind, String id) async {
|
||||
final api = await ref.read(likesApiProvider.future);
|
||||
final current = state.value ??
|
||||
const LikedIds(artists: {}, albums: {}, tracks: {});
|
||||
|
||||
final isLiked = current.has(kind, id);
|
||||
final optimistic = _flip(current, kind, id);
|
||||
state = AsyncData(optimistic);
|
||||
|
||||
try {
|
||||
if (isLiked) {
|
||||
await api.unlike(kind, id);
|
||||
} else {
|
||||
await api.like(kind, id);
|
||||
}
|
||||
} catch (e, st) {
|
||||
state = AsyncData(current);
|
||||
Error.throwWithStackTrace(e, st);
|
||||
}
|
||||
}
|
||||
|
||||
LikedIds _flip(LikedIds before, LikeKind kind, String id) {
|
||||
Set<String> swap(Set<String> s) {
|
||||
final next = {...s};
|
||||
if (s.contains(id)) {
|
||||
next.remove(id);
|
||||
} else {
|
||||
next.add(id);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
return switch (kind) {
|
||||
LikeKind.artist => LikedIds(
|
||||
artists: swap(before.artists),
|
||||
albums: before.albums,
|
||||
tracks: before.tracks,
|
||||
),
|
||||
LikeKind.album => LikedIds(
|
||||
artists: before.artists,
|
||||
albums: swap(before.albums),
|
||||
tracks: before.tracks,
|
||||
),
|
||||
LikeKind.track => LikedIds(
|
||||
artists: before.artists,
|
||||
albums: before.albums,
|
||||
tracks: swap(before.tracks),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
final likedIdsProvider =
|
||||
AsyncNotifierProvider<LikedIdsController, LikedIds>(LikedIdsController.new);
|
||||
Reference in New Issue
Block a user