diff --git a/flutter_client/lib/library/library_screen.dart b/flutter_client/lib/library/library_screen.dart index b3e85380..ebe6ea27 100644 --- a/flutter_client/lib/library/library_screen.dart +++ b/flutter_client/lib/library/library_screen.dart @@ -16,7 +16,9 @@ import '../models/page.dart' as wire; import '../models/quarantine_mine.dart'; import '../models/track.dart'; import '../player/player_provider.dart'; +import '../quarantine/quarantine_provider.dart'; import '../shared/widgets/main_app_bar_actions.dart'; +import '../shared/widgets/server_image.dart'; import '../theme/theme_extension.dart'; import 'widgets/album_card.dart'; import 'widgets/artist_card.dart'; @@ -138,9 +140,9 @@ final _likedArtistsProvider = FutureProvider>((ref) async return (await ref.watch(_likesApiProvider.future)).listArtists(); }); -final _quarantineProvider = FutureProvider>((ref) async { - return (await ref.watch(_meApiProvider.future)).quarantineMine(); -}); +// Hidden tab uses the canonical `myQuarantineProvider` (AsyncNotifier with +// optimistic flag/unflag) so flagging from any kebab and unflagging from +// the Hidden tab keep one source of truth. class LibraryScreen extends ConsumerStatefulWidget { const LibraryScreen({super.key}); @@ -459,7 +461,7 @@ class _HiddenTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; - return ref.watch(_quarantineProvider).when( + return ref.watch(myQuarantineProvider).when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))), data: (rows) => rows.isEmpty @@ -471,7 +473,7 @@ class _HiddenTab extends ConsumerWidget { ), ) : RefreshIndicator( - onRefresh: () async => ref.refresh(_quarantineProvider.future), + onRefresh: () async => ref.refresh(myQuarantineProvider.future), child: ListView.separated( itemCount: rows.length, separatorBuilder: (_, __) => Divider(height: 1, color: fs.iron), @@ -482,12 +484,12 @@ class _HiddenTab extends ConsumerWidget { } } -class _QuarantineTile extends StatelessWidget { +class _QuarantineTile extends ConsumerWidget { const _QuarantineTile({required this.row}); final QuarantineMineRow row; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; final reasonLabel = switch (row.reason) { 'bad_rip' => 'Bad rip', @@ -496,9 +498,25 @@ class _QuarantineTile extends StatelessWidget { 'duplicate' => 'Duplicate', _ => 'Other', }; + final coverUrl = row.albumId.isNotEmpty ? '/api/albums/${row.albumId}/cover' : ''; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: SizedBox( + width: 56, + height: 56, + child: coverUrl.isEmpty + ? Container(color: fs.slate) + : ServerImage( + url: coverUrl, + fit: BoxFit.cover, + fallback: Container(color: fs.slate), + ), + ), + ), + const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( @@ -515,17 +533,41 @@ class _QuarantineTile extends StatelessWidget { ), Padding( padding: const EdgeInsets.only(top: 4), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: fs.iron, - borderRadius: BorderRadius.circular(4), + child: Row(children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: fs.iron, + borderRadius: BorderRadius.circular(4), + ), + child: Text(reasonLabel, style: TextStyle(color: fs.ash, fontSize: 11)), ), - child: Text(reasonLabel, style: TextStyle(color: fs.ash, fontSize: 11)), - ), + if (row.createdAt.isNotEmpty) ...[ + const SizedBox(width: 8), + Text(_relativeTime(row.createdAt), + style: TextStyle(color: fs.ash, fontSize: 11)), + ], + ]), ), ]), ), + IconButton( + tooltip: 'Unhide', + icon: Icon(Icons.restore, color: fs.ash, size: 20), + onPressed: () async { + try { + await ref.read(myQuarantineProvider.notifier).unflag(row.trackId); + } catch (_) { + // Optimistic rollback handled by the notifier; surface + // the failure only if the user kept the tab open. + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not unhide; try again.')), + ); + } + } + }, + ), ]), ); }