Queue support added for both Desktop & Mobile

This commit is contained in:
Kingkor Roy Tirtho 2022-07-04 11:10:17 +06:00
parent a985c19ad8
commit 7e24059900
8 changed files with 277 additions and 158 deletions

View File

@ -240,6 +240,7 @@ class ArtistProfile extends HookConsumerWidget {
duration: duration, duration: duration,
track: track, track: track,
thumbnailUrl: thumbnailUrl, thumbnailUrl: thumbnailUrl,
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) => onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist( playPlaylist(
topTracks.toList(), topTracks.toList(),

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.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/DownloadTrackButton.dart';
import 'package:spotube/components/Shared/HeartButton.dart'; import 'package:spotube/components/Shared/HeartButton.dart';
import 'package:spotube/hooks/useForceUpdate.dart'; import 'package:spotube/hooks/useForceUpdate.dart';
@ -27,6 +28,29 @@ class PlayerActions extends HookConsumerWidget {
return Row( return Row(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
children: [ 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( DownloadTrackButton(
track: playback.track, track: playback.track,
), ),

View File

@ -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);
},
),
);
}),
],
),
),
],
),
),
);
}
}

View File

@ -114,6 +114,7 @@ class Search extends HookConsumerWidget {
duration: duration, duration: duration,
thumbnailUrl: thumbnailUrl:
imageToUrlString(track.value.album?.images), imageToUrlString(track.value.album?.images),
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) async { onTrackPlayButtonPressed: (currentTrack) async {
var isPlaylistPlaying = playback.playlist?.id != var isPlaylistPlaying = playback.playlist?.id !=
null && null &&

View File

@ -1,30 +1,29 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class NotFound extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( final widgets = [
children: [ SizedBox(
SizedBox( height: 150,
height: 150, width: 150,
width: 150, child: Image.asset("assets/empty_box.png"),
child: Image.asset("assets/empty_box.png"), ),
), Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
children: [ Text("Nothing found", style: Theme.of(context).textTheme.headline6),
Text("Nothing found", style: Theme.of(context).textTheme.headline6), Text(
Text( "The box is empty",
"The box is empty", style: Theme.of(context).textTheme.subtitle1,
style: Theme.of(context).textTheme.subtitle1, ),
), ],
], ),
), ];
], return vertical ? Column(children: widgets) : Row(children: widgets);
);
} }
} }

View File

@ -24,14 +24,21 @@ class TrackTile extends HookConsumerWidget {
final bool userPlaylist; final bool userPlaylist;
// null playlistId indicates its not inside a playlist // null playlistId indicates its not inside a playlist
final String? playlistId; final String? playlistId;
final bool showAlbum;
final bool isActive;
TrackTile( TrackTile(
this.playback, { this.playback, {
required this.track, required this.track,
required this.duration, required this.duration,
required this.isActive,
this.playlistId, this.playlistId,
this.userPlaylist = false, this.userPlaylist = false,
this.thumbnailUrl, this.thumbnailUrl,
this.onTrackPlayButtonPressed, this.onTrackPlayButtonPressed,
this.showAlbum = true,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -162,151 +169,160 @@ class TrackTile extends HookConsumerWidget {
}); });
} }
return Row( return AnimatedContainer(
children: [ duration: const Duration(milliseconds: 500),
SizedBox( decoration: BoxDecoration(
height: 20, color: isActive
width: 25, ? Theme.of(context).popupMenuTheme.color
child: Text( : Colors.transparent,
(track.key + 1).toString(), borderRadius: BorderRadius.circular(isActive ? 10 : 0),
textAlign: TextAlign.center, ),
), child: Row(
), children: [
if (thumbnailUrl != null) SizedBox(
Padding( height: 20,
padding: EdgeInsets.symmetric( width: 25,
horizontal: breakpoint.isMoreThan(Breakpoints.md) ? 8.0 : 0, child: Text(
vertical: 8.0, (track.key + 1).toString(),
textAlign: TextAlign.center,
), ),
child: ClipRRect( ),
borderRadius: const BorderRadius.all(Radius.circular(5)), if (thumbnailUrl != null)
child: CachedNetworkImage( Padding(
placeholder: (context, url) { padding: EdgeInsets.symmetric(
return Container( horizontal: breakpoint.isMoreThan(Breakpoints.md) ? 8.0 : 0,
height: 40, vertical: 8.0,
width: 40, ),
color: Theme.of(context).primaryColor, child: ClipRRect(
); borderRadius: const BorderRadius.all(Radius.circular(5)),
}, child: CachedNetworkImage(
imageUrl: thumbnailUrl!, placeholder: (context, url) {
maxHeightDiskCache: 40, return Container(
maxWidthDiskCache: 40, 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( Expanded(
icon: Icon( child: Column(
playback.track?.id != null && playback.track?.id == track.value.id crossAxisAlignment: CrossAxisAlignment.start,
? Icons.pause_circle_rounded children: [
: Icons.play_circle_rounded, Text(
color: Theme.of(context).primaryColor, track.value.name ?? "",
), style: TextStyle(
onPressed: () => onTrackPlayButtonPressed?.call( fontWeight: FontWeight.bold,
track.value, fontSize: breakpoint.isSm ? 14 : 17,
), ),
), overflow: TextOverflow.ellipsis,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.value.name ?? "",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: breakpoint.isSm ? 14 : 17,
), ),
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, 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), 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;
}
},
),
],
); );
} }
} }

View File

@ -92,6 +92,7 @@ class TracksTableView extends HookConsumerWidget {
duration: duration, duration: duration,
thumbnailUrl: thumbnailUrl, thumbnailUrl: thumbnailUrl,
userPlaylist: userPlaylist, userPlaylist: userPlaylist,
isActive: playback.track?.id == track.value.id,
onTrackPlayButtonPressed: onTrackPlayButtonPressed, onTrackPlayButtonPressed: onTrackPlayButtonPressed,
); );
}).toList() }).toList()

View File

@ -3,7 +3,7 @@ import 'package:spotube/extensions/ShimmerColorTheme.dart';
final materialWhite = MaterialColor(Colors.white.value, { final materialWhite = MaterialColor(Colors.white.value, {
50: Colors.white, 50: Colors.white,
100: Colors.blueGrey[50]!, 100: Colors.blueGrey[100]!,
200: Colors.white, 200: Colors.white,
300: Colors.white, 300: Colors.white,
400: Colors.blueGrey[300]!, 400: Colors.blueGrey[300]!,