import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../api.dart'; import '../player.dart'; import '../theme.dart'; import '../widgets.dart'; class OverviewScreen extends StatefulWidget { const OverviewScreen({super.key}); @override State createState() => _OverviewScreenState(); } class _OverviewScreenState extends State { late Future<(Map, List)> _data = _load(); Future<(Map, List)> _load() async => (await AdminApi.overview(), await AdminApi.recentPending()); void _reload() => setState(() => _data = _load()); @override Widget build(BuildContext context) => Loader( future: _data, onRetry: _reload, builder: (context, data) { final (n, pending) = data; final attention = [ if (n['pending']! > 0) _Todo(Icons.hourglass_top, NkColors.sun, '${n['pending']} songs waiting for review', 'Listen, fix the details and approve them.', '/songs?show=pending'), if (n['applications']! > 0) _Todo(Icons.how_to_reg, NkColors.sun, '${n['applications']} creator applications', 'Check their links and ID, then approve or ask for more.', '/applications'), if (n['takedowns']! > 0) _Todo(Icons.report_outlined, NkColors.roofRed, '${n['takedowns']} open takedown requests', 'Handle these in Studio (takedown_requests) for now.', null), if (n['no_singer']! > 0) _Todo(Icons.mic_off_outlined, NkColors.info, '${n['no_singer']} songs without a singer', 'Credit the singers so listeners can find them.', '/songs?show=no_singer'), if (n['no_album']! > 0) _Todo(Icons.album_outlined, NkColors.info, '${n['no_album']} songs without an album', 'Group them into albums.', '/songs?show=no_album'), if (n['needs_changes']! > 0) _Todo(Icons.edit_note, NkColors.info, '${n['needs_changes']} songs marked "needs changes"', 'Follow up once they are fixed.', '/songs?show=needs_changes'), if (n['empty_albums']! > 0) _Todo(Icons.folder_off_outlined, NkColors.muted, '${n['empty_albums']} empty albums', 'Delete them or move songs in.', '/albums'), ]; return RefreshIndicator( onRefresh: () async => _reload(), child: ListView( padding: const EdgeInsets.only(bottom: 32), children: [ PageHeader('Overview', subtitle: 'The Nanga Kathai catalog at a glance', actions: [ IconButton(tooltip: 'Refresh', onPressed: _reload, icon: const Icon(Icons.refresh)), ]), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Wrap( spacing: 12, runSpacing: 12, children: [ _Stat('Live songs', n['approved']!, '/songs?show=live'), _Stat('Waiting for review', n['pending']!, '/songs?show=pending', highlight: n['pending']! > 0), _Stat('Hidden', n['hidden']!, '/songs?show=hidden'), _Stat('All songs', n['songs']!, '/songs'), _Stat('Albums', n['albums']!, '/albums'), _Stat('Singers', n['singers']!, '/singers'), _Stat('Listener accounts', n['listeners']!, null), ], ), ), const SizedBox(height: 28), _sectionTitle(context, 'Needs attention'), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Card( child: attention.isEmpty ? const EmptyState('All clear. Nothing needs attention right now.', icon: Icons.check_circle_outline) : Column( children: [ for (final (i, t) in attention.indexed) ...[ if (i > 0) const Divider(), ListTile( leading: Icon(t.icon, color: t.color), title: Text(t.title, style: const TextStyle(fontWeight: FontWeight.w700)), subtitle: Text(t.hint), trailing: t.path == null ? null : const Icon(Icons.chevron_right), onTap: t.path == null ? null : () => context.go(t.path!), ), ], ], ), ), ), if (pending.isNotEmpty) ...[ const SizedBox(height: 28), _sectionTitle(context, 'Latest waiting for review'), Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Card( child: Column( children: [ for (final t in pending) ListTile( leading: PlayButton(t), title: Text(t.title), subtitle: Text('${creditLine(t)} ยท ${t.albumTitle ?? 'No album'}'), trailing: Text(formatDate(t.createdAt), style: Theme.of(context).textTheme.bodySmall), ), const Divider(), TextButton( onPressed: () => context.go('/songs?show=pending'), child: const Text('Review all'), ), ], ), ), ), ], ], ), ); }, ); Widget _sectionTitle(BuildContext context, String text) => Padding( padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), child: Text(text, style: Theme.of(context).textTheme.titleLarge), ); } class _Todo { const _Todo(this.icon, this.color, this.title, this.hint, this.path); final IconData icon; final Color color; final String title; final String hint; final String? path; } class _Stat extends StatelessWidget { const _Stat(this.label, this.value, this.path, {this.highlight = false}); final String label; final int value; final String? path; final bool highlight; @override Widget build(BuildContext context) => SizedBox( width: 180, child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide(color: highlight ? NkColors.sun : NkColors.outline), ), clipBehavior: Clip.antiAlias, child: InkWell( onTap: path == null ? null : () => context.go(path!), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('$value', style: Theme.of(context) .textTheme .headlineMedium ?.copyWith(color: highlight ? NkColors.sun : NkColors.cream)), const SizedBox(height: 4), Text(label, style: Theme.of(context).textTheme.bodySmall), ], ), ), ), ), ); }