From 4b21cc829954fb079d1a0081b9147377063da3ec Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 25 Oct 2022 14:12:17 +0600 Subject: [PATCH] feat(playback): change current track youtube source panel and tooltips for player icon buttons --- lib/components/Player/PlayerActions.dart | 28 +++ lib/components/Player/PlayerControls.dart | 48 +++-- lib/components/Player/SiblingTracksSheet.dart | 95 ++++++++++ lib/components/Shared/HeartButton.dart | 8 + lib/provider/Playback.dart | 170 +++++++++++------- lib/utils/primitive_utils.dart | 7 + 6 files changed, 274 insertions(+), 82 deletions(-) create mode 100644 lib/components/Player/SiblingTracksSheet.dart diff --git a/lib/components/Player/PlayerActions.dart b/lib/components/Player/PlayerActions.dart index c129b95d1..53f93595d 100644 --- a/lib/components/Player/PlayerActions.dart +++ b/lib/components/Player/PlayerActions.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Library/UserLocalTracks.dart'; import 'package:spotube/components/Player/PlayerQueue.dart'; +import 'package:spotube/components/Player/SiblingTracksSheet.dart'; import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Downloader.dart'; @@ -49,6 +50,7 @@ class PlayerActions extends HookConsumerWidget { children: [ IconButton( icon: const Icon(Icons.queue_music_rounded), + tooltip: 'Queue', onPressed: playback.playlist != null ? () { showModalBottomSheet( @@ -71,6 +73,31 @@ class PlayerActions extends HookConsumerWidget { } : null, ), + IconButton( + icon: const Icon(Icons.alt_route_rounded), + tooltip: "Alternative Track Sources", + onPressed: playback.track != null + ? () { + showModalBottomSheet( + context: context, + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + backgroundColor: Colors.black12, + barrierColor: Colors.black12, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * .5, + ), + builder: (context) { + return SiblingTracksSheet(floating: floatingQueue); + }, + ); + } + : null, + ), if (!kIsWeb) if (isInQueue) const SizedBox( @@ -82,6 +109,7 @@ class PlayerActions extends HookConsumerWidget { ) else IconButton( + tooltip: 'Download track', icon: Icon( isDownloaded ? Icons.download_done_rounded diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index e3c5c7198..d0a2c413e 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -94,23 +94,26 @@ class PlayerControls extends HookConsumerWidget { return Column( children: [ - Slider.adaptive( - focusNode: FocusNode(), - // cannot divide by zero - // there's an edge case for value being bigger - // than total duration. Keeping it resolved - value: progress.value.toDouble(), - onChanged: (v) { - progress.value = v; - }, - onChangeEnd: (value) async { - await playback.seekPosition( - Duration( - seconds: (value * sliderMax).toInt(), - ), - ); - }, - activeColor: iconColor, + Tooltip( + message: "Slide to seek forward or backward", + child: Slider.adaptive( + focusNode: FocusNode(), + // cannot divide by zero + // there's an edge case for value being bigger + // than total duration. Keeping it resolved + value: progress.value.toDouble(), + onChanged: (v) { + progress.value = v; + }, + onChangeEnd: (value) async { + await playback.seekPosition( + Duration( + seconds: (value * sliderMax).toInt(), + ), + ); + }, + activeColor: iconColor, + ), ), Padding( padding: const EdgeInsets.symmetric( @@ -136,6 +139,11 @@ class PlayerControls extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( + tooltip: playback.isLoop + ? "Repeat playlist" + : playback.isShuffled + ? "Loop track" + : "Shuffle playlist", icon: Icon( playback.isLoop ? Icons.repeat_one_rounded @@ -149,12 +157,16 @@ class PlayerControls extends HookConsumerWidget { : playback.cyclePlaybackMode, ), IconButton( + tooltip: "Previous track", icon: const Icon(Icons.skip_previous_rounded), color: iconColor, onPressed: () { onPrevious(); }), IconButton( + tooltip: playback.isPlaying + ? "Pause playback" + : "Resume playback", icon: playback.status == PlaybackStatus.loading ? const SizedBox( height: 20, @@ -173,11 +185,13 @@ class PlayerControls extends HookConsumerWidget { ), ), IconButton( + tooltip: "Next track", icon: const Icon(Icons.skip_next_rounded), onPressed: () => onNext(), color: iconColor, ), IconButton( + tooltip: "Stop playback", icon: const Icon(Icons.stop_rounded), color: iconColor, onPressed: playback.track != null diff --git a/lib/components/Player/SiblingTracksSheet.dart b/lib/components/Player/SiblingTracksSheet.dart new file mode 100644 index 000000000..605bf1bd5 --- /dev/null +++ b/lib/components/Player/SiblingTracksSheet.dart @@ -0,0 +1,95 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/Shared/UniversalImage.dart'; +import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/utils/primitive_utils.dart'; + +class SiblingTracksSheet extends HookConsumerWidget { + final bool floating; + const SiblingTracksSheet({ + Key? key, + this.floating = true, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final playback = ref.watch(playbackProvider); + final borderRadius = floating + ? BorderRadius.circular(10) + : const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + ); + + useEffect(() { + if (playback.siblingYtVideos.isEmpty) { + playback.toSpotubeTrack(playback.track!, ignoreCache: true); + } + return null; + }, [playback.siblingYtVideos]); + + return BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 12.0, + sigmaY: 12.0, + ), + child: Container( + margin: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + borderRadius: borderRadius, + color: Theme.of(context) + .navigationRailTheme + .backgroundColor + ?.withOpacity(0.5), + ), + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + centerTitle: true, + title: const Text('Alternative Tracks Sources'), + automaticallyImplyLeading: false, + backgroundColor: Colors.transparent, + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ListView.builder( + itemCount: playback.siblingYtVideos.length, + itemBuilder: (context, index) { + final video = playback.siblingYtVideos[index]; + return ListTile( + title: Text(video.title), + leading: UniversalImage( + path: video.thumbnails.lowResUrl, + height: 60, + width: 60, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5), + ), + horizontalTitleGap: 10, + trailing: Text( + PrimitiveUtils.toReadableDuration( + video.duration ?? Duration.zero, + ), + ), + subtitle: Text(video.author), + enabled: playback.status != PlaybackStatus.loading, + selected: video.id == playback.track!.ytTrack.id, + selectedTileColor: Theme.of(context).popupMenuTheme.color, + onTap: () { + if (video.id != playback.track!.ytTrack.id) { + playback.changeToSiblingVideo(video, playback.track!); + } + }, + ); + }, + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/Shared/HeartButton.dart b/lib/components/Shared/HeartButton.dart index 1ab38c3bb..24954b341 100644 --- a/lib/components/Shared/HeartButton.dart +++ b/lib/components/Shared/HeartButton.dart @@ -16,10 +16,12 @@ class HeartButton extends ConsumerWidget { final void Function()? onPressed; final IconData? icon; final Color? color; + final String? tooltip; const HeartButton({ required this.isLiked, required this.onPressed, this.color, + this.tooltip, this.icon, Key? key, }) : super(key: key); @@ -31,6 +33,7 @@ class HeartButton extends ConsumerWidget { if (!auth.isLoggedIn) return Container(); return IconButton( + tooltip: tooltip, icon: Icon( icon ?? (!isLiked @@ -122,6 +125,7 @@ class TrackHeartButton extends HookConsumerWidget { } return HeartButton( + tooltip: toggler.item1 ? "Remove from Favorite" : "Add to Favorite", isLiked: toggler.item1, onPressed: savedTracks.hasData ? () { @@ -181,6 +185,9 @@ class PlaylistHeartButton extends HookConsumerWidget { return HeartButton( isLiked: isLikedQuery.data ?? false, + tooltip: isLikedQuery.data ?? false + ? "Remove from Favorite" + : "Add to Favorite", color: color?.titleTextColor, onPressed: isLikedQuery.hasData ? () { @@ -232,6 +239,7 @@ class AlbumHeartButton extends HookConsumerWidget { return HeartButton( isLiked: isLiked, + tooltip: isLiked ? "Remove from Favorite" : "Add to Favorite", onPressed: albumIsSaved.hasData ? () { toggleAlbumLike diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index 5649a5114..4d416f467 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -66,6 +66,7 @@ class Playback extends PersistedChangeNotifier { late LazyBox cache; CurrentPlaylist? playlist; SpotubeTrack? track; + List