835592f073
Mechanical sweep across 30 files: every Material Icons.* replaced with the signed-off Lucide equivalent + a flutter_lucide import per file. Zero Material Icons.* remain in lib/; no unused imports. Judgment-call mappings: album->disc_3, library_music->library_big, playlist_play->list_video, graphic_eq->audio_lines, system_update->download, restore->archive_restore, download_done->circle_check_big. track_actions_sheet like menu row: collapsed `liked ? favorite : favorite_border` to a single LucideIcons.heart (the row's Like/Unlike text label conveys state). Icon-only LikeButton + the notification keep the filled-vs-outline shape per the design decision. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
4.7 KiB
Dart
138 lines
4.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_lucide/flutter_lucide.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(LucideIcons.list_music, color: fs.ash)
|
|
: ServerImage(
|
|
url: playlist.coverUrl,
|
|
fit: BoxFit.cover,
|
|
fallback: Icon(LucideIcons.list_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(LucideIcons.chevron_right, color: fs.ash),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|