From b2c5d64fc519a46ef5ea1da786803caadb1d4d3a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 10 Aug 2025 16:57:11 +0800 Subject: [PATCH] :sparkles: Keyboard navigation basis --- lib/main.dart | 51 +++++++++-------- lib/widgets/keyboard_navigation.dart | 86 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 lib/widgets/keyboard_navigation.dart diff --git a/lib/main.dart b/lib/main.dart index 1c7abb5..19a257a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,6 +28,7 @@ import 'package:relative_time/relative_time.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:island/widgets/keyboard_navigation.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:flutter_langdetect/flutter_langdetect.dart' as langdetect; @@ -244,30 +245,32 @@ class IslandApp extends HookConsumerWidget { final router = ref.watch(routerProvider); - return MaterialApp.router( - theme: theme?.light, - darkTheme: theme?.dark, - themeMode: ThemeMode.system, - routerConfig: router, - supportedLocales: context.supportedLocales, - localizationsDelegates: [ - ...context.localizationDelegates, - CroppyLocalizations.delegate, - RelativeTimeLocalizations.delegate, - ], - locale: context.locale, - builder: (context, child) { - return Overlay( - key: globalOverlay, - initialEntries: [ - OverlayEntry( - builder: - (_) => - WindowScaffold(child: child ?? const SizedBox.shrink()), - ), - ], - ); - }, + return KeyboardNavigation( + child: MaterialApp.router( + theme: theme?.light, + darkTheme: theme?.dark, + themeMode: ThemeMode.system, + routerConfig: router, + supportedLocales: context.supportedLocales, + localizationsDelegates: [ + ...context.localizationDelegates, + CroppyLocalizations.delegate, + RelativeTimeLocalizations.delegate, + ], + locale: context.locale, + builder: (context, child) { + return Overlay( + key: globalOverlay, + initialEntries: [ + OverlayEntry( + builder: + (_) => + WindowScaffold(child: child ?? const SizedBox.shrink()), + ), + ], + ); + }, + ), ); } } diff --git a/lib/widgets/keyboard_navigation.dart b/lib/widgets/keyboard_navigation.dart new file mode 100644 index 0000000..508a80d --- /dev/null +++ b/lib/widgets/keyboard_navigation.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +enum VimMode { normal, insert } + +class KeyboardNavigation extends StatefulWidget { + const KeyboardNavigation({super.key, required this.child}); + + final Widget child; + + @override + State createState() => _KeyboardNavigationState(); +} + +class _KeyboardNavigationState extends State { + VimMode _mode = VimMode.normal; + final FocusScopeNode _focusScopeNode = FocusScopeNode(); + + @override + void dispose() { + _focusScopeNode.dispose(); + super.dispose(); + } + + KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { + if (event is! KeyDownEvent && event is! KeyRepeatEvent) { + return KeyEventResult.ignored; + } + + if (_mode == VimMode.normal) { + if (event.logicalKey == LogicalKeyboardKey.keyJ) { + node.focusInDirection(TraversalDirection.down); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyK) { + node.focusInDirection(TraversalDirection.up); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyH) { + final focusNode = FocusManager.instance.primaryFocus; + if (focusNode != null) { + final scrollable = Scrollable.of(focusNode.context!); + if (scrollable.position.axis == Axis.horizontal) { + scrollable.position.moveTo(scrollable.position.pixels - 50); + return KeyEventResult.handled; + } + } + node.focusInDirection(TraversalDirection.left); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyL) { + final focusNode = FocusManager.instance.primaryFocus; + if (focusNode != null) { + final scrollable = Scrollable.of(focusNode.context!); + if (scrollable.position.axis == Axis.horizontal) { + scrollable.position.moveTo(scrollable.position.pixels + 50); + return KeyEventResult.handled; + } + } + node.focusInDirection(TraversalDirection.right); + return KeyEventResult.handled; + } else if (event.logicalKey == LogicalKeyboardKey.keyI) { + setState(() { + _mode = VimMode.insert; + }); + return KeyEventResult.handled; + } + } else if (_mode == VimMode.insert) { + if (event.logicalKey == LogicalKeyboardKey.escape) { + setState(() { + _mode = VimMode.normal; + }); + // Unfocus the current widget to prevent typing + node.unfocus(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + } + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: _focusScopeNode, + onKeyEvent: _handleKeyEvent, + child: widget.child, + ); + } +}