fix: manage libraries misc
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user