refactor: centralize library state in LibrariesProvider
This commit is contained in:
@@ -19,6 +19,7 @@ import 'providers/server_state_provider.dart';
|
||||
import 'providers/theme_provider.dart';
|
||||
import 'providers/settings_provider.dart';
|
||||
import 'providers/hidden_libraries_provider.dart';
|
||||
import 'providers/libraries_provider.dart';
|
||||
import 'providers/playback_state_provider.dart';
|
||||
import 'providers/download_provider.dart';
|
||||
import 'providers/offline_mode_provider.dart';
|
||||
@@ -245,6 +246,7 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
ChangeNotifierProvider(create: (context) => ThemeProvider()),
|
||||
ChangeNotifierProvider(create: (context) => SettingsProvider(), lazy: true),
|
||||
ChangeNotifierProvider(create: (context) => HiddenLibrariesProvider(), lazy: true),
|
||||
ChangeNotifierProvider(create: (context) => LibrariesProvider()),
|
||||
ChangeNotifierProvider(create: (context) => PlaybackStateProvider()),
|
||||
ChangeNotifierProvider(create: (context) => WatchTogetherProvider()),
|
||||
],
|
||||
@@ -357,6 +359,11 @@ class _SetupScreenState extends State<SetupScreen> {
|
||||
appLogger.i('Successfully connected to $connectedCount servers');
|
||||
|
||||
if (mounted) {
|
||||
// Initialize and load libraries
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
librariesProvider.initialize(multiServerProvider.aggregationService);
|
||||
await librariesProvider.loadLibraries();
|
||||
|
||||
// Now that Plex clients are available, trigger initial watch sync
|
||||
context.read<OfflineWatchSyncService>().onServersConnected();
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/plex_library.dart';
|
||||
import '../services/data_aggregation_service.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../utils/content_utils.dart';
|
||||
|
||||
/// Load state for the libraries provider
|
||||
enum LibrariesLoadState { initial, loading, loaded, error }
|
||||
|
||||
/// Provider that serves as the single source of truth for library data.
|
||||
/// Both SideNavigationRail and LibrariesScreen consume this provider
|
||||
/// instead of independently fetching library data.
|
||||
class LibrariesProvider extends ChangeNotifier {
|
||||
DataAggregationService? _aggregationService;
|
||||
List<PlexLibrary> _libraries = [];
|
||||
LibrariesLoadState _loadState = LibrariesLoadState.initial;
|
||||
String? _errorMessage;
|
||||
|
||||
/// Unmodifiable list of all libraries (filtered for supported types, ordered)
|
||||
List<PlexLibrary> get libraries => List.unmodifiable(_libraries);
|
||||
|
||||
/// Whether libraries are currently being loaded
|
||||
bool get isLoading => _loadState == LibrariesLoadState.loading;
|
||||
|
||||
/// Whether libraries have been loaded at least once
|
||||
bool get hasLoaded => _loadState == LibrariesLoadState.loaded;
|
||||
|
||||
/// Current load state
|
||||
LibrariesLoadState get loadState => _loadState;
|
||||
|
||||
/// Error message if loading failed
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
/// Whether libraries are available
|
||||
bool get hasLibraries => _libraries.isNotEmpty;
|
||||
|
||||
/// Initialize the provider with the aggregation service.
|
||||
/// This should be called after server connection is established.
|
||||
void initialize(DataAggregationService service) {
|
||||
_aggregationService = service;
|
||||
}
|
||||
|
||||
/// Load libraries from all connected servers.
|
||||
/// Filters out music libraries and applies saved ordering.
|
||||
Future<void> loadLibraries() async {
|
||||
if (_aggregationService == null) {
|
||||
appLogger.w('LibrariesProvider: Cannot load libraries - not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
_loadState = LibrariesLoadState.loading;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
// Fetch libraries from all servers
|
||||
final allLibraries = await _aggregationService!.getLibrariesFromAllServers();
|
||||
|
||||
// Filter out music libraries (not supported)
|
||||
final filteredLibraries = allLibraries.where((lib) => !ContentTypeHelper.isMusicLibrary(lib)).toList();
|
||||
|
||||
// Apply saved library order
|
||||
final storage = await StorageService.getInstance();
|
||||
final savedOrder = storage.getLibraryOrder();
|
||||
final orderedLibraries = _applyLibraryOrder(filteredLibraries, savedOrder);
|
||||
|
||||
_libraries = orderedLibraries;
|
||||
_loadState = LibrariesLoadState.loaded;
|
||||
_errorMessage = null;
|
||||
|
||||
appLogger.i('LibrariesProvider: Loaded ${_libraries.length} libraries');
|
||||
notifyListeners();
|
||||
} catch (e, stackTrace) {
|
||||
appLogger.e('LibrariesProvider: Failed to load libraries', error: e, stackTrace: stackTrace);
|
||||
_loadState = LibrariesLoadState.error;
|
||||
_errorMessage = e.toString();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh libraries by clearing cache and reloading.
|
||||
Future<void> refresh() async {
|
||||
if (_aggregationService == null) {
|
||||
appLogger.w('LibrariesProvider: Cannot refresh - not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear aggregation service cache
|
||||
_aggregationService!.clearCache();
|
||||
|
||||
// Reload libraries
|
||||
await loadLibraries();
|
||||
}
|
||||
|
||||
/// Update the library order and persist it.
|
||||
Future<void> updateLibraryOrder(List<PlexLibrary> orderedLibraries) async {
|
||||
_libraries = List.from(orderedLibraries);
|
||||
notifyListeners();
|
||||
|
||||
// Save the new order
|
||||
final storage = await StorageService.getInstance();
|
||||
final libraryKeys = orderedLibraries.map((lib) => lib.globalKey).toList();
|
||||
await storage.saveLibraryOrder(libraryKeys);
|
||||
|
||||
appLogger.d('LibrariesProvider: Updated library order');
|
||||
}
|
||||
|
||||
/// Clear all library data (for profile switch or logout).
|
||||
void clear() {
|
||||
_libraries = [];
|
||||
_loadState = LibrariesLoadState.initial;
|
||||
_errorMessage = null;
|
||||
notifyListeners();
|
||||
appLogger.d('LibrariesProvider: Cleared library data');
|
||||
}
|
||||
|
||||
/// Apply saved library order to a list of libraries.
|
||||
List<PlexLibrary> _applyLibraryOrder(List<PlexLibrary> libraries, List<String>? savedOrder) {
|
||||
if (savedOrder == null || savedOrder.isEmpty) {
|
||||
return libraries;
|
||||
}
|
||||
|
||||
// Create a map for quick lookup
|
||||
final libraryMap = {for (var lib in libraries) lib.globalKey: lib};
|
||||
|
||||
// Build ordered list based on saved order
|
||||
final orderedLibraries = <PlexLibrary>[];
|
||||
for (final key in savedOrder) {
|
||||
final lib = libraryMap.remove(key);
|
||||
if (lib != null) {
|
||||
orderedLibraries.add(lib);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any new libraries that weren't in the saved order
|
||||
orderedLibraries.addAll(libraryMap.values);
|
||||
|
||||
return orderedLibraries;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import '../../models/plex_library.dart';
|
||||
import '../../models/plex_metadata.dart';
|
||||
import '../../models/plex_sort.dart';
|
||||
import '../../providers/hidden_libraries_provider.dart';
|
||||
import '../../providers/libraries_provider.dart';
|
||||
import '../../providers/multi_server_provider.dart';
|
||||
import '../../utils/app_logger.dart';
|
||||
import '../../utils/platform_detector.dart';
|
||||
@@ -81,8 +82,6 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
final _collectionsTabKey = GlobalKey<State<LibraryCollectionsTab>>();
|
||||
final _playlistsTabKey = GlobalKey<State<LibraryPlaylistsTab>>();
|
||||
|
||||
List<PlexLibrary> _allLibraries = []; // All libraries from API (unfiltered)
|
||||
bool _isLoadingLibraries = true;
|
||||
String? _errorMessage;
|
||||
String? _selectedLibraryGlobalKey;
|
||||
bool _isInitialLoad = true;
|
||||
@@ -123,13 +122,61 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
_tabController.addListener(_onTabChanged);
|
||||
_loadLibraries();
|
||||
|
||||
// Initialize with libraries from the provider
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_initializeWithLibraries();
|
||||
});
|
||||
|
||||
// Register L1/R1 callbacks for tab navigation
|
||||
GamepadService.onL1Pressed = _goToPreviousTab;
|
||||
GamepadService.onR1Pressed = _goToNextTab;
|
||||
}
|
||||
|
||||
/// Initialize the screen with libraries from the provider.
|
||||
/// This handles initial library selection and content loading.
|
||||
Future<void> _initializeWithLibraries() async {
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
final hiddenLibrariesProvider = context.read<HiddenLibrariesProvider>();
|
||||
final allLibraries = librariesProvider.libraries;
|
||||
|
||||
if (allLibraries.isEmpty) {
|
||||
// No libraries available yet
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute visible libraries for initial load
|
||||
final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys;
|
||||
final visibleLibraries = allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
|
||||
// Load saved preferences
|
||||
final storage = await StorageService.getInstance();
|
||||
final savedLibraryKey = storage.getSelectedLibraryKey();
|
||||
|
||||
// Find the library by key in visible libraries
|
||||
String? libraryGlobalKeyToLoad;
|
||||
if (savedLibraryKey != null) {
|
||||
// Check if saved library exists and is visible
|
||||
final libraryExists = visibleLibraries.any((lib) => lib.globalKey == savedLibraryKey);
|
||||
if (libraryExists) {
|
||||
libraryGlobalKeyToLoad = savedLibraryKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first visible library if saved key not found
|
||||
if (libraryGlobalKeyToLoad == null && visibleLibraries.isNotEmpty) {
|
||||
libraryGlobalKeyToLoad = visibleLibraries.first.globalKey;
|
||||
}
|
||||
|
||||
if (libraryGlobalKeyToLoad != null && mounted) {
|
||||
final savedFilters = storage.getLibraryFilters(sectionId: libraryGlobalKeyToLoad);
|
||||
if (savedFilters.isNotEmpty) {
|
||||
_selectedFilters = Map.from(savedFilters);
|
||||
}
|
||||
_loadLibraryContent(libraryGlobalKeyToLoad);
|
||||
}
|
||||
}
|
||||
|
||||
void _goToPreviousTab() {
|
||||
if (_tabController.index > 0) {
|
||||
setState(() {
|
||||
@@ -353,118 +400,13 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
}
|
||||
|
||||
/// Check if libraries come from multiple servers
|
||||
bool get _hasMultipleServers {
|
||||
final uniqueServerIds = _allLibraries.where((lib) => lib.serverId != null).map((lib) => lib.serverId).toSet();
|
||||
bool _hasMultipleServers(List<PlexLibrary> libraries) {
|
||||
final uniqueServerIds = libraries.where((lib) => lib.serverId != null).map((lib) => lib.serverId).toSet();
|
||||
return uniqueServerIds.length > 1;
|
||||
}
|
||||
|
||||
Future<void> _loadLibraries() async {
|
||||
// Extract context dependencies before async gap
|
||||
final multiServerProvider = Provider.of<MultiServerProvider>(context, listen: false);
|
||||
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false);
|
||||
|
||||
setState(() {
|
||||
_isLoadingLibraries = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
// Check if we have any connected servers
|
||||
if (!multiServerProvider.hasConnectedServers) {
|
||||
throw Exception(t.errors.noClientAvailable);
|
||||
}
|
||||
|
||||
final storage = await StorageService.getInstance();
|
||||
|
||||
// Fetch libraries from all servers
|
||||
final allLibraries = await multiServerProvider.aggregationService.getLibrariesFromAllServers();
|
||||
|
||||
// Filter out music libraries (type: 'artist') since music playback is not yet supported
|
||||
// Only show movie and TV show libraries
|
||||
final filteredLibraries = allLibraries.where((lib) => !ContentTypeHelper.isMusicLibrary(lib)).toList();
|
||||
|
||||
// Load saved library order and apply it
|
||||
final savedOrder = storage.getLibraryOrder();
|
||||
final orderedLibraries = _applyLibraryOrder(filteredLibraries, savedOrder);
|
||||
|
||||
_updateState(() {
|
||||
_allLibraries = orderedLibraries; // Store all libraries with ordering applied
|
||||
_isLoadingLibraries = false;
|
||||
});
|
||||
|
||||
if (allLibraries.isNotEmpty) {
|
||||
// Compute visible libraries for initial load
|
||||
final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys;
|
||||
final visibleLibraries = allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
|
||||
// Load saved preferences
|
||||
final savedLibraryKey = storage.getSelectedLibraryKey();
|
||||
|
||||
// Find the library by key in visible libraries
|
||||
String? libraryGlobalKeyToLoad;
|
||||
if (savedLibraryKey != null) {
|
||||
// Check if saved library exists and is visible
|
||||
final libraryExists = visibleLibraries.any((lib) => lib.globalKey == savedLibraryKey);
|
||||
if (libraryExists) {
|
||||
libraryGlobalKeyToLoad = savedLibraryKey;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first visible library if saved key not found
|
||||
if (libraryGlobalKeyToLoad == null && visibleLibraries.isNotEmpty) {
|
||||
libraryGlobalKeyToLoad = visibleLibraries.first.globalKey;
|
||||
}
|
||||
|
||||
if (libraryGlobalKeyToLoad != null && mounted) {
|
||||
final savedFilters = storage.getLibraryFilters(sectionId: libraryGlobalKeyToLoad);
|
||||
if (savedFilters.isNotEmpty) {
|
||||
_selectedFilters = Map.from(savedFilters);
|
||||
}
|
||||
_loadLibraryContent(libraryGlobalKeyToLoad);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_updateState(() {
|
||||
_errorMessage = _getErrorMessage(e, 'libraries');
|
||||
_isLoadingLibraries = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
List<PlexLibrary> _applyLibraryOrder(List<PlexLibrary> libraries, List<String>? savedOrder) {
|
||||
if (savedOrder == null || savedOrder.isEmpty) {
|
||||
return libraries;
|
||||
}
|
||||
|
||||
// Create a map for quick lookup
|
||||
final libraryMap = {for (var lib in libraries) lib.globalKey: lib};
|
||||
|
||||
// Build ordered list based on saved order
|
||||
final orderedLibraries = <PlexLibrary>[];
|
||||
final addedKeys = <String>{};
|
||||
|
||||
// Add libraries in saved order
|
||||
for (final key in savedOrder) {
|
||||
if (libraryMap.containsKey(key)) {
|
||||
orderedLibraries.add(libraryMap[key]!);
|
||||
addedKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any new libraries that weren't in the saved order
|
||||
for (final library in libraries) {
|
||||
if (!addedKeys.contains(library.globalKey)) {
|
||||
orderedLibraries.add(library);
|
||||
}
|
||||
}
|
||||
|
||||
return orderedLibraries;
|
||||
}
|
||||
|
||||
Future<void> _saveLibraryOrder() async {
|
||||
final storage = await StorageService.getInstance();
|
||||
final libraryKeys = _allLibraries.map((lib) => lib.globalKey).toList();
|
||||
await storage.saveLibraryOrder(libraryKeys);
|
||||
/// Notify parent that library order changed
|
||||
void _notifyLibraryOrderChanged() {
|
||||
widget.onLibraryOrderChanged?.call();
|
||||
}
|
||||
|
||||
@@ -475,10 +417,14 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
}
|
||||
|
||||
Future<void> _loadLibraryContent(String libraryGlobalKey) async {
|
||||
// Get libraries from provider
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
final allLibraries = librariesProvider.libraries;
|
||||
|
||||
// Compute visible libraries based on current provider state
|
||||
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false);
|
||||
final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys;
|
||||
final visibleLibraries = _allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
final visibleLibraries = allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
|
||||
// Find the library by key
|
||||
final libraryIndex = visibleLibraries.indexWhere((lib) => lib.globalKey == libraryGlobalKey);
|
||||
@@ -672,7 +618,8 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
// Public method to refresh content (for normal navigation)
|
||||
@override
|
||||
void refresh() {
|
||||
_loadLibraries();
|
||||
// Reinitialize with current libraries
|
||||
_initializeWithLibraries();
|
||||
}
|
||||
|
||||
// Refresh the currently active tab
|
||||
@@ -709,14 +656,19 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
@override
|
||||
void fullRefresh() {
|
||||
appLogger.d('LibrariesScreen.fullRefresh() called - reloading all content');
|
||||
// Reload libraries and clear any selected library/filters
|
||||
// Clear local state
|
||||
_selectedLibraryGlobalKey = null;
|
||||
_selectedFilters.clear();
|
||||
_items.clear();
|
||||
_loadLibraries();
|
||||
_errorMessage = null;
|
||||
setState(() {});
|
||||
|
||||
// Reinitialize with current libraries from provider
|
||||
_initializeWithLibraries();
|
||||
}
|
||||
|
||||
Future<void> _toggleLibraryVisibility(PlexLibrary library) async {
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false);
|
||||
final isHidden = hiddenLibrariesProvider.hiddenLibraryKeys.contains(library.globalKey);
|
||||
|
||||
@@ -731,7 +683,8 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
// If we just hid the selected library, select the first visible one
|
||||
if (isCurrentlySelected) {
|
||||
// Compute visible libraries after hiding
|
||||
final visibleLibraries = _allLibraries
|
||||
final allLibraries = librariesProvider.libraries;
|
||||
final visibleLibraries = allLibraries
|
||||
.where((lib) => !hiddenLibrariesProvider.hiddenLibraryKeys.contains(lib.globalKey))
|
||||
.toList();
|
||||
|
||||
@@ -799,20 +752,20 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
}
|
||||
|
||||
void _showLibraryManagementSheet() {
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
final hiddenLibrariesProvider = Provider.of<HiddenLibrariesProvider>(context, listen: false);
|
||||
final allLibraries = librariesProvider.libraries;
|
||||
|
||||
if (PlatformDetector.isTV()) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => _LibraryManagementSheet(
|
||||
isDialog: true,
|
||||
allLibraries: List.from(_allLibraries),
|
||||
allLibraries: List.from(allLibraries),
|
||||
hiddenLibraryKeys: hiddenLibrariesProvider.hiddenLibraryKeys,
|
||||
onReorder: (reorderedLibraries) {
|
||||
setState(() {
|
||||
_allLibraries = reorderedLibraries;
|
||||
});
|
||||
_saveLibraryOrder();
|
||||
librariesProvider.updateLibraryOrder(reorderedLibraries);
|
||||
_notifyLibraryOrderChanged();
|
||||
},
|
||||
onToggleVisibility: _toggleLibraryVisibility,
|
||||
getLibraryMenuItems: _getLibraryMenuItems,
|
||||
@@ -824,13 +777,11 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => _LibraryManagementSheet(
|
||||
allLibraries: List.from(_allLibraries),
|
||||
allLibraries: List.from(allLibraries),
|
||||
hiddenLibraryKeys: hiddenLibrariesProvider.hiddenLibraryKeys,
|
||||
onReorder: (reorderedLibraries) {
|
||||
setState(() {
|
||||
_allLibraries = reorderedLibraries;
|
||||
});
|
||||
_saveLibraryOrder();
|
||||
librariesProvider.updateLibraryOrder(reorderedLibraries);
|
||||
_notifyLibraryOrderChanged();
|
||||
},
|
||||
onToggleVisibility: _toggleLibraryVisibility,
|
||||
getLibraryMenuItems: _getLibraryMenuItems,
|
||||
@@ -1057,7 +1008,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
children: [
|
||||
AppIcon(ContentTypeHelper.getLibraryIcon(selectedLibrary.type), fill: 1, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
if (_hasMultipleServers && selectedLibrary.serverName != null)
|
||||
if (_hasMultipleServers(visibleLibraries) && selectedLibrary.serverName != null)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -1083,12 +1034,17 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Watch libraries provider for updates
|
||||
final librariesProvider = context.watch<LibrariesProvider>();
|
||||
final allLibraries = librariesProvider.libraries;
|
||||
final isLoadingLibraries = librariesProvider.isLoading;
|
||||
|
||||
// Watch for hidden libraries changes to trigger rebuild
|
||||
final hiddenLibrariesProvider = context.watch<HiddenLibrariesProvider>();
|
||||
final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys;
|
||||
|
||||
// Compute visible libraries (filtered from all libraries)
|
||||
final visibleLibraries = _allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
final visibleLibraries = allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
|
||||
return Scaffold(
|
||||
body: CustomScrollView(
|
||||
@@ -1102,7 +1058,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
shadowColor: Colors.transparent,
|
||||
scrolledUnderElevation: 0,
|
||||
actions: [
|
||||
if (_allLibraries.isNotEmpty)
|
||||
if (allLibraries.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const AppIcon(Symbols.edit_rounded, fill: 1),
|
||||
tooltip: t.libraries.manageLibraries,
|
||||
@@ -1115,14 +1071,17 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_isLoadingLibraries)
|
||||
if (isLoadingLibraries)
|
||||
const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))
|
||||
else if (_errorMessage != null && visibleLibraries.isEmpty)
|
||||
SliverFillRemaining(
|
||||
child: ErrorStateWidget(
|
||||
message: _errorMessage!,
|
||||
icon: Symbols.error_outline_rounded,
|
||||
onRetry: _loadLibraries,
|
||||
onRetry: () {
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
librariesProvider.refresh();
|
||||
},
|
||||
),
|
||||
)
|
||||
else if (visibleLibraries.isEmpty)
|
||||
@@ -1164,7 +1123,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
children: [
|
||||
LibraryRecommendedTab(
|
||||
key: _recommendedTabKey,
|
||||
library: _allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
library: allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
isActive: _tabController.index == 0,
|
||||
suppressAutoFocus: _suppressAutoFocus,
|
||||
onDataLoaded: () => _handleTabDataLoaded(0),
|
||||
@@ -1172,7 +1131,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
),
|
||||
LibraryBrowseTab(
|
||||
key: _browseTabKey,
|
||||
library: _allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
library: allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
isActive: _tabController.index == 1,
|
||||
suppressAutoFocus: _suppressAutoFocus,
|
||||
onDataLoaded: () => _handleTabDataLoaded(1),
|
||||
@@ -1180,7 +1139,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
),
|
||||
LibraryCollectionsTab(
|
||||
key: _collectionsTabKey,
|
||||
library: _allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
library: allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
isActive: _tabController.index == 2,
|
||||
suppressAutoFocus: _suppressAutoFocus,
|
||||
onDataLoaded: () => _handleTabDataLoaded(2),
|
||||
@@ -1188,7 +1147,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
|
||||
),
|
||||
LibraryPlaylistsTab(
|
||||
key: _playlistsTabKey,
|
||||
library: _allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
library: allLibraries.firstWhere((lib) => lib.globalKey == _selectedLibraryGlobalKey),
|
||||
isActive: _tabController.index == 3,
|
||||
suppressAutoFocus: _suppressAutoFocus,
|
||||
onDataLoaded: () => _handleTabDataLoaded(3),
|
||||
|
||||
@@ -18,6 +18,7 @@ import '../navigation/navigation_tabs.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../providers/server_state_provider.dart';
|
||||
import '../providers/hidden_libraries_provider.dart';
|
||||
import '../providers/libraries_provider.dart';
|
||||
import '../providers/playback_state_provider.dart';
|
||||
import '../providers/settings_provider.dart';
|
||||
import '../services/offline_watch_sync_service.dart';
|
||||
@@ -478,8 +479,12 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
|
||||
final multiServerProvider = context.read<MultiServerProvider>();
|
||||
final serverStateProvider = context.read<ServerStateProvider>();
|
||||
final hiddenLibrariesProvider = context.read<HiddenLibrariesProvider>();
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
final playbackStateProvider = context.read<PlaybackStateProvider>();
|
||||
|
||||
// Clear libraries provider state before reconnecting
|
||||
librariesProvider.clear();
|
||||
|
||||
// Reconnect to all servers with new profile tokens
|
||||
if (servers.isNotEmpty) {
|
||||
final storage = await StorageService.getInstance();
|
||||
@@ -492,6 +497,10 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
|
||||
if (connectedCount > 0) {
|
||||
if (!mounted) return;
|
||||
context.read<OfflineWatchSyncService>().onServersConnected();
|
||||
|
||||
// Reload libraries after reconnection
|
||||
librariesProvider.initialize(multiServerProvider.aggregationService);
|
||||
await librariesProvider.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,8 +526,7 @@ class _MainScreenState extends State<MainScreen> with RouteAware, WindowListener
|
||||
refreshable.fullRefresh();
|
||||
}
|
||||
|
||||
// Refresh sidebar libraries for new profile
|
||||
_sideNavKey.currentState?.reloadLibraries();
|
||||
// Sidebar automatically updates since it watches LibrariesProvider
|
||||
}
|
||||
|
||||
void _selectTab(int index) {
|
||||
|
||||
@@ -12,27 +12,12 @@ import 'plex_auth_service.dart';
|
||||
class DataAggregationService {
|
||||
final MultiServerManager _serverManager;
|
||||
|
||||
// Cache for libraries with TTL
|
||||
Map<String, List<PlexLibrary>>? _cachedLibrariesByServer;
|
||||
DateTime? _librariesCacheTime;
|
||||
static const Duration _librariesCacheTTL = Duration(hours: 1);
|
||||
|
||||
DataAggregationService(this._serverManager);
|
||||
|
||||
/// Clear the libraries cache (useful for server changes or logout)
|
||||
/// Clear any cached data (for compatibility with existing callers)
|
||||
void clearCache() {
|
||||
_cachedLibrariesByServer = null;
|
||||
_librariesCacheTime = null;
|
||||
}
|
||||
|
||||
/// Check if libraries cache is still valid
|
||||
bool get _isLibrariesCacheValid {
|
||||
if (_cachedLibrariesByServer == null || _librariesCacheTime == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final cacheAge = DateTime.now().difference(_librariesCacheTime!);
|
||||
return cacheAge < _librariesCacheTTL;
|
||||
// Cache is now managed by LibrariesProvider
|
||||
// This method is kept for compatibility
|
||||
}
|
||||
|
||||
/// Fetch libraries from all online servers
|
||||
@@ -83,32 +68,6 @@ class DataAggregationService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Fetch libraries from all servers and cache them for hub fetching
|
||||
/// This allows libraries to be fetched in parallel with other operations
|
||||
Future<Map<String, List<PlexLibrary>>> getLibrariesFromAllServersGrouped({bool forceRefresh = false}) async {
|
||||
// Return cached libraries if still valid and not forcing refresh
|
||||
if (!forceRefresh && _isLibrariesCacheValid) {
|
||||
appLogger.d('Using cached libraries data');
|
||||
return _cachedLibrariesByServer!;
|
||||
}
|
||||
|
||||
final librariesByServer = await _perServerGrouped<PlexLibrary>(
|
||||
operationName: 'fetching libraries',
|
||||
operation: (serverId, client, server) async {
|
||||
return await client.getLibraries();
|
||||
},
|
||||
);
|
||||
|
||||
// Cache the results
|
||||
_cachedLibrariesByServer = librariesByServer;
|
||||
_librariesCacheTime = DateTime.now();
|
||||
|
||||
final totalLibraries = librariesByServer.values.fold<int>(0, (sum, libs) => sum + libs.length);
|
||||
appLogger.d('Fetched $totalLibraries libraries from ${librariesByServer.length} servers');
|
||||
|
||||
return librariesByServer;
|
||||
}
|
||||
|
||||
/// Fetch recommendation hubs from all servers
|
||||
/// When useGlobalHubs is true (default), uses the global /hubs endpoint
|
||||
/// to get the true home page hubs like "Recently Added Movies", "Recently Added TV"
|
||||
@@ -208,8 +167,8 @@ class DataAggregationService {
|
||||
Set<String>? hiddenLibraryKeys,
|
||||
Map<String, List<PlexLibrary>>? librariesByServer,
|
||||
}) async {
|
||||
// Use pre-fetched libraries or fetch them if not provided
|
||||
final libraries = librariesByServer ?? await getLibrariesFromAllServersGrouped();
|
||||
// Use pre-fetched libraries or fetch and group them
|
||||
final libraries = librariesByServer ?? groupLibrariesByServer(await getLibrariesFromAllServers());
|
||||
|
||||
appLogger.d('Fetching per-library hubs from ${clients.length} servers');
|
||||
|
||||
@@ -395,15 +354,4 @@ class DataAggregationService {
|
||||
final results = await _perServerRaw(operationName: operationName, operation: operation);
|
||||
return [for (final (_, items) in results) ...items];
|
||||
}
|
||||
|
||||
/// Higher-order helper for per-server fan-out operations that groups results by server
|
||||
///
|
||||
/// Similar to [_perServer] but returns a Map with results grouped by serverId.
|
||||
Future<Map<String, List<T>>> _perServerGrouped<T>({
|
||||
required String operationName,
|
||||
required Future<List<T>> Function(String serverId, PlexClient client, PlexServer? server) operation,
|
||||
}) async {
|
||||
final results = await _perServerRaw(operationName: operationName, operation: operation);
|
||||
return {for (final (id, items) in results) id: items};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,9 @@ import '../focus/focus_memory_tracker.dart';
|
||||
import '../models/plex_library.dart';
|
||||
import '../navigation/navigation_tabs.dart';
|
||||
import '../providers/hidden_libraries_provider.dart';
|
||||
import '../providers/multi_server_provider.dart';
|
||||
import '../providers/libraries_provider.dart';
|
||||
import '../services/fullscreen_state_manager.dart';
|
||||
import '../services/storage_service.dart';
|
||||
import '../theme/mono_tokens.dart';
|
||||
import '../utils/content_utils.dart';
|
||||
import '../i18n/strings.g.dart';
|
||||
|
||||
/// Reusable navigation rail item widget that handles focus, selection, and interaction
|
||||
@@ -143,8 +141,6 @@ class SideNavigationRail extends StatefulWidget {
|
||||
|
||||
class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
bool _librariesExpanded = true;
|
||||
List<PlexLibrary> _libraries = [];
|
||||
bool _isLoadingLibraries = true;
|
||||
|
||||
// Collapsed/expanded state
|
||||
bool _isHovered = false;
|
||||
@@ -175,7 +171,6 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
},
|
||||
debugLabelPrefix: 'nav',
|
||||
);
|
||||
_loadLibraries();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -215,64 +210,15 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
_focusTracker.restoreFocus(fallbackKey: _kHome);
|
||||
}
|
||||
|
||||
/// Fetch, filter, and order libraries (pure logic, no state changes)
|
||||
Future<List<PlexLibrary>> _resolveLibraries(MultiServerProvider provider, StorageService storage) async {
|
||||
if (!provider.hasConnectedServers) return [];
|
||||
|
||||
final libraries = await provider.aggregationService.getLibrariesFromAllServers();
|
||||
|
||||
// Filter out unsupported library types (music)
|
||||
var filtered = libraries.where((lib) => !ContentTypeHelper.isMusicLibrary(lib)).toList();
|
||||
|
||||
// Apply saved order
|
||||
final savedOrder = storage.getLibraryOrder();
|
||||
if (savedOrder == null || savedOrder.isEmpty) return filtered;
|
||||
|
||||
final libraryMap = {for (var lib in filtered) lib.globalKey: lib};
|
||||
final ordered = <PlexLibrary>[];
|
||||
for (final key in savedOrder) {
|
||||
final lib = libraryMap.remove(key);
|
||||
if (lib != null) ordered.add(lib);
|
||||
}
|
||||
ordered.addAll(libraryMap.values); // New libraries not in saved order
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/// Build the set of valid focus keys (main nav + current libraries)
|
||||
Set<String> _buildValidFocusKeys(List<PlexLibrary> libraries) {
|
||||
return {_kHome, _kLibraries, _kSearch, _kDownloads, _kSettings, ...libraries.map((lib) => lib.globalKey)};
|
||||
}
|
||||
|
||||
Future<void> _loadLibraries() async {
|
||||
final provider = context.read<MultiServerProvider>();
|
||||
final storage = await StorageService.getInstance();
|
||||
|
||||
try {
|
||||
final libraries = await _resolveLibraries(provider, storage);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_libraries = libraries;
|
||||
_isLoadingLibraries = false;
|
||||
});
|
||||
// Prune stale library focus nodes
|
||||
_focusTracker.pruneExcept(_buildValidFocusKeys(libraries));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingLibraries = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reload libraries (called when servers change)
|
||||
/// Reload libraries (called when servers change or profile switches)
|
||||
void reloadLibraries() {
|
||||
setState(() {
|
||||
_isLoadingLibraries = true;
|
||||
});
|
||||
_loadLibraries();
|
||||
final librariesProvider = context.read<LibrariesProvider>();
|
||||
librariesProvider.refresh();
|
||||
}
|
||||
|
||||
IconData _getLibraryIcon(String type) {
|
||||
@@ -309,11 +255,16 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = tokens(context);
|
||||
final librariesProvider = context.watch<LibrariesProvider>();
|
||||
final hiddenLibrariesProvider = context.watch<HiddenLibrariesProvider>();
|
||||
final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys;
|
||||
|
||||
// Filter visible libraries
|
||||
final visibleLibraries = _libraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
// Get libraries from provider and filter visible ones
|
||||
final allLibraries = librariesProvider.libraries;
|
||||
final visibleLibraries = allLibraries.where((lib) => !hiddenKeys.contains(lib.globalKey)).toList();
|
||||
|
||||
// Prune stale focus nodes when libraries change
|
||||
_focusTracker.pruneExcept(_buildValidFocusKeys(allLibraries));
|
||||
|
||||
final isCollapsed = !_shouldExpand;
|
||||
|
||||
@@ -461,6 +412,8 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
}
|
||||
|
||||
Widget _buildLibrariesSection(List<PlexLibrary> visibleLibraries, dynamic t, {bool isCollapsed = false}) {
|
||||
final librariesProvider = context.watch<LibrariesProvider>();
|
||||
final isLoading = librariesProvider.isLoading;
|
||||
final isLibrariesSelected = widget.selectedIndex == 1 && widget.selectedLibraryKey == null;
|
||||
final isLibrariesFocused = _focusTracker.isFocused(_kLibraries);
|
||||
|
||||
@@ -565,7 +518,7 @@ class SideNavigationRailState extends State<SideNavigationRail> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
_isLoadingLibraries
|
||||
isLoading
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
|
||||
Reference in New Issue
Block a user