import 'package:flutter/material.dart'; import '../api.dart'; import '../pickers.dart'; import '../theme.dart'; import '../widgets.dart'; typedef _Lookups = (List, List); Future<_Lookups> _lookups() async => (await AdminApi.artists(), await AdminApi.albums()); /// Edits one song. Returns true when something was saved. Future editSong(BuildContext context, AdminTrack track) async => await showDialog(context: context, builder: (_) => _LookupsDialog(builder: (l) => _SongForm(track, l))) ?? false; /// Changes chosen fields on many songs at once. Returns true when something was saved. Future bulkEditSongs(BuildContext context, List ids) async => await showDialog(context: context, builder: (_) => _LookupsDialog(builder: (l) => _BulkForm(ids, l))) ?? false; class _LookupsDialog extends StatefulWidget { const _LookupsDialog({required this.builder}); final Widget Function(_Lookups lookups) builder; @override State<_LookupsDialog> createState() => _LookupsDialogState(); } class _LookupsDialogState extends State<_LookupsDialog> { late Future<_Lookups> _data = _lookups(); @override Widget build(BuildContext context) => FutureBuilder( future: _data, builder: (context, snap) { if (snap.hasData) return widget.builder(snap.data!); return AlertDialog( content: SizedBox( width: 320, height: 120, child: snap.hasError ? Center(child: Text(errorText(snap.error!))) : const Center(child: CircularProgressIndicator()), ), actions: [ if (snap.hasError) TextButton(onPressed: () => setState(() => _data = _lookups()), child: const Text('Retry')), TextButton(onPressed: () => Navigator.pop(context), child: const Text('Close')), ], ); }, ); } Widget _occasionField(String? value, ValueChanged onChanged) => DropdownButtonFormField( initialValue: value, decoration: const InputDecoration(labelText: 'Occasion'), items: [for (final e in occasions.entries) DropdownMenuItem(value: e.key, child: Text(e.value))], onChanged: onChanged, ); Widget _statusField(SongStatus? value, ValueChanged onChanged) => DropdownButtonFormField( initialValue: value, decoration: const InputDecoration(labelText: 'Status'), items: [for (final s in SongStatus.values) DropdownMenuItem(value: s, child: Text(s.label))], onChanged: onChanged, ); class _SongForm extends StatefulWidget { const _SongForm(this.track, this.lookups); final AdminTrack track; final _Lookups lookups; @override State<_SongForm> createState() => _SongFormState(); } class _SongFormState extends State<_SongForm> { late final t = widget.track; late final _title = TextEditingController(text: t.title); late final _tamil = TextEditingController(text: t.titleTamil ?? ''); late final _meaning = TextEditingController(text: t.meaning ?? ''); late final _year = TextEditingController(text: t.year?.toString() ?? ''); late String? _albumId = t.albumId; late String _occasion = t.occasion; late List _singers = [...t.singerIds]; late SongStatus _status = t.status; bool _saving = false; @override void dispose() { for (final c in [_title, _tamil, _meaning, _year]) { c.dispose(); } super.dispose(); } Future _save() async { if (_title.text.trim().isEmpty) { showMessage(context, 'The song needs a title.'); return; } final year = int.tryParse(_year.text.trim()); if (_year.text.trim().isNotEmpty && year == null) { showMessage(context, 'The year should be a number, like 1998.'); return; } setState(() => _saving = true); try { await AdminApi.saveSong( t, title: _title.text.trim(), titleTamil: trimmedOrNull(_tamil.text), meaning: trimmedOrNull(_meaning.text), year: year, albumId: _albumId, occasion: _occasion, status: _status, singerIds: _singers, ); if (mounted) Navigator.pop(context, true); } catch (e) { if (mounted) showError(context, e); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final (artists, albums) = widget.lookups; return AlertDialog( title: const Text('Edit song'), content: DialogBody( width: 560, children: [ TextField(controller: _title, decoration: const InputDecoration(labelText: 'Title (English letters)')), TextField(controller: _tamil, decoration: const InputDecoration(labelText: 'Title in Tamil script')), TextField(controller: _meaning, decoration: const InputDecoration(labelText: 'Meaning in English')), Row( children: [ Expanded(child: _occasionField(_occasion, (v) => setState(() => _occasion = v ?? _occasion))), const SizedBox(width: 12), SizedBox( width: 120, child: TextField( controller: _year, keyboardType: TextInputType.number, decoration: const InputDecoration(labelText: 'Year'), ), ), ], ), AlbumPicker(albums: albums, albumId: _albumId, onChanged: (v) => setState(() => _albumId = v)), Text('Singers', style: Theme.of(context).textTheme.titleMedium), SingerPicker(artists: artists, selected: _singers, onChanged: (v) => setState(() => _singers = v)), _statusField(_status, (v) => setState(() => _status = v ?? _status)), if (t.status == SongStatus.pending && _status == SongStatus.pending) const Text('Tip: set the status to "Live" to publish this song in the app.', style: TextStyle(color: NkColors.muted)), _Facts(t), ], ), actions: [ TextButton(onPressed: _saving ? null : () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton(onPressed: _saving ? null : _save, child: Text(_saving ? 'Saving…' : 'Save')), ], ); } } class _Facts extends StatelessWidget { const _Facts(this.t); final AdminTrack t; @override Widget build(BuildContext context) { final style = Theme.of(context).textTheme.bodySmall; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Divider(), const SizedBox(height: 8), Text('Added ${formatDate(t.createdAt)} · ${formatDuration(t.duration)}', style: style), if (t.sourceNote != null) Text('Source: ${t.sourceNote}', style: style), if (t.audioKey != null) SelectableText('File: ${t.audioKey}', style: style), if (t.reviewNote != null) Text('Review note: ${t.reviewNote}', style: style), ], ); } } class _BulkForm extends StatefulWidget { const _BulkForm(this.ids, this.lookups); final List ids; final _Lookups lookups; @override State<_BulkForm> createState() => _BulkFormState(); } class _BulkFormState extends State<_BulkForm> { bool _setAlbum = false, _setOccasion = false, _setSingers = false, _setStatus = false; String? _albumId; String? _occasion; List _singers = []; bool _replaceSingers = false; SongStatus? _status; bool _saving = false; Future _save() async { if (_setOccasion && _occasion == null || _setStatus && _status == null) { showMessage(context, 'Choose a value for each ticked field.'); return; } if (_setSingers && _singers.isEmpty && !_replaceSingers) { showMessage(context, 'Choose at least one singer to add.'); return; } if (!(_setAlbum || _setOccasion || _setSingers || _setStatus)) { showMessage(context, 'Tick at least one field to change.'); return; } setState(() => _saving = true); try { await AdminApi.updateSongs( widget.ids, SongChanges( albumId: _setAlbum ? _albumId : null, clearAlbum: _setAlbum && _albumId == null, occasion: _setOccasion ? _occasion : null, status: _setStatus ? _status : null, singerIds: _setSingers ? _singers : null, replaceSingers: _replaceSingers, ), ); if (mounted) Navigator.pop(context, true); } catch (e) { if (mounted) showError(context, e); } finally { if (mounted) setState(() => _saving = false); } } Widget _toggle(String label, bool value, ValueChanged onChanged, Widget child) => Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ CheckboxListTile( contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, title: Text(label, style: const TextStyle(fontWeight: FontWeight.w700)), value: value, onChanged: (v) => setState(() => onChanged(v ?? false)), ), if (value) Padding(padding: const EdgeInsets.only(left: 40), child: child), ], ); @override Widget build(BuildContext context) { final (artists, albums) = widget.lookups; return AlertDialog( title: Text('Edit ${widget.ids.length} songs'), content: DialogBody( width: 560, children: [ const Text('Tick what to change. Everything else stays as it is.', style: TextStyle(color: NkColors.muted)), _toggle('Album', _setAlbum, (v) => _setAlbum = v, Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AlbumPicker(albums: albums, albumId: _albumId, onChanged: (v) => setState(() => _albumId = v)), if (_albumId == null) const Padding( padding: EdgeInsets.only(top: 6), child: Text('No album chosen: the songs will be removed from their album.', style: TextStyle(color: NkColors.muted)), ), ], )), _toggle('Occasion', _setOccasion, (v) => _setOccasion = v, _occasionField(_occasion, (v) => setState(() => _occasion = v))), _toggle('Singers', _setSingers, (v) => _setSingers = v, Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SegmentedButton( segments: const [ ButtonSegment(value: false, label: Text('Add to existing')), ButtonSegment(value: true, label: Text('Replace all')), ], selected: {_replaceSingers}, onSelectionChanged: (s) => setState(() => _replaceSingers = s.first), ), const SizedBox(height: 10), SingerPicker(artists: artists, selected: _singers, onChanged: (v) => setState(() => _singers = v)), ], )), _toggle('Status', _setStatus, (v) => _setStatus = v, _statusField(_status, (v) => setState(() => _status = v))), ], ), actions: [ TextButton(onPressed: _saving ? null : () => Navigator.pop(context, false), child: const Text('Cancel')), FilledButton( onPressed: _saving ? null : _save, child: Text(_saving ? 'Saving…' : 'Apply to ${widget.ids.length} songs'), ), ], ); } }