6f20a75f9b
Same fix shape as albums: drift's CachedPlaylistAdapter.toRef was returning coverUrl: '' because the cache table doesn't persist the server-derived URL. Set it to /api/playlists/<id>/cover in the adapter — handleGetPlaylistCover serves the cached collage from disk, so the URL is deterministic and the round-trip through drift no longer drops it. PlaylistCard + PlaylistsListScreen pass the existing queue_music icon as ServerImage's fallback, so when the server hasn't built a collage yet (system playlists with no tracks at build time), the endpoint 404s and the icon shows over the slate background instead of an empty box.
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}'),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|