diff --git a/lib/screens/libraries_screen.dart b/lib/screens/libraries_screen.dart index bd2b4cc6..80fb3416 100644 --- a/lib/screens/libraries_screen.dart +++ b/lib/screens/libraries_screen.dart @@ -35,6 +35,7 @@ class _LibrariesScreenState extends State int _selectedLibraryIndex = 0; Map _selectedFilters = {}; bool _isInitialLoad = true; + bool _isReorderMode = false; @override void initState() { @@ -59,8 +60,14 @@ class _LibrariesScreenState extends State } final libraries = await client.getLibraries(); + + // Load saved library order and apply it + final storage = await StorageService.getInstance(); + final savedOrder = storage.getLibraryOrder(); + final orderedLibraries = _applyLibraryOrder(libraries, savedOrder); + setState(() { - _libraries = libraries; + _libraries = orderedLibraries; _isLoadingLibraries = false; }); @@ -91,6 +98,67 @@ class _LibrariesScreenState extends State } } + List _applyLibraryOrder( + List libraries, + List? savedOrder, + ) { + if (savedOrder == null || savedOrder.isEmpty) { + return libraries; + } + + // Create a map for quick lookup + final libraryMap = {for (var lib in libraries) lib.key: lib}; + + // Build ordered list based on saved order + final orderedLibraries = []; + final addedKeys = {}; + + // 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.key)) { + orderedLibraries.add(library); + } + } + + return orderedLibraries; + } + + Future _saveLibraryOrder() async { + final storage = await StorageService.getInstance(); + final libraryKeys = _libraries.map((lib) => lib.key).toList(); + await storage.saveLibraryOrder(libraryKeys); + } + + void _reorderLibraries(int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final library = _libraries.removeAt(oldIndex); + _libraries.insert(newIndex, library); + + // Update selected index to follow the moved library if needed + if (oldIndex == _selectedLibraryIndex) { + _selectedLibraryIndex = newIndex; + } else if (oldIndex < _selectedLibraryIndex && + newIndex >= _selectedLibraryIndex) { + _selectedLibraryIndex -= 1; + } else if (oldIndex > _selectedLibraryIndex && + newIndex <= _selectedLibraryIndex) { + _selectedLibraryIndex += 1; + } + }); + _saveLibraryOrder(); + } + Future _loadLibraryContent(int index) async { if (index < 0 || index >= _libraries.length) return; @@ -261,6 +329,18 @@ class _LibrariesScreenState extends State shadowColor: Colors.transparent, scrolledUnderElevation: 0, actions: [ + if (_libraries.isNotEmpty) + IconButton( + icon: Icon( + _isReorderMode ? Icons.check : Icons.edit, + semanticLabel: _isReorderMode ? 'Done' : 'Reorder', + ), + onPressed: () { + setState(() { + _isReorderMode = !_isReorderMode; + }); + }, + ), if (_filters.isNotEmpty) IconButton( icon: Badge( @@ -326,53 +406,104 @@ class _LibrariesScreenState extends State // Library selector chips SliverToBoxAdapter( child: Container( + height: 48, padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: List.generate(_libraries.length, (index) { - final library = _libraries[index]; - final isSelected = index == _selectedLibraryIndex; - final t = tokens(context); - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - _getLibraryIcon(library.type), - size: 16, - color: isSelected ? t.bg : t.text, + child: _isReorderMode + ? ReorderableListView.builder( + scrollDirection: Axis.horizontal, + onReorder: _reorderLibraries, + itemCount: _libraries.length, + proxyDecorator: (child, index, animation) { + return Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + child: child, + ); + }, + itemBuilder: (context, index) { + final library = _libraries[index]; + final isSelected = index == _selectedLibraryIndex; + final t = tokens(context); + return Container( + key: ValueKey(library.key), + margin: const EdgeInsets.only(right: 8), + child: Chip( + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.drag_handle, + size: 16, + color: isSelected ? t.bg : t.text, + ), + const SizedBox(width: 6), + Icon( + _getLibraryIcon(library.type), + size: 16, + color: isSelected ? t.bg : t.text, + ), + const SizedBox(width: 6), + Text(library.title), + ], ), - const SizedBox(width: 6), - Text(library.title), - ], - ), - selected: isSelected, - onSelected: (selected) { - if (selected) { - _loadLibraryContent(index); - } - }, - backgroundColor: t.surface, - selectedColor: t.text, - side: BorderSide(color: t.outline), - labelStyle: TextStyle( - color: isSelected ? t.bg : t.text, - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.w400, - ), - showCheckmark: false, + backgroundColor: isSelected ? t.text : t.surface, + side: BorderSide(color: t.outline), + labelStyle: TextStyle( + color: isSelected ? t.bg : t.text, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w400, + ), + ), + ); + }, + ) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(_libraries.length, (index) { + final library = _libraries[index]; + final isSelected = index == _selectedLibraryIndex; + final t = tokens(context); + return Padding( + padding: const EdgeInsets.only(right: 8), + child: ChoiceChip( + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getLibraryIcon(library.type), + size: 16, + color: isSelected ? t.bg : t.text, + ), + const SizedBox(width: 6), + Text(library.title), + ], + ), + selected: isSelected, + onSelected: (selected) { + if (selected) { + _loadLibraryContent(index); + } + }, + backgroundColor: t.surface, + selectedColor: t.text, + side: BorderSide(color: t.outline), + labelStyle: TextStyle( + color: isSelected ? t.bg : t.text, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.w400, + ), + showCheckmark: false, + ), + ); + }), ), - ); - }), - ), - ), + ), ), ), diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 7d569b97..dfe3b4fe 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -9,6 +9,7 @@ class StorageService { static const String _keyClientId = 'client_identifier'; static const String _keySelectedLibraryIndex = 'selected_library_index'; static const String _keyLibraryFilters = 'library_filters'; + static const String _keyLibraryOrder = 'library_order'; static const String _keyUserProfile = 'user_profile'; static const String _keyCurrentUserUUID = 'current_user_uuid'; static const String _keyHomeUsersCache = 'home_users_cache'; @@ -167,9 +168,28 @@ class StorageService { await Future.wait([ _prefs.remove(_keySelectedLibraryIndex), _prefs.remove(_keyLibraryFilters), + _prefs.remove(_keyLibraryOrder), ]); } + // Library Order (stored as JSON list of library keys) + Future saveLibraryOrder(List libraryKeys) async { + final jsonString = json.encode(libraryKeys); + await _prefs.setString(_keyLibraryOrder, jsonString); + } + + List? getLibraryOrder() { + final jsonString = _prefs.getString(_keyLibraryOrder); + if (jsonString == null) return null; + + try { + final decoded = json.decode(jsonString) as List; + return decoded.map((e) => e.toString()).toList(); + } catch (e) { + return null; + } + } + // User Profile (stored as JSON string) Future saveUserProfile(Map profileJson) async { final jsonString = json.encode(profileJson);