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>
This commit is contained in:
copilot-swe-agent[bot]
2025-11-13 19:07:31 +01:00
committed by Doezer
co-authored by Doezer copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent 4845d2e659
commit 9be219ce15
9 changed files with 92 additions and 13 deletions
+9
View File
@@ -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<void> removeFromOnDeck(String ratingKey) async {
await _dio.put(
'/actions/removeFromContinueWatching',
queryParameters: {'ratingKey': ratingKey},
);
}
/// Get server preferences
Future<Map<String, dynamic>> getServerPreferences() async {
final response = await _dio.get('/:/prefs');
+14 -2
View File
@@ -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}';
+2
View File
@@ -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}",
+2
View File
@@ -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}",
+2
View File
@@ -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}",
+6 -1
View File
@@ -627,7 +627,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
),
),
),
_buildHorizontalList(_onDeck, isLarge: false),
_buildHorizontalList(_onDeck, isLarge: false, isInContinueWatching: true),
],
@@ -1236,6 +1236,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
Widget _buildHorizontalList(
List<PlexMetadata> items, {
bool isLarge = false,
bool isInContinueWatching = false,
}) {
return SliverToBoxAdapter(
child: LayoutBuilder(
@@ -1278,7 +1279,11 @@ class _DiscoverScreenState extends State<DiscoverScreen>
width: cardWidth,
height: posterHeight,
onRefresh: updateItem,
onRemoveFromContinueWatching: isInContinueWatching
? _refreshContinueWatching
: null,
forceGridMode: true,
isInContinueWatching: isInContinueWatching,
),
);
},
+6
View File
@@ -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<MediaCard> {
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,
+41
View File
@@ -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<MediaContextMenu> {
);
}
// 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<MediaContextMenu> {
);
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,
+10 -10
View File
@@ -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: