diff --git a/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart b/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart index 6aea0d51d..4204e62ea 100644 --- a/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart +++ b/lib/components/AddToPlaylistScreen/add_to_playlist_list.dart @@ -4,6 +4,7 @@ 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/downloads_service.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; import 'package:flutter/material.dart'; @@ -163,23 +164,31 @@ class AddToPlaylistTile extends StatefulWidget { class _AddToPlaylistTileState extends State { String? playlistItemId; int? childCount; - bool knownMissing = false; + bool? itemIsIncluded; @override void initState() { super.initState(); - if (!widget.isLoading) { - playlistItemId = widget.playlistItemId; - childCount = widget.playlist.childCount; - } + _updateState(); } @override void didUpdateWidget(AddToPlaylistTile oldWidget) { super.didUpdateWidget(oldWidget); + _updateState(); + } + + void _updateState() { if (!widget.isLoading) { playlistItemId = widget.playlistItemId; childCount = widget.playlist.childCount; + if (widget.playlistItemId != null) { + itemIsIncluded = true; + } else { + final downloadsService = GetIt.instance(); + itemIsIncluded = + downloadsService.checkIfInCollection(widget.playlist, widget.song); + } } } @@ -192,15 +201,33 @@ class _AddToPlaylistTileState extends State { subtitle: AppLocalizations.of(context)!.songCount(childCount ?? 0), leading: AlbumImage(item: widget.playlist), positiveIcon: TablerIcons.circle_check_filled, - negativeIcon: knownMissing - ? TablerIcons.circle_plus + negativeIcon: itemIsIncluded == null // we don't actually know if the track is part of the playlist - : TablerIcons.circle_dashed_plus, - initialState: playlistItemId != null, + ? TablerIcons.circle_dashed_plus + : TablerIcons.circle_plus, + initialState: itemIsIncluded ?? false, onToggle: (bool currentState) async { if (currentState) { + // If playlistItemId is null, we need to fetch from the server before we can remove if (playlistItemId == null) { - throw "Cannot remove item from playlist, missing playlistItemId"; + final jellyfinApiHelper = GetIt.instance(); + var newItems = await jellyfinApiHelper.getItems( + parentItem: widget.playlist, fields: ""); + + playlistItemId = newItems + ?.firstWhereOrNull((x) => x.id == widget.song.id) + ?.playlistItemId; + if (playlistItemId == null) { + // We were already not part of the playlist,. so removal is complete + setState(() { + childCount = newItems?.length ?? 0; + itemIsIncluded = false; + }); + return false; + } + if (!context.mounted) { + return true; + } } // part of playlist, remove bool removed = await removeFromPlaylist( @@ -209,7 +236,7 @@ class _AddToPlaylistTileState extends State { if (removed) { setState(() { childCount = childCount == null ? null : childCount! - 1; - knownMissing = true; + itemIsIncluded = false; }); } return !removed; @@ -226,6 +253,7 @@ class _AddToPlaylistTileState extends State { playlistItemId = newItems ?.firstWhereOrNull((x) => x.id == widget.song.id) ?.playlistItemId; + itemIsIncluded = true; }); return playlistItemId != null; } diff --git a/lib/components/AlbumScreen/download_dialog.dart b/lib/components/AlbumScreen/download_dialog.dart index a58cfccb5..df07213ec 100644 --- a/lib/components/AlbumScreen/download_dialog.dart +++ b/lib/components/AlbumScreen/download_dialog.dart @@ -45,11 +45,14 @@ class DownloadDialog extends StatefulWidget { final finampUserHelper = GetIt.instance(); viewId = finampUserHelper.currentUser!.currentViewId; } - bool needTranscode = - FinampSettingsHelper.finampSettings.shouldTranscodeDownloads == + bool needTranscode = FinampSettingsHelper + .finampSettings.shouldTranscodeDownloads == TranscodeDownloadsSetting.ask && - // Skipp asking for transcode for image only collection - item.finampCollection?.type != FinampCollectionType.libraryImages; + // Skip asking for transcode for image only collection + item.finampCollection?.type != FinampCollectionType.libraryImages || + // Skip asking for transcode for metadata +image collection + (item.finampCollection?.type != FinampCollectionType.allPlaylists && + infoOnly); String? downloadLocation = FinampSettingsHelper.finampSettings.defaultDownloadLocation; if (!FinampSettingsHelper.finampSettings.downloadLocationsMap diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3b88c66d1..8171c1999 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1504,5 +1504,13 @@ "description": "Subheader for adding to a playlist in the add to/remove from playlist popup menu" }, "trackOfflineFavorites": "Sync all favorite statuses", - "trackOfflineFavoritesSubtitle": "This allows showing more up-to-date favorite statuses while offline. Does not download any additional files." + "trackOfflineFavoritesSubtitle": "This allows showing more up-to-date favorite statuses while offline. Does not download any additional files.", + "allPlaylistsInfoSetting": "Show all playlists offline", + "allPlaylistsInfoSettingSubtitle": "Sync metadata for all playlists to show partially downloaded playlists offline", + "downloadFavoritesSetting": "Download all favorites", + "downloadAllPlaylistsSetting": "Download all playlists", + "fiveLatestAlbumsSetting": "Download 5 latest albums", + "fiveLatestAlbumsSettingSubtitle": "Downloads will be removed as they age out. Lock the download to prevent an album from being removed.", + "cacheLibraryImagesSettings": "Cache current library images", + "cacheLibraryImagesSettingsSubtitle": "All album, artist, genre, and playlist covers in the currently active library will be downloaded." } diff --git a/lib/models/jellyfin_models.dart b/lib/models/jellyfin_models.dart index bcc866d15..515e26263 100644 --- a/lib/models/jellyfin_models.dart +++ b/lib/models/jellyfin_models.dart @@ -7,8 +7,8 @@ /// /// These classes should be correct with Jellyfin 10.7.5 -import 'package:finamp/models/finamp_models.dart'; import 'package:collection/collection.dart'; +import 'package:finamp/models/finamp_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hive/hive.dart'; @@ -2294,6 +2294,9 @@ class BaseItemDto with RunTimeTickDuration { other.normalizationGain == normalizationGain && other.playlistItemId == playlistItemId; } + + DownloadItemType get downloadType => + type! == "Audio" ? DownloadItemType.song : DownloadItemType.collection; } @JsonSerializable( diff --git a/lib/screens/downloads_settings_screen.dart b/lib/screens/downloads_settings_screen.dart index 0b69ff5e4..4efe5cc70 100644 --- a/lib/screens/downloads_settings_screen.dart +++ b/lib/screens/downloads_settings_screen.dart @@ -40,9 +40,9 @@ class DownloadsSettingsScreen extends StatelessWidget { const SyncFavoritesSwitch(), ListTile( // TODO real UI for this - title: const Text("Show all playlists offline"), - subtitle: const Text( - "Sync metadata for all playlists to show partially downloaded playlists offline"), + title: Text(AppLocalizations.of(context)!.allPlaylistsInfoSetting), + subtitle: Text( + AppLocalizations.of(context)!.allPlaylistsInfoSettingSubtitle), trailing: DownloadButton( infoOnly: true, item: DownloadStub.fromFinampCollection( @@ -52,32 +52,34 @@ class DownloadsSettingsScreen extends StatelessWidget { if (!Platform.isIOS) const ConcurentDownloadsSelector(), ListTile( // TODO real UI for this - title: const Text("Download all favorites"), + title: Text(AppLocalizations.of(context)!.downloadFavoritesSetting), trailing: DownloadButton( item: DownloadStub.fromFinampCollection( FinampCollection(type: FinampCollectionType.favorites))), ), ListTile( // TODO real UI for this - title: const Text("Download all playlists"), + title: + Text(AppLocalizations.of(context)!.downloadAllPlaylistsSetting), trailing: DownloadButton( item: DownloadStub.fromFinampCollection( FinampCollection(type: FinampCollectionType.allPlaylists))), ), ListTile( // TODO real UI for this - title: const Text("Download 5 latest albums"), - subtitle: const Text( - "Downloads will be removed as they age out. Lock the download to prevent an album from being removed."), + title: Text(AppLocalizations.of(context)!.fiveLatestAlbumsSetting), + subtitle: Text( + AppLocalizations.of(context)!.fiveLatestAlbumsSettingSubtitle), trailing: DownloadButton( item: DownloadStub.fromFinampCollection(FinampCollection( type: FinampCollectionType.latest5Albums))), ), ListTile( // TODO real UI for this - title: const Text("Cache current library images"), - subtitle: const Text( - "All album, artist, genre, and playlist covers in the currently active library will be downloaded."), + title: + Text(AppLocalizations.of(context)!.cacheLibraryImagesSettings), + subtitle: Text(AppLocalizations.of(context)! + .cacheLibraryImagesSettingsSubtitle), trailing: DownloadButton( item: DownloadStub.fromFinampCollection(FinampCollection( type: FinampCollectionType.libraryImages, diff --git a/lib/services/downloads_service.dart b/lib/services/downloads_service.dart index 2e31801ae..4497f2ac4 100644 --- a/lib/services/downloads_service.dart +++ b/lib/services/downloads_service.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'dart:collection'; import 'dart:io'; -import 'package:finamp/components/global_snackbar.dart'; -import 'package:finamp/services/jellyfin_api_helper.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; +import 'package:finamp/components/global_snackbar.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_riverpod/flutter_riverpod.dart'; @@ -289,7 +289,6 @@ class DownloadsService { // If app is in background for more than 5 hours, treat it like a restart // and potentially resync all items if (FinampSettingsHelper.finampSettings.resyncOnStartup && - !FinampSettingsHelper.finampSettings.isOffline && _appPauseTime != null && DateTime.now().difference(_appPauseTime!).inHours > 5) { _isar.writeTxnSync(() { @@ -365,8 +364,7 @@ class DownloadsService { /// Begin processing stored downloads/deletes. This should only be called /// after background_downloader is fully set up. Future startQueues() async { - if (FinampSettingsHelper.finampSettings.resyncOnStartup && - !FinampSettingsHelper.finampSettings.isOffline) { + if (FinampSettingsHelper.finampSettings.resyncOnStartup) { _isar.writeTxnSync(() { syncBuffer.addAll([_anchor.isarId], [], null); }); @@ -1253,6 +1251,15 @@ class DownloadsService { .findFirstSync(); } + /// Check whether the given item is part of the given collection. Returns null + /// if there is no metadata for the collection. + bool? checkIfInCollection(BaseItemDto collection, BaseItemDto item) { + var parent = _isar.downloadItems.getSync( + DownloadStub.getHash(collection.id, DownloadItemType.collection)); + var childId = DownloadStub.getHash(item.id, item.downloadType); + return parent?.orderedChildren?.contains(childId); + } + // This is for album/playlist screen /// Get all songs in a collection, ordered correctly. Used to show songs on /// album/playlist screen. Can return all songs in the album/playlist or @@ -1438,11 +1445,7 @@ class DownloadsService { } bool? isFavorite(BaseItemDto item) { - var stubId = DownloadStub.getHash( - item.id, - item.type == "Audio" - ? DownloadItemType.song - : DownloadItemType.collection); + var stubId = DownloadStub.getHash(item.id, item.downloadType); return _getFavoriteIds()?.contains(stubId); } diff --git a/lib/services/downloads_service_backend.dart b/lib/services/downloads_service_backend.dart index 016310ab0..4146932da 100644 --- a/lib/services/downloads_service_backend.dart +++ b/lib/services/downloads_service_backend.dart @@ -3,10 +3,10 @@ import 'dart:convert'; import 'dart:core'; import 'dart:io'; -import 'package:finamp/components/global_snackbar.dart'; -import 'package:finamp/services/downloads_service.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; +import 'package:finamp/components/global_snackbar.dart'; +import 'package:finamp/services/downloads_service.dart'; import 'package:flutter/scheduler.dart'; import 'package:get_it/get_it.dart'; import 'package:isar/isar.dart'; @@ -1410,11 +1410,7 @@ class DownloadsSyncService { _downloadsService.resetConnectionErrors(); var stubList = outputItems .map((e) => DownloadStub.fromItem( - item: e, - type: typeOverride ?? - (e.type == "Audio" - ? DownloadItemType.song - : DownloadItemType.collection))) + item: e, type: typeOverride ?? e.downloadType)) .toList(); for (var element in stubList) { _metadataCache[element.id] = Future.value(element); diff --git a/lib/services/favorite_provider.dart b/lib/services/favorite_provider.dart index cfba4f967..f73b080f4 100644 --- a/lib/services/favorite_provider.dart +++ b/lib/services/favorite_provider.dart @@ -12,7 +12,7 @@ import 'jellyfin_api_helper.dart'; part 'favorite_provider.g.dart'; -/// All DefaultValues should be considered equal +/// All favoriteRequests with the same BaseItemDto id should be considered equal. class FavoriteRequest { final BaseItemDto? item; @@ -33,8 +33,6 @@ class IsFavorite extends _$IsFavorite { Future? _initializing; @override - // Because DefaultValue is always equal and we never invalidate, this should only - // be called once per itemId and all other DefaultValues should be ignored bool build(FavoriteRequest value) { if (value.item == null) { return false;