From 9be219ce15db38a17cb75c72d893559b2265f123 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 20:38:18 +0000 Subject: [PATCH] feat: Add functionality to remove items from Continue Watching - Updated the DiscoverScreen to include a callback for removing items from Continue Watching. - Enhanced MediaCard to accept a new callback for removing items from Continue Watching. - Modified MediaContextMenu to handle the removal of items from Continue Watching without affecting their watched status or progress. - Updated dependencies in pubspec.lock to their latest versions. Co-authored-by: Doezer <11655673+Doezer@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- lib/client/plex_client.dart | 9 +++++++ lib/i18n/strings.g.dart | 16 +++++++++-- lib/i18n/strings.i18n.json | 2 ++ lib/i18n/strings_it.i18n.json | 2 ++ lib/i18n/strings_sv.i18n.json | 2 ++ lib/screens/discover_screen.dart | 7 ++++- lib/widgets/media_card.dart | 6 +++++ lib/widgets/media_context_menu.dart | 41 +++++++++++++++++++++++++++++ pubspec.lock | 20 +++++++------- 9 files changed, 92 insertions(+), 13 deletions(-) diff --git a/lib/client/plex_client.dart b/lib/client/plex_client.dart index 961e6834..ebce3753 100644 --- a/lib/client/plex_client.dart +++ b/lib/client/plex_client.dart @@ -869,6 +869,15 @@ class PlexClient { ); } + /// Remove item from Continue Watching (On Deck) without affecting watch status or progress + /// This uses the same endpoint Plex Web uses to hide items from Continue Watching + Future removeFromOnDeck(String ratingKey) async { + await _dio.put( + '/actions/removeFromContinueWatching', + queryParameters: {'ratingKey': ratingKey}, + ); + } + /// Get server preferences Future> getServerPreferences() async { final response = await _dio.get('/:/prefs'); diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index aa6deed4..b0ddc374 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 3 -/// Strings: 930 (310 per locale) +/// Strings: 936 (312 per locale) /// -/// Built on 2025-11-12 at 17:03 UTC +/// Built on 2025-11-13 at 18:06 UTC // coverage:ignore-file // ignore_for_file: type=lint @@ -426,6 +426,7 @@ class _StringsMediaMenuEn { // Translations String get markAsWatched => 'Mark as Watched'; String get markAsUnwatched => 'Mark as Unwatched'; + String get removeFromContinueWatching => 'Remove from Continue Watching'; String get goToSeries => 'Go to series'; String get goToSeason => 'Go to season'; String get shufflePlay => 'Shuffle Play'; @@ -487,6 +488,7 @@ class _StringsMessagesEn { // Translations String get markedAsWatched => 'Marked as watched'; String get markedAsUnwatched => 'Marked as unwatched'; + String get removedFromContinueWatching => 'Removed from Continue Watching'; String errorLoading({required Object error}) => 'Error: ${error}'; String get fileInfoNotAvailable => 'File information not available'; String errorLoadingFileInfo({required Object error}) => 'Error loading file info: ${error}'; @@ -1032,6 +1034,7 @@ class _StringsMediaMenuIt implements _StringsMediaMenuEn { // Translations @override String get markAsWatched => 'Segna come visto'; @override String get markAsUnwatched => 'Segna come non visto'; + @override String get removeFromContinueWatching => 'Rimuovi da Continua a guardare'; @override String get goToSeries => 'Vai alle serie'; @override String get goToSeason => 'Vai alla stagione'; @override String get shufflePlay => 'Riproduzione casuale'; @@ -1093,6 +1096,7 @@ class _StringsMessagesIt implements _StringsMessagesEn { // Translations @override String get markedAsWatched => 'Segna come visto'; @override String get markedAsUnwatched => 'Segna come non visto'; + @override String get removedFromContinueWatching => 'Rimosso da Continua a guardare'; @override String errorLoading({required Object error}) => 'Errore: ${error}'; @override String get fileInfoNotAvailable => 'Informazioni sul file non disponibili'; @override String errorLoadingFileInfo({required Object error}) => 'Errore caricamento informazioni sul file: ${error}'; @@ -1638,6 +1642,7 @@ class _StringsMediaMenuSv implements _StringsMediaMenuEn { // Translations @override String get markAsWatched => 'Markera som sedd'; @override String get markAsUnwatched => 'Markera som osedd'; + @override String get removeFromContinueWatching => 'Ta bort från Fortsätt titta'; @override String get goToSeries => 'Gå till serie'; @override String get goToSeason => 'Gå till säsong'; @override String get shufflePlay => 'Blanda uppspelning'; @@ -1699,6 +1704,7 @@ class _StringsMessagesSv implements _StringsMessagesEn { // Translations @override String get markedAsWatched => 'Markerad som sedd'; @override String get markedAsUnwatched => 'Markerad som osedd'; + @override String get removedFromContinueWatching => 'Borttagen från Fortsätt titta'; @override String errorLoading({required Object error}) => 'Fel: ${error}'; @override String get fileInfoNotAvailable => 'Filinformation inte tillgänglig'; @override String errorLoadingFileInfo({required Object error}) => 'Fel vid laddning av filinformation: ${error}'; @@ -2100,6 +2106,7 @@ extension on Translations { case 'fileInfo.has64bitOffsets': return '64-bit Offsets'; case 'mediaMenu.markAsWatched': return 'Mark as Watched'; case 'mediaMenu.markAsUnwatched': return 'Mark as Unwatched'; + case 'mediaMenu.removeFromContinueWatching': return 'Remove from Continue Watching'; case 'mediaMenu.goToSeries': return 'Go to series'; case 'mediaMenu.goToSeason': return 'Go to season'; case 'mediaMenu.shufflePlay': return 'Shuffle Play'; @@ -2125,6 +2132,7 @@ extension on Translations { case 'userStatus.protected': return 'Protected'; case 'messages.markedAsWatched': return 'Marked as watched'; case 'messages.markedAsUnwatched': return 'Marked as unwatched'; + case 'messages.removedFromContinueWatching': return 'Removed from Continue Watching'; case 'messages.errorLoading': return ({required Object error}) => 'Error: ${error}'; case 'messages.fileInfoNotAvailable': return 'File information not available'; case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Error loading file info: ${error}'; @@ -2418,6 +2426,7 @@ extension on _StringsIt { case 'fileInfo.has64bitOffsets': return 'Offset a 64-bit'; case 'mediaMenu.markAsWatched': return 'Segna come visto'; case 'mediaMenu.markAsUnwatched': return 'Segna come non visto'; + case 'mediaMenu.removeFromContinueWatching': return 'Rimuovi da Continua a guardare'; case 'mediaMenu.goToSeries': return 'Vai alle serie'; case 'mediaMenu.goToSeason': return 'Vai alla stagione'; case 'mediaMenu.shufflePlay': return 'Riproduzione casuale'; @@ -2443,6 +2452,7 @@ extension on _StringsIt { case 'userStatus.protected': return 'Protetto'; case 'messages.markedAsWatched': return 'Segna come visto'; case 'messages.markedAsUnwatched': return 'Segna come non visto'; + case 'messages.removedFromContinueWatching': return 'Rimosso da Continua a guardare'; case 'messages.errorLoading': return ({required Object error}) => 'Errore: ${error}'; case 'messages.fileInfoNotAvailable': return 'Informazioni sul file non disponibili'; case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Errore caricamento informazioni sul file: ${error}'; @@ -2736,6 +2746,7 @@ extension on _StringsSv { case 'fileInfo.has64bitOffsets': return '64-bit offset'; case 'mediaMenu.markAsWatched': return 'Markera som sedd'; case 'mediaMenu.markAsUnwatched': return 'Markera som osedd'; + case 'mediaMenu.removeFromContinueWatching': return 'Ta bort från Fortsätt titta'; case 'mediaMenu.goToSeries': return 'Gå till serie'; case 'mediaMenu.goToSeason': return 'Gå till säsong'; case 'mediaMenu.shufflePlay': return 'Blanda uppspelning'; @@ -2761,6 +2772,7 @@ extension on _StringsSv { case 'userStatus.protected': return 'Skyddad'; case 'messages.markedAsWatched': return 'Markerad som sedd'; case 'messages.markedAsUnwatched': return 'Markerad som osedd'; + case 'messages.removedFromContinueWatching': return 'Borttagen från Fortsätt titta'; case 'messages.errorLoading': return ({required Object error}) => 'Fel: ${error}'; case 'messages.fileInfoNotAvailable': return 'Filinformation inte tillgänglig'; case 'messages.errorLoadingFileInfo': return ({required Object error}) => 'Fel vid laddning av filinformation: ${error}'; diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json index b82ac74c..b9ac755c 100644 --- a/lib/i18n/strings.i18n.json +++ b/lib/i18n/strings.i18n.json @@ -171,6 +171,7 @@ "mediaMenu": { "markAsWatched": "Mark as Watched", "markAsUnwatched": "Mark as Unwatched", + "removeFromContinueWatching": "Remove from Continue Watching", "goToSeries": "Go to series", "goToSeason": "Go to season", "shufflePlay": "Shuffle Play", @@ -204,6 +205,7 @@ "messages": { "markedAsWatched": "Marked as watched", "markedAsUnwatched": "Marked as unwatched", + "removedFromContinueWatching": "Removed from Continue Watching", "errorLoading": "Error: ${error}", "fileInfoNotAvailable": "File information not available", "errorLoadingFileInfo": "Error loading file info: ${error}", diff --git a/lib/i18n/strings_it.i18n.json b/lib/i18n/strings_it.i18n.json index 8592ffa7..dacdd65e 100644 --- a/lib/i18n/strings_it.i18n.json +++ b/lib/i18n/strings_it.i18n.json @@ -171,6 +171,7 @@ "mediaMenu": { "markAsWatched": "Segna come visto", "markAsUnwatched": "Segna come non visto", + "removeFromContinueWatching": "Rimuovi da Continua a guardare", "goToSeries": "Vai alle serie", "goToSeason": "Vai alla stagione", "shufflePlay": "Riproduzione casuale", @@ -204,6 +205,7 @@ "messages": { "markedAsWatched": "Segna come visto", "markedAsUnwatched": "Segna come non visto", + "removedFromContinueWatching": "Rimosso da Continua a guardare", "errorLoading": "Errore: ${error}", "fileInfoNotAvailable": "Informazioni sul file non disponibili", "errorLoadingFileInfo": "Errore caricamento informazioni sul file: ${error}", diff --git a/lib/i18n/strings_sv.i18n.json b/lib/i18n/strings_sv.i18n.json index a4aab696..641b9c78 100644 --- a/lib/i18n/strings_sv.i18n.json +++ b/lib/i18n/strings_sv.i18n.json @@ -171,6 +171,7 @@ "mediaMenu": { "markAsWatched": "Markera som sedd", "markAsUnwatched": "Markera som osedd", + "removeFromContinueWatching": "Ta bort från Fortsätt titta", "goToSeries": "Gå till serie", "goToSeason": "Gå till säsong", "shufflePlay": "Blanda uppspelning", @@ -204,6 +205,7 @@ "messages": { "markedAsWatched": "Markerad som sedd", "markedAsUnwatched": "Markerad som osedd", + "removedFromContinueWatching": "Borttagen från Fortsätt titta", "errorLoading": "Fel: ${error}", "fileInfoNotAvailable": "Filinformation inte tillgänglig", "errorLoadingFileInfo": "Fel vid laddning av filinformation: ${error}", diff --git a/lib/screens/discover_screen.dart b/lib/screens/discover_screen.dart index b213b930..a133ca17 100644 --- a/lib/screens/discover_screen.dart +++ b/lib/screens/discover_screen.dart @@ -627,7 +627,7 @@ class _DiscoverScreenState extends State ), ), ), - _buildHorizontalList(_onDeck, isLarge: false), + _buildHorizontalList(_onDeck, isLarge: false, isInContinueWatching: true), ], @@ -1236,6 +1236,7 @@ class _DiscoverScreenState extends State Widget _buildHorizontalList( List items, { bool isLarge = false, + bool isInContinueWatching = false, }) { return SliverToBoxAdapter( child: LayoutBuilder( @@ -1278,7 +1279,11 @@ class _DiscoverScreenState extends State width: cardWidth, height: posterHeight, onRefresh: updateItem, + onRemoveFromContinueWatching: isInContinueWatching + ? _refreshContinueWatching + : null, forceGridMode: true, + isInContinueWatching: isInContinueWatching, ), ); }, diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 96a26e6e..fe03780a 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -19,7 +19,9 @@ class MediaCard extends StatefulWidget { final double? width; final double? height; final void Function(String ratingKey)? onRefresh; + final VoidCallback? onRemoveFromContinueWatching; final bool forceGridMode; + final bool isInContinueWatching; const MediaCard({ super.key, @@ -27,7 +29,9 @@ class MediaCard extends StatefulWidget { this.width, this.height, this.onRefresh, + this.onRemoveFromContinueWatching, this.forceGridMode = false, + this.isInContinueWatching = false, }); @override @@ -99,7 +103,9 @@ class _MediaCardState extends State { return MediaContextMenu( metadata: widget.item, onRefresh: widget.onRefresh, + onRemoveFromContinueWatching: widget.onRemoveFromContinueWatching, onTap: () => _handleTap(context), + isInContinueWatching: widget.isInContinueWatching, child: viewMode == ViewMode.grid ? _MediaCardGrid( item: widget.item, diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 355647ad..f3dcddde 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -22,15 +22,19 @@ class _MenuAction { class MediaContextMenu extends StatefulWidget { final PlexMetadata metadata; final void Function(String ratingKey)? onRefresh; + final VoidCallback? onRemoveFromContinueWatching; final VoidCallback? onTap; final Widget child; + final bool isInContinueWatching; const MediaContextMenu({ super.key, required this.metadata, this.onRefresh, + this.onRemoveFromContinueWatching, this.onTap, required this.child, + this.isInContinueWatching = false, }); @override @@ -83,6 +87,17 @@ class _MediaContextMenuState extends State { ); } + // Remove from Continue Watching (only in continue watching section) + if (widget.isInContinueWatching) { + menuActions.add( + _MenuAction( + value: 'remove_from_continue_watching', + icon: Icons.close, + label: t.mediaMenu.removeFromContinueWatching, + ), + ); + } + // Go to Series (for episodes and seasons) if ((itemType == 'episode' || itemType == 'season') && widget.metadata.grandparentTitle != null) { @@ -227,6 +242,32 @@ class _MediaContextMenuState extends State { ); break; + case 'remove_from_continue_watching': + // Remove from Continue Watching without affecting watch status or progress + // This preserves the progression for partially watched items + // and doesn't mark unwatched next episodes as watched + try { + await client.removeFromOnDeck(widget.metadata.ratingKey); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.messages.removedFromContinueWatching)), + ); + // Use specific callback if provided, otherwise fallback to onRefresh + if (widget.onRemoveFromContinueWatching != null) { + widget.onRemoveFromContinueWatching!(); + } else { + widget.onRefresh?.call(widget.metadata.ratingKey); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(t.messages.errorLoading(error: e.toString()))), + ); + } + } + break; + case 'series': await _navigateToRelated( context, diff --git a/pubspec.lock b/pubspec.lock index 6aca0407..d49b164d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" build_resolvers: dependency: transitive description: @@ -396,10 +396,10 @@ packages: dependency: transitive description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -611,10 +611,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -812,10 +812,10 @@ packages: dependency: transitive description: name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" url: "https://pub.dev" source: hosted - version: "0.27.7" + version: "0.28.0" safe_local_storage: dependency: transitive description: @@ -1097,10 +1097,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" timing: dependency: transitive description: