import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../app/theme.dart'; import '../../audio/player_providers.dart'; import '../../core/models.dart'; import '../../offline/download_store.dart'; import '../../shared/widgets/artwork.dart'; import '../../shared/widgets/section_header.dart'; /// Songs saved on this phone. Works with no internet: everything shown here /// comes from the download index, not the server. class DownloadsScreen extends ConsumerWidget { const DownloadsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final d = ref.watch(downloadsProvider); final controller = ref.read(playerControllerProvider); final store = ref.read(downloadStoreProvider); final text = Theme.of(context).textTheme; final current = ref.watch(mediaItemProvider).value?.id; return SafeArea( bottom: false, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(18, 12, 8, 4), child: Row( children: [ Expanded(child: Text('Downloads', style: text.headlineMedium)), if (d.entries.isNotEmpty) PopupMenuButton( tooltip: 'More', itemBuilder: (_) => [ PopupMenuItem( onTap: () => _confirmRemoveAll(context, store), child: const Text('Remove all downloads'), ), ], ), ], ), ), if (d.entries.isNotEmpty || d.progress.isNotEmpty) Padding( padding: const EdgeInsets.fromLTRB(18, 0, 18, 8), child: Row( children: [ Expanded( child: Text( '${d.entries.length} songs · ${_size(d.totalBytes)}' '${d.progress.isEmpty ? '' : ' · ${d.progress.length} downloading'}', style: const TextStyle(color: NkColors.muted), ), ), if (d.entries.isNotEmpty) ...[ IconButton( tooltip: 'Shuffle', icon: const Icon(Icons.shuffle, color: NkColors.sun), onPressed: () { final list = [...d.entries]..shuffle(); controller.playDownloads(list); }, ), IconButton.filled( tooltip: 'Play all', onPressed: () => controller.playDownloads(d.entries), icon: const Icon(Icons.play_arrow_rounded), ), ], ], ), ), Expanded( child: d.entries.isEmpty && d.progress.isEmpty ? const Center( child: SingleChildScrollView( child: EmptyState( icon: Icons.download_for_offline_outlined, title: 'No downloads yet', message: 'Tap ⋮ on any song, or "Download album", to listen without signal in the hills. ' 'Downloads play only inside Nanga Kathai.', ), ), ) : ListView( padding: const EdgeInsets.fromLTRB(18, 0, 18, 24), children: [ for (final p in d.progress.entries) Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ const SizedBox.square( dimension: 52, child: Center(child: Icon(Icons.downloading_rounded, color: NkColors.muted)), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(p.value == 0 ? 'Waiting…' : 'Downloading ${(p.value * 100).round()}%', style: const TextStyle(color: NkColors.muted)), const SizedBox(height: 6), LinearProgressIndicator(value: p.value == 0 ? null : p.value), ], ), ), ], ), ), for (var i = 0; i < d.entries.length; i++) _DownloadTile( entry: d.entries[i], isCurrent: d.entries[i].trackId == current, onTap: () => controller.playDownloads(d.entries, index: i), onRemove: () => store.remove(d.entries[i].trackId), ), ], ), ), ], ), ); } static String _size(int bytes) => bytes >= 1 << 30 ? '${(bytes / (1 << 30)).toStringAsFixed(1)} GB' : '${(bytes / (1 << 20)).round()} MB'; void _confirmRemoveAll(BuildContext context, DownloadStore store) { showDialog( context: context, builder: (dialog) => AlertDialog( title: const Text('Remove all downloads?'), content: const Text('The songs stay in the app; you can download them again any time.'), actions: [ TextButton(onPressed: () => Navigator.pop(dialog), child: const Text('Cancel')), FilledButton( onPressed: () { store.removeAll(); Navigator.pop(dialog); }, child: const Text('Remove'), ), ], ), ); } } class _DownloadTile extends StatelessWidget { const _DownloadTile({required this.entry, required this.isCurrent, required this.onTap, required this.onRemove}); final DownloadEntry entry; final bool isCurrent; final VoidCallback onTap; final VoidCallback onRemove; @override Widget build(BuildContext context) { final text = Theme.of(context).textTheme; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row( children: [ ClipRRect( borderRadius: BorderRadius.circular(10), child: SizedBox.square( dimension: 52, child: OccasionCover(occasion: Occasion.fromId(entry.occasion), size: 52), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(entry.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: text.titleMedium?.copyWith(color: isCurrent ? NkColors.sun : NkColors.cream)), const SizedBox(height: 2), Row( children: [ const Icon(Icons.download_done_rounded, size: 16, color: NkColors.teaLeaf), const SizedBox(width: 4), Expanded( child: Text( '${entry.credit} · ${formatDuration(Duration(milliseconds: entry.durationMs))}', maxLines: 1, overflow: TextOverflow.ellipsis, style: text.bodyMedium?.copyWith(color: NkColors.muted), ), ), ], ), ], ), ), IconButton( tooltip: 'Remove download', icon: const Icon(Icons.delete_outline, color: NkColors.muted), onPressed: onRemove, ), ], ), ), ); } }