-
Notifications
You must be signed in to change notification settings - Fork 137
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #720 from Komodo5197/redesign-playlist-remove
[Redesign] Playlist removal from queue
- Loading branch information
Showing
57 changed files
with
3,149 additions
and
1,891 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
lib/components/AddToPlaylistScreen/add_to_playlist_button.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import 'package:Finamp/components/PlayerScreen/queue_source_helper.dart'; | ||
import 'package:Finamp/components/global_snackbar.dart'; | ||
import 'package:Finamp/models/finamp_models.dart'; | ||
import 'package:Finamp/models/jellyfin_models.dart'; | ||
import 'package:Finamp/services/favorite_provider.dart'; | ||
import 'package:Finamp/services/finamp_settings_helper.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||
import 'package:flutter_riverpod/flutter_riverpod.dart'; | ||
|
||
import 'playlist_actions_menu.dart'; | ||
|
||
class AddToPlaylistButton extends ConsumerStatefulWidget { | ||
const AddToPlaylistButton({ | ||
super.key, | ||
required this.item, | ||
this.queueItem, | ||
this.color, | ||
this.size, | ||
this.visualDensity, | ||
}); | ||
|
||
final BaseItemDto? item; | ||
final FinampQueueItem? queueItem; | ||
final Color? color; | ||
final double? size; | ||
final VisualDensity? visualDensity; | ||
|
||
@override | ||
ConsumerState<AddToPlaylistButton> createState() => | ||
_AddToPlaylistButtonState(); | ||
} | ||
|
||
class _AddToPlaylistButtonState extends ConsumerState<AddToPlaylistButton> { | ||
@override | ||
Widget build(BuildContext context) { | ||
if (widget.item == null) { | ||
return const SizedBox.shrink(); | ||
} | ||
|
||
bool isFav = ref | ||
.watch(isFavoriteProvider(widget.item?.id, DefaultValue(widget.item))); | ||
return GestureDetector( | ||
onLongPress: () async { | ||
ref | ||
.read(isFavoriteProvider(widget.item?.id, DefaultValue()).notifier) | ||
.updateFavorite(!isFav); | ||
}, | ||
child: IconButton( | ||
icon: Icon( | ||
isFav ? Icons.favorite : Icons.favorite_outline, | ||
size: widget.size ?? 24.0, | ||
), | ||
color: widget.color ?? IconTheme.of(context).color, | ||
disabledColor: | ||
(widget.color ?? IconTheme.of(context).color)!.withOpacity(0.3), | ||
visualDensity: widget.visualDensity ?? VisualDensity.compact, | ||
// tooltip: AppLocalizations.of(context)!.addToPlaylistTooltip, | ||
onPressed: () async { | ||
if (FinampSettingsHelper.finampSettings.isOffline) { | ||
return GlobalSnackbar.message((context) => | ||
AppLocalizations.of(context)!.notAvailableInOfflineMode); | ||
} | ||
|
||
bool inPlaylist = queueItemInPlaylist(widget.queueItem); | ||
await showPlaylistActionsMenu( | ||
context: context, | ||
item: widget.item!, | ||
parentPlaylist: inPlaylist ? widget.queueItem!.source.item : null, | ||
usePlayerTheme: true, | ||
); | ||
}), | ||
); | ||
} | ||
} |
257 changes: 201 additions & 56 deletions
257
lib/components/AddToPlaylistScreen/add_to_playlist_list.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,93 +1,238 @@ | ||
import 'dart:async'; | ||
|
||
import 'package:Finamp/models/finamp_models.dart'; | ||
import 'package:Finamp/services/downloads_service.dart'; | ||
import 'package:collection/collection.dart'; | ||
import 'package:Finamp/components/Buttons/cta_medium.dart'; | ||
import 'package:Finamp/components/PlayerScreen/queue_source_helper.dart'; | ||
import 'package:Finamp/components/album_image.dart'; | ||
import 'package:Finamp/services/finamp_settings_helper.dart'; | ||
import 'package:Finamp/services/jellyfin_api_helper.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; | ||
import 'package:get_it/get_it.dart'; | ||
|
||
import '../../models/jellyfin_models.dart'; | ||
import '../../services/jellyfin_api_helper.dart'; | ||
import '../MusicScreen/album_item.dart'; | ||
import '../global_snackbar.dart'; | ||
import 'new_playlist_dialog.dart'; | ||
import 'playlist_actions_menu.dart'; | ||
|
||
class AddToPlaylistList extends StatefulWidget { | ||
const AddToPlaylistList({ | ||
Key? key, | ||
required this.itemToAddId, | ||
}) : super(key: key); | ||
super.key, | ||
required this.itemToAdd, | ||
required this.playlistsFuture, | ||
}); | ||
|
||
final String itemToAddId; | ||
final BaseItemDto itemToAdd; | ||
final Future<List<BaseItemDto>> playlistsFuture; | ||
|
||
@override | ||
State<AddToPlaylistList> createState() => _AddToPlaylistListState(); | ||
} | ||
|
||
class _AddToPlaylistListState extends State<AddToPlaylistList> { | ||
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); | ||
late Future<List<BaseItemDto>?> addToPlaylistListFuture; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
addToPlaylistListFuture = jellyfinApiHelper.getItems( | ||
includeItemTypes: "Playlist", | ||
sortBy: "SortName", | ||
); | ||
playlistsFuture = widget.playlistsFuture.then( | ||
(value) => value.map((e) => (e, false, null as String?)).toList()); | ||
} | ||
|
||
// playlist, isLoading, playlistItemId | ||
late Future<List<(BaseItemDto, bool, String?)>> playlistsFuture; | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return FutureBuilder<List<BaseItemDto>?>( | ||
future: addToPlaylistListFuture, | ||
return FutureBuilder( | ||
future: playlistsFuture, | ||
builder: (context, snapshot) { | ||
if (snapshot.hasData) { | ||
return ListView.builder( | ||
itemCount: snapshot.data!.length, | ||
itemBuilder: (context, index) { | ||
return AlbumItem( | ||
album: snapshot.data![index], | ||
parentType: snapshot.data![index].type, | ||
isPlaylist: true, | ||
onTap: () async { | ||
try { | ||
await jellyfinApiHelper.addItemstoPlaylist( | ||
playlistId: snapshot.data![index].id, | ||
ids: [widget.itemToAddId], | ||
); | ||
final downloadsService = GetIt.instance<DownloadsService>(); | ||
unawaited(downloadsService.resync( | ||
DownloadStub.fromItem( | ||
type: DownloadItemType.collection, | ||
item: snapshot.data![index]), | ||
null, | ||
keepSlow: true)); | ||
|
||
if (!context.mounted) return; | ||
GlobalSnackbar.message( | ||
(scaffold) => AppLocalizations.of(context)! | ||
.confirmAddedToPlaylist, | ||
isConfirmation: true); | ||
Navigator.pop(context); | ||
} catch (e) { | ||
GlobalSnackbar.error(e); | ||
return; | ||
} | ||
}, | ||
); | ||
return SliverList( | ||
delegate: SliverChildBuilderDelegate( | ||
(context, index) { | ||
if (index == snapshot.data!.length) { | ||
return createNewPlaylistButton(context); | ||
} | ||
final (playlist, isLoading, playListItemId) = | ||
snapshot.data![index]; | ||
return AddToPlaylistTile( | ||
playlist: playlist, | ||
song: widget.itemToAdd, | ||
playlistItemId: playListItemId, | ||
isLoading: isLoading); | ||
}, | ||
); | ||
childCount: snapshot.data!.length + 1, | ||
)); | ||
} else if (snapshot.hasError) { | ||
GlobalSnackbar.error(snapshot.error); | ||
return const Center( | ||
child: Icon(Icons.error, size: 64), | ||
return const SliverToBoxAdapter( | ||
child: Center( | ||
heightFactor: 3.0, | ||
child: Icon(Icons.error, size: 64), | ||
), | ||
); | ||
} else { | ||
return const Center( | ||
child: CircularProgressIndicator.adaptive(), | ||
); | ||
return SliverList( | ||
delegate: SliverChildBuilderDelegate((context, index) { | ||
if (index == 1) { | ||
return createNewPlaylistButton(context); | ||
} else { | ||
return const Center( | ||
child: CircularProgressIndicator.adaptive(), | ||
); | ||
} | ||
}, childCount: 2)); | ||
} | ||
}, | ||
); | ||
} | ||
|
||
Widget createNewPlaylistButton(BuildContext context) { | ||
return Padding( | ||
padding: const EdgeInsets.only(top: 16.0), | ||
child: Row( | ||
mainAxisAlignment: MainAxisAlignment.center, | ||
children: [ | ||
CTAMedium( | ||
text: AppLocalizations.of(context)!.newPlaylist, | ||
icon: TablerIcons.plus, | ||
//accentColor: Theme.of(context).colorScheme.primary, | ||
onPressed: () async { | ||
var dialogResult = await showDialog<(Future<String>, String?)?>( | ||
context: context, | ||
builder: (context) => | ||
NewPlaylistDialog(itemToAdd: widget.itemToAdd.id), | ||
); | ||
if (dialogResult != null) { | ||
var oldFuture = playlistsFuture; | ||
setState(() { | ||
var loadingItem = [ | ||
( | ||
BaseItemDto(id: "pending", name: dialogResult.$2), | ||
true, | ||
null as String? | ||
) | ||
]; | ||
playlistsFuture = | ||
oldFuture.then((value) => value + loadingItem); | ||
}); | ||
try { | ||
var newId = await dialogResult.$1; | ||
// Give the server time to calculate an initial playlist image | ||
await Future.delayed(const Duration(seconds: 1)); | ||
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); | ||
var playlist = await jellyfinApiHelper.getItemById(newId); | ||
var playlistItems = await jellyfinApiHelper.getItems( | ||
parentItem: playlist, fields: ""); | ||
var song = playlistItems?.firstWhere( | ||
(element) => element.id == widget.itemToAdd.id); | ||
setState(() { | ||
var newItem = [(playlist, false, song?.playlistItemId)]; | ||
playlistsFuture = | ||
oldFuture.then((value) => value + newItem); | ||
}); | ||
} catch (e) { | ||
GlobalSnackbar.error(e); | ||
} | ||
} | ||
}, | ||
), | ||
], | ||
), | ||
); | ||
} | ||
} | ||
|
||
class AddToPlaylistTile extends StatefulWidget { | ||
const AddToPlaylistTile( | ||
{super.key, | ||
required this.playlist, | ||
this.playlistItemId, | ||
required this.song, | ||
this.isLoading = false}); | ||
|
||
final BaseItemDto playlist; | ||
final BaseItemDto song; | ||
final String? playlistItemId; | ||
final bool isLoading; | ||
|
||
@override | ||
State<AddToPlaylistTile> createState() => _AddToPlaylistTileState(); | ||
} | ||
|
||
class _AddToPlaylistTileState extends State<AddToPlaylistTile> { | ||
String? playlistItemId; | ||
int? childCount; | ||
bool knownMissing = false; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
if (!widget.isLoading) { | ||
playlistItemId = widget.playlistItemId; | ||
childCount = widget.playlist.childCount; | ||
} | ||
} | ||
|
||
@override | ||
void didUpdateWidget(AddToPlaylistTile oldWidget) { | ||
super.didUpdateWidget(oldWidget); | ||
if (!widget.isLoading) { | ||
playlistItemId = widget.playlistItemId; | ||
childCount = widget.playlist.childCount; | ||
} | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
final isOffline = FinampSettingsHelper.finampSettings.isOffline; | ||
return ToggleableListTile( | ||
forceLoading: widget.isLoading, | ||
title: widget.playlist.name ?? AppLocalizations.of(context)!.unknownName, | ||
subtitle: AppLocalizations.of(context)!.songCount(childCount ?? 0), | ||
leading: AlbumImage(item: widget.playlist), | ||
positiveIcon: TablerIcons.circle_check_filled, | ||
negativeIcon: knownMissing | ||
? TablerIcons.circle_plus | ||
// we don't actually know if the track is part of the playlist | ||
: TablerIcons.circle_dashed_plus, | ||
initialState: playlistItemId != null, | ||
onToggle: (bool currentState) async { | ||
if (currentState) { | ||
if (playlistItemId == null) { | ||
throw "Cannot remove item from playlist, missing playlistItemId"; | ||
} | ||
// part of playlist, remove | ||
bool removed = await removeFromPlaylist( | ||
context, widget.song, widget.playlist, playlistItemId!, | ||
confirm: false); | ||
if (removed) { | ||
setState(() { | ||
childCount = childCount == null ? null : childCount! - 1; | ||
knownMissing = true; | ||
}); | ||
} | ||
return !removed; | ||
} else { | ||
// add to playlist | ||
bool added = | ||
await addItemToPlaylist(context, widget.song, widget.playlist); | ||
if (added) { | ||
final jellyfinApiHelper = GetIt.instance<JellyfinApiHelper>(); | ||
var newItems = await jellyfinApiHelper.getItems( | ||
parentItem: widget.playlist, fields: ""); | ||
setState(() { | ||
childCount = newItems?.length ?? 0; | ||
playlistItemId = newItems | ||
?.firstWhereOrNull((x) => x.id == widget.song.id) | ||
?.playlistItemId; | ||
}); | ||
return playlistItemId != null; | ||
} | ||
return false; | ||
} | ||
}, | ||
enabled: !isOffline, | ||
); | ||
} | ||
} |
Oops, something went wrong.