From 8e1a33c77559dbd611f96eba658c07ad12ca0cd0 Mon Sep 17 00:00:00 2001 From: Yesterday17 Date: Mon, 16 Sep 2024 22:51:24 +0800 Subject: [PATCH] feat: move simple progress indicator to bottom, add swipe gesture --- lib/services/playback/playback_playing.dart | 4 + lib/services/playback/playback_service.dart | 22 ++--- .../bottom_player/bottom_player_mobile.dart | 88 ++++++------------- lib/ui/widgets/slide_up.dart | 2 +- lib/ui/widgets/swiper.dart | 76 ++++++++++++++++ 5 files changed, 118 insertions(+), 74 deletions(-) create mode 100644 lib/ui/widgets/swiper.dart diff --git a/lib/services/playback/playback_playing.dart b/lib/services/playback/playback_playing.dart index a5f1f93..9ad714a 100644 --- a/lib/services/playback/playback_playing.dart +++ b/lib/services/playback/playback_playing.dart @@ -37,6 +37,10 @@ class PlayingTrack extends ChangeNotifier { Duration position = Duration.zero; Duration duration = Duration.zero; + double get progress => duration.inMilliseconds == 0 + ? 0 + : position.inMilliseconds / duration.inMilliseconds; + void updatePosition(final Duration position) { this.position = position; notifyListeners(); diff --git a/lib/services/playback/playback_service.dart b/lib/services/playback/playback_service.dart index cdbb541..80d69c4 100644 --- a/lib/services/playback/playback_service.dart +++ b/lib/services/playback/playback_service.dart @@ -124,7 +124,7 @@ class PlaybackService extends ChangeNotifier { this.queue = queue .map((final e) => AnnilAudioSource.fromJson(jsonDecode(e))) .toList(); - setPlayingIndex(playingIndex); + _setPlayingIndex(playingIndex); } final loopMode = preferences.getInt('player.loopMode'); @@ -224,7 +224,7 @@ class PlaybackService extends ChangeNotifier { final currentIndex = playingIndex; if (queue.isNotEmpty && currentIndex != null) { if (shuffleMode == ShuffleMode.on) { - await setPlayingIndex(rng.nextInt(queue.length)); + await _setPlayingIndex(rng.nextInt(queue.length)); await play(reload: true); return; } @@ -233,13 +233,13 @@ class PlaybackService extends ChangeNotifier { case LoopMode.off: // to the next song / stop if (currentIndex > 0) { - await setPlayingIndex(currentIndex - 1); + await _setPlayingIndex(currentIndex - 1); await play(reload: true); } break; case LoopMode.all: // to the previous song / last song - await setPlayingIndex( + await _setPlayingIndex( (currentIndex > 0 ? currentIndex : queue.length) - 1); await play(reload: true); break; @@ -257,7 +257,7 @@ class PlaybackService extends ChangeNotifier { final currentIndex = playingIndex; if (queue.isNotEmpty && currentIndex != null) { if (shuffleMode == ShuffleMode.on) { - await setPlayingIndex(rng.nextInt(queue.length)); + await _setPlayingIndex(rng.nextInt(queue.length)); await play(reload: true); return; } @@ -266,7 +266,7 @@ class PlaybackService extends ChangeNotifier { case LoopMode.off: // to the next song / stop if (currentIndex < queue.length - 1) { - await setPlayingIndex(currentIndex + 1); + await _setPlayingIndex(currentIndex + 1); await play(reload: true); } else { await stop(); @@ -274,7 +274,7 @@ class PlaybackService extends ChangeNotifier { break; case LoopMode.all: // to the next song / first song - await setPlayingIndex((currentIndex + 1) % queue.length); + await _setPlayingIndex((currentIndex + 1) % queue.length); await play(reload: true); break; case LoopMode.one: @@ -306,7 +306,7 @@ class PlaybackService extends ChangeNotifier { queue.removeAt(index); if (removeCurrentPlayingTrack) { - await setPlayingIndex(index, notify: false); + await _setPlayingIndex(index, notify: false); await play(reload: true); } notifyListeners(); @@ -318,7 +318,7 @@ class PlaybackService extends ChangeNotifier { final to = index % queue.length; if (to != playingIndex) { // index changed, set new audio source - await setPlayingIndex(to); + await _setPlayingIndex(to); await play(reload: true); } else { // index not changed, seek to start @@ -339,7 +339,7 @@ class PlaybackService extends ChangeNotifier { ref.read(preferencesProvider).set('player.shuffleMode', shuffleMode.index); } - Future setPlayingIndex(final int index, + Future _setPlayingIndex(final int index, {final bool reload = false, final bool notify = true}) async { loadedAndPaused = false; @@ -360,7 +360,7 @@ class PlaybackService extends ChangeNotifier { queue = songs; // 2. set playing index if (songs.isNotEmpty) { - await setPlayingIndex(initialIndex % songs.length, + await _setPlayingIndex(initialIndex % songs.length, reload: true, notify: false); } else { playing.setSource(null); diff --git a/lib/ui/bottom_player/bottom_player_mobile.dart b/lib/ui/bottom_player/bottom_player_mobile.dart index f8ad693..4fc5cfe 100644 --- a/lib/ui/bottom_player/bottom_player_mobile.dart +++ b/lib/ui/bottom_player/bottom_player_mobile.dart @@ -1,7 +1,7 @@ import 'package:annix/providers.dart'; -import 'package:annix/ui/widgets/artist_text.dart'; import 'package:annix/ui/widgets/cover.dart'; import 'package:annix/ui/widgets/buttons/play_pause_button.dart'; +import 'package:annix/ui/widgets/swiper.dart'; import 'package:annix/utils/context_extension.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -28,69 +28,33 @@ class MobileBottomPlayer extends StatelessWidget { ), ), height: height, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Stack( + alignment: Alignment.bottomCenter, children: [ - Container( - padding: const EdgeInsets.all(8), - child: const PlayingMusicCover(card: true, animated: false), - ), - Expanded( - flex: 1, - child: Consumer( - builder: (final context, final ref, final child) { - final playing = ref.watch(playingProvider); - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - playing.source?.track.title ?? '', - style: context.textTheme.titleSmall, - overflow: TextOverflow.ellipsis, - softWrap: false, - ), - ArtistText( - playing.source?.track.artist ?? '', - style: context.textTheme.bodySmall, - ), - ], - ); - }, - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Consumer( - builder: (final context, final ref, final child) { - final double? progress = - ref.watch(playingProvider.select((final playing) { - if (playing.source == null) { - return null; - } - - if (playing.duration == Duration.zero) { - return 0; - } - - return playing.position.inMicroseconds / - playing.duration.inMicroseconds; - })); - if (progress == null) { - return child!; - } - - return Stack( - alignment: Alignment.center, - children: [ - CircularProgressIndicator(value: progress), - child!, - ], - ); - }, - child: PlayPauseButton.small(), - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(8), + child: const PlayingMusicCover(card: true, animated: false), + ), + const Expanded( + flex: 1, + child: PlayingTrackSwiper(), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: PlayPauseButton.small(), + ), + ], ), + Consumer(builder: (context, ref, child) { + final playing = ref.watch(playingProvider); + return LinearProgressIndicator( + value: playing.progress, + minHeight: 2, + ); + }), ], ), ); diff --git a/lib/ui/widgets/slide_up.dart b/lib/ui/widgets/slide_up.dart index 2febff9..85b78eb 100644 --- a/lib/ui/widgets/slide_up.dart +++ b/lib/ui/widgets/slide_up.dart @@ -238,7 +238,7 @@ class _SlidingUpPanelState extends ConsumerState { if (_scrollableAxis == null) { if (e.delta.dx.abs() > e.delta.dy.abs()) { _scrollableAxis = Axis.horizontal; - } else { + } else if (e.delta.dx.abs() < e.delta.dy.abs()) { _scrollableAxis = Axis.vertical; } } diff --git a/lib/ui/widgets/swiper.dart b/lib/ui/widgets/swiper.dart new file mode 100644 index 0000000..ce852fc --- /dev/null +++ b/lib/ui/widgets/swiper.dart @@ -0,0 +1,76 @@ +import 'package:annix/providers.dart'; +import 'package:annix/ui/widgets/artist_text.dart'; +import 'package:annix/ui/widgets/slide_up.dart'; +import 'package:annix/utils/context_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class HarderScrollPhysics extends ScrollPhysics { + const HarderScrollPhysics({super.parent}); + + @override + HarderScrollPhysics applyTo(ScrollPhysics? ancestor) { + return HarderScrollPhysics(parent: buildParent(ancestor)); + } + + @override + double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { + return offset * 0.7; + } +} + +class PlayingTrackSwiper extends HookConsumerWidget { + const PlayingTrackSwiper({super.key}); + + @override + Widget build(BuildContext context, final ref) { + final player = ref.watch(playbackProvider); + final queue = player.queue; + final playingIndex = player.playingIndex; + final controller = usePageController(initialPage: playingIndex ?? 0); + + useEffect(() { + if (playingIndex != null && controller.hasClients) { + controller.jumpToPage(playingIndex); + } + return null; + }, [queue, playingIndex]); + + return HorizontalScrollableWidget( + child: PageView.builder( + // if we need to add option to disable swipe + // physics: const NeverScrollableScrollPhysics(), + physics: const HarderScrollPhysics(), + controller: controller, + onPageChanged: (index) { + if (playingIndex != null && index != playingIndex) { + WidgetsBinding.instance.addPostFrameCallback((final _) { + player.jump(index); + }); + } + }, + itemCount: queue.length, + itemBuilder: (context, index) { + final track = queue[index].track; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + track.title, + style: context.textTheme.titleSmall, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), + ArtistText( + track.artist, + style: context.textTheme.bodySmall, + ), + ], + ); + }, + ), + ); + } +}