import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../models/admin_request.dart'; import '../shared/live_events_provider.dart'; import '../shared/widgets/main_app_bar_actions.dart'; import '../theme/theme_extension.dart'; import 'requests_provider.dart'; class RequestsScreen extends ConsumerWidget { const RequestsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; // #402 wire-up: refresh the user's own requests list on // request.status_changed. Server-side events are user-scoped // (admin actions on someone else's request route to that other // user's stream); the dispatcher's filter already drops mismatches. ref.listen>(liveEventsProvider, (_, next) { final e = next.asData?.value; if (e?.kind == 'request.status_changed') { ref.invalidate(myRequestsProvider); } }); final requests = ref.watch(myRequestsProvider); return Scaffold( backgroundColor: fs.obsidian, appBar: AppBar( backgroundColor: fs.obsidian, elevation: 0, leading: IconButton( icon: Icon(LucideIcons.arrow_left, color: fs.parchment), onPressed: () => context.pop(), ), title: Text('Your requests', style: TextStyle(color: fs.parchment)), actions: const [MainAppBarActions(currentRoute: '/requests')], ), body: SafeArea( child: requests.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Padding( padding: const EdgeInsets.all(24), child: Text('$e', style: TextStyle(color: fs.error)), ), ), data: (rows) { if (rows.isEmpty) { return Center( child: Text('Nothing requested yet.', style: TextStyle(color: fs.ash)), ); } final notifier = ref.read(myRequestsProvider.notifier); return RefreshIndicator( onRefresh: () async => ref.refresh(myRequestsProvider.future), child: ListView.separated( itemCount: rows.length, padding: const EdgeInsets.symmetric(vertical: 8), separatorBuilder: (_, __) => Divider(color: fs.iron, height: 1), itemBuilder: (_, i) => _RequestRow( key: Key('request_row_${rows[i].id}'), request: rows[i], onCancel: () => notifier.cancel(rows[i].id), ), ), ); }, ), ), ); } } class _RequestRow extends StatelessWidget { const _RequestRow({ super.key, required this.request, required this.onCancel, }); final AdminRequest request; final VoidCallback onCancel; @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; final href = _listenHref(request); return ListTile( leading: _kindAvatar(fs), title: Text( request.displayName, style: TextStyle( color: fs.parchment, fontFamily: 'Fraunces', fontSize: 16, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Row( children: [ _KindPill(label: request.kind), const SizedBox(width: 6), _StatusPill(status: request.status), ], ), if (request.importedAlbumCount > 0 || request.importedTrackCount > 0) ...[ const SizedBox(height: 4), Text( _ingestProgressText(request), style: TextStyle(color: fs.accent, fontSize: 13), ), ], if (request.status == 'rejected' && (request.notes ?? '').isNotEmpty) ...[ const SizedBox(height: 4), Text(request.notes!, style: TextStyle(color: fs.ash, fontSize: 13)), ], ], ), trailing: _trailing(context, fs, href), ); } Widget _kindAvatar(FabledSwordTheme fs) { final icon = switch (request.kind) { 'artist' => LucideIcons.disc_3, 'album' => LucideIcons.library_big, _ => LucideIcons.music, }; return CircleAvatar( backgroundColor: fs.iron, child: Icon(icon, size: 20, color: fs.ash), ); } Widget? _trailing(BuildContext context, FabledSwordTheme fs, String? href) { if (request.status == 'pending') { return TextButton.icon( onPressed: () async { final ok = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Cancel request?'), content: Text('Cancel "${request.displayName}"?'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('Keep'), ), TextButton( onPressed: () => Navigator.pop(ctx, true), style: TextButton.styleFrom(foregroundColor: fs.oxblood), child: const Text('Cancel request'), ), ], ), ); if (ok == true) onCancel(); }, icon: const Icon(LucideIcons.x, size: 16), label: const Text('Cancel'), style: TextButton.styleFrom(foregroundColor: fs.ash), ); } if (request.status == 'completed' && href != null) { return TextButton.icon( onPressed: () => context.push(href), icon: const Icon(LucideIcons.play, size: 16), label: const Text('Listen'), style: TextButton.styleFrom(foregroundColor: fs.accent), ); } return null; } String _ingestProgressText(AdminRequest r) { if (r.kind == 'artist') { final albums = '${r.importedAlbumCount} ' '${r.importedAlbumCount == 1 ? 'album' : 'albums'}'; final tracks = '${r.importedTrackCount} ' '${r.importedTrackCount == 1 ? 'track' : 'tracks'} ingested'; return '$albums ยท $tracks'; } if (r.kind == 'album') { return '${r.importedTrackCount} ' '${r.importedTrackCount == 1 ? 'track' : 'tracks'} ingested'; } return 'Track ingested'; } String? _listenHref(AdminRequest r) { if (r.matchedTrackId != null) return '/albums/${r.matchedAlbumId ?? r.matchedTrackId}'; if (r.matchedAlbumId != null) return '/albums/${r.matchedAlbumId}'; if (r.matchedArtistId != null) return '/artists/${r.matchedArtistId}'; return null; } } class _KindPill extends StatelessWidget { const _KindPill({required this.label}); final String label; @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: fs.accent.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(999), ), child: Text( label, style: TextStyle(color: fs.accent, fontSize: 11), ), ); } } class _StatusPill extends StatelessWidget { const _StatusPill({required this.status}); final String status; @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; final color = switch (status) { 'pending' => fs.ash, 'approved' => fs.bronze, 'completed' => fs.moss, 'rejected' => fs.oxblood, 'failed' => fs.oxblood, _ => fs.ash, }; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(999), ), child: Text( status, style: TextStyle(color: color, fontSize: 11), ), ); } }