Files
minstrel/flutter_client/lib/admin/admin_quarantine_screen.dart
bvandeusen 3e52ff7fa3 feat(#402): wire screen-scoped providers to liveEventsProvider
#392's dispatcher only invalidates publicly-importable providers
(myQuarantine + home). Screen-scoped providers (file-private in their
feature folders) get their own ref.listen(liveEventsProvider, ...) so
they go live without needing back-edge dependencies from /shared.

Five screens wired:

- library_screen.dart _LikedTab — invalidates _likedTracksProvider /
  _likedAlbumsProvider / _likedArtistsProvider on any of the six
  track/album/artist like/unlike kinds.

- playlist_detail_screen.dart — invalidates playlistDetailProvider(id)
  on playlist.updated / playlist.tracks_changed matching the visible
  playlist_id. On playlist.deleted matching the visible id, pops back
  so the user isn't left staring at a gone playlist.

- admin_requests_screen.dart — invalidates adminRequestsProvider on
  request.status_changed (covers user create/cancel + admin
  approve/reject + reconciler complete).

- admin_quarantine_screen.dart — invalidates adminQuarantineProvider
  on any quarantine.* event (flag from a user / admin resolve / file
  delete / lidarr delete).

- requests_screen.dart (own requests) — invalidates myRequestsProvider
  on request.status_changed. Server-side events are user-scoped via
  publishRequestStatusChanged's row.UserID, so admin actions on
  someone else's request route to the right stream.

History tab is NOT wired (no server-side play.scrobbled event yet —
documented in #402 body as deferred until that event ships).

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

68 lines
2.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../shared/live_events_provider.dart';
import '../shared/widgets/main_app_bar_actions.dart';
import '../theme/theme_extension.dart';
import 'admin_providers.dart';
import 'widgets/admin_quarantine_row.dart';
class AdminQuarantineScreen extends ConsumerWidget {
const AdminQuarantineScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
// #402 wire-up: any quarantine event (flag from a user / admin
// resolve / file delete / lidarr delete) refreshes the admin queue.
ref.listen<AsyncValue<LiveEvent>>(liveEventsProvider, (_, next) {
final e = next.asData?.value;
if (e == null) return;
if (e.kind.startsWith('quarantine.')) {
ref.invalidate(adminQuarantineProvider);
}
});
final items = ref.watch(adminQuarantineProvider);
return Scaffold(
backgroundColor: fs.obsidian,
appBar: AppBar(
backgroundColor: fs.obsidian,
elevation: 0,
title: Text('Quarantine', style: TextStyle(color: fs.parchment)),
actions: const [MainAppBarActions(currentRoute: '/admin/quarantine')],
),
body: SafeArea(
child: items.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) =>
Center(child: Text('$e', style: TextStyle(color: fs.error))),
data: (rows) {
if (rows.isEmpty) {
return Center(
child: Text('No quarantined tracks.',
style: TextStyle(color: fs.ash)),
);
}
final notifier = ref.read(adminQuarantineProvider.notifier);
return RefreshIndicator(
onRefresh: () async =>
ref.refresh(adminQuarantineProvider.future),
child: ListView.builder(
itemCount: rows.length,
itemBuilder: (_, i) => AdminQuarantineRow(
key: Key('admin_quarantine_row_${rows[i].trackId}'),
item: rows[i],
onResolve: () => notifier.resolve(rows[i].trackId),
onDeleteFile: () => notifier.deleteFile(rows[i].trackId),
onDeleteViaLidarr: () =>
notifier.deleteViaLidarr(rows[i].trackId),
),
),
);
},
),
),
);
}
}