From 7e24059900cb074be5038d43a8ecf01481c50cda Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 4 Jul 2022 11:10:17 +0600 Subject: [PATCH] Queue support added for both Desktop & Mobile --- lib/components/Artist/ArtistProfile.dart | 1 + lib/components/Player/PlayerActions.dart | 24 ++ lib/components/Player/PlayerQueue.dart | 77 ++++++ lib/components/Search/Search.dart | 1 + lib/components/Shared/NotFound.dart | 43 ++-- lib/components/Shared/TrackTile.dart | 286 +++++++++++---------- lib/components/Shared/TracksTableView.dart | 1 + lib/themes/light-theme.dart | 2 +- 8 files changed, 277 insertions(+), 158 deletions(-) create mode 100644 lib/components/Player/PlayerQueue.dart diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 8501fa0b..29314fd3 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -240,6 +240,7 @@ class ArtistProfile extends HookConsumerWidget { duration: duration, track: track, thumbnailUrl: thumbnailUrl, + isActive: playback.track?.id == track.value.id, onTrackPlayButtonPressed: (currentTrack) => playPlaylist( topTracks.toList(), diff --git a/lib/components/Player/PlayerActions.dart b/lib/components/Player/PlayerActions.dart index 3a7bfa72..dec78928 100644 --- a/lib/components/Player/PlayerActions.dart +++ b/lib/components/Player/PlayerActions.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Player/PlayerQueue.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/hooks/useForceUpdate.dart'; @@ -27,6 +28,29 @@ class PlayerActions extends HookConsumerWidget { return Row( mainAxisAlignment: mainAxisAlignment, children: [ + IconButton( + icon: const Icon(Icons.queue_music_rounded), + onPressed: playback.playlist != null + ? () { + showModalBottomSheet( + context: context, + isDismissible: true, + enableDrag: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * .7, + ), + builder: (context) { + return const PlayerQueue(); + }, + ); + } + : null, + ), DownloadTrackButton( track: playback.track, ), diff --git a/lib/components/Player/PlayerQueue.dart b/lib/components/Player/PlayerQueue.dart new file mode 100644 index 00000000..4f0b35a4 --- /dev/null +++ b/lib/components/Player/PlayerQueue.dart @@ -0,0 +1,77 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/components/Shared/NotFound.dart'; +import 'package:spotube/components/Shared/TrackTile.dart'; +import 'package:spotube/helpers/image-to-url-string.dart'; +import 'package:spotube/helpers/zero-pad-num-str.dart'; +import 'package:spotube/provider/Playback.dart'; +import 'package:collection/collection.dart'; + +class PlayerQueue extends HookConsumerWidget { + const PlayerQueue({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final playback = ref.watch(playbackProvider); + final tracks = playback.playlist?.tracks ?? []; + + if (tracks.isEmpty) { + return const NotFound(vertical: true); + } + + return BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .navigationRailTheme + .backgroundColor + ?.withOpacity(0.5), + borderRadius: BorderRadius.circular(10), + ), + margin: const EdgeInsets.all(8.0), + padding: const EdgeInsets.only( + top: 5.0, + ), + child: Column( + children: [ + Text( + "Queue (${playback.playlist?.name})", + style: Theme.of(context).textTheme.headline4, + overflow: TextOverflow.ellipsis, + ), + Flexible( + child: ListView( + shrinkWrap: true, + children: [ + ...tracks.asMap().entries.mapIndexed((i, track) { + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TrackTile( + playback, + track: track, + duration: duration, + thumbnailUrl: + imageToUrlString(track.value.album?.images), + isActive: playback.track?.id == track.value.id, + onTrackPlayButtonPressed: (currentTrack) async { + if (playback.track?.id == track.value.id) return; + await playback.setPlaylistPosition(i); + }, + ), + ); + }), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index 10320eef..e11507ed 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -114,6 +114,7 @@ class Search extends HookConsumerWidget { duration: duration, thumbnailUrl: imageToUrlString(track.value.album?.images), + isActive: playback.track?.id == track.value.id, onTrackPlayButtonPressed: (currentTrack) async { var isPlaylistPlaying = playback.playlist?.id != null && diff --git a/lib/components/Shared/NotFound.dart b/lib/components/Shared/NotFound.dart index 05b143d1..2a3d4bba 100644 --- a/lib/components/Shared/NotFound.dart +++ b/lib/components/Shared/NotFound.dart @@ -1,30 +1,29 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; class NotFound extends StatelessWidget { - const NotFound({Key? key}) : super(key: key); + final bool vertical; + const NotFound({Key? key, this.vertical = false}) : super(key: key); @override Widget build(BuildContext context) { - return Row( - children: [ - SizedBox( - height: 150, - width: 150, - child: Image.asset("assets/empty_box.png"), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Nothing found", style: Theme.of(context).textTheme.headline6), - Text( - "The box is empty", - style: Theme.of(context).textTheme.subtitle1, - ), - ], - ), - ], - ); + final widgets = [ + SizedBox( + height: 150, + width: 150, + child: Image.asset("assets/empty_box.png"), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text("Nothing found", style: Theme.of(context).textTheme.headline6), + Text( + "The box is empty", + style: Theme.of(context).textTheme.subtitle1, + ), + ], + ), + ]; + return vertical ? Column(children: widgets) : Row(children: widgets); } } diff --git a/lib/components/Shared/TrackTile.dart b/lib/components/Shared/TrackTile.dart index f9fdcc94..3fc7e5b1 100644 --- a/lib/components/Shared/TrackTile.dart +++ b/lib/components/Shared/TrackTile.dart @@ -24,14 +24,21 @@ class TrackTile extends HookConsumerWidget { final bool userPlaylist; // null playlistId indicates its not inside a playlist final String? playlistId; + + final bool showAlbum; + + final bool isActive; + TrackTile( this.playback, { required this.track, required this.duration, + required this.isActive, this.playlistId, this.userPlaylist = false, this.thumbnailUrl, this.onTrackPlayButtonPressed, + this.showAlbum = true, Key? key, }) : super(key: key); @@ -162,151 +169,160 @@ class TrackTile extends HookConsumerWidget { }); } - return Row( - children: [ - SizedBox( - height: 20, - width: 25, - child: Text( - (track.key + 1).toString(), - textAlign: TextAlign.center, - ), - ), - if (thumbnailUrl != null) - Padding( - padding: EdgeInsets.symmetric( - horizontal: breakpoint.isMoreThan(Breakpoints.md) ? 8.0 : 0, - vertical: 8.0, + return AnimatedContainer( + duration: const Duration(milliseconds: 500), + decoration: BoxDecoration( + color: isActive + ? Theme.of(context).popupMenuTheme.color + : Colors.transparent, + borderRadius: BorderRadius.circular(isActive ? 10 : 0), + ), + child: Row( + children: [ + SizedBox( + height: 20, + width: 25, + child: Text( + (track.key + 1).toString(), + textAlign: TextAlign.center, ), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(5)), - child: CachedNetworkImage( - placeholder: (context, url) { - return Container( - height: 40, - width: 40, - color: Theme.of(context).primaryColor, - ); - }, - imageUrl: thumbnailUrl!, - maxHeightDiskCache: 40, - maxWidthDiskCache: 40, + ), + if (thumbnailUrl != null) + Padding( + padding: EdgeInsets.symmetric( + horizontal: breakpoint.isMoreThan(Breakpoints.md) ? 8.0 : 0, + vertical: 8.0, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(5)), + child: CachedNetworkImage( + placeholder: (context, url) { + return Container( + height: 40, + width: 40, + color: Theme.of(context).primaryColor, + ); + }, + imageUrl: thumbnailUrl!, + maxHeightDiskCache: 40, + maxWidthDiskCache: 40, + ), ), ), + IconButton( + icon: Icon( + playback.track?.id != null && playback.track?.id == track.value.id + ? Icons.pause_circle_rounded + : Icons.play_circle_rounded, + color: Theme.of(context).primaryColor, + ), + onPressed: () => onTrackPlayButtonPressed?.call( + track.value, + ), ), - IconButton( - icon: Icon( - playback.track?.id != null && playback.track?.id == track.value.id - ? Icons.pause_circle_rounded - : Icons.play_circle_rounded, - color: Theme.of(context).primaryColor, - ), - onPressed: () => onTrackPlayButtonPressed?.call( - track.value, - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - track.value.name ?? "", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: breakpoint.isSm ? 14 : 17, + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + track.value.name ?? "", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: breakpoint.isSm ? 14 : 17, + ), + overflow: TextOverflow.ellipsis, ), + artistsToClickableArtists(track.value.artists ?? [], + textStyle: TextStyle( + fontSize: + breakpoint.isLessThan(Breakpoints.lg) ? 12 : 14)), + ], + ), + ), + if (breakpoint.isMoreThan(Breakpoints.md) && showAlbum) + Expanded( + child: LinkText( + track.value.album!.name!, + "/album/${track.value.album?.id}", + extra: track.value.album, overflow: TextOverflow.ellipsis, ), - artistsToClickableArtists(track.value.artists ?? [], - textStyle: TextStyle( - fontSize: - breakpoint.isLessThan(Breakpoints.lg) ? 12 : 14)), - ], - ), - ), - if (breakpoint.isMoreThan(Breakpoints.md)) - Expanded( - child: LinkText( - track.value.album!.name!, - "/album/${track.value.album?.id}", - extra: track.value.album, - overflow: TextOverflow.ellipsis, ), - ), - if (!breakpoint.isSm) ...[ + if (!breakpoint.isSm) ...[ + const SizedBox(width: 10), + Text(duration), + ], const SizedBox(width: 10), - Text(duration), + PopupMenuButton( + icon: const Icon(Icons.more_horiz_rounded), + itemBuilder: (context) { + return [ + if (auth.isLoggedIn) + PopupMenuItem( + child: Row( + children: const [ + Icon(Icons.add_box_rounded), + SizedBox(width: 10), + Text("Add to Playlist"), + ], + ), + value: "add-playlist", + ), + if (userPlaylist && auth.isLoggedIn) + PopupMenuItem( + child: Row( + children: const [ + Icon(Icons.remove_circle_outline_rounded), + SizedBox(width: 10), + Text("Remove from Playlist"), + ], + ), + value: "remove-playlist", + ), + if (auth.isLoggedIn) + PopupMenuItem( + child: Row( + children: [ + Icon(isSaved + ? Icons.favorite_rounded + : Icons.favorite_border_rounded), + const SizedBox(width: 10), + const Text("Favorite") + ], + ), + value: "favorite", + ), + PopupMenuItem( + child: Row( + children: const [ + Icon(Icons.share_rounded), + SizedBox(width: 10), + Text("Share") + ], + ), + value: "share", + ) + ]; + }, + onSelected: (value) { + switch (value) { + case "favorite": + actionFavorite(isSaved); + break; + case "add-playlist": + actionAddToPlaylist(); + break; + case "remove-playlist": + actionRemoveFromPlaylist(); + break; + case "share": + actionShare(track.value); + break; + } + }, + ), ], - const SizedBox(width: 10), - PopupMenuButton( - icon: const Icon(Icons.more_horiz_rounded), - itemBuilder: (context) { - return [ - if (auth.isLoggedIn) - PopupMenuItem( - child: Row( - children: const [ - Icon(Icons.add_box_rounded), - SizedBox(width: 10), - Text("Add to Playlist"), - ], - ), - value: "add-playlist", - ), - if (userPlaylist && auth.isLoggedIn) - PopupMenuItem( - child: Row( - children: const [ - Icon(Icons.remove_circle_outline_rounded), - SizedBox(width: 10), - Text("Remove from Playlist"), - ], - ), - value: "remove-playlist", - ), - if (auth.isLoggedIn) - PopupMenuItem( - child: Row( - children: [ - Icon(isSaved - ? Icons.favorite_rounded - : Icons.favorite_border_rounded), - const SizedBox(width: 10), - const Text("Favorite") - ], - ), - value: "favorite", - ), - PopupMenuItem( - child: Row( - children: const [ - Icon(Icons.share_rounded), - SizedBox(width: 10), - Text("Share") - ], - ), - value: "share", - ) - ]; - }, - onSelected: (value) { - switch (value) { - case "favorite": - actionFavorite(isSaved); - break; - case "add-playlist": - actionAddToPlaylist(); - break; - case "remove-playlist": - actionRemoveFromPlaylist(); - break; - case "share": - actionShare(track.value); - break; - } - }, - ), - ], + ), ); } } diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index 70bd9069..c8c153a7 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -92,6 +92,7 @@ class TracksTableView extends HookConsumerWidget { duration: duration, thumbnailUrl: thumbnailUrl, userPlaylist: userPlaylist, + isActive: playback.track?.id == track.value.id, onTrackPlayButtonPressed: onTrackPlayButtonPressed, ); }).toList() diff --git a/lib/themes/light-theme.dart b/lib/themes/light-theme.dart index 65bb88cd..c8b7b92c 100644 --- a/lib/themes/light-theme.dart +++ b/lib/themes/light-theme.dart @@ -3,7 +3,7 @@ import 'package:spotube/extensions/ShimmerColorTheme.dart'; final materialWhite = MaterialColor(Colors.white.value, { 50: Colors.white, - 100: Colors.blueGrey[50]!, + 100: Colors.blueGrey[100]!, 200: Colors.white, 300: Colors.white, 400: Colors.blueGrey[300]!,