Files
minstrel/flutter_client/lib/quarantine/quarantine_provider.dart
T
bvandeusen 5159bcd3f4 feat(flutter/quarantine): MyQuarantineController + provider
AsyncNotifier wrapping /api/quarantine/mine with optimistic flag /
unflag mutations and an isHidden(trackId) convenience for menu state.
Mirrors the LikedIdsController pattern: optimistic update, rollback
on server error.

Used by the next slice's TrackActionsSheet to power Hide / Unhide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:39:32 -04:00

71 lines
2.5 KiB
Dart

import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/endpoints/me.dart';
import '../api/endpoints/quarantine.dart';
import '../library/library_providers.dart' show dioProvider;
import '../models/quarantine_mine.dart';
import '../models/track.dart';
final quarantineApiProvider = FutureProvider<QuarantineApi>((ref) async {
return QuarantineApi(await ref.watch(dioProvider.future));
});
class MyQuarantineController extends AsyncNotifier<List<QuarantineMineRow>> {
@override
Future<List<QuarantineMineRow>> build() async {
final dio = await ref.watch(dioProvider.future);
return MeApi(dio).quarantineMine();
}
/// True when this track is in the caller's quarantine list.
bool isHidden(String trackId) =>
(state.value ?? const []).any((r) => r.trackId == trackId);
/// Optimistic flag: prepends a synthetic row, calls server,
/// rolls back on error.
Future<void> flag(TrackRef track, String reason, String notes) async {
final api = await ref.read(quarantineApiProvider.future);
final current = state.value ?? const <QuarantineMineRow>[];
if (current.any((r) => r.trackId == track.id)) return; // already hidden
final synthetic = QuarantineMineRow(
trackId: track.id,
reason: reason,
notes: notes.isEmpty ? null : notes,
createdAt: DateTime.now().toUtc().toIso8601String(),
trackTitle: track.title,
trackDurationMs: track.durationSec * 1000,
albumId: track.albumId,
albumTitle: track.albumTitle,
artistId: track.artistId,
artistName: track.artistName,
);
state = AsyncData([synthetic, ...current]);
try {
await api.flag(track.id, reason, notes: notes);
} catch (e, st) {
state = AsyncData(current);
Error.throwWithStackTrace(e, st);
}
}
/// Optimistic unflag: removes the row, calls server, rolls back on error.
Future<void> unflag(String trackId) async {
final api = await ref.read(quarantineApiProvider.future);
final current = state.value ?? const <QuarantineMineRow>[];
final removed = current.where((r) => r.trackId == trackId).toList();
if (removed.isEmpty) return;
state = AsyncData(current.where((r) => r.trackId != trackId).toList());
try {
await api.unflag(trackId);
} catch (e, st) {
state = AsyncData(current);
Error.throwWithStackTrace(e, st);
}
}
}
final myQuarantineProvider =
AsyncNotifierProvider<MyQuarantineController, List<QuarantineMineRow>>(
MyQuarantineController.new,
);