diff --git a/flutter_client/lib/player/now_playing_screen.dart b/flutter_client/lib/player/now_playing_screen.dart index 138974f7..93e48614 100644 --- a/flutter_client/lib/player/now_playing_screen.dart +++ b/flutter_client/lib/player/now_playing_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import '../theme/theme_extension.dart'; import 'player_provider.dart'; @@ -28,6 +29,13 @@ class NowPlayingScreen extends ConsumerWidget { icon: Icon(Icons.expand_more, color: fs.parchment), onPressed: () => Navigator.of(context).pop(), ), + actions: [ + IconButton( + icon: Icon(Icons.queue_music, color: fs.parchment), + tooltip: 'Queue', + onPressed: () => GoRouter.of(context).push('/queue'), + ), + ], ), body: SafeArea( child: Padding( diff --git a/flutter_client/lib/player/queue_screen.dart b/flutter_client/lib/player/queue_screen.dart new file mode 100644 index 00000000..f09c45c1 --- /dev/null +++ b/flutter_client/lib/player/queue_screen.dart @@ -0,0 +1,118 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../theme/theme_extension.dart'; +import 'player_provider.dart'; + +class QueueScreen extends ConsumerWidget { + const QueueScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final fs = Theme.of(context).extension()!; + final queue = ref.watch(queueProvider).value ?? const []; + final current = ref.watch(mediaItemProvider).value; + return Scaffold( + backgroundColor: fs.obsidian, + appBar: AppBar( + backgroundColor: fs.obsidian, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: fs.parchment), + onPressed: () => context.pop(), + ), + title: Text('Queue', style: TextStyle(color: fs.parchment)), + ), + body: queue.isEmpty + ? Center( + child: Text('Queue is empty.', style: TextStyle(color: fs.ash)), + ) + : ListView.separated( + itemCount: queue.length, + separatorBuilder: (_, __) => Divider(height: 1, color: fs.iron), + itemBuilder: (ctx, i) => _QueueRow( + item: queue[i], + index: i, + isCurrent: current?.id == queue[i].id, + onTap: () async { + final h = ref.read(audioHandlerProvider); + await h.skipToQueueItem(i); + await h.play(); + }, + ), + ), + ); + } +} + +class _QueueRow extends StatelessWidget { + const _QueueRow({ + required this.item, + required this.index, + required this.isCurrent, + required this.onTap, + }); + + final MediaItem item; + final int index; + final bool isCurrent; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final fs = Theme.of(context).extension()!; + final dur = item.duration; + String? durLabel; + if (dur != null) { + final m = (dur.inSeconds ~/ 60).toString().padLeft(2, '0'); + final s = (dur.inSeconds % 60).toString().padLeft(2, '0'); + durLabel = '$m:$s'; + } + return InkWell( + onTap: isCurrent ? null : onTap, + child: Container( + decoration: BoxDecoration( + border: isCurrent + ? Border(left: BorderSide(color: fs.accent, width: 2)) + : null, + color: isCurrent ? fs.iron : null, + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row(children: [ + if (isCurrent) + Padding( + padding: const EdgeInsets.only(right: 8), + child: Icon(Icons.graphic_eq, color: fs.accent, size: 16), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: isCurrent ? fs.accent : fs.parchment, + fontSize: 14, + fontWeight: isCurrent ? FontWeight.w500 : FontWeight.w400, + ), + ), + Text( + '${item.artist ?? ''} ยท ${item.album ?? ''}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.ash, fontSize: 12), + ), + ], + ), + ), + if (durLabel != null) + Text(durLabel, style: TextStyle(color: fs.ash, fontSize: 12)), + ]), + ), + ); + } +} diff --git a/flutter_client/lib/shared/routing.dart b/flutter_client/lib/shared/routing.dart index 7631f309..79972043 100644 --- a/flutter_client/lib/shared/routing.dart +++ b/flutter_client/lib/shared/routing.dart @@ -11,6 +11,7 @@ import '../library/home_screen.dart'; import '../library/library_screen.dart'; import '../player/now_playing_screen.dart'; import '../player/player_bar.dart'; +import '../player/queue_screen.dart'; import '../playlists/playlist_detail_screen.dart'; import '../playlists/playlists_list_screen.dart'; import '../search/search_screen.dart'; @@ -55,6 +56,7 @@ GoRouter buildRouter(Ref ref) { builder: (_, s) => AlbumDetailScreen(id: s.pathParameters['id']!), ), GoRoute(path: '/now-playing', builder: (_, __) => const NowPlayingScreen()), + GoRoute(path: '/queue', builder: (_, __) => const QueueScreen()), GoRoute(path: '/search', builder: (_, __) => const SearchScreen()), GoRoute(path: '/library', builder: (_, __) => const LibraryScreen()), GoRoute(path: '/playlists', builder: (_, __) => const PlaylistsListScreen()),