From 2a6098c3ba4d81a9d21d6a0df7ff40d87bc31077 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:54:31 +0100 Subject: [PATCH] feat: load library in parts --- lib/client/plex_client.dart | 4 +- lib/screens/libraries_screen.dart | 167 ++++++++++++++++++++++++------ 2 files changed, 138 insertions(+), 33 deletions(-) diff --git a/lib/client/plex_client.dart b/lib/client/plex_client.dart index d496289b..e533385c 100644 --- a/lib/client/plex_client.dart +++ b/lib/client/plex_client.dart @@ -38,7 +38,7 @@ class PlexClient { baseUrl: config.baseUrl, headers: config.headers, connectTimeout: const Duration(seconds: 10), - receiveTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 120), validateStatus: (status) => status != null && status < 500, responseType: ResponseType.json, contentType: 'application/json; charset=utf-8', @@ -228,6 +228,7 @@ class PlexClient { int? start, int? size, Map? filters, + CancelToken? cancelToken, }) async { final queryParams = {}; if (start != null) queryParams['X-Plex-Container-Start'] = start; @@ -241,6 +242,7 @@ class PlexClient { final response = await _dio.get( '/library/sections/$sectionId/all', queryParameters: queryParams, + cancelToken: cancelToken, ); return _extractMetadataList(response); diff --git a/lib/screens/libraries_screen.dart b/lib/screens/libraries_screen.dart index a5365662..4feba0d3 100644 --- a/lib/screens/libraries_screen.dart +++ b/lib/screens/libraries_screen.dart @@ -46,6 +46,13 @@ class _LibrariesScreenState extends State bool _isSortDescending = false; bool _isInitialLoad = true; + // Pagination state + int _currentPage = 0; + bool _hasMoreItems = true; + CancelToken? _cancelToken; + int _requestId = 0; + static const int _pageSize = 1000; + @override void initState() { super.initState(); @@ -201,7 +208,6 @@ class _LibrariesScreenState extends State await storage.saveLibraryOrder(libraryKeys); } - Future _loadLibraryContent(String libraryKey) async { // Compute visible libraries based on current provider state final hiddenLibrariesProvider = Provider.of( @@ -257,6 +263,18 @@ class _LibrariesScreenState extends State await storage.saveLibraryFilters({}); } + // Cancel any existing requests + _cancelToken?.cancel(); + _cancelToken = CancelToken(); + final currentRequestId = ++_requestId; + + // Reset pagination state + setState(() { + _currentPage = 0; + _hasMoreItems = true; + _items = []; + }); + try { // Load filters and sort options for the new library _loadFilters(libraryKey); @@ -270,16 +288,19 @@ class _LibrariesScreenState extends State ); } - // Load content - final items = await client.getLibraryContent( + // Load pages sequentially + await _loadAllPagesSequentially( libraryKey, - filters: filtersWithSort, + filtersWithSort, + currentRequestId, + client, ); - setState(() { - _items = items; - _isLoadingItems = false; - }); } catch (e) { + // Ignore cancellation errors + if (e is DioException && e.type == DioExceptionType.cancel) { + return; + } + setState(() { _errorMessage = _getErrorMessage(e, 'library content'); _isLoadingItems = false; @@ -287,6 +308,54 @@ class _LibrariesScreenState extends State } } + /// Load all pages sequentially until all items are fetched + Future _loadAllPagesSequentially( + String libraryKey, + Map filtersWithSort, + int requestId, + PlexClient client, + ) async { + while (_hasMoreItems && requestId == _requestId) { + try { + final items = await client.getLibraryContent( + libraryKey, + start: _currentPage * _pageSize, + size: _pageSize, + filters: filtersWithSort, + cancelToken: _cancelToken, + ); + + // Check if request is still valid + if (requestId != _requestId) { + return; // Request was superseded + } + + setState(() { + _items.addAll(items); + _currentPage++; + _hasMoreItems = items.length >= _pageSize; + + // Mark as not loading if this is the last page + if (!_hasMoreItems) { + _isLoadingItems = false; + } + }); + } catch (e) { + // Check if it's a cancellation + if (e is DioException && e.type == DioExceptionType.cancel) { + return; + } + + // For other errors, update state and rethrow + setState(() { + _isLoadingItems = false; + _hasMoreItems = false; + }); + rethrow; + } + } + } + Future _loadFilters(String libraryKey) async { try { final clientProvider = Provider.of( @@ -360,9 +429,17 @@ class _LibrariesScreenState extends State } Future _applyFilters() async { + // Cancel any existing requests + _cancelToken?.cancel(); + _cancelToken = CancelToken(); + final currentRequestId = ++_requestId; + setState(() { _isLoadingItems = true; _errorMessage = null; + _currentPage = 0; + _hasMoreItems = true; + _items = []; }); try { @@ -383,15 +460,19 @@ class _LibrariesScreenState extends State ); } - final items = await client.getLibraryContent( + // Load pages sequentially + await _loadAllPagesSequentially( _selectedLibraryKey!, - filters: filtersWithSort, + filtersWithSort, + currentRequestId, + client, ); - setState(() { - _items = items; - _isLoadingItems = false; - }); } catch (e) { + // Ignore cancellation errors + if (e is DioException && e.type == DioExceptionType.cancel) { + return; + } + setState(() { _errorMessage = 'Failed to load library content: $e'; _isLoadingItems = false; @@ -912,7 +993,7 @@ class _LibrariesScreenState extends State ), // Content grid - if (_isLoadingItems) + if (_isLoadingItems && _items.isEmpty) const SliverFillRemaining( child: Center(child: CircularProgressIndicator()), ) @@ -952,24 +1033,21 @@ class _LibrariesScreenState extends State ), ), ) - else + else ...[ Consumer( builder: (context, settingsProvider, child) { if (settingsProvider.viewMode == ViewMode.list) { return SliverPadding( padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final item = _items[index]; - return MediaCard( - key: Key(item.ratingKey), - item: item, - onRefresh: updateItem, - ); - }, - childCount: _items.length, - ), + delegate: SliverChildBuilderDelegate((context, index) { + final item = _items[index]; + return MediaCard( + key: Key(item.ratingKey), + item: item, + onRefresh: updateItem, + ); + }, childCount: _items.length), ), ); } else { @@ -998,6 +1076,24 @@ class _LibrariesScreenState extends State } }, ), + // Show loading indicator if there are more items to load + if (_hasMoreItems && _isLoadingItems) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 8), + Text( + 'Loading library... (${_items.length} items loaded)', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ), + ], ], ], ), @@ -1450,7 +1546,9 @@ class _SortBottomSheetState extends State<_SortBottomSheet> { child: RadioGroup( groupValue: _tempSelectedSort?.key, onChanged: (value) { - final sort = widget.sortOptions.firstWhere((s) => s.key == value); + final sort = widget.sortOptions.firstWhere( + (s) => s.key == value, + ); setState(() { _tempSelectedSort = sort; // Use default direction for newly selected sort @@ -1483,7 +1581,10 @@ class _SortBottomSheetState extends State<_SortBottomSheet> { ), ButtonSegment( value: true, - icon: Icon(Icons.arrow_downward, size: 16), + icon: Icon( + Icons.arrow_downward, + size: 16, + ), ), ], selected: {_tempDescending}, @@ -1717,9 +1818,11 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> { padding: const EdgeInsets.only(right: 12), child: Icon( Icons.drag_indicator, - color: Theme.of( - context, - ).textTheme.bodyMedium?.color?.withValues(alpha: 0.5), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.5), ), ), ),