✨ Keyboard navigation basis
This commit is contained in:
		@@ -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()),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								lib/widgets/keyboard_navigation.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/widgets/keyboard_navigation.dart
									
									
									
									
									
										Normal file
									
								
							@@ -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<KeyboardNavigation> createState() => _KeyboardNavigationState();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class _KeyboardNavigationState extends State<KeyboardNavigation> {
 | 
			
		||||
  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,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user