import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../api.dart'; import '../pickers.dart'; import '../theme.dart'; import '../widgets.dart'; class SingersScreen extends StatefulWidget { const SingersScreen({super.key}); @override State createState() => _SingersScreenState(); } class _SingersScreenState extends State { String _query = ''; late Future> _data = AdminApi.artists(); void _reload() => setState(() => _data = AdminApi.artists(query: _query)); Future _run(Future job, String done) async { try { await job; if (mounted) showMessage(context, done); _reload(); } catch (e) { if (mounted) showError(context, e); } } Future _edit([AdminArtist? a]) async { final name = TextEditingController(text: a?.name ?? ''); final tamil = TextEditingController(text: a?.nameTamil ?? ''); final hatti = TextEditingController(text: a?.hattiName ?? ''); final bio = TextEditingController(text: a?.bio ?? ''); var verified = a?.verified ?? false; final ok = await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setLocal) => AlertDialog( title: Text(a == null ? 'New singer' : 'Edit singer'), content: DialogBody(children: [ TextField(controller: name, autofocus: true, decoration: const InputDecoration(labelText: 'Name (English letters)')), TextField(controller: tamil, decoration: const InputDecoration(labelText: 'Name in Tamil script')), TextField(controller: hatti, decoration: const InputDecoration(labelText: 'Village / hatti')), TextField(controller: bio, maxLines: 4, decoration: const InputDecoration(labelText: 'About')), SwitchListTile( contentPadding: EdgeInsets.zero, title: const Text('Verified'), subtitle: const Text('We have confirmed who this singer is.'), value: verified, onChanged: (v) => setLocal(() => verified = v), ), ]), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Save')), ], ), ), ); if (ok != true || name.text.trim().isEmpty) return; if (a == null) { await _run( AdminApi.createArtist( name: name.text.trim(), nameTamil: trimmedOrNull(tamil.text), hatti: hatti.text, bio: trimmedOrNull(bio.text), verified: verified), 'Singer added', ); } else { await _run( AdminApi.updateArtist(a.id, name: name.text.trim(), nameTamil: trimmedOrNull(tamil.text), hatti: hatti.text, bio: trimmedOrNull(bio.text), verified: verified), 'Singer saved', ); } } Future _merge(AdminArtist from, List all) async { var into = []; final ok = await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setLocal) => AlertDialog( title: Text('Merge "${from.name}"'), content: DialogBody(children: [ Text('Their ${from.trackCount} songs are credited to the singer you choose, then "${from.name}" is ' 'deleted. Use this when the same person was entered twice.'), SingerPicker( artists: [for (final a in all) if (a.id != from.id) a], selected: into, onChanged: (v) => setLocal(() => into = v.isEmpty ? v : [v.last]), ), ]), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton(onPressed: into.isEmpty ? null : () => Navigator.pop(context, true), child: const Text('Merge')), ], ), ), ); if (ok == true && into.isNotEmpty) await _run(AdminApi.mergeArtists(from.id, into.single), 'Singers merged'); } Future _delete(AdminArtist a) async { final body = a.trackCount == 0 ? 'No songs credit this singer.' : 'Their name is removed from ${a.trackCount} songs. The songs stay.'; if (await confirm(context, 'Delete "${a.name}"?', body, action: 'Delete', danger: true)) { await _run(AdminApi.deleteArtist(a.id), 'Singer deleted'); } } @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ PageHeader('Singers', subtitle: 'Names, villages, verification and duplicates', actions: [ SearchField( hint: 'Search singers', onChanged: (q) { _query = q; _reload(); }, ), FilledButton.icon(onPressed: _edit, icon: const Icon(Icons.add), label: const Text('New singer')), ]), Expanded( child: Loader( future: _data, onRetry: _reload, builder: (context, artists) => artists.isEmpty ? const EmptyState('No singers found.', icon: Icons.mic_off_outlined) : ListView( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), children: [ Card( clipBehavior: Clip.antiAlias, child: Column( children: [ for (final (i, a) in artists.indexed) ...[ if (i > 0) const Divider(), ListTile( leading: CircleAvatar( backgroundColor: NkColors.raised, child: Text(a.name.characters.first.toUpperCase(), style: const TextStyle(color: NkColors.sun, fontWeight: FontWeight.w800)), ), title: Text(a.name, style: const TextStyle(fontWeight: FontWeight.w700)), subtitle: Text([?a.nameTamil, ?a.hattiName, '${a.trackCount} songs'].join(' · ')), onTap: () => context.go(Uri(path: '/songs', queryParameters: {'singer': a.id, 'name': a.name}).toString()), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (a.verified) const Tag('Verified', color: NkColors.teaLeaf), if (a.claimed) ...[const SizedBox(width: 6), const Tag('Has account', color: NkColors.info)], IconButton(tooltip: 'Edit', onPressed: () => _edit(a), icon: const Icon(Icons.edit_outlined)), PopupMenuButton( tooltip: 'More', onSelected: (v) => v == 'merge' ? _merge(a, artists) : _delete(a), itemBuilder: (context) => const [ PopupMenuItem(value: 'merge', child: Text('Merge into another singer…')), PopupMenuItem(value: 'delete', child: Text('Delete')), ], ), ], ), ), ], ], ), ), ], ), ), ), ], ); }