From 08f92da41be546430fb1483f89ff5d8abe53adec Mon Sep 17 00:00:00 2001 From: Tazio Date: Tue, 2 Dec 2025 16:43:28 +0100 Subject: [PATCH] feat: continue watching in library --- lib/client/plex_client.dart | 12 ++- .../library_tabs/library_recommended_tab.dart | 89 +++++++++++++++++-- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/lib/client/plex_client.dart b/lib/client/plex_client.dart index 45fcfab3..a62b1c2f 100644 --- a/lib/client/plex_client.dart +++ b/lib/client/plex_client.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import '../config/plex_config.dart'; +import '../models/play_queue_response.dart'; import '../models/plex_file_info.dart'; import '../models/plex_filter.dart'; import '../models/plex_hub.dart'; @@ -13,7 +14,6 @@ import '../models/plex_metadata.dart'; import '../models/plex_playlist.dart'; import '../models/plex_sort.dart'; import '../models/plex_video_playback_data.dart'; -import '../models/play_queue_response.dart'; import '../network/endpoint_failover_interceptor.dart'; import '../utils/app_logger.dart'; import '../utils/log_redaction_manager.dart'; @@ -603,6 +603,16 @@ class PlexClient { return []; } + /// Get on deck items filtered by a specific library section + Future> getOnDeckForLibrary(String sectionId) async { + final allOnDeck = await getOnDeck(); + + // Filter items to only include those from the specified library section + return allOnDeck.where((item) { + return item.librarySectionID == int.tryParse(sectionId); + }).toList(); + } + /// Get children of a metadata item (e.g., seasons for a show, episodes for a season) Future> getChildren(String ratingKey) async { final response = await _dio.get('/library/metadata/$ratingKey/children'); diff --git a/lib/screens/library_tabs/library_recommended_tab.dart b/lib/screens/library_tabs/library_recommended_tab.dart index 65b594d6..fc2067f9 100644 --- a/lib/screens/library_tabs/library_recommended_tab.dart +++ b/lib/screens/library_tabs/library_recommended_tab.dart @@ -1,12 +1,16 @@ import 'package:flutter/material.dart'; -import '../../models/plex_hub.dart'; -import '../../widgets/hub_section.dart'; -import '../../widgets/hub_navigation_controller.dart'; + +import '../../client/plex_client.dart'; import '../../i18n/strings.g.dart'; +import '../../mixins/item_updatable.dart'; +import '../../models/plex_hub.dart'; +import '../../models/plex_metadata.dart'; +import '../../widgets/hub_navigation_controller.dart'; +import '../../widgets/hub_section.dart'; import 'base_library_tab.dart'; /// Recommended tab for library screen -/// Shows library-specific hubs and recommendations +/// Shows library-specific hubs and recommendations, including dedicated Continue Watching class LibraryRecommendedTab extends BaseLibraryTab { const LibraryRecommendedTab({super.key, required super.library}); @@ -15,7 +19,8 @@ class LibraryRecommendedTab extends BaseLibraryTab { } class _LibraryRecommendedTabState - extends BaseLibraryTabState { + extends BaseLibraryTabState + with ItemUpdatable { final HubNavigationController _hubNavigationController = HubNavigationController(); @@ -26,10 +31,27 @@ class _LibraryRecommendedTabState } /// Focus the first item in the first hub + @override void focusFirstItem() { _hubNavigationController.focusHub(0, 0); } + @override + PlexClient get client => getClientForLibrary(); + + @override + void updateItemInLists(String ratingKey, PlexMetadata updatedMetadata) { + // Update the item in any hub that contains it + for (final hub in items) { + final itemIndex = hub.items.indexWhere( + (item) => item.ratingKey == ratingKey, + ); + if (itemIndex != -1) { + hub.items[itemIndex] = updatedMetadata; + } + } + } + @override IconData get emptyIcon => Icons.recommend; @@ -44,8 +66,47 @@ class _LibraryRecommendedTabState // Use server-specific client for this library final client = getClientForLibrary(); - // Hubs are now tagged with server info at the source - return await client.getLibraryHubs(widget.library.key, limit: 12); + // Load both continue watching items and regular hubs in parallel + final results = await Future.wait([ + client.getOnDeckForLibrary(widget.library.key), + client.getLibraryHubs(widget.library.key, limit: 12), + ]); + + final continueWatchingItems = results[0] as List; + final hubs = results[1] as List; + + // Filter out any existing Continue Watching hubs since we're adding our own + final filteredHubs = hubs.where((hub) { + final title = hub.title.toLowerCase(); + final hubId = hub.hubIdentifier?.toLowerCase() ?? ''; + return !title.contains('continue watching') && + !title.contains('on deck') && + !hubId.contains('ondeck') && + !hubId.contains('continue'); + }).toList(); + + final finalHubs = []; + + // Add Continue Watching as the first hub if there are items + if (continueWatchingItems.isNotEmpty) { + final continueWatchingHub = PlexHub( + hubKey: 'library_continue_watching_${widget.library.key}', + title: t.discover.continueWatching, + type: 'mixed', + hubIdentifier: '_library_continue_watching_', + size: continueWatchingItems.length, + more: false, + items: continueWatchingItems, + serverId: widget.library.serverId, + serverName: widget.library.serverName, + ); + finalHubs.add(continueWatchingHub); + } + + // Add the filtered regular hubs + finalHubs.addAll(filteredHubs); + + return finalHubs; } @override @@ -57,16 +118,30 @@ class _LibraryRecommendedTabState itemCount: items.length, itemBuilder: (context, index) { final hub = items[index]; + final isContinueWatching = + hub.hubIdentifier == '_library_continue_watching_'; + return HubSection( hub: hub, icon: _getHubIcon(hub), navigationOrder: index, + isInContinueWatching: isContinueWatching, + onRefresh: updateItem, + onRemoveFromContinueWatching: isContinueWatching + ? _refreshContinueWatching + : null, ); }, ), ); } + /// Refresh the Continue Watching section + void _refreshContinueWatching() { + // Reload all data to refresh the continue watching section + loadItems(); + } + IconData _getHubIcon(PlexHub hub) { final title = hub.title.toLowerCase(); if (title.contains('continue watching') || title.contains('on deck')) {