a3c0aed63e
Same pattern as album + artist detail. PlaylistDetailScreen accepts optional Playlist seed via go_router extra. While playlistDetailProvider is still resolving, render the header (description if any + track count) plus a "loading tracks…" inline spinner instead of an opaque full-screen CircularProgressIndicator. The body fills in when tracks arrive via drift watch re-emit. Routing reads s.extra as Playlist. PlaylistCard + PlaylistsListScreen pass the playlist via extra. Surfaces with no seed available (deep links etc.) fall back to the original full- screen spinner. Track row rendering already uses ListView.builder so visible row count was never the bottleneck — the wait was for the detail fetch itself. Header-on-tap is the win that makes the screen feel snappy.
137 lines
4.6 KiB
Dart
137 lines
4.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../models/playlist.dart';
|
|
import '../shared/widgets/main_app_bar_actions.dart';
|
|
import '../shared/widgets/server_image.dart';
|
|
import '../theme/theme_extension.dart';
|
|
import 'playlists_provider.dart';
|
|
|
|
class PlaylistsListScreen extends ConsumerWidget {
|
|
const PlaylistsListScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
|
// "all" returns user-created + system mixes. Most useful default
|
|
// on mobile where the user wants to see for-you/discover alongside
|
|
// their own playlists in one tap.
|
|
final list = ref.watch(playlistsListProvider('all'));
|
|
return Scaffold(
|
|
backgroundColor: fs.obsidian,
|
|
appBar: AppBar(
|
|
backgroundColor: fs.obsidian,
|
|
elevation: 0,
|
|
title: Text('Playlists', style: TextStyle(color: fs.parchment)),
|
|
actions: const [MainAppBarActions(currentRoute: '/playlists')],
|
|
),
|
|
body: list.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(
|
|
child: Text('$e', style: TextStyle(color: fs.error)),
|
|
),
|
|
data: (lists) {
|
|
final items = lists.all;
|
|
if (items.isEmpty) {
|
|
return Center(
|
|
child: Text(
|
|
'No playlists yet.',
|
|
style: TextStyle(color: fs.ash),
|
|
),
|
|
);
|
|
}
|
|
return RefreshIndicator(
|
|
onRefresh: () async => ref.refresh(playlistsListProvider('all').future),
|
|
child: ListView.separated(
|
|
itemCount: items.length,
|
|
separatorBuilder: (_, __) =>
|
|
Divider(height: 1, color: fs.iron),
|
|
itemBuilder: (ctx, i) {
|
|
final p = items[i];
|
|
return _PlaylistTile(
|
|
playlist: p,
|
|
onTap: () => ctx.push('/playlists/${p.id}', extra: p),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PlaylistTile extends StatelessWidget {
|
|
const _PlaylistTile({required this.playlist, required this.onTap});
|
|
final Playlist playlist;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
|
return InkWell(
|
|
onTap: onTap,
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
child: Row(children: [
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(6),
|
|
child: Container(
|
|
width: 56,
|
|
height: 56,
|
|
color: fs.slate,
|
|
child: playlist.coverUrl.isEmpty
|
|
? Icon(Icons.queue_music, color: fs.ash)
|
|
: ServerImage(
|
|
url: playlist.coverUrl,
|
|
fit: BoxFit.cover,
|
|
fallback: Icon(Icons.queue_music, color: fs.ash),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(children: [
|
|
Flexible(
|
|
child: Text(
|
|
playlist.name,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(color: fs.parchment, fontSize: 15),
|
|
),
|
|
),
|
|
if (playlist.isSystem) ...[
|
|
const SizedBox(width: 8),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 6, vertical: 2),
|
|
decoration: BoxDecoration(
|
|
color: fs.iron,
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Text(
|
|
playlist.systemVariant!.replaceAll('_', ' '),
|
|
style: TextStyle(color: fs.accent, fontSize: 11),
|
|
),
|
|
),
|
|
],
|
|
]),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
'${playlist.trackCount} ${playlist.trackCount == 1 ? "track" : "tracks"}',
|
|
style: TextStyle(color: fs.ash, fontSize: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, color: fs.ash),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|