@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/widgets/app_icon.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import '../../focus/dpad_navigator.dart';
|
||||
import '../../models/plex_sort.dart';
|
||||
import '../../widgets/bottom_sheet_header.dart';
|
||||
import '../../widgets/focusable_bottom_sheet.dart';
|
||||
@@ -46,13 +47,20 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleSortChange(PlexSort sort, bool descending) {
|
||||
void _handleSortSelect(PlexSort sort) {
|
||||
final descending = (_currentSort?.key == sort.key) ? _currentDescending : sort.isDefaultDescending;
|
||||
setState(() {
|
||||
_currentSort = sort;
|
||||
_currentDescending = descending;
|
||||
});
|
||||
widget.onSortChanged(sort, descending);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
void _handleDirectionChange(PlexSort sort, bool descending) {
|
||||
setState(() {
|
||||
_currentDescending = descending;
|
||||
});
|
||||
widget.onSortChanged(sort, descending);
|
||||
}
|
||||
|
||||
void _handleClear() {
|
||||
@@ -61,7 +69,6 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
_currentDescending = false;
|
||||
});
|
||||
widget.onClear?.call();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,25 +90,38 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
: null,
|
||||
),
|
||||
Expanded(
|
||||
child: RadioGroup<PlexSort>(
|
||||
groupValue: _currentSort,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
_handleSortChange(value, value.isDefaultDescending);
|
||||
}
|
||||
},
|
||||
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;
|
||||
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 FocusableRadioListTile<PlexSort>(
|
||||
return Focus(
|
||||
canRequestFocus: false,
|
||||
skipTraversal: true,
|
||||
onKeyEvent: (node, event) {
|
||||
if (!event.isActionable) return KeyEventResult.ignored;
|
||||
if (!isSelected) return KeyEventResult.ignored;
|
||||
if (event.logicalKey.isLeftKey) {
|
||||
_handleDirectionChange(sort, false);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (event.logicalKey.isRightKey) {
|
||||
_handleDirectionChange(sort, true);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: FocusableRadioListTile<PlexSort>(
|
||||
focusNode: index == 0 ? _initialFocusNode : null,
|
||||
title: Text(sort.title),
|
||||
value: sort,
|
||||
groupValue: _currentSort,
|
||||
onChanged: (value) {
|
||||
if (value != null) _handleSortSelect(value);
|
||||
},
|
||||
secondary: isSelected
|
||||
? SegmentedButton<bool>(
|
||||
showSelectedIcon: false,
|
||||
@@ -117,13 +137,13 @@ class _SortBottomSheetState extends State<SortBottomSheet> {
|
||||
],
|
||||
selected: {_currentDescending},
|
||||
onSelectionChanged: (Set<bool> newSelection) {
|
||||
_handleSortChange(sort, newSelection.first);
|
||||
_handleDirectionChange(sort, newSelection.first);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../../focus/dpad_navigator.dart';
|
||||
import '../../../../services/plex_client.dart';
|
||||
import '../../../models/plex_metadata.dart';
|
||||
import '../../../models/plex_filter.dart';
|
||||
@@ -402,40 +403,50 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
|
||||
}
|
||||
|
||||
void _showGroupingBottomSheet() {
|
||||
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
|
||||
var pendingGrouping = _selectedGrouping;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (sheetContext) {
|
||||
final options = _getGroupingOptions();
|
||||
return RadioGroup<String>(
|
||||
groupValue: _selectedGrouping,
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
_selectedGrouping = value;
|
||||
});
|
||||
|
||||
final storage = await StorageService.getInstance();
|
||||
await storage.saveLibraryGrouping(widget.library.globalKey, value);
|
||||
|
||||
if (!sheetContext.mounted || !mounted) return;
|
||||
|
||||
Navigator.pop(sheetContext);
|
||||
_loadItems();
|
||||
return StatefulBuilder(
|
||||
builder: (context, setSheetState) {
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, index) {
|
||||
final grouping = options[index];
|
||||
return RadioListTile<String>(
|
||||
title: Text(_getGroupingLabel(grouping)),
|
||||
value: grouping,
|
||||
groupValue: pendingGrouping,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setSheetState(() {
|
||||
pendingGrouping = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (context, index) {
|
||||
final grouping = options[index];
|
||||
return RadioListTile<String>(title: Text(_getGroupingLabel(grouping)), value: grouping);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
).then((_) {
|
||||
if (!mounted) return;
|
||||
if (pendingGrouping == _selectedGrouping) return;
|
||||
setState(() {
|
||||
_selectedGrouping = pendingGrouping;
|
||||
});
|
||||
StorageService.getInstance().then((storage) {
|
||||
storage.saveLibraryGrouping(widget.library.globalKey, pendingGrouping);
|
||||
});
|
||||
_loadItems();
|
||||
});
|
||||
}
|
||||
|
||||
void _showFiltersBottomSheet() {
|
||||
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -460,6 +471,12 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
|
||||
}
|
||||
|
||||
void _showSortBottomSheet() {
|
||||
SelectKeyUpSuppressor.suppressSelectUntilKeyUp();
|
||||
// Track pending state in local variables so the callbacks don't trigger
|
||||
// setState/_loadItems while the sheet is open (which would steal focus).
|
||||
PlexSort? pendingSort = _selectedSort;
|
||||
bool pendingDescending = _isSortDescending;
|
||||
bool pendingCleared = false;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -468,19 +485,36 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
|
||||
selectedSort: _selectedSort,
|
||||
isSortDescending: _isSortDescending,
|
||||
onSortChanged: (sort, descending) {
|
||||
setState(() {
|
||||
_selectedSort = sort;
|
||||
_isSortDescending = descending;
|
||||
});
|
||||
|
||||
StorageService.getInstance().then((storage) {
|
||||
storage.saveLibrarySort(widget.library.globalKey, sort.key, descending: descending);
|
||||
});
|
||||
|
||||
_loadItems();
|
||||
pendingSort = sort;
|
||||
pendingDescending = descending;
|
||||
pendingCleared = false;
|
||||
},
|
||||
onClear: () {
|
||||
pendingSort = null;
|
||||
pendingDescending = false;
|
||||
pendingCleared = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
).then((_) {
|
||||
if (!mounted) return;
|
||||
if (pendingCleared) {
|
||||
setState(() {
|
||||
_selectedSort = null;
|
||||
_isSortDescending = false;
|
||||
});
|
||||
_loadItems();
|
||||
} else if (pendingSort != null &&
|
||||
(pendingSort!.key != _selectedSort?.key || pendingDescending != _isSortDescending)) {
|
||||
setState(() {
|
||||
_selectedSort = pendingSort;
|
||||
_isSortDescending = pendingDescending;
|
||||
});
|
||||
StorageService.getInstance().then((storage) {
|
||||
storage.saveLibrarySort(widget.library.globalKey, pendingSort!.key, descending: pendingDescending);
|
||||
});
|
||||
_loadItems();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Navigate focus from chips down to the grid item.
|
||||
|
||||
@@ -111,6 +111,7 @@ class _FocusableListTileState extends State<FocusableListTile> {
|
||||
/// A RadioListTile that accepts a FocusNode for keyboard/controller navigation.
|
||||
///
|
||||
/// Uses Flutter's native RadioListTile focus support - no custom styling wrapper.
|
||||
/// Can be used standalone with [groupValue]/[onChanged] or inside a [RadioGroup].
|
||||
class FocusableRadioListTile<T> extends StatelessWidget {
|
||||
/// The primary content of the list tile.
|
||||
final Widget? title;
|
||||
@@ -124,6 +125,14 @@ class FocusableRadioListTile<T> extends StatelessWidget {
|
||||
/// The value represented by this radio button.
|
||||
final T value;
|
||||
|
||||
/// The currently selected value for the group.
|
||||
/// When provided, the widget works without a [RadioGroup] ancestor.
|
||||
final T? groupValue;
|
||||
|
||||
/// Called when this radio button is selected.
|
||||
/// When provided, the widget works without a [RadioGroup] ancestor.
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// Whether this radio button is part of a vertically dense list.
|
||||
final bool dense;
|
||||
|
||||
@@ -142,6 +151,8 @@ class FocusableRadioListTile<T> extends StatelessWidget {
|
||||
this.subtitle,
|
||||
this.secondary,
|
||||
required this.value,
|
||||
this.groupValue,
|
||||
this.onChanged,
|
||||
this.dense = false,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
@@ -155,6 +166,8 @@ class FocusableRadioListTile<T> extends StatelessWidget {
|
||||
subtitle: subtitle,
|
||||
secondary: secondary,
|
||||
value: value,
|
||||
groupValue: groupValue,
|
||||
onChanged: onChanged,
|
||||
dense: dense,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
|
||||
Reference in New Issue
Block a user