diff --git a/flutter_client/lib/playlists/playlist_detail_screen.dart b/flutter_client/lib/playlists/playlist_detail_screen.dart index 468ce5a2..37d7ce9a 100644 --- a/flutter_client/lib/playlists/playlist_detail_screen.dart +++ b/flutter_client/lib/playlists/playlist_detail_screen.dart @@ -12,13 +12,26 @@ import '../theme/theme_extension.dart'; import 'playlists_provider.dart'; class PlaylistDetailScreen extends ConsumerWidget { - const PlaylistDetailScreen({required this.id, super.key}); + const PlaylistDetailScreen({required this.id, this.seed, super.key}); final String id; + /// Optional Playlist passed via go_router extra so the header + /// (title, cover, track count, play/download CTAs) can render + /// before the full detail fetch resolves. Same pattern as album + /// + artist nav hydration. + final Playlist? seed; + @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; final detail = ref.watch(playlistDetailProvider(id)); + + // Resolve the best playlist info available for the AppBar title. + final livePlaylist = detail.value?.playlist; + final headerName = (livePlaylist != null && livePlaylist.name.isNotEmpty) + ? livePlaylist.name + : (seed?.name ?? ''); + return Scaffold( backgroundColor: fs.obsidian, appBar: AppBar( @@ -28,17 +41,21 @@ class PlaylistDetailScreen extends ConsumerWidget { 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(), - ), + title: headerName.isEmpty + ? const SizedBox.shrink() + : Text( + headerName, + style: TextStyle(color: fs.parchment), + overflow: TextOverflow.ellipsis, + ), ), body: detail.when( - loading: () => const Center(child: CircularProgressIndicator()), + // Loading from a seed: render the header immediately + a + // small inline "loading tracks" hint, instead of an opaque + // full-screen spinner. The body fills in when tracks arrive. + loading: () => seed == null + ? const Center(child: CircularProgressIndicator()) + : _SeedBody(playlist: seed!), error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))), data: (d) => _Body(detail: d), @@ -47,6 +64,43 @@ class PlaylistDetailScreen extends ConsumerWidget { } } +/// Loading-state body: renders just the header from the seed +/// playlist + a "loading tracks…" placeholder. CTA buttons are +/// hidden until the real track list arrives (they need playable +/// refs to do anything useful). +class _SeedBody extends StatelessWidget { + const _SeedBody({required this.playlist}); + final Playlist playlist; + + @override + Widget build(BuildContext context) { + final fs = Theme.of(context).extension()!; + return ListView(children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (playlist.description.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Text( + playlist.description, + style: TextStyle(color: fs.ash, fontSize: 13), + ), + ), + Text( + '${playlist.trackCount} ${playlist.trackCount == 1 ? "track" : "tracks"}', + style: TextStyle(color: fs.ash, fontSize: 12), + ), + ]), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 24), + child: Center(child: CircularProgressIndicator()), + ), + ]); + } +} + class _Body extends ConsumerWidget { const _Body({required this.detail}); final PlaylistDetail detail; diff --git a/flutter_client/lib/playlists/playlists_list_screen.dart b/flutter_client/lib/playlists/playlists_list_screen.dart index 7cf4172a..bfddbf9a 100644 --- a/flutter_client/lib/playlists/playlists_list_screen.dart +++ b/flutter_client/lib/playlists/playlists_list_screen.dart @@ -51,7 +51,7 @@ class PlaylistsListScreen extends ConsumerWidget { final p = items[i]; return _PlaylistTile( playlist: p, - onTap: () => ctx.push('/playlists/${p.id}'), + onTap: () => ctx.push('/playlists/${p.id}', extra: p), ); }, ), diff --git a/flutter_client/lib/playlists/widgets/playlist_card.dart b/flutter_client/lib/playlists/widgets/playlist_card.dart index 060351a4..515bd55d 100644 --- a/flutter_client/lib/playlists/widgets/playlist_card.dart +++ b/flutter_client/lib/playlists/widgets/playlist_card.dart @@ -21,7 +21,8 @@ class PlaylistCard extends StatelessWidget { child: Material( color: Colors.transparent, child: InkWell( - onTap: () => context.push('/playlists/${playlist.id}'), + onTap: () => + context.push('/playlists/${playlist.id}', extra: playlist), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/flutter_client/lib/shared/routing.dart b/flutter_client/lib/shared/routing.dart index 73c3eac5..cc29e387 100644 --- a/flutter_client/lib/shared/routing.dart +++ b/flutter_client/lib/shared/routing.dart @@ -9,6 +9,7 @@ import '../library/album_detail_screen.dart'; import '../library/artist_detail_screen.dart'; import '../models/album.dart'; import '../models/artist.dart'; +import '../models/playlist.dart'; import '../discover/discover_screen.dart'; import '../library/home_screen.dart'; import '../library/library_screen.dart'; @@ -120,7 +121,10 @@ GoRouter buildRouter(Ref ref) { GoRoute(path: '/playlists', builder: (_, __) => const PlaylistsListScreen()), GoRoute( path: '/playlists/:id', - builder: (_, s) => PlaylistDetailScreen(id: s.pathParameters['id']!), + builder: (_, s) => PlaylistDetailScreen( + id: s.pathParameters['id']!, + seed: s.extra is Playlist ? s.extra as Playlist : null, + ), ), GoRoute(path: '/requests', builder: (_, __) => const RequestsScreen()), GoRoute(path: '/admin', builder: (_, __) => const AdminLandingScreen()),