mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Queue support added for both Desktop & Mobile
This commit is contained in:
parent
a985c19ad8
commit
7e24059900
@ -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(),
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
77
lib/components/Player/PlayerQueue.dart
Normal file
77
lib/components/Player/PlayerQueue.dart
Normal 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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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 &&
|
||||||
|
@ -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);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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]!,
|
||||||
|
Loading…
Reference in New Issue
Block a user