From 8a241578320f2b80ce42624ba4e45c4a4f3768a7 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 22 Nov 2025 04:42:11 +0100 Subject: [PATCH] fix: library manager --- lib/screens/libraries_screen.dart | 399 ++++++++++++++++++++++++------ lib/services/storage_service.dart | 30 ++- 2 files changed, 351 insertions(+), 78 deletions(-) diff --git a/lib/screens/libraries_screen.dart b/lib/screens/libraries_screen.dart index 730c9a22..2b7b71ed 100644 --- a/lib/screens/libraries_screen.dart +++ b/lib/screens/libraries_screen.dart @@ -48,6 +48,7 @@ class _LibrariesScreenState extends State String? _errorMessage; String? _selectedLibraryGlobalKey; bool _isInitialLoad = true; + List? _serverOrder; // Cached server order from storage Map _selectedFilters = {}; PlexSort? _selectedSort; @@ -112,6 +113,37 @@ class _LibrariesScreenState extends State return uniqueServerIds.length > 1; } + /// Get ordered list of server IDs from libraries + List _getOrderedServerIds(List libraries) { + // Get unique server IDs from libraries + final serverIds = libraries + .where((lib) => lib.serverId != null) + .map((lib) => lib.serverId!) + .toSet() + .toList(); + + if (_serverOrder == null || _serverOrder!.isEmpty) { + return serverIds; + } + + // Apply saved order, but include any new servers not in the saved order + final ordered = []; + for (final id in _serverOrder!) { + if (serverIds.contains(id)) { + ordered.add(id); + } + } + + // Add any servers not in saved order + for (final id in serverIds) { + if (!ordered.contains(id)) { + ordered.add(id); + } + } + + return ordered; + } + Future _loadLibraries() async { // Extract context dependencies before async gap final multiServerProvider = Provider.of( @@ -153,9 +185,13 @@ class _LibrariesScreenState extends State savedOrder, ); + // Load saved server order + final savedServerOrder = storage.getServerOrder(); + _updateState(() { _allLibraries = orderedLibraries; // Store all libraries with ordering applied + _serverOrder = savedServerOrder; _isLoadingLibraries = false; }); @@ -163,7 +199,7 @@ class _LibrariesScreenState extends State // Compute visible libraries for initial load final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys; final visibleLibraries = allLibraries - .where((lib) => !hiddenKeys.contains(lib.key)) + .where((lib) => !hiddenKeys.contains(lib.globalKey)) .toList(); // Load saved preferences @@ -251,7 +287,7 @@ class _LibrariesScreenState extends State ); final hiddenKeys = hiddenLibrariesProvider.hiddenLibraryKeys; final visibleLibraries = _allLibraries - .where((lib) => !hiddenKeys.contains(lib.key)) + .where((lib) => !hiddenKeys.contains(lib.globalKey)) .toList(); // Find the library by key @@ -499,25 +535,25 @@ class _LibrariesScreenState extends State listen: false, ); final isHidden = hiddenLibrariesProvider.hiddenLibraryKeys.contains( - library.key, + library.globalKey, ); if (isHidden) { - await hiddenLibrariesProvider.unhideLibrary(library.key); + await hiddenLibrariesProvider.unhideLibrary(library.globalKey); } else { // Check if we're hiding the currently selected library final isCurrentlySelected = _selectedLibraryGlobalKey == library.globalKey; - await hiddenLibrariesProvider.hideLibrary(library.key); + await hiddenLibrariesProvider.hideLibrary(library.globalKey); // If we just hid the selected library, select the first visible one if (isCurrentlySelected) { // Compute visible libraries after hiding final visibleLibraries = _allLibraries .where( - (lib) => - !hiddenLibrariesProvider.hiddenLibraryKeys.contains(lib.key), + (lib) => !hiddenLibrariesProvider.hiddenLibraryKeys + .contains(lib.globalKey), ) .toList(); @@ -746,7 +782,8 @@ class _LibrariesScreenState extends State groupedLibraries.putIfAbsent(serverKey, () => []).add(library); } - final serverKeys = groupedLibraries.keys.toList(); + // Use ordered server keys + final serverKeys = _getOrderedServerIds(visibleLibraries); for (int i = 0; i < serverKeys.length; i++) { final serverKey = serverKeys[i]; final libraries = groupedLibraries[serverKey]!; @@ -892,7 +929,7 @@ class _LibrariesScreenState extends State // Compute visible libraries (filtered from all libraries) final visibleLibraries = _allLibraries - .where((lib) => !hiddenKeys.contains(lib.key)) + .where((lib) => !hiddenKeys.contains(lib.globalKey)) .toList(); return Scaffold( @@ -1072,11 +1109,70 @@ class _LibraryManagementSheet extends StatefulWidget { class _LibraryManagementSheetState extends State<_LibraryManagementSheet> { late List _tempLibraries; + List? _serverOrder; + bool _isLoadingServerOrder = true; @override void initState() { super.initState(); _tempLibraries = List.from(widget.allLibraries); + _loadServerOrder(); + } + + /// Load server order from storage + Future _loadServerOrder() async { + final storage = await StorageService.getInstance(); + final savedOrder = storage.getServerOrder(); + + if (mounted) { + setState(() { + _serverOrder = savedOrder; + _isLoadingServerOrder = false; + }); + } + } + + /// Save server order to storage + Future _saveServerOrder(List serverIds) async { + final storage = await StorageService.getInstance(); + await storage.saveServerOrder(serverIds); + + if (mounted) { + setState(() { + _serverOrder = serverIds; + }); + } + } + + /// Get ordered list of server IDs + List _getOrderedServerIds() { + // Get unique server IDs from libraries + final serverIds = _tempLibraries + .where((lib) => lib.serverId != null) + .map((lib) => lib.serverId!) + .toSet() + .toList(); + + if (_serverOrder == null || _serverOrder!.isEmpty) { + return serverIds; + } + + // Apply saved order, but include any new servers not in the saved order + final ordered = []; + for (final id in _serverOrder!) { + if (serverIds.contains(id)) { + ordered.add(id); + } + } + + // Add any servers not in saved order + for (final id in serverIds) { + if (!ordered.contains(id)) { + ordered.add(id); + } + } + + return ordered; } /// Check if libraries come from multiple servers @@ -1232,80 +1328,229 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> { ), ), - // Reorderable library list + // Library list (grouped by server if multiple servers) Expanded( - child: ReorderableListView.builder( - scrollController: scrollController, - onReorder: _reorderLibraries, - itemCount: _tempLibraries.length, - padding: const EdgeInsets.symmetric(vertical: 8), - buildDefaultDragHandles: false, - itemBuilder: (context, index) { - final library = _tempLibraries[index]; - final isHidden = hiddenLibraryKeys.contains(library.key); - - return Opacity( - key: ValueKey(library.key), - opacity: isHidden ? 0.5 : 1.0, - child: ListTile( - leading: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ReorderableDragStartListener( - index: index, - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: Icon( - Icons.drag_indicator, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withValues(alpha: 0.5), - ), - ), - ), - const SizedBox(width: 8), - Icon(_getLibraryIcon(library.type)), - ], - ), - title: Row( - children: [ - Expanded(child: Text(library.title)), - if (_hasMultipleServers && library.serverName != null) - ServerBadge(serverName: library.serverName), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - isHidden - ? Icons.visibility_off - : Icons.visibility, - ), - onPressed: () => widget.onToggleVisibility(library), - tooltip: isHidden - ? t.libraries.showLibrary - : t.libraries.hideLibrary, - ), - IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () => - _showLibraryMenuBottomSheet(context, library), - tooltip: t.libraries.libraryOptions, - ), - ], - ), + child: _hasMultipleServers + ? _buildGroupedLibraryList( + scrollController, + hiddenLibraryKeys, + ) + : _buildFlatLibraryList( + scrollController, + hiddenLibraryKeys, ), - ); - }, - ), ), ], ); }, ); } + + + /// Build flat library list (single server) + Widget _buildFlatLibraryList( + ScrollController scrollController, + Set hiddenLibraryKeys, + ) { + return ReorderableListView.builder( + scrollController: scrollController, + onReorder: _reorderLibraries, + itemCount: _tempLibraries.length, + padding: const EdgeInsets.symmetric(vertical: 8), + buildDefaultDragHandles: false, + itemBuilder: (context, index) { + final library = _tempLibraries[index]; + return _buildLibraryTile(library, index, hiddenLibraryKeys); + }, + ); + } + + /// Build grouped library list (multiple servers) + Widget _buildGroupedLibraryList( + ScrollController scrollController, + Set hiddenLibraryKeys, + ) { + // Group libraries by server + final Map> groupedLibraries = {}; + for (final library in _tempLibraries) { + final serverKey = library.serverId ?? 'unknown'; + groupedLibraries.putIfAbsent(serverKey, () => []).add(library); + } + + // Use ordered server keys + final serverKeys = _getOrderedServerIds(); + + return ReorderableListView.builder( + scrollController: scrollController, + padding: const EdgeInsets.symmetric(vertical: 8), + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + // Reorder servers + final reorderedServerIds = List.from(serverKeys); + if (newIndex > oldIndex) { + newIndex -= 1; + } + final serverId = reorderedServerIds.removeAt(oldIndex); + reorderedServerIds.insert(newIndex, serverId); + _saveServerOrder(reorderedServerIds); + }, + itemCount: serverKeys.length, + itemBuilder: (context, serverIndex) { + final serverKey = serverKeys[serverIndex]; + final libraries = groupedLibraries[serverKey]!; + final serverName = libraries.first.serverName ?? 'Unknown Server'; + + return Column( + key: ValueKey(serverKey), + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Server header with drag handle + ListTile( + leading: ReorderableDragStartListener( + index: serverIndex, + child: Icon( + Icons.drag_indicator, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.5), + ), + ), + title: Text( + serverName, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + + // Libraries for this server (reorderable within server) + ...libraries.asMap().entries.map((entry) { + final index = entry.key; + final library = entry.value; + return _buildLibraryTile( + library, + index, + hiddenLibraryKeys, + showServerBadge: false, + enableDrag: false, // Disable drag for individual libraries + ); + }), + ], + ); + }, + ); + } + + /// Reorder libraries within a server group + void _reorderLibrariesInServer( + String serverKey, + List serverLibraries, + int oldIndex, + int newIndex, + ) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + + // Get the library being moved + final library = serverLibraries[oldIndex]; + + // Find global indices + final globalOldIndex = _tempLibraries.indexOf(library); + + // Calculate the new global index + // We need to find where this library should go in the global list + final targetLibrary = newIndex < serverLibraries.length + ? serverLibraries[newIndex] + : serverLibraries.last; + final globalNewIndex = _tempLibraries.indexOf(targetLibrary); + + // Reorder in global list + _tempLibraries.removeAt(globalOldIndex); + _tempLibraries.insert( + globalOldIndex < globalNewIndex ? globalNewIndex : globalNewIndex, + library, + ); + }); + + // Apply immediately + widget.onReorder(_tempLibraries); + } + + /// Build a single library tile + Widget _buildLibraryTile( + PlexLibrary library, + int index, + Set hiddenLibraryKeys, { + bool showServerBadge = true, + bool enableDrag = true, + }) { + final isHidden = hiddenLibraryKeys.contains(library.globalKey); + + return Opacity( + key: ValueKey(library.globalKey), + opacity: isHidden ? 0.5 : 1.0, + child: ListTile( + leading: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (enableDrag) + ReorderableDragStartListener( + index: index, + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: Icon( + Icons.drag_indicator, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withValues(alpha: 0.5), + ), + ), + ), + if (enableDrag) const SizedBox(width: 8), + if (!enableDrag) const SizedBox(width: 12), + Icon(_getLibraryIcon(library.type)), + ], + ), + title: Row( + children: [ + Expanded(child: Text(library.title)), + if (showServerBadge && + _hasMultipleServers && + library.serverName != null) + ServerBadge( + serverName: library.serverName, + showFullName: true, + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + isHidden ? Icons.visibility_off : Icons.visibility, + ), + onPressed: () => widget.onToggleVisibility(library), + tooltip: + isHidden ? t.libraries.showLibrary : t.libraries.hideLibrary, + ), + IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () => _showLibraryMenuBottomSheet(context, library), + tooltip: t.libraries.libraryOptions, + ), + ], + ), + ), + ); + } } diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index b1db1fa5..16aeeebd 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -21,6 +21,7 @@ class StorageService { static const String _keyHiddenLibraries = 'hidden_libraries'; static const String _keyServersList = 'servers_list'; static const String _keyEnabledServers = 'enabled_servers'; + static const String _keyServerOrder = 'server_order'; static StorageService? _instance; late SharedPreferences _prefs; @@ -376,7 +377,34 @@ class StorageService { /// Clear all multi-server data Future clearMultiServerData() async { - await Future.wait([clearServersList(), clearEnabledServers()]); + await Future.wait([ + clearServersList(), + clearEnabledServers(), + clearServerOrder(), + ]); + } + + /// Server Order (stored as JSON list of server IDs) + Future saveServerOrder(List serverIds) async { + final jsonString = json.encode(serverIds); + await _prefs.setString(_keyServerOrder, jsonString); + } + + List? getServerOrder() { + final jsonString = _prefs.getString(_keyServerOrder); + if (jsonString == null) return null; + + try { + final decoded = json.decode(jsonString) as List; + return decoded.map((e) => e.toString()).toList(); + } catch (e) { + return null; + } + } + + /// Clear server order + Future clearServerOrder() async { + await _prefs.remove(_keyServerOrder); } // Private helper methods