feat: add D-pad navigation for phones

This commit is contained in:
randogoth 2026-10-13 11:47:01 +03:00
parent 414f8e8484
commit b3df7fba9f
6 changed files with 380 additions and 31 deletions

View file

@ -49,6 +49,8 @@ class _NoteListScreenState extends State<NoteListScreen> {
Timer? _debounce;
int? _highlighted; // Note picked with the arrow keys, opened by Enter.
final _highlightedKey = GlobalKey();
final _listFocus = FocusNode();
final _menuKey = GlobalKey<PopupMenuButtonState<String>>();
@override
void initState() {
@ -73,6 +75,7 @@ class _NoteListScreenState extends State<NoteListScreen> {
void dispose() {
_debounce?.cancel();
searchController.dispose();
_listFocus.dispose();
super.dispose();
}
@ -387,6 +390,38 @@ Happy note taking! ✨
});
}
void _enterList() {
if (filteredNotes.isEmpty) return;
_listFocus.requestFocus();
if (_highlighted == null) _moveHighlight(1);
}
void _leaveList() {
setState(() => _highlighted = null);
searchFocusNode.requestFocus();
}
void _listUp() {
if ((_highlighted ?? 0) == 0) {
_leaveList();
} else {
_moveHighlight(-1);
}
}
/// Acts like tapping the blaster: clears the search, or opens the menu.
void _pressBlaster() {
if (searchFocusNode.hasFocus && searchController.text.isNotEmpty) {
searchController.clear();
return;
}
openMenuWithFocus(_menuKey);
}
void _openHighlighted() {
if (_highlighted != null) _openNote(filteredNotes[_highlighted!]);
}
void _focusSearch() {
searchFocusNode.requestFocus();
searchController.selection = TextSelection(
@ -415,6 +450,7 @@ Happy note taking! ✨
print("❌ Failed to open external editor: $e");
}
} else {
final fromList = _listFocus.hasFocus;
await Navigator.push(
context,
MaterialPageRoute(
@ -425,9 +461,15 @@ Happy note taking! ✨
),
),
);
_loadNotes(); // ✅ Reload notes after returning
setState(() {}); // ✅ Force UI refresh
searchFocusNode.requestFocus();
await _loadNotes(); // ✅ Reload notes after returning
if (!mounted) return;
final index = filteredNotes.indexWhere((n) => n.path == noteFile.path);
if (fromList && index != -1) {
setState(() => _highlighted = index);
_listFocus.requestFocus();
} else {
searchFocusNode.requestFocus();
}
}
}
@ -751,12 +793,19 @@ Happy note taking! ✨
title: Padding(
padding: EdgeInsets.symmetric(horizontal: 10),
child: CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.arrowDown): () =>
_moveHighlight(1),
const SingleActivator(LogicalKeyboardKey.arrowUp): () =>
_moveHighlight(-1),
},
bindings: dpadNavigation
? {
const SingleActivator(LogicalKeyboardKey.arrowDown):
_enterList,
const SingleActivator(LogicalKeyboardKey.arrowRight):
_pressBlaster,
}
: {
const SingleActivator(LogicalKeyboardKey.arrowDown): () =>
_moveHighlight(1),
const SingleActivator(LogicalKeyboardKey.arrowUp): () =>
_moveHighlight(-1),
},
child: TextField(
focusNode: searchFocusNode,
enableInteractiveSelection: true,
@ -794,6 +843,7 @@ Happy note taking! ✨
}
},
child: PopupMenuButton<String>(
key: _menuKey,
enabled: searchController.text.isEmpty,
icon: SvgPicture.asset(
"assets/blaster.svg",
@ -805,6 +855,12 @@ Happy note taking! ✨
case 'Toggle Tags':
widget.toggleTags();
break;
case 'Include Subdirectories':
_toggleIncludeSubdirectories(!_includeSubdirectories);
break;
case 'Include File Content':
_toggleIncludeFileContent(!_includeFileContent);
break;
case 'Toggle Theme':
setState(() {
widget.toggleTheme(!widget.isDarkMode);
@ -857,10 +913,6 @@ Happy note taking! ✨
child: ListTile(
leading: Icon(_includeSubdirectories ? Icons.file_copy_outlined : Icons.folder_copy_outlined),
title: Text(_includeSubdirectories ? 'Exclude Subdirectories' : 'Search Subdirectories'),
onTap: () {
_toggleIncludeSubdirectories(!_includeSubdirectories);
Navigator.pop(context); // Close menu after toggle
},
),
),
PopupMenuItem<String>(
@ -872,10 +924,6 @@ Happy note taking! ✨
title: Text(
_includeFileContent ? 'Exclude File Content' : 'Search File Content',
),
onTap: () {
_toggleIncludeFileContent(!_includeFileContent);
Navigator.pop(context);
},
),
),
PopupMenuItem<String>(
@ -932,7 +980,20 @@ Happy note taking! ✨
),
body: Padding(
padding: EdgeInsets.only(left: 10),
child: NotificationListener<ScrollNotification>(
child: CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.arrowDown): () =>
_moveHighlight(1),
const SingleActivator(LogicalKeyboardKey.arrowUp): _listUp,
const SingleActivator(LogicalKeyboardKey.arrowLeft): _leaveList,
const SingleActivator(LogicalKeyboardKey.arrowRight):
_pressBlaster,
const SingleActivator(LogicalKeyboardKey.select): _openHighlighted,
const SingleActivator(LogicalKeyboardKey.enter): _openHighlighted,
},
child: Focus(
focusNode: _listFocus,
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (!_isLoadingMore && scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 100) {
_loadMoreNotes(); // ✅ Lazy load when user scrolls
@ -1007,6 +1068,8 @@ Happy note taking! ✨
},
),
),
),
),
),
),
);