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

[Redesign] Playlist removal from queue #720

Merged
merged 38 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d0554c0
Initial testing.
Komodo5197 May 3, 2024
2b9b8fe
Mostly working.
Komodo5197 May 4, 2024
4cff372
Fix current track offset desyncing on track change.
Komodo5197 May 7, 2024
1c62d8b
Merge branch 'desktop-beta' of https://github.com/jmshrv/finamp into …
Komodo5197 May 7, 2024
23cc5ac
Allow removal after queue restore, if possible.
Komodo5197 May 7, 2024
20d0d41
Clear removal cache on queue replacement.
Komodo5197 May 9, 2024
be323c5
close menu after confirming removing from playlist, handle playlists …
Chaphasilor May 9, 2024
3dd489d
added "quick actions" menu for when an item can either be added to or…
Chaphasilor May 9, 2024
fef2204
Fix lyrics screen autoscroll not stopping on desktop. Suppress inacc…
Komodo5197 May 10, 2024
43cec4c
Use correct theme on quick menu.
Komodo5197 May 10, 2024
2f28735
Fix player initialization. Show greyed out option to remove from pla…
Komodo5197 May 10, 2024
377874b
Use blurhashs for current track path. Add right click to open song m…
Komodo5197 May 10, 2024
cc16052
Merge branch 'desktop-beta' of https://github.com/jmshrv/finamp into …
Komodo5197 May 10, 2024
806dd57
Rough initial implementation of add/remove from playlist menu.
Komodo5197 May 11, 2024
eec02b8
add/remove from favorites and current playlist, refactor list of play…
Chaphasilor May 11, 2024
e1b7fa5
replace favorite button with add to playlist button for tracks, add d…
Chaphasilor May 11, 2024
687e0d2
rename quick actions menu to playlist actions menu
Chaphasilor May 11, 2024
eb31588
Move favorite checking/updating code into provider. Clean up other p…
Komodo5197 May 11, 2024
f967f5f
Fix favorite icon 'album' image size.
Komodo5197 May 11, 2024
cf6ea09
Theme propogation tweaks.
Komodo5197 May 12, 2024
b00956b
Hid current parent playlist in addToPlaylists. Added new playlist bu…
Komodo5197 May 12, 2024
1770e16
Use correct childcount across menu reloads.
Komodo5197 May 12, 2024
f96f895
updated new playlist button design, changed like gesture to long press
Chaphasilor May 12, 2024
3e2c7ec
Hide asking about transcode on album cache finamp collection. Mess w…
Komodo5197 May 13, 2024
7066914
add immediate haptic feedback onTap for ToggleableListTile
Chaphasilor May 13, 2024
94d19ad
Fix ProviderScope error. Tweak favorite haptic feedback. Tweak chec…
Komodo5197 May 13, 2024
86efc5f
Merge remote-tracking branch 'origin/redesign-playlist-remove' into r…
Komodo5197 May 13, 2024
1ae03d4
Merge branch 'desktop-beta' of https://github.com/jmshrv/finamp into …
Komodo5197 May 13, 2024
0a127e1
Change playlist add icons. Move dragHandle into themed_bottom_sheet.…
Komodo5197 May 13, 2024
b367f22
condense SongInfo header for playlist actions menu
Chaphasilor May 13, 2024
9e03c8a
updated menu strings, fixed overflow in condensed song info header fo…
Chaphasilor May 13, 2024
fdde8ba
Merge branch 'redesign-playlist-remove' of https://github.com/komodo5…
Chaphasilor May 13, 2024
6fa8bdc
Improve popping player/lyric screen when in splitscreen mode.
Komodo5197 May 13, 2024
44361f3
Upgrade dependancies.
Komodo5197 May 13, 2024
60d6fa8
Merge remote-tracking branch 'origin/redesign-playlist-remove' into r…
Komodo5197 May 13, 2024
4a3a7df
Improve logging of top level errors. Increase metadataProvider null …
Komodo5197 May 14, 2024
be61be1
Try to fix github build checks.
Komodo5197 May 14, 2024
21ca257
Merge branch 'desktop-beta' into pr/Komodo5197/720
Chaphasilor May 14, 2024
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
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ jobs:
- run: flutter pub get
- run: flutter gen-l10n
# - run: flutter test
- run: flutter build windows
# TODO pack in redistributables?
- uses: actions/upload-artifact@v4
with:
name: finamp-windows-zip
path: build/windows/x64/runner/Release/
- run: dart run msix:create --install-certificate false
#TODO would be nice to have an old-school installer here that can take the .exe + libraries and install them to the device + create a shortcut
- uses: actions/upload-artifact@v4
Expand Down
75 changes: 75 additions & 0 deletions lib/components/AddToPlaylistScreen/add_to_playlist_button.dart
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 lib/components/AddToPlaylistScreen/add_to_playlist_list.dart
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,
);
}
}
Loading
Loading