From a2bc08bbd9f8fdbff368be810df4c10db704a71b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Tue, 27 Aug 2024 20:49:48 +0800 Subject: [PATCH] :sparkles: Full screen player --- lib/main.dart | 2 +- lib/providers/audio_player.dart | 10 +- lib/providers/audio_player_stream.dart | 9 +- lib/router.dart | 6 - lib/screens/player/view.dart | 224 +++++++++++++++++++- lib/widgets/player/bottom_player.dart | 23 +- lib/widgets/tracks/playlist_track_list.dart | 2 + pubspec.lock | 8 + pubspec.yaml | 1 + 9 files changed, 262 insertions(+), 23 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 16173a0..d7f245b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,6 +59,7 @@ class MyApp extends StatelessWidget { void _initializeProviders(BuildContext context) async { Get.lazyPut(() => SpotifyProvider()); + Get.put(AudioPlayerProvider()); Get.put(ActiveSourcedTrackProvider()); Get.put(AudioPlayerStreamProvider()); @@ -69,7 +70,6 @@ class MyApp extends StatelessWidget { Get.put(ScrobblerProvider()); Get.put(UserPreferencesProvider()); - Get.put(AudioPlayerProvider()); Get.put(QueryingTrackInfoProvider()); Get.put(SourcedTrackProvider()); diff --git a/lib/providers/audio_player.dart b/lib/providers/audio_player.dart index e979b1c..8356432 100644 --- a/lib/providers/audio_player.dart +++ b/lib/providers/audio_player.dart @@ -128,11 +128,11 @@ class AudioPlayerProvider extends GetxController { // Giving the initial track a boost so MediaKit won't skip // because of timeout - final intendedActiveTrack = medias.elementAt(initialIndex); - if (intendedActiveTrack.track is! LocalTrack) { - await Get.find() - .fetch(RhythmMedia(intendedActiveTrack.track)); - } + // final intendedActiveTrack = medias.elementAt(initialIndex); + // if (intendedActiveTrack.track is! LocalTrack) { + // await Get.find() + // .fetch(RhythmMedia(intendedActiveTrack.track)); + // } if (medias.isEmpty) return; diff --git a/lib/providers/audio_player_stream.dart b/lib/providers/audio_player_stream.dart index fdf1c99..0fe9ac5 100644 --- a/lib/providers/audio_player_stream.dart +++ b/lib/providers/audio_player_stream.dart @@ -20,12 +20,15 @@ class AudioPlayerStreamProvider extends GetxController { List? _subscriptions; - @override - void onInit() { - super.onInit(); + AudioPlayerStreamProvider() { AudioServices.create().then( (value) => notificationService = value, ); + } + + @override + void onInit() { + super.onInit(); _subscriptions = [ subscribeToPlaylist(), diff --git a/lib/router.dart b/lib/router.dart index f94de03..77b5ce3 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,6 +1,5 @@ import 'package:go_router/go_router.dart'; import 'package:rhythm_box/screens/explore.dart'; -import 'package:rhythm_box/screens/player/view.dart'; import 'package:rhythm_box/screens/playlist/view.dart'; import 'package:rhythm_box/screens/settings.dart'; import 'package:rhythm_box/shells/nav_shell.dart'; @@ -28,9 +27,4 @@ final router = GoRouter(routes: [ ), ], ), - GoRoute( - path: '/player', - name: 'player', - builder: (context, state) => const PlayerScreen(), - ), ]); diff --git a/lib/screens/player/view.dart b/lib/screens/player/view.dart index 42433ef..dd109c0 100644 --- a/lib/screens/player/view.dart +++ b/lib/screens/player/view.dart @@ -1,15 +1,235 @@ +import 'dart:async'; + +import 'package:dismissible_page/dismissible_page.dart'; import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:get/get.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:rhythm_box/providers/audio_player.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'; +import 'package:rhythm_box/services/audio_services/image.dart'; +import 'package:rhythm_box/widgets/tracks/querying_track_info.dart'; class PlayerScreen extends StatefulWidget { - const PlayerScreen({super.key}); + final Duration durationCurrent, durationTotal; + + const PlayerScreen({ + super.key, + required this.durationCurrent, + required this.durationTotal, + }); @override State createState() => _PlayerScreenState(); } class _PlayerScreenState extends State { + late final AudioPlayerProvider _playback = Get.find(); + late final QueryingTrackInfoProvider _query = Get.find(); + + String? get _albumArt => + (_playback.state.value.activeTrack?.album?.images).asUrlString( + index: + (_playback.state.value.activeTrack?.album?.images?.length ?? 1) - 1, + ); + + bool get _isPlaying => _playback.isPlaying.value; + bool get _isFetchingActiveTrack => _query.isQueryingTrackInfo.value; + + 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 { + if (!audioPlayer.isPlaying) { + await audioPlayer.resume(); + } else { + await audioPlayer.pause(); + } + setState(() {}); + } + + String _formatDuration(Duration duration) { + String negativeSign = duration.isNegative ? '-' : ''; + String twoDigits(int n) => n.toString().padLeft(2, '0'); + String twoDigitMinutes = twoDigits(duration.inMinutes.abs()); + String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60).abs()); + return '$negativeSign$twoDigitMinutes:$twoDigitSeconds'; + } + + double? _draggingValue; + + @override + void initState() { + super.initState(); + _durationCurrent = widget.durationCurrent; + _durationTotal = widget.durationTotal; + _subscriptions = [ + audioPlayer.durationStream.listen(_updateDurationTotal), + audioPlayer.positionStream.listen(_updateDurationCurrent), + ]; + } + + @override + void dispose() { + if (_subscriptions != null) { + for (final subscription in _subscriptions!) { + subscription.cancel(); + } + } + super.dispose(); + } + @override Widget build(BuildContext context) { - return const Placeholder(); + final size = MediaQuery.of(context).size; + + return DismissiblePage( + backgroundColor: Theme.of(context).colorScheme.surface, + onDismissed: () { + Navigator.of(context).pop(); + }, + direction: DismissiblePageDismissDirection.down, + child: Material( + color: Colors.transparent, + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Hero( + tag: const Key('current-active-track-album-art'), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: AspectRatio( + aspectRatio: 1, + child: _albumArt != null + ? AutoCacheImage( + _albumArt!, + width: size.width, + height: size.width, + ) + : Container( + color: Theme.of(context) + .colorScheme + .surfaceContainerHigh, + width: 64, + height: 64, + child: const Center(child: Icon(Icons.image)), + ), + ), + ).marginSymmetric(horizontal: 24), + ), + const Gap(24), + Text( + _playback.state.value.activeTrack?.name ?? 'Not playing', + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + _playback.state.value.activeTrack?.artists?.asString() ?? + 'No author', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + const Gap(24), + Column( + children: [ + SliderTheme( + data: SliderThemeData( + trackHeight: 2, + trackShape: _PlayerProgressTrackShape(), + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + overlayShape: SliderComponentShape.noOverlay, + ), + child: Slider( + value: _draggingValue ?? + _durationCurrent.inMilliseconds.toDouble(), + min: 0, + max: _durationTotal.inMilliseconds.toDouble(), + onChanged: (value) { + setState(() => _draggingValue = value); + }, + onChangeEnd: (value) { + print('Seek to $value ms'); + audioPlayer.seek(Duration(milliseconds: value.toInt())); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _formatDuration(_durationCurrent), + style: GoogleFonts.robotoMono(fontSize: 12), + ), + Text( + _formatDuration(_durationTotal), + style: GoogleFonts.robotoMono(fontSize: 12), + ), + ], + ).paddingSymmetric(horizontal: 8, vertical: 4), + ], + ).paddingSymmetric(horizontal: 24), + const Gap(24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 56, + height: 56, + child: IconButton.filled( + icon: _isFetchingActiveTrack + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2.5, + ), + ) + : Icon( + !_isPlaying ? Icons.play_arrow : Icons.pause, + size: 28, + ), + onPressed: + _isFetchingActiveTrack ? null : _togglePlayState, + ), + ), + ], + ) + ], + ), + ).marginAll(24), + ), + ); + } +} + +class _PlayerProgressTrackShape extends RoundedRectSliderTrackShape { + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + final trackHeight = sliderTheme.trackHeight; + final trackLeft = offset.dx; + final trackTop = offset.dy + (parentBox.size.height - trackHeight!) / 2; + final trackWidth = parentBox.size.width; + return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); } } diff --git a/lib/widgets/player/bottom_player.dart b/lib/widgets/player/bottom_player.dart index 1e12e71..e86bb65 100644 --- a/lib/widgets/player/bottom_player.dart +++ b/lib/widgets/player/bottom_player.dart @@ -1,11 +1,12 @@ import 'dart:async'; import 'dart:math'; +import 'package:dismissible_page/dismissible_page.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:get/get.dart'; -import 'package:go_router/go_router.dart'; import 'package:rhythm_box/providers/audio_player.dart'; +import 'package:rhythm_box/screens/player/view.dart'; import 'package:rhythm_box/services/audio_player/audio_player.dart'; import 'package:rhythm_box/services/audio_services/image.dart'; import 'package:rhythm_box/widgets/auto_cache_image.dart'; @@ -71,6 +72,12 @@ class _BottomPlayerState extends State _subscriptions = [ audioPlayer.durationStream.listen(_updateDurationTotal), audioPlayer.positionStream.listen(_updateDurationCurrent), + _playback.state.listen((state) { + if (state.playlist.medias.isNotEmpty && !_isLifted) { + _animationController.animateTo(1); + _isLifted = true; + } + }), _playback.isPlaying.listen((value) { if (value && !_isLifted) { _animationController.animateTo(1); @@ -104,6 +111,7 @@ class _BottomPlayerState extends State axisAlignment: -1, child: Obx( () => GestureDetector( + behavior: HitTestBehavior.translucent, child: Column( children: [ if (_durationCurrent != Duration.zero) @@ -122,10 +130,10 @@ class _BottomPlayerState extends State Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: Hero( - tag: const Key('current-active-track-album-art'), + Hero( + tag: const Key('current-active-track-album-art'), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), child: _albumArt != null ? AutoCacheImage(_albumArt!, width: 64, height: 64) : Container( @@ -172,7 +180,10 @@ class _BottomPlayerState extends State ], ), onTap: () { - GoRouter.of(context).pushNamed('player'); + context.pushTransparentRoute(PlayerScreen( + durationCurrent: _durationCurrent, + durationTotal: _durationTotal, + )); }, ), ), diff --git a/lib/widgets/tracks/playlist_track_list.dart b/lib/widgets/tracks/playlist_track_list.dart index 6db2205..9c6cb24 100644 --- a/lib/widgets/tracks/playlist_track_list.dart +++ b/lib/widgets/tracks/playlist_track_list.dart @@ -65,6 +65,8 @@ class _PlaylistTrackListState extends State { title: Text(item?.name ?? 'Loading...'), subtitle: Text( item?.artists?.asString() ?? 'Please stand by...', + maxLines: 1, + overflow: TextOverflow.ellipsis, ), onTap: () { if (item == null) return; diff --git a/pubspec.lock b/pubspec.lock index ec9b2d1..61bc072 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -326,6 +326,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + dismissible_page: + dependency: "direct main" + description: + name: dismissible_page + sha256: "5b2316f770fe83583f770df1f6505cb19102081c5971979806e77f2e507a9958" + url: "https://pub.dev" + source: hosted + version: "1.0.2" drift: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 726c718..11b8fe7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,6 +81,7 @@ dependencies: git: url: https://github.com/KRTirtho/scrobblenaut.git ref: dart-3-support + dismissible_page: ^1.0.2 dev_dependencies: flutter_test: