Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Play Next & Add to Queue for songs and albums. #481

Merged
merged 5 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions lib/components/AlbumScreen/song_list_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'downloaded_indicator.dart';

enum SongListTileMenuItems {
addToQueue,
playNext,
replaceQueueWithItem,
addToPlaylist,
removeFromPlaylist,
Expand Down Expand Up @@ -238,13 +239,22 @@ class _SongListTileState extends State<SongListTile> {
screenSize.height - details.globalPosition.dy,
),
items: [
PopupMenuItem<SongListTileMenuItems>(
value: SongListTileMenuItems.addToQueue,
child: ListTile(
leading: const Icon(Icons.queue_music),
title: Text(AppLocalizations.of(context)!.addToQueue),
if (_audioServiceHelper.hasQueueItems()) ...[
PopupMenuItem<SongListTileMenuItems>(
value: SongListTileMenuItems.addToQueue,
child: ListTile(
leading: const Icon(Icons.queue_music),
title: Text(AppLocalizations.of(context)!.addToQueue),
),
),
),
PopupMenuItem<SongListTileMenuItems>(
value: SongListTileMenuItems.playNext,
child: ListTile(
leading: const Icon(Icons.queue_music),
title: Text(AppLocalizations.of(context)!.playNext),
),
),
],
PopupMenuItem<SongListTileMenuItems>(
value: SongListTileMenuItems.replaceQueueWithItem,
child: ListTile(
Expand Down Expand Up @@ -324,7 +334,7 @@ class _SongListTileState extends State<SongListTile> {

switch (selection) {
case SongListTileMenuItems.addToQueue:
await _audioServiceHelper.addQueueItem(widget.item);
await _audioServiceHelper.addQueueItems([widget.item]);

if (!mounted) return;

Expand All @@ -333,6 +343,16 @@ class _SongListTileState extends State<SongListTile> {
));
break;

case SongListTileMenuItems.playNext:
await _audioServiceHelper.insertQueueItemsNext([widget.item]);

if (!mounted) return;

ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context)!.insertedIntoQueue),
));
break;

case SongListTileMenuItems.replaceQueueWithItem:
await _audioServiceHelper
.replaceQueueWithItem(itemList: [widget.item]);
Expand Down Expand Up @@ -451,7 +471,7 @@ class _SongListTileState extends State<SongListTile> {
),
),
confirmDismiss: (direction) async {
await _audioServiceHelper.addQueueItem(widget.item);
await _audioServiceHelper.addQueueItems([widget.item]);

if (!mounted) return false;

Expand Down
67 changes: 62 additions & 5 deletions lib/components/MusicScreen/album_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:get_it/get_it.dart';

import '../../models/jellyfin_models.dart';
import '../../services/audio_service_helper.dart';
import '../../services/jellyfin_api_helper.dart';
import '../../screens/artist_screen.dart';
import '../../screens/album_screen.dart';
import '../error_snackbar.dart';
import 'album_item_card.dart';

enum _AlbumListTileMenuItems {
addToQueue,
playNext,
addFavourite,
removeFavourite,
addToMixList,
Expand Down Expand Up @@ -58,6 +61,8 @@ class AlbumItem extends StatefulWidget {
}

class _AlbumItemState extends State<AlbumItem> {
final _audioServiceHelper = GetIt.instance<AudioServiceHelper>();

late BaseItemDto mutableAlbum;

late Function() onTap;
Expand Down Expand Up @@ -93,11 +98,7 @@ class _AlbumItemState extends State<AlbumItem> {
onLongPressStart: (details) async {
Feedback.forLongPress(context);

if (FinampSettingsHelper.finampSettings.isOffline) {
// If offline, don't show the context menu since the only options here
// are for online.
return;
}
final isOffline = FinampSettingsHelper.finampSettings.isOffline;

final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>();

Expand All @@ -110,34 +111,58 @@ class _AlbumItemState extends State<AlbumItem> {
screenSize.height - details.globalPosition.dy,
),
items: [
if (_audioServiceHelper.hasQueueItems()) ...[
PopupMenuItem<_AlbumListTileMenuItems>(
value: _AlbumListTileMenuItems.addToQueue,
child: ListTile(
leading: const Icon(Icons.queue_music),
title: Text(AppLocalizations.of(context)!.addToQueue),
),
),
PopupMenuItem<_AlbumListTileMenuItems>(
value: _AlbumListTileMenuItems.playNext,
child: ListTile(
leading: const Icon(Icons.queue_music),
title: Text(AppLocalizations.of(context)!.playNext),
),
),
],
mutableAlbum.userData!.isFavorite
? PopupMenuItem<_AlbumListTileMenuItems>(
enabled: !isOffline,
value: _AlbumListTileMenuItems.removeFavourite,
child: ListTile(
enabled: !isOffline,
leading: const Icon(Icons.favorite_border),
title:
Text(AppLocalizations.of(context)!.removeFavourite),
),
)
: PopupMenuItem<_AlbumListTileMenuItems>(
enabled: !isOffline,
value: _AlbumListTileMenuItems.addFavourite,
child: ListTile(
enabled: !isOffline,
leading: const Icon(Icons.favorite),
title: Text(AppLocalizations.of(context)!.addFavourite),
),
),
jellyfinApiHelper.selectedMixAlbumIds.contains(mutableAlbum.id)
? PopupMenuItem<_AlbumListTileMenuItems>(
enabled: !isOffline,
value: _AlbumListTileMenuItems.removeFromMixList,
child: ListTile(
enabled: !isOffline,
leading: const Icon(Icons.explore_off),
title:
Text(AppLocalizations.of(context)!.removeFromMix),
),
)
: PopupMenuItem<_AlbumListTileMenuItems>(
enabled: !isOffline,
value: _AlbumListTileMenuItems.addToMixList,
child: ListTile(
enabled: !isOffline,
leading: const Icon(Icons.explore),
title: Text(AppLocalizations.of(context)!.addToMix),
),
Expand All @@ -148,6 +173,38 @@ class _AlbumItemState extends State<AlbumItem> {
if (!mounted) return;

switch (selection) {
case _AlbumListTileMenuItems.addToQueue:
final children = await jellyfinApiHelper.getItems(
parentItem: widget.album,
sortBy: "ParentIndexNumber,IndexNumber,SortName",
includeItemTypes: "Audio",
isGenres: false,
);
await _audioServiceHelper.addQueueItems(children!);

if (!mounted) return;

ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context)!.addedToQueue),
));
break;

case _AlbumListTileMenuItems.playNext:
final children = await jellyfinApiHelper.getItems(
parentItem: widget.album,
sortBy: "ParentIndexNumber,IndexNumber,SortName",
includeItemTypes: "Audio",
isGenres: false,
);
await _audioServiceHelper.insertQueueItemsNext(children!);

if (!mounted) return;

ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context)!.insertedIntoQueue),
));
break;

case _AlbumListTileMenuItems.addFavourite:
try {
final newUserData =
Expand Down
16 changes: 14 additions & 2 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,13 @@
"queue": "Queue",
"@queue": {},
"addToQueue": "Add to Queue",
"@addToQueue": {},
"@addToQueue": {
"description": "Popup menu item title for adding an item to the end of the play queue."
},
"playNext": "Play Next",
"@playNext": {
"description": "Popup menu item title for inserting an item into the play queue after the currently-playing item."
},
"replaceQueue": "Replace Queue",
"@replaceQueue": {},
"instantMix": "Instant Mix",
Expand All @@ -429,7 +435,13 @@
"addFavourite": "Add Favourite",
"@addFavourite": {},
"addedToQueue": "Added to queue.",
"@addedToQueue": {},
"@addedToQueue": {
"description": "Snackbar message that shows when the user successfully adds items to the end of the play queue."
},
"insertedIntoQueue": "Inserted into queue.",
"@insertedIntoQueue": {
"description": "Snackbar message that shows when the user successfully inserts items into the play queue at a location that is not necessarily the end."
},
"queueReplaced": "Queue replaced.",
"@queueReplaced": {},
"removedFromPlaylist": "Removed from playlist.",
Expand Down
33 changes: 30 additions & 3 deletions lib/services/audio_service_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,44 @@ class AudioServiceHelper {
}
}

bool hasQueueItems() {
return (_audioHandler.queue.valueOrNull?.length ?? 0) != 0;
}

@Deprecated("Use addQueueItems instead")
Future<void> addQueueItem(BaseItemDto item) async {
await addQueueItems([item]);
}
Comment on lines +71 to +74
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be marked deprecated? It would be more ergonomic to allow adding single items, and would remove the need to change everything that adds a single item

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial train of thought was the few extra characters for one less function call on the stack and a slightly more concise API surface was a worthwhile tradeoff, but I don't really want to die on that hill (and truth be told I don't know much about inlining in modern Dart) so I can drop the deprecated attributes if desired.


Future<void> addQueueItems(List<BaseItemDto> items) async {
try {
// If the queue is empty (like when the app is first launched), run the
// replace queue function instead so that the song gets played
if ((_audioHandler.queue.valueOrNull?.length ?? 0) == 0) {
await replaceQueueWithItem(itemList: [item]);
await replaceQueueWithItem(itemList: items);
return;
}

final mediaItems =
await Future.wait(items.map((i) => _generateMediaItem(i)));
await _audioHandler.addQueueItems(mediaItems);
} catch (e) {
audioServiceHelperLogger.severe(e);
return Future.error(e);
}
}

Future<void> insertQueueItemsNext(List<BaseItemDto> items) async {
try {
// See above comment in addQueueItem
if ((_audioHandler.queue.valueOrNull?.length ?? 0) == 0) {
await replaceQueueWithItem(itemList: items);
return;
}

final itemMediaItem = await _generateMediaItem(item);
await _audioHandler.addQueueItem(itemMediaItem);
final mediaItems =
await Future.wait(items.map((i) => _generateMediaItem(i)));
await _audioHandler.insertQueueItemsNext(mediaItems);
} catch (e) {
audioServiceHelperLogger.severe(e);
return Future.error(e);
Expand Down
Loading
Loading