fix: manage libraries misc

This commit is contained in:
edde746
2026-02-01 23:45:49 +01:00
parent 6eb78af965
commit ef386b4357
+67 -41
View File
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import '../../focus/dpad_navigator.dart'; import '../../focus/dpad_navigator.dart';
import '../../focus/focus_theme.dart';
import '../../focus/input_mode_tracker.dart'; import '../../focus/input_mode_tracker.dart';
import '../../focus/key_event_utils.dart'; import '../../focus/key_event_utils.dart';
import '../../services/gamepad_service.dart'; import '../../services/gamepad_service.dart';
@@ -1310,7 +1311,8 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
int? _originalIndex; // Original position before move (for cancel) int? _originalIndex; // Original position before move (for cancel)
List<PlexLibrary>? _originalOrder; // Original order before move (for cancel) List<PlexLibrary>? _originalOrder; // Original order before move (for cancel)
final FocusNode _listFocusNode = FocusNode(); final FocusNode _listFocusNode = FocusNode();
final Map<int, GlobalKey> _tileKeys = {}; // For dialog mode scroll-into-view final ScrollController _dialogScrollController = ScrollController();
bool _backKeyDownSeen = false;
@override @override
void initState() { void initState() {
@@ -1321,24 +1323,54 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
@override @override
void dispose() { void dispose() {
_listFocusNode.dispose(); _listFocusNode.dispose();
_dialogScrollController.dispose();
super.dispose(); super.dispose();
} }
void _ensureFocusedVisible() { void _ensureFocusedVisible() {
if (!widget.isDialog) return; if (!widget.isDialog) return;
WidgetsBinding.instance.addPostFrameCallback((_) { if (!_dialogScrollController.hasClients) return;
if (!mounted) return;
final key = _tileKeys[_focusedIndex]; const double itemHeight = 72.0; // Material ListTile with subtitle
final context = key?.currentContext; const double listTopPadding = 8.0;
if (context != null) { final double targetTop = listTopPadding + (_focusedIndex * itemHeight);
Scrollable.ensureVisible(context, alignment: 0.25, duration: const Duration(milliseconds: 200)); final double targetBottom = targetTop + itemHeight;
}
}); final double viewportTop = _dialogScrollController.offset;
final double viewportHeight = _dialogScrollController.position.viewportDimension;
final double viewportBottom = viewportTop + viewportHeight;
// Already fully visible — skip
if (targetTop >= viewportTop && targetBottom <= viewportBottom) return;
// Place item at ~25% from top of viewport
final double destination = (targetTop - viewportHeight * 0.25)
.clamp(0.0, _dialogScrollController.position.maxScrollExtent);
_dialogScrollController.animateTo(
destination,
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
);
} }
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
final key = event.logicalKey; final key = event.logicalKey;
// Track back key down/up pairing. If focus was elsewhere during KeyDown
// (e.g., on a bottom sheet) and returns here before KeyUp, we get a stray
// KeyUp that would incorrectly pop the dialog. Consume it instead.
if (key.isBackKey) {
if (event is KeyDownEvent) {
_backKeyDownSeen = true;
} else if (event is KeyUpEvent && !_backKeyDownSeen) {
return KeyEventResult.handled;
}
if (event is KeyUpEvent) {
_backKeyDownSeen = false;
}
}
final backResult = handleBackKeyAction(event, () { final backResult = handleBackKeyAction(event, () {
if (_movingIndex != null) { if (_movingIndex != null) {
// Cancel move - restore original position // Cancel move - restore original position
@@ -1359,7 +1391,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
return backResult; return backResult;
} }
if (event is! KeyDownEvent) return KeyEventResult.ignored; if (!event.isActionable) return KeyEventResult.ignored;
if (_movingIndex != null) { if (_movingIndex != null) {
// Move mode - arrows reorder the item // Move mode - arrows reorder the item
@@ -1455,9 +1487,6 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
} }
final library = _tempLibraries.removeAt(oldIndex); final library = _tempLibraries.removeAt(oldIndex);
_tempLibraries.insert(newIndex, library); _tempLibraries.insert(newIndex, library);
if (widget.isDialog) {
_tileKeys.clear();
}
}); });
// Apply immediately // Apply immediately
widget.onReorder(_tempLibraries); widget.onReorder(_tempLibraries);
@@ -1533,25 +1562,29 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
if (widget.isDialog) { if (widget.isDialog) {
return Dialog( return Dialog(
child: Scaffold( child: PopScope(
appBar: AppBar( canPop: false, // Prevent system back from double-popping; handled by _handleKeyEvent
title: Row( onPopInvokedWithResult: (didPop, result) {},
children: [ child: Scaffold(
const AppIcon(Symbols.edit_rounded, fill: 1), appBar: AppBar(
const SizedBox(width: 12), title: Row(
Text(t.libraries.manageLibraries), children: [
const AppIcon(Symbols.edit_rounded, fill: 1),
const SizedBox(width: 12),
Text(t.libraries.manageLibraries),
],
),
automaticallyImplyLeading: false,
actions: [
IconButton(icon: const AppIcon(Symbols.close_rounded, fill: 1), onPressed: () => Navigator.pop(context)),
], ],
), ),
automaticallyImplyLeading: false, body: Focus(
actions: [ focusNode: _listFocusNode,
IconButton(icon: const AppIcon(Symbols.close_rounded, fill: 1), onPressed: () => Navigator.pop(context)), autofocus: InputModeTracker.isKeyboardMode(context),
], onKeyEvent: _handleKeyEvent,
), child: _buildFlatLibraryListDialog(hiddenLibraryKeys),
body: Focus( ),
focusNode: _listFocusNode,
autofocus: InputModeTracker.isKeyboardMode(context),
onKeyEvent: _handleKeyEvent,
child: _buildFlatLibraryListDialog(hiddenLibraryKeys),
), ),
), ),
); );
@@ -1610,6 +1643,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
final isKeyboardMode = InputModeTracker.isKeyboardMode(context); final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
return ReorderableListView.builder( return ReorderableListView.builder(
scrollController: _dialogScrollController,
onReorder: _reorderLibraries, onReorder: _reorderLibraries,
itemCount: _tempLibraries.length, itemCount: _tempLibraries.length,
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
@@ -1620,8 +1654,6 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
final isFocused = isKeyboardMode && index == _focusedIndex; final isFocused = isKeyboardMode && index == _focusedIndex;
final isMoving = index == _movingIndex; final isMoving = index == _movingIndex;
_tileKeys.putIfAbsent(index, () => GlobalKey());
return _buildLibraryTile( return _buildLibraryTile(
library, library,
index, index,
@@ -1630,7 +1662,6 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
isFocused: isFocused, isFocused: isFocused,
isMoving: isMoving, isMoving: isMoving,
focusedColumn: isFocused ? _focusedColumn : null, focusedColumn: isFocused ? _focusedColumn : null,
tileKey: _tileKeys[index],
); );
}, },
); );
@@ -1674,7 +1705,6 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
bool isFocused = false, bool isFocused = false,
bool isMoving = false, bool isMoving = false,
int? focusedColumn, int? focusedColumn,
Key? tileKey,
}) { }) {
final isHidden = hiddenLibraryKeys.contains(library.globalKey); final isHidden = hiddenLibraryKeys.contains(library.globalKey);
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
@@ -1693,7 +1723,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
final isOptionsButtonFocused = isFocused && focusedColumn == 2; final isOptionsButtonFocused = isFocused && focusedColumn == 2;
return Opacity( return Opacity(
key: tileKey ?? ValueKey(library.globalKey), key: ValueKey(library.globalKey),
opacity: isHidden ? 0.5 : 1.0, opacity: isHidden ? 0.5 : 1.0,
child: Container( child: Container(
decoration: BoxDecoration(color: tileColor), decoration: BoxDecoration(color: tileColor),
@@ -1727,9 +1757,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
decoration: isVisibilityButtonFocused decoration: FocusTheme.focusBackgroundDecoration(isFocused: isVisibilityButtonFocused, borderRadius: 20),
? BoxDecoration(color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20))
: null,
child: IconButton( child: IconButton(
icon: AppIcon(isHidden ? Symbols.visibility_off_rounded : Symbols.visibility_rounded, fill: 1), icon: AppIcon(isHidden ? Symbols.visibility_off_rounded : Symbols.visibility_rounded, fill: 1),
tooltip: isHidden ? t.libraries.showLibrary : t.libraries.hideLibrary, tooltip: isHidden ? t.libraries.showLibrary : t.libraries.hideLibrary,
@@ -1737,9 +1765,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet> {
), ),
), ),
Container( Container(
decoration: isOptionsButtonFocused decoration: FocusTheme.focusBackgroundDecoration(isFocused: isOptionsButtonFocused, borderRadius: 20),
? BoxDecoration(color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(20))
: null,
child: IconButton( child: IconButton(
icon: const AppIcon(Symbols.more_vert_rounded, fill: 1), icon: const AppIcon(Symbols.more_vert_rounded, fill: 1),
tooltip: t.libraries.libraryOptions, tooltip: t.libraries.libraryOptions,