import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../api.dart'; import '../cover_editor.dart'; import '../pickers.dart'; import '../theme.dart'; import '../widgets.dart'; class AlbumsScreen extends StatefulWidget { const AlbumsScreen({super.key}); @override State createState() => _AlbumsScreenState(); } class _AlbumsScreenState extends State { String _query = ''; late Future> _data = AdminApi.albums(); void _reload() => setState(() => _data = AdminApi.albums(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([AdminAlbum? album]) async { final title = TextEditingController(text: album?.title ?? ''); final tamil = TextEditingController(text: album?.titleTamil ?? ''); final year = TextEditingController(text: album?.year?.toString() ?? ''); final ok = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(album == null ? 'New album' : 'Edit album'), content: DialogBody(children: [ TextField(controller: title, autofocus: true, decoration: const InputDecoration(labelText: 'Title (English letters)')), TextField(controller: tamil, decoration: const InputDecoration(labelText: 'Title in Tamil script')), TextField(controller: year, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Year')), if (album != null) CoverEditor( table: 'albums', id: album.id, coverUrl: album.coverUrl, hint: 'Shown for the album and for its songs that have no cover of their own.', ) else const Text('You can add a cover photo after saving.', style: TextStyle(color: NkColors.muted)), ]), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Save')), ], ), ); if (ok != true || title.text.trim().isEmpty) { if (album != null) _reload(); // the cover may have changed return; } final fields = (title.text.trim(), trimmedOrNull(tamil.text), int.tryParse(year.text.trim())); if (album == null) { await _run(AdminApi.createAlbum(title: fields.$1, titleTamil: fields.$2, year: fields.$3), 'Album added'); } else { await _run(AdminApi.updateAlbum(album.id, title: fields.$1, titleTamil: fields.$2, year: fields.$3), 'Album saved'); } } Future _merge(AdminAlbum from, List all) async { String? into; final ok = await showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setLocal) => AlertDialog( title: Text('Merge "${from.title}"'), content: DialogBody(children: [ Text('Its ${from.trackCount} songs move into the album you choose, then "${from.title}" is deleted. ' 'Use this for duplicates.'), AlbumPicker( albums: [for (final a in all) if (a.id != from.id) a], albumId: into, onChanged: (v) => setLocal(() => into = v), ), ]), actions: [ TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton(onPressed: into == null ? null : () => Navigator.pop(context, true), child: const Text('Merge')), ], ), ), ); if (ok == true && into != null) await _run(AdminApi.mergeAlbums(from.id, into!), 'Albums merged'); } Future _delete(AdminAlbum a) async { final body = a.trackCount == 0 ? 'This album has no songs.' : 'Its ${a.trackCount} songs stay in the catalog without an album.'; if (await confirm(context, 'Delete "${a.title}"?', body, action: 'Delete', danger: true)) { await _run(AdminApi.deleteAlbum(a.id), 'Album deleted'); } } @override Widget build(BuildContext context) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ PageHeader('Albums', subtitle: 'Rename, merge duplicates and open their songs', actions: [ SearchField( hint: 'Search albums', onChanged: (q) { _query = q; _reload(); }, ), FilledButton.icon(onPressed: _edit, icon: const Icon(Icons.add), label: const Text('New album')), ]), Expanded( child: Loader( future: _data, onRetry: _reload, builder: (context, albums) => albums.isEmpty ? const EmptyState('No albums found.', icon: Icons.album_outlined) : ListView( padding: const EdgeInsets.fromLTRB(24, 0, 24, 24), children: [ Card( clipBehavior: Clip.antiAlias, child: Column( children: [ for (final (i, a) in albums.indexed) ...[ if (i > 0) const Divider(), ListTile( leading: a.coverUrl == null ? const Icon(Icons.album, color: NkColors.muted) : ClipRRect( borderRadius: BorderRadius.circular(6), child: Image.network(a.coverUrl!, width: 40, height: 40, fit: BoxFit.cover), ), title: Text(a.title, style: const TextStyle(fontWeight: FontWeight.w700)), subtitle: Text([ ?a.titleTamil, if (a.year != null) '${a.year}', '${a.trackCount} songs, ${a.approvedCount} live', ].join(' · ')), onTap: () => context.go(Uri(path: '/songs', queryParameters: {'album': a.id, 'name': a.title}).toString()), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (a.trackCount == 0) const Tag('Empty'), IconButton(tooltip: 'Edit', onPressed: () => _edit(a), icon: const Icon(Icons.edit_outlined)), PopupMenuButton( tooltip: 'More', onSelected: (v) => v == 'merge' ? _merge(a, albums) : _delete(a), itemBuilder: (context) => const [ PopupMenuItem(value: 'merge', child: Text('Merge into another album…')), PopupMenuItem(value: 'delete', child: Text('Delete')), ], ), ], ), ), ], ], ), ), ], ), ), ), ], ); }