From 785da526d35f420bda0a9d3c969215e6aa29340d Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 28 Aug 2024 01:23:37 +0800 Subject: [PATCH] :sparkles: Player queue --- lib/providers/audio_player.dart | 9 ++ lib/screens/player/queue.dart | 26 ++++++ lib/screens/player/view.dart | 90 ++++++++++++++++++-- lib/widgets/player/music_queue.dart | 125 ++++++++++++++++++++++++++++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 6 files changed, 250 insertions(+), 9 deletions(-) create mode 100644 lib/screens/player/queue.dart create mode 100644 lib/widgets/player/music_queue.dart diff --git a/lib/providers/audio_player.dart b/lib/providers/audio_player.dart index 553dee8..9034da5 100644 --- a/lib/providers/audio_player.dart +++ b/lib/providers/audio_player.dart @@ -311,6 +311,15 @@ class AudioPlayerProvider extends GetxController { newIndex > state.value.tracks.length - 1 || oldIndex > state.value.tracks.length - 1) return; + final item = state.value.playlist.medias.removeAt(oldIndex); + + state.value = state.value.copyWith( + playlist: state.value.playlist.copyWith( + medias: state.value.playlist.medias + ..insert(oldIndex < newIndex ? newIndex - 1 : 0, item), + ), + ); + await audioPlayer.moveTrack(oldIndex, newIndex); } diff --git a/lib/screens/player/queue.dart b/lib/screens/player/queue.dart new file mode 100644 index 0000000..e599dfd --- /dev/null +++ b/lib/screens/player/queue.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rhythm_box/widgets/player/music_queue.dart'; + +class PlayerQueuePopup extends StatelessWidget { + const PlayerQueuePopup({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.85, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Queue', + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + const Expanded( + child: PlayerQueue(), + ) + ], + ), + ); + } +} diff --git a/lib/screens/player/view.dart b/lib/screens/player/view.dart index b9cfcd9..7b5c1e9 100644 --- a/lib/screens/player/view.dart +++ b/lib/screens/player/view.dart @@ -6,7 +6,9 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:get/get.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:media_kit/media_kit.dart'; import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/screens/player/queue.dart'; import 'package:rhythm_box/services/artist.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; @@ -39,20 +41,13 @@ class _PlayerScreenState extends State { bool get _isPlaying => _playback.isPlaying.value; bool get _isFetchingActiveTrack => _query.isQueryingTrackInfo.value; + PlaylistMode get _loopMode => _playback.state.value.loopMode; double _bufferProgress = 0; Duration _durationCurrent = Duration.zero; Duration _durationTotal = Duration.zero; - void _updateDurationCurrent(Duration dur) { - setState(() => _durationCurrent = dur); - } - - void _updateDurationTotal(Duration dur) { - setState(() => _durationTotal = dur); - } - List? _subscriptions; Future _togglePlayState() async { @@ -199,6 +194,26 @@ class _PlayerScreenState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + StreamBuilder( + stream: audioPlayer.shuffledStream, + builder: (context, snapshot) { + final shuffled = snapshot.data ?? false; + return IconButton( + icon: Icon( + shuffled ? Icons.shuffle_on_outlined : Icons.shuffle, + ), + onPressed: _isFetchingActiveTrack + ? null + : () { + if (shuffled) { + audioPlayer.setShuffle(false); + } else { + audioPlayer.setShuffle(true); + } + }, + ); + }, + ), IconButton( icon: const Icon(Icons.skip_previous), onPressed: _isFetchingActiveTrack @@ -233,8 +248,65 @@ class _PlayerScreenState extends State { onPressed: _isFetchingActiveTrack ? null : audioPlayer.skipToNext, ), + Obx( + () => IconButton( + icon: Icon( + _loopMode == PlaylistMode.none + ? Icons.repeat + : _loopMode == PlaylistMode.loop + ? Icons.repeat_on_outlined + : Icons.repeat_one_on_outlined, + ), + onPressed: _isFetchingActiveTrack + ? null + : () async { + await audioPlayer.setLoopMode( + switch (_loopMode) { + PlaylistMode.loop => PlaylistMode.single, + PlaylistMode.single => PlaylistMode.none, + PlaylistMode.none => PlaylistMode.loop, + }, + ); + }, + ), + ), ], - ) + ), + const Gap(20), + Row( + children: [ + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.queue_music), + label: const Text('Queue'), + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + isScrollControlled: true, + context: context, + builder: (context) => const PlayerQueuePopup(), + ); + }, + ), + ), + const Gap(4), + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.lyrics), + label: const Text('Lyrics'), + onPressed: () {}, + ), + ), + const Gap(4), + Expanded( + child: TextButton.icon( + icon: const Icon(Icons.merge), + label: const Text('Sources'), + onPressed: () {}, + ), + ), + ], + ), ], ), ).marginAll(24), diff --git a/lib/widgets/player/music_queue.dart b/lib/widgets/player/music_queue.dart new file mode 100644 index 0000000..7d19926 --- /dev/null +++ b/lib/widgets/player/music_queue.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/widgets/auto_cache_image.dart'; +import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:spotify/spotify.dart'; +import 'package:rhythm_box/services/artist.dart'; + +class PlayerQueue extends StatefulWidget { + const PlayerQueue({super.key}); + + @override + State createState() => _PlayerQueueState(); +} + +class _PlayerQueueState extends State { + final AutoScrollController _autoScrollController = AutoScrollController(); + + final AudioPlayerProvider _playback = Get.find(); + + List get _tracks => _playback.state.value.tracks; + + bool _getIsActiveTrack(Track track) { + return track.id == _playback.state.value.activeTrack!.id; + } + + @override + void initState() { + super.initState(); + if (_playback.state.value.activeTrack != null) { + final idx = _tracks + .indexWhere((x) => x.id == _playback.state.value.activeTrack!.id); + if (idx != -1) { + _autoScrollController.scrollToIndex( + idx, + preferPosition: AutoScrollPosition.middle, + ); + } + } + } + + @override + void dispose() { + _autoScrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Obx( + () => CustomScrollView( + controller: _autoScrollController, + slivers: [ + SliverReorderableList( + itemCount: _tracks.length, + onReorder: (prev, now) async { + _playback.moveTrack(prev, now); + }, + itemBuilder: (context, idx) { + final item = _tracks[idx]; + return AutoScrollTag( + key: ValueKey(idx), + controller: _autoScrollController, + index: idx, + child: Material( + color: Colors.transparent, + child: Row( + children: [ + ReorderableDragStartListener( + index: idx, + child: const Icon(Icons.drag_indicator).paddingOnly( + left: 8, + ), + ), + Expanded( + child: ListTile( + tileColor: _getIsActiveTrack(item) + ? Theme.of(context) + .colorScheme + .secondaryContainer + : null, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), + ), + ), + contentPadding: const EdgeInsets.only( + left: 8, + right: 24, + ), + leading: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: AutoCacheImage( + item.album!.images!.first.url!, + width: 64.0, + height: 64.0, + ), + ), + title: Text(item.name ?? 'Loading...'), + subtitle: Text( + item.artists?.asString() ?? 'Please stand by...', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + onTap: () { + _playback.jumpToTrack(item); + }, + ), + ), + ], + ), + ), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 13011e0..ab1057e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1030,6 +1030,14 @@ packages: url: "https://github.com/KRTirtho/scrobblenaut.git" source: git version: "3.0.0" + scroll_to_index: + dependency: "direct main" + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 4bdbfce..dd7aeda 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -82,6 +82,7 @@ dependencies: ref: dart-3-support dismissible_page: ^1.0.2 shared_preferences: ^2.3.2 + scroll_to_index: ^3.0.1 dev_dependencies: flutter_test: