5613fec5ef
The server has always returned an enveloped response from GET
/api/playlists; the existing client decoded it as a flat List which
fails at runtime against the actual Map shape. Existing playlists
list screen would have been broken on any production instance.
list() now returns PlaylistsList { owned, public }. The existing
list screen consumes lists.all (owned + public flattened) — same
visible output as the previous flat-list assumption.
This unblocks the home parity slice which needs the same endpoint
for the new Playlists row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
133 lines
4.5 KiB
Dart
133 lines
4.5 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),
|
|
),
|
|
),
|
|
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),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|