feat: add keyboard shortcuts for notes, the note list and the app
This commit is contained in:
parent
ed2de5e67f
commit
722390ecec
9 changed files with 532 additions and 60 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
|
@ -10,6 +11,7 @@ import 'package:permission_handler/permission_handler.dart';
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'note_editor.dart';
|
||||
import 'shortcuts.dart';
|
||||
import 'read_only.dart';
|
||||
import 'texts.dart';
|
||||
|
||||
|
|
@ -45,6 +47,8 @@ class _NoteListScreenState extends State<NoteListScreen> {
|
|||
bool _includeFileContent = false;
|
||||
bool _includeSubdirectories = true;
|
||||
Timer? _debounce;
|
||||
int? _highlighted; // Note picked with the arrow keys, opened by Enter.
|
||||
final _highlightedKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -321,6 +325,7 @@ Happy note taking! ✨
|
|||
final query = searchController.text.trim().toLowerCase();
|
||||
|
||||
setState(() {
|
||||
_highlighted = null;
|
||||
if (query.isEmpty) {
|
||||
filteredNotes = List.from(notes);
|
||||
return;
|
||||
|
|
@ -361,6 +366,71 @@ Happy note taking! ✨
|
|||
return [if (text.isNotEmpty) text, ...tags];
|
||||
}
|
||||
|
||||
void _moveHighlight(int step) {
|
||||
if (filteredNotes.isEmpty) return;
|
||||
final next = (_highlighted ?? -1) + step;
|
||||
setState(() {
|
||||
// Moving up past the first note clears the highlight, so Enter creates a note again.
|
||||
_highlighted = next < 0 ? null : next.clamp(0, filteredNotes.length - 1);
|
||||
if (_highlighted != null && _highlighted! >= _visibleLimit) {
|
||||
_visibleLimit = _highlighted! + 1;
|
||||
}
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final context = _highlightedKey.currentContext;
|
||||
if (context != null) {
|
||||
Scrollable.ensureVisible(context,
|
||||
alignmentPolicy: step > 0
|
||||
? ScrollPositionAlignmentPolicy.keepVisibleAtEnd
|
||||
: ScrollPositionAlignmentPolicy.keepVisibleAtStart);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _focusSearch() {
|
||||
searchFocusNode.requestFocus();
|
||||
searchController.selection = TextSelection(
|
||||
baseOffset: 0, extentOffset: searchController.text.length);
|
||||
}
|
||||
|
||||
Future<void> _openNote(File noteFile) async {
|
||||
if (_useExternalEditor) {
|
||||
try {
|
||||
if (Platform.isLinux) {
|
||||
final result = await Process.run('which', ['xdg-open']);
|
||||
if (result.stdout.toString().trim().isNotEmpty) {
|
||||
await Process.run('xdg-open', [noteFile.path]);
|
||||
} else {
|
||||
print("❌ `xdg-open` is not available.");
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
await _requestStoragePermission();
|
||||
final result = await OpenFilex.open(
|
||||
noteFile.path,
|
||||
type: "text/markdown",
|
||||
);
|
||||
print("✅ OpenFilex result: ${result.type}, message: ${result.message}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Failed to open external editor: $e");
|
||||
}
|
||||
} else {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: noteFile,
|
||||
notesDirectoryPath: notesDirectoryPath,
|
||||
searchTerms: _noteSearchTerms(),
|
||||
),
|
||||
),
|
||||
);
|
||||
_loadNotes(); // ✅ Reload notes after returning
|
||||
setState(() {}); // ✅ Force UI refresh
|
||||
searchFocusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void _loadMoreNotes() {
|
||||
if (_isLoadingMore || _visibleNotes.length >= filteredNotes.length) return;
|
||||
|
||||
|
|
@ -463,7 +533,6 @@ Happy note taking! ✨
|
|||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: newNote,
|
||||
isDarkMode: widget.isDarkMode,
|
||||
notesDirectoryPath: notesDirectoryPath,
|
||||
),
|
||||
),
|
||||
|
|
@ -665,7 +734,6 @@ Happy note taking! ✨
|
|||
builder: (context) => ReadOnlyPage(
|
||||
title: title,
|
||||
content: content,
|
||||
isDarkMode: widget.isDarkMode,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -673,11 +741,23 @@ Happy note taking! ✨
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return CallbackShortcuts(
|
||||
bindings: {
|
||||
primaryShortcut(LogicalKeyboardKey.keyL): _focusSearch,
|
||||
primaryShortcut(LogicalKeyboardKey.keyT): widget.toggleTags,
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
child: TextField(
|
||||
child: CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.arrowDown): () =>
|
||||
_moveHighlight(1),
|
||||
const SingleActivator(LogicalKeyboardKey.arrowUp): () =>
|
||||
_moveHighlight(-1),
|
||||
},
|
||||
child: TextField(
|
||||
focusNode: searchFocusNode,
|
||||
enableInteractiveSelection: true,
|
||||
autocorrect: false,
|
||||
|
|
@ -689,15 +769,20 @@ Happy note taking! ✨
|
|||
border: InputBorder.none,
|
||||
),
|
||||
style: GoogleFonts.ibmPlexMono(color: Colors.grey[300]),
|
||||
// Keep focus after Enter so the list's shortcuts stay reachable.
|
||||
onEditingComplete: () {},
|
||||
onSubmitted: (text) {
|
||||
final trimmedText = text.trim();
|
||||
if (trimmedText.isNotEmpty && _isNewNote(trimmedText)) {
|
||||
if (_highlighted != null) {
|
||||
_openNote(filteredNotes[_highlighted!]);
|
||||
} else if (trimmedText.isNotEmpty && _isNewNote(trimmedText)) {
|
||||
_createNewNote(trimmedText);
|
||||
searchController
|
||||
.clear(); // Clears the search field after creation
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
|
|
@ -883,6 +968,10 @@ Happy note taking! ✨
|
|||
),
|
||||
onDismissed: (direction) => _deleteNote(noteFile),
|
||||
child: ListTile(
|
||||
key: index == _highlighted ? _highlightedKey : null,
|
||||
selected: index == _highlighted,
|
||||
selectedColor: Colors.orange,
|
||||
selectedTileColor: Colors.orange.withAlpha(30),
|
||||
title: Text(noteFile.uri.pathSegments.last.replaceAll('.md', '')),
|
||||
subtitle: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
|
|
@ -912,49 +1001,14 @@ Happy note taking! ✨
|
|||
);
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
if (_useExternalEditor) {
|
||||
try {
|
||||
if (Platform.isLinux) {
|
||||
final result = await Process.run('which', ['xdg-open']);
|
||||
if (result.stdout.toString().trim().isNotEmpty) {
|
||||
await Process.run('xdg-open', [noteFile.path]);
|
||||
} else {
|
||||
print("❌ `xdg-open` is not available.");
|
||||
}
|
||||
} else if (Platform.isAndroid) {
|
||||
await _requestStoragePermission();
|
||||
final result = await OpenFilex.open(
|
||||
noteFile.path,
|
||||
type: "text/markdown",
|
||||
);
|
||||
print("✅ OpenFilex result: ${result.type}, message: ${result.message}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("❌ Failed to open external editor: $e");
|
||||
}
|
||||
} else {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NoteEditorScreen(
|
||||
note: noteFile,
|
||||
isDarkMode: widget.isDarkMode,
|
||||
notesDirectoryPath: notesDirectoryPath,
|
||||
searchTerms: _noteSearchTerms(),
|
||||
),
|
||||
),
|
||||
);
|
||||
_loadNotes(); // ✅ Reload notes after returning
|
||||
setState(() {}); // ✅ Force UI refresh
|
||||
}
|
||||
},
|
||||
onTap: () => _openNote(noteFile),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue