import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../cache/audio_cache_manager.dart'; import '../cache/db.dart'; import '../models/playlist.dart'; import '../models/track.dart'; import '../player/player_provider.dart'; import '../shared/widgets/track_actions/track_actions_button.dart'; import '../theme/theme_extension.dart'; import 'playlists_provider.dart'; class PlaylistDetailScreen extends ConsumerWidget { const PlaylistDetailScreen({required this.id, super.key}); final String id; @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; final detail = ref.watch(playlistDetailProvider(id)); return Scaffold( backgroundColor: fs.obsidian, appBar: AppBar( backgroundColor: fs.obsidian, elevation: 0, leading: IconButton( icon: Icon(Icons.arrow_back, color: fs.parchment), onPressed: () => context.pop(), ), title: detail.maybeWhen( data: (d) => Text( d.playlist.name, style: TextStyle(color: fs.parchment), overflow: TextOverflow.ellipsis, ), orElse: () => const SizedBox.shrink(), ), ), body: detail.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))), data: (d) => _Body(detail: d), ), ); } } class _Body extends ConsumerWidget { const _Body({required this.detail}); final PlaylistDetail detail; @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; final tracks = detail.tracks; final playable = tracks.where((t) => t.isAvailable).toList(); return RefreshIndicator( onRefresh: () async => ref.refresh(playlistDetailProvider(detail.playlist.id).future), child: ListView.builder( // Header + (track rows | empty hint). itemCount: tracks.isEmpty ? 2 : tracks.length + 1, itemBuilder: (ctx, i) { if (i == 0) return _Header(detail: detail, playable: playable); if (tracks.isEmpty) { return Padding( padding: const EdgeInsets.all(24), child: Center( child: Text( 'No tracks yet. Add some via the "Add to playlist…" entry on any track row.', style: TextStyle(color: fs.ash), textAlign: TextAlign.center, ), ), ); } final t = tracks[i - 1]; return _PlaylistTrackRow( row: t, onTap: t.isAvailable ? () { final ref = ProviderScope.containerOf(ctx); final liveTrack = _toTrackRef(t); final playableRefs = playable.map(_toTrackRef).toList(growable: false); final startIdx = playable.indexWhere((p) => p.trackId == t.trackId); ref.read(playerActionsProvider).playTracks( playableRefs, initialIndex: startIdx >= 0 ? startIdx : 0, ); // Keep liveTrack referenced to avoid an unused-variable // warning while we leave hooks for menu wiring later. assert(liveTrack.id == t.trackId); } : null, ); }, ), ); } } TrackRef _toTrackRef(PlaylistTrack t) => TrackRef( id: t.trackId ?? '', title: t.title, albumId: t.albumId ?? '', albumTitle: t.albumTitle, artistId: t.artistId ?? '', artistName: t.artistName, durationSec: t.durationSec, streamUrl: t.streamUrl ?? '', ); class _Header extends ConsumerWidget { const _Header({required this.detail, required this.playable}); final PlaylistDetail detail; final List playable; @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; final p = detail.playlist; return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ if (p.description.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 12), child: Text( p.description, style: TextStyle(color: fs.ash, fontSize: 13), ), ), Row(children: [ Text( '${p.trackCount} ${p.trackCount == 1 ? "track" : "tracks"}', style: TextStyle(color: fs.ash, fontSize: 12), ), const Spacer(), if (playable.isNotEmpty) ...[ OutlinedButton.icon( key: const Key('download_playlist_button'), onPressed: () { final mgr = ref.read(audioCacheManagerProvider); for (final t in playable) { final id = t.trackId ?? ''; if (id.isEmpty) continue; // Fire-and-forget; downloads happen in background. // ignore: unawaited_futures mgr.pin(id, source: CacheSource.autoPlaylist); } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Downloading ${playable.length} tracks…')), ); }, icon: const Icon(Icons.download, size: 16), label: const Text('Download'), ), const SizedBox(width: 8), FilledButton.icon( onPressed: () { final refs = playable.map(_toTrackRef).toList(growable: false); ref.read(playerActionsProvider).playTracks(refs); }, icon: const Icon(Icons.play_arrow), label: const Text('Play'), style: FilledButton.styleFrom( backgroundColor: fs.accent, foregroundColor: fs.parchment, ), ), ], ]), ]), ); } } class _PlaylistTrackRow extends StatelessWidget { const _PlaylistTrackRow({required this.row, required this.onTap}); final PlaylistTrack row; final VoidCallback? onTap; @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; final mins = (row.durationSec ~/ 60).toString().padLeft(2, '0'); final secs = (row.durationSec % 60).toString().padLeft(2, '0'); final color = row.isAvailable ? fs.parchment : fs.ash; return InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row(children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( row.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: color, fontSize: 14, decoration: row.isAvailable ? null : TextDecoration.lineThrough, ), ), Text( '${row.artistName} · ${row.albumTitle}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: fs.ash, fontSize: 12), ), ], ), ), Text('$mins:$secs', style: TextStyle(color: fs.ash, fontSize: 12)), if (row.trackId != null) TrackActionsButton(track: _toTrackRef(row)), ]), ), ); } }