From 03495b1ea3a6a3f22f7602b93fe349dbc545fa85 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:49:25 +0100 Subject: [PATCH] refactor: deduplicate sort widget --- lib/screens/hub_detail_screen.dart | 154 +---------------- lib/screens/libraries_screen.dart | 267 ++++++++--------------------- lib/widgets/sort_bottom_sheet.dart | 151 ++++++++++++++++ 3 files changed, 227 insertions(+), 345 deletions(-) create mode 100644 lib/widgets/sort_bottom_sheet.dart diff --git a/lib/screens/hub_detail_screen.dart b/lib/screens/hub_detail_screen.dart index 3b539b5b..a6ad1f66 100644 --- a/lib/screens/hub_detail_screen.dart +++ b/lib/screens/hub_detail_screen.dart @@ -10,6 +10,7 @@ import '../utils/provider_extensions.dart'; import '../utils/app_logger.dart'; import '../widgets/media_card.dart'; import '../widgets/desktop_app_bar.dart'; +import '../widgets/sort_bottom_sheet.dart'; import '../mixins/refreshable.dart'; /// Screen to display full content of a recommendation hub @@ -165,7 +166,7 @@ class _HubDetailScreenState extends State with Refreshable { showModalBottomSheet( context: context, isScrollControlled: true, - builder: (context) => _SortBottomSheet( + builder: (context) => SortBottomSheet( sortOptions: _sortOptions, selectedSort: _selectedSort, isSortDescending: _isSortDescending, @@ -248,9 +249,8 @@ class _HubDetailScreenState extends State with Refreshable { return Scaffold( body: CustomScrollView( slivers: [ - DesktopSliverAppBar( + CustomAppBar( title: Text(widget.hub.title), - floating: true, pinned: true, actions: [ IconButton( @@ -360,151 +360,3 @@ class _HubDetailScreenState extends State with Refreshable { } } } - -// Bottom sheet for sorting -class _SortBottomSheet extends StatefulWidget { - final List sortOptions; - final PlexSort? selectedSort; - final bool isSortDescending; - final Function(PlexSort, bool) onSortChanged; - final VoidCallback onClear; - - const _SortBottomSheet({ - required this.sortOptions, - required this.selectedSort, - required this.isSortDescending, - required this.onSortChanged, - required this.onClear, - }); - - @override - State<_SortBottomSheet> createState() => _SortBottomSheetState(); -} - -class _SortBottomSheetState extends State<_SortBottomSheet> { - late PlexSort? _currentSort; - late bool _currentDescending; - - @override - void initState() { - super.initState(); - _currentSort = widget.selectedSort; - _currentDescending = widget.isSortDescending; - } - - void _handleSortChange(PlexSort sort, bool descending) { - setState(() { - _currentSort = sort; - _currentDescending = descending; - }); - widget.onSortChanged(sort, descending); - } - - void _handleClear() { - setState(() { - _currentSort = null; - _currentDescending = false; - }); - widget.onClear(); - Navigator.pop(context); - } - - @override - Widget build(BuildContext context) { - return DraggableScrollableSheet( - initialChildSize: 0.6, - minChildSize: 0.4, - maxChildSize: 0.9, - expand: false, - builder: (context, scrollController) { - return Column( - children: [ - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor), - ), - ), - child: Row( - children: [ - const Expanded( - child: Text( - 'Sort By', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - TextButton( - onPressed: _handleClear, - child: const Text('Clear'), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - Expanded( - child: ListView.builder( - controller: scrollController, - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: widget.sortOptions.length, - itemBuilder: (context, index) { - final sort = widget.sortOptions[index]; - final isSelected = _currentSort?.key == sort.key; - - return ListTile( - title: Text(sort.title), - trailing: isSelected - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - SegmentedButton( - showSelectedIcon: false, - segments: const [ - ButtonSegment( - value: false, - icon: Icon(Icons.arrow_upward, size: 16), - ), - ButtonSegment( - value: true, - icon: Icon(Icons.arrow_downward, size: 16), - ), - ], - selected: {_currentDescending}, - onSelectionChanged: (Set newSelection) { - _handleSortChange(sort, newSelection.first); - }, - ), - ], - ) - : null, - leading: Radio( - value: sort, - groupValue: _currentSort, - onChanged: (PlexSort? value) { - if (value != null) { - _handleSortChange( - value, - value.defaultDirection == 'desc', - ); - } - }, - ), - onTap: () { - _handleSortChange(sort, sort.defaultDirection == 'desc'); - }, - ); - }, - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/screens/libraries_screen.dart b/lib/screens/libraries_screen.dart index aeec432c..3621b55f 100644 --- a/lib/screens/libraries_screen.dart +++ b/lib/screens/libraries_screen.dart @@ -14,6 +14,7 @@ import '../utils/app_logger.dart'; import '../widgets/media_card.dart'; import '../widgets/desktop_app_bar.dart'; import '../widgets/app_bar_back_button.dart'; +import '../widgets/sort_bottom_sheet.dart'; import '../services/storage_service.dart'; import '../services/settings_service.dart'; import '../mixins/refreshable.dart'; @@ -105,10 +106,14 @@ class _LibrariesScreenState extends State // Load saved library order and apply it final savedOrder = storage.getLibraryOrder(); - final orderedLibraries = _applyLibraryOrder(filteredLibraries, savedOrder); + final orderedLibraries = _applyLibraryOrder( + filteredLibraries, + savedOrder, + ); setState(() { - _allLibraries = orderedLibraries; // Store all libraries with ordering applied + _allLibraries = + orderedLibraries; // Store all libraries with ordering applied _isLoadingLibraries = false; }); @@ -127,8 +132,9 @@ class _LibrariesScreenState extends State String? libraryKeyToLoad; if (savedLibraryKey != null) { // Check if saved library exists and is visible - final libraryExists = visibleLibraries - .any((lib) => lib.key == savedLibraryKey); + final libraryExists = visibleLibraries.any( + (lib) => lib.key == savedLibraryKey, + ); if (libraryExists) { libraryKeyToLoad = savedLibraryKey; } @@ -217,12 +223,14 @@ class _LibrariesScreenState extends State .where((lib) => !hiddenKeys.contains(lib.key)) .toList(); - // Find the library by key - final libraryIndex = visibleLibraries.indexWhere((lib) => lib.key == libraryKey); + final libraryIndex = visibleLibraries.indexWhere( + (lib) => lib.key == libraryKey, + ); if (libraryIndex == -1) return; // Library not found or hidden - final isChangingLibrary = !_isInitialLoad && _selectedLibraryKey != libraryKey; + final isChangingLibrary = + !_isInitialLoad && _selectedLibraryKey != libraryKey; // Extract context dependencies before async operations final clientProvider = context.plexClient; @@ -410,10 +418,7 @@ class _LibrariesScreenState extends State // Save sort preference for this library final storage = await StorageService.getInstance(); final sortKey = sort.getSortKey(descending: descending); - await storage.saveLibrarySort( - _selectedLibraryKey!, - sortKey, - ); + await storage.saveLibrarySort(_selectedLibraryKey!, sortKey); // Reload content with new sort _applyFilters(); @@ -440,7 +445,9 @@ class _LibrariesScreenState extends State context, listen: false, ); - final isHidden = hiddenLibrariesProvider.hiddenLibraryKeys.contains(library.key); + final isHidden = hiddenLibrariesProvider.hiddenLibraryKeys.contains( + library.key, + ); if (isHidden) { await hiddenLibrariesProvider.unhideLibrary(library.key); @@ -454,7 +461,10 @@ class _LibrariesScreenState extends State if (isCurrentlySelected) { // Compute visible libraries after hiding final visibleLibraries = _allLibraries - .where((lib) => !hiddenLibrariesProvider.hiddenLibraryKeys.contains(lib.key)) + .where( + (lib) => + !hiddenLibrariesProvider.hiddenLibraryKeys.contains(lib.key), + ) .toList(); if (visibleLibraries.isNotEmpty) { @@ -464,7 +474,6 @@ class _LibrariesScreenState extends State } } - void _showFiltersBottomSheet() { showModalBottomSheet( context: context, @@ -492,7 +501,7 @@ class _LibrariesScreenState extends State showModalBottomSheet( context: context, isScrollControlled: true, - builder: (context) => _SortBottomSheet( + builder: (context) => SortBottomSheet( sortOptions: _sortOptions, selectedSort: _selectedSort, isSortDescending: _isSortDescending, @@ -527,7 +536,6 @@ class _LibrariesScreenState extends State ); } - @override Widget build(BuildContext context) { // Watch for hidden libraries changes to trigger rebuild @@ -553,7 +561,10 @@ class _LibrariesScreenState extends State actions: [ if (_allLibraries.isNotEmpty) IconButton( - icon: const Icon(Icons.edit, semanticLabel: 'Manage Libraries'), + icon: const Icon( + Icons.edit, + semanticLabel: 'Manage Libraries', + ), onPressed: _showLibraryManagementSheet, ), if (_sortOptions.isNotEmpty) @@ -631,48 +642,48 @@ class _LibrariesScreenState extends State vertical: 8, ), child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: List.generate(visibleLibraries.length, (index) { - final library = visibleLibraries[index]; - final isSelected = library.key == _selectedLibraryKey; - 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(library.key); - } - }, - 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, + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(visibleLibraries.length, (index) { + final library = visibleLibraries[index]; + final isSelected = library.key == _selectedLibraryKey; + 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(library.key); + } + }, + 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, ), - ), + ); + }), + ), + ), ), ), @@ -1122,141 +1133,6 @@ class _FiltersBottomSheetState extends State<_FiltersBottomSheet> { } } -class _SortBottomSheet extends StatefulWidget { - final List sortOptions; - final PlexSort? selectedSort; - final bool isSortDescending; - final Function(PlexSort, bool) onSortChanged; - - const _SortBottomSheet({ - required this.sortOptions, - required this.selectedSort, - required this.isSortDescending, - required this.onSortChanged, - }); - - @override - State<_SortBottomSheet> createState() => _SortBottomSheetState(); -} - -class _SortBottomSheetState extends State<_SortBottomSheet> { - late PlexSort? _tempSelectedSort; - late bool _tempDescending; - - @override - void initState() { - super.initState(); - _tempSelectedSort = widget.selectedSort; - _tempDescending = widget.isSortDescending; - } - - @override - Widget build(BuildContext context) { - return DraggableScrollableSheet( - initialChildSize: 0.6, - minChildSize: 0.4, - maxChildSize: 0.9, - expand: false, - builder: (context, scrollController) { - return Column( - children: [ - // Header - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor), - ), - ), - child: Row( - children: [ - const Expanded( - child: Text( - 'Sort By', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - - // Sort options list - Expanded( - child: ListView.builder( - controller: scrollController, - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: widget.sortOptions.length, - itemBuilder: (context, index) { - final sort = widget.sortOptions[index]; - final isSelected = _tempSelectedSort?.key == sort.key; - - return ListTile( - title: Text(sort.title), - trailing: isSelected - ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Direction toggle buttons - SegmentedButton( - showSelectedIcon: false, - segments: const [ - ButtonSegment( - value: false, - icon: Icon(Icons.arrow_upward, size: 16), - ), - ButtonSegment( - value: true, - icon: Icon(Icons.arrow_downward, size: 16), - ), - ], - selected: {_tempDescending}, - onSelectionChanged: (Set selected) { - widget.onSortChanged(sort, selected.first); - }, - ), - ], - ) - : null, - leading: Radio( - value: sort.key, - groupValue: _tempSelectedSort?.key, - onChanged: (value) { - setState(() { - _tempSelectedSort = sort; - // Use default direction for newly selected sort - _tempDescending = sort.isDefaultDescending; - }); - // Apply sort immediately with default direction - widget.onSortChanged(sort, sort.isDefaultDescending); - }, - ), - onTap: () { - setState(() { - _tempSelectedSort = sort; - // Use default direction for newly selected sort - _tempDescending = sort.isDefaultDescending; - }); - // Apply sort immediately with default direction - widget.onSortChanged(sort, sort.isDefaultDescending); - }, - ); - }, - ), - ), - ], - ); - }, - ); - } -} - class _LibraryManagementSheet extends StatefulWidget { final List allLibraries; final Set hiddenLibraryKeys; @@ -1271,7 +1147,8 @@ class _LibraryManagementSheet extends StatefulWidget { }); @override - State<_LibraryManagementSheet> createState() => _LibraryManagementSheetState(); + State<_LibraryManagementSheet> createState() => + _LibraryManagementSheetState(); } class _LibraryManagementSheetState extends State<_LibraryManagementSheet> { @@ -1378,7 +1255,9 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> { padding: const EdgeInsets.only(right: 12), child: Icon( Icons.drag_indicator, - color: Theme.of(context).textTheme.bodyMedium?.color?.withOpacity(0.5), + color: Theme.of( + context, + ).textTheme.bodyMedium?.color?.withOpacity(0.5), ), ), ), diff --git a/lib/widgets/sort_bottom_sheet.dart b/lib/widgets/sort_bottom_sheet.dart new file mode 100644 index 00000000..ac45a4b5 --- /dev/null +++ b/lib/widgets/sort_bottom_sheet.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import '../models/plex_sort.dart'; + +class SortBottomSheet extends StatefulWidget { + final List sortOptions; + final PlexSort? selectedSort; + final bool isSortDescending; + final Function(PlexSort, bool) onSortChanged; + final VoidCallback? onClear; + + const SortBottomSheet({ + super.key, + required this.sortOptions, + required this.selectedSort, + required this.isSortDescending, + required this.onSortChanged, + this.onClear, + }); + + @override + State createState() => _SortBottomSheetState(); +} + +class _SortBottomSheetState extends State { + late PlexSort? _currentSort; + late bool _currentDescending; + + @override + void initState() { + super.initState(); + _currentSort = widget.selectedSort; + _currentDescending = widget.isSortDescending; + } + + void _handleSortChange(PlexSort sort, bool descending) { + setState(() { + _currentSort = sort; + _currentDescending = descending; + }); + widget.onSortChanged(sort, descending); + } + + void _handleClear() { + setState(() { + _currentSort = null; + _currentDescending = false; + }); + widget.onClear?.call(); + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.4, + maxChildSize: 0.9, + expand: false, + builder: (context, scrollController) { + return Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: Row( + children: [ + const Expanded( + child: Text( + 'Sort By', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + if (widget.onClear != null) + TextButton( + onPressed: _handleClear, + child: const Text('Clear'), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ), + Expanded( + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.symmetric(vertical: 8), + itemCount: widget.sortOptions.length, + itemBuilder: (context, index) { + final sort = widget.sortOptions[index]; + final isSelected = _currentSort?.key == sort.key; + + return ListTile( + title: Text(sort.title), + trailing: isSelected + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + SegmentedButton( + showSelectedIcon: false, + segments: const [ + ButtonSegment( + value: false, + icon: Icon(Icons.arrow_upward, size: 16), + ), + ButtonSegment( + value: true, + icon: Icon(Icons.arrow_downward, size: 16), + ), + ], + selected: {_currentDescending}, + onSelectionChanged: (Set newSelection) { + _handleSortChange(sort, newSelection.first); + }, + ), + ], + ) + : null, + leading: Radio( + value: sort, + groupValue: _currentSort, + onChanged: (PlexSort? value) { + if (value != null) { + _handleSortChange( + value, + value.defaultDirection == 'desc', + ); + } + }, + ), + onTap: () { + _handleSortChange(sort, sort.defaultDirection == 'desc'); + }, + ); + }, + ), + ), + ], + ); + }, + ); + } +}