diff --git a/lib/util/adaptive_layout/adaptive_layout.dart b/lib/util/adaptive_layout/adaptive_layout.dart index b073506..7e0eb83 100644 --- a/lib/util/adaptive_layout/adaptive_layout.dart +++ b/lib/util/adaptive_layout/adaptive_layout.dart @@ -9,6 +9,7 @@ import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/screens/home_screen.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart'; import 'package:fladder/util/debug_banner.dart'; +import 'package:fladder/util/input_detector.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/poster_defaults.dart'; import 'package:fladder/util/resolution_checker.dart'; @@ -188,11 +189,6 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { final selectedViewSize = selectAvailableOrSmaller(viewSize, acceptedViewSizes, ViewSize.values); final selectedLayoutMode = selectAvailableOrSmaller(layoutMode, acceptedLayouts, LayoutMode.values); - final input = htpcMode - ? InputDevice.dPad - : (isDesktop || kIsWeb) - ? InputDevice.pointer - : InputDevice.touch; final posterDefaults = const PosterDefaults(size: 350, ratio: 0.55); @@ -200,7 +196,7 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { AdaptiveLayoutModel( viewSize: selectedViewSize, layoutMode: selectedLayoutMode, - inputDevice: input, + inputDevice: InputDevice.pointer, platform: currentPlatform, isDesktop: isDesktop, sideBarWidth: 0, @@ -213,33 +209,38 @@ class _AdaptiveLayoutBuilderState extends ConsumerState { return ValueListenableBuilder( valueListenable: isKeyboardOpen, builder: (context, value, child) { - return MediaQuery( - data: mediaQuery.copyWith( - padding: (isDesktop || kIsWeb - ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) - : mediaQuery.padding), - viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, - ), - child: AdaptiveLayout( - data: currentLayout.copyWith( - viewSize: selectedViewSize, - layoutMode: selectedLayoutMode, - inputDevice: input, - platform: currentPlatform, - isDesktop: isDesktop, - controller: scrollControllers, - posterDefaults: posterDefaults, + return InputDetector( + isDesktop: isDesktop, + htpcMode: htpcMode, + child: (input) => MediaQuery( + data: mediaQuery.copyWith( + navigationMode: input == InputDevice.dPad ? NavigationMode.directional : NavigationMode.traditional, + padding: (isDesktop || kIsWeb + ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) + : mediaQuery.padding), + viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, ), - child: Builder( - builder: (context) => isDesktop - ? ResolutionChecker( - child: widget.adaptiveLayout == null - ? DebugBanner(child: widget.child(context)) - : widget.child(context), - ) - : widget.adaptiveLayout == null - ? DebugBanner(child: widget.child(context)) - : widget.child(context), + child: AdaptiveLayout( + data: currentLayout.copyWith( + viewSize: selectedViewSize, + layoutMode: selectedLayoutMode, + inputDevice: input, + platform: currentPlatform, + isDesktop: isDesktop, + controller: scrollControllers, + posterDefaults: posterDefaults, + ), + child: Builder( + builder: (context) => isDesktop + ? ResolutionChecker( + child: widget.adaptiveLayout == null + ? DebugBanner(child: widget.child(context)) + : widget.child(context), + ) + : widget.adaptiveLayout == null + ? DebugBanner(child: widget.child(context)) + : widget.child(context), + ), ), ), ); diff --git a/lib/util/adaptive_layout/adaptive_layout_model.dart b/lib/util/adaptive_layout/adaptive_layout_model.dart index 3c2e593..f5473b1 100644 --- a/lib/util/adaptive_layout/adaptive_layout_model.dart +++ b/lib/util/adaptive_layout/adaptive_layout_model.dart @@ -86,7 +86,10 @@ class AdaptiveLayoutModel { @override bool operator ==(covariant AdaptiveLayoutModel other) { if (identical(this, other)) return true; - return other.viewSize == viewSize && other.layoutMode == layoutMode && other.sideBarWidth == sideBarWidth; + return other.viewSize == viewSize && + other.layoutMode == layoutMode && + other.sideBarWidth == sideBarWidth && + other.inputDevice == inputDevice; } @override diff --git a/lib/util/focus_provider.dart b/lib/util/focus_provider.dart index 17ecc36..971e83f 100644 --- a/lib/util/focus_provider.dart +++ b/lib/util/focus_provider.dart @@ -146,6 +146,8 @@ class FocusButtonState extends State { child: Focus( focusNode: focusNode, autofocus: widget.autoFocus, + canRequestFocus: widget.onTap != null || widget.onLongPress != null || widget.onSecondaryTapDown != null, + skipTraversal: widget.onTap == null && widget.onLongPress == null && widget.onSecondaryTapDown != null, onFocusChange: (value) { widget.onFocusChanged?.call(value); if (value) { diff --git a/lib/util/input_detector.dart b/lib/util/input_detector.dart new file mode 100644 index 0000000..70e0849 --- /dev/null +++ b/lib/util/input_detector.dart @@ -0,0 +1,106 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; + +class InputDetector extends StatefulWidget { + final bool isDesktop; + final bool htpcMode; + final Widget Function(InputDevice input) child; + + const InputDetector({ + super.key, + required this.isDesktop, + required this.htpcMode, + required this.child, + }); + + @override + State createState() => _InputDetectorState(); +} + +class _InputDetectorState extends State { + late InputDevice _currentInput = widget.htpcMode + ? InputDevice.dPad + : (widget.isDesktop || kIsWeb) + ? InputDevice.pointer + : InputDevice.touch; + + @override + void initState() { + super.initState(); + _startListeningToKeyboard(); + } + + void _startListeningToKeyboard() { + ServicesBinding.instance.keyboard.addHandler(_handleKeyPress); + } + + @override + void dispose() { + ServicesBinding.instance.keyboard.removeHandler(_handleKeyPress); + super.dispose(); + } + + bool _handleKeyPress(KeyEvent event) { + if (event is KeyDownEvent) { + if (_isEditableTextFocused() && + (event.logicalKey == LogicalKeyboardKey.arrowUp || + event.logicalKey == LogicalKeyboardKey.arrowDown || + event.logicalKey == LogicalKeyboardKey.arrowLeft || + event.logicalKey == LogicalKeyboardKey.arrowRight)) { + return false; + } + + if (event.logicalKey == LogicalKeyboardKey.arrowUp || + event.logicalKey == LogicalKeyboardKey.arrowDown || + event.logicalKey == LogicalKeyboardKey.arrowLeft || + event.logicalKey == LogicalKeyboardKey.arrowRight || + event.logicalKey == LogicalKeyboardKey.select) { + _updateInputDevice(InputDevice.dPad); + } + } + return false; + } + + bool _isEditableTextFocused() { + final focus = FocusManager.instance.primaryFocus; + if (focus == null) return false; + final ctx = focus.context; + if (ctx == null) return false; + + if (ctx.widget is EditableText) return true; + return ctx.findAncestorWidgetOfExactType() != null; + } + + void _handlePointerEvent(PointerEvent event) { + if (event is PointerDownEvent) { + if (event.kind == PointerDeviceKind.touch) { + _updateInputDevice(InputDevice.touch); + } else if (event.kind == PointerDeviceKind.mouse) { + _updateInputDevice(InputDevice.pointer); + } + } + } + + void _updateInputDevice(InputDevice device) { + if (_currentInput != device) { + setState(() { + _currentInput = device; + }); + } + } + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: _handlePointerEvent, + behavior: HitTestBehavior.translucent, + child: Builder( + builder: (context) => widget.child(_currentInput), + ), + ); + } +} diff --git a/lib/widgets/media_query_scaler.dart b/lib/widgets/media_query_scaler.dart index 1acd266..77b18ff 100644 --- a/lib/widgets/media_query_scaler.dart +++ b/lib/widgets/media_query_scaler.dart @@ -19,7 +19,6 @@ class MediaQueryScaler extends StatelessWidget { final screenSize = MediaQuery.sizeOf(context) * scale; final scaledMedia = mediaQuery.copyWith( - navigationMode: NavigationMode.directional, size: screenSize, padding: mediaQuery.padding * scale, viewInsets: mediaQuery.viewInsets * scale, diff --git a/lib/widgets/shared/enum_selection.dart b/lib/widgets/shared/enum_selection.dart index 101754f..da8d1d7 100644 --- a/lib/widgets/shared/enum_selection.dart +++ b/lib/widgets/shared/enum_selection.dart @@ -10,7 +10,11 @@ class EnumBox extends StatelessWidget { final String current; final List Function(BuildContext context) itemBuilder; - const EnumBox({required this.current, required this.itemBuilder, super.key}); + const EnumBox({ + required this.current, + required this.itemBuilder, + super.key, + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/shared/modal_bottom_sheet.dart b/lib/widgets/shared/modal_bottom_sheet.dart index 8733092..32b0a14 100644 --- a/lib/widgets/shared/modal_bottom_sheet.dart +++ b/lib/widgets/shared/modal_bottom_sheet.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/theme.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; Future showBottomSheetPill({ @@ -44,7 +45,9 @@ Future showBottomSheetPill({ height: 8, width: 35, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSurface, + color: AdaptiveLayout.inputDeviceOf(context) == InputDevice.touch + ? Theme.of(context).colorScheme.onSurface + : Colors.transparent, borderRadius: FladderTheme.largeShape.borderRadius, ), ),