fix: Improve keyboard input handling (#102)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-11-02 08:40:39 +01:00 committed by GitHub
parent 2038847552
commit 76ac1aaa5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 584 additions and 571 deletions

View file

@ -16,6 +16,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_settings.dart';
import 'package:fladder/screens/shared/default_titlebar.dart'; import 'package:fladder/screens/shared/default_titlebar.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/throttler.dart'; import 'package:fladder/util/throttler.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart';
@ -75,12 +76,10 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
viewController.visibilityChanged.addListener(() { viewController.visibilityChanged.addListener(() {
toggleControls(value: viewController.controlsVisible); toggleControls(value: viewController.controlsVisible);
}); });
ServicesBinding.instance.keyboard.addHandler(_onKey);
} }
@override @override
void dispose() { void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
WakelockPlus.disable(); WakelockPlus.disable();
ScreenBrightness().resetScreenBrightness(); ScreenBrightness().resetScreenBrightness();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []);
@ -134,6 +133,8 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
return MediaQuery.removePadding( return MediaQuery.removePadding(
context: context, context: context,
child: InputHandler(
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: Stack( child: Stack(
children: [ children: [
IgnorePointer( IgnorePointer(
@ -359,6 +360,7 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
) )
], ],
), ),
),
); );
} }

View file

@ -16,6 +16,7 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/input_fields.dart'; import 'package:fladder/screens/shared/input_fields.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/throttler.dart'; import 'package:fladder/util/throttler.dart';
@ -99,7 +100,7 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
timerController.playPause(); timerController.playPause();
return true; return true;
} }
if (value.logicalKey == LogicalKeyboardKey.keyF) { if (value.logicalKey == LogicalKeyboardKey.space) {
widget.toggleOverlay?.call(null); widget.toggleOverlay?.call(null);
return true; return true;
} }
@ -117,12 +118,10 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
timerController.reset(); timerController.reset();
}, },
); );
ServicesBinding.instance.keyboard.addHandler(_onKey);
} }
@override @override
void onWindowMinimize() { void onWindowMinimize() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
timerController.cancel(); timerController.cancel();
super.onWindowMinimize(); super.onWindowMinimize();
} }
@ -145,9 +144,9 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
final padding = MediaQuery.of(context).padding; final padding = MediaQuery.of(context).padding;
return PopScope( return PopScope(
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async => await WakelockPlus.disable(),
await WakelockPlus.disable(); child: InputHandler(
}, onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: Stack( child: Stack(
children: [ children: [
Align( Align(
@ -217,8 +216,10 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
children: [ children: [
Text( Text(
"${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ", "${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ",
style: style: Theme.of(context)
Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), .textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
), ),
if (widget.loadingMoreItems) if (widget.loadingMoreItems)
const SizedBox.square( const SizedBox.square(
@ -254,10 +255,11 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
IntInputField( IntInputField(
controller: controller: TextEditingController(
TextEditingController(text: (widget.currentIndex + 1).toString()), text: (widget.currentIndex + 1).toString()),
onSubmitted: (value) { onSubmitted: (value) {
final position = ((value ?? 0) - 1).clamp(0, widget.itemCount - 1); final position =
((value ?? 0) - 1).clamp(0, widget.itemCount - 1);
widget.pageController.jumpToPage(position); widget.pageController.jumpToPage(position);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
@ -299,7 +301,8 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)), padding:
const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)),
child: Row( child: Row(
children: [ children: [
ElevatedIconButton( ElevatedIconButton(
@ -332,6 +335,7 @@ class _PhotoViewerControllsState extends ConsumerState<PhotoViewerControls> with
), ),
], ],
), ),
),
); );
} }

View file

@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
class PassCodeInput extends ConsumerStatefulWidget { class PassCodeInput extends ConsumerStatefulWidget {
@ -19,18 +20,6 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
final passCodeLength = 4; final passCodeLength = 4;
var currentPasscode = ""; var currentPasscode = "";
@override
void initState() {
super.initState();
ServicesBinding.instance.keyboard.addHandler(_onKey);
}
@override
void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
super.dispose();
}
bool _onKey(KeyEvent value) { bool _onKey(KeyEvent value) {
if (value is KeyDownEvent) { if (value is KeyDownEvent) {
final keyInt = int.tryParse(value.logicalKey.keyLabel); final keyInt = int.tryParse(value.logicalKey.keyLabel);
@ -48,7 +37,9 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return InputHandler(
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: AlertDialog(
scrollable: true, scrollable: true,
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -103,6 +94,7 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
) )
].addPadding(const EdgeInsets.symmetric(vertical: 8)), ].addPadding(const EdgeInsets.symmetric(vertical: 8)),
), ),
),
); );
} }

View file

@ -5,6 +5,7 @@ import 'package:async/async.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
class VideoPlayerSeekIndicator extends ConsumerStatefulWidget { class VideoPlayerSeekIndicator extends ConsumerStatefulWidget {
@ -20,18 +21,6 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
bool visible = false; bool visible = false;
int seekPosition = 0; int seekPosition = 0;
@override
void initState() {
super.initState();
ServicesBinding.instance.keyboard.addHandler(_onKey);
}
@override
void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
super.dispose();
}
void onSeekEnd() { void onSeekEnd() {
setState(() { setState(() {
visible = false; visible = false;
@ -46,7 +35,7 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
void onSeekStart(int value) { void onSeekStart(int value) {
if (timer == null) { if (timer == null) {
timer = RestartableTimer(const Duration(seconds: 2), () => onSeekEnd()); timer = RestartableTimer(const Duration(milliseconds: 500), () => onSeekEnd());
setState(() { setState(() {
seekPosition = 0; seekPosition = 0;
}); });
@ -85,7 +74,10 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IgnorePointer( return InputHandler(
autoFocus: true,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: IgnorePointer(
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
opacity: (visible && seekPosition != 0) ? 1 : 0, opacity: (visible && seekPosition != 0) ? 1 : 0,
@ -112,6 +104,7 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
), ),
), ),
), ),
),
); );
} }

View file

@ -10,10 +10,10 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
class VideoSubtitles extends ConsumerStatefulWidget { class VideoSubtitles extends ConsumerStatefulWidget {
final VideoController controller; final VideoController controller;
final bool overlayed; final bool overLayed;
const VideoSubtitles({ const VideoSubtitles({
required this.controller, required this.controller,
this.overlayed = false, this.overLayed = false,
super.key, super.key,
}); });
@ -56,7 +56,7 @@ class _VideoSubtitlesState extends ConsumerState<VideoSubtitles> {
return SubtitleText( return SubtitleText(
subModel: settings, subModel: settings,
padding: padding, padding: padding,
offset: (widget.overlayed ? 0.5 : settings.verticalOffset), offset: (widget.overLayed ? 0.5 : settings.verticalOffset),
text: text, text: text,
); );
} }

View file

@ -29,6 +29,7 @@ import 'package:fladder/screens/video_player/components/video_subtitles.dart';
import 'package:fladder/screens/video_player/components/video_volume_slider.dart'; import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/util/string_extensions.dart';
@ -49,7 +50,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
); );
final fadeDuration = const Duration(milliseconds: 350); final fadeDuration = const Duration(milliseconds: 350);
final focusNode = FocusNode();
bool showOverlay = true; bool showOverlay = true;
bool wasPlaying = false; bool wasPlaying = false;
@ -79,7 +79,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
} else if (showCreditSkipButton) { } else if (showCreditSkipButton) {
skipCredits(introSkipModel); skipCredits(introSkipModel);
} }
focusNode.requestFocus();
} }
if (value.logicalKey == LogicalKeyboardKey.escape) { if (value.logicalKey == LogicalKeyboardKey.escape) {
disableFullscreen(); disableFullscreen();
@ -103,27 +102,14 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
return false; return false;
} }
@override
void initState() {
super.initState();
ServicesBinding.instance.keyboard.addHandler(_onKey);
timer.reset();
}
@override
void dispose() {
ServicesBinding.instance.keyboard.removeHandler(_onKey);
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel)); final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel));
final player = ref.watch(videoPlayerProvider.select((value) => value.controller)); final player = ref.watch(videoPlayerProvider.select((value) => value.controller));
if (AdaptiveLayout.of(context).isDesktop) { return InputHandler(
focusNode.requestFocus(); autoFocus: false,
} onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
return Listener( child: Listener(
onPointerSignal: (event) => resetTimer(), onPointerSignal: (event) => resetTimer(),
child: PopScope( child: PopScope(
canPop: false, canPop: false,
@ -145,7 +131,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
VideoSubtitles( VideoSubtitles(
key: const Key('subtitles'), key: const Key('subtitles'),
controller: player, controller: player,
overlayed: showOverlay, overLayed: showOverlay,
), ),
if (AdaptiveLayout.of(context).isDesktop) if (AdaptiveLayout.of(context).isDesktop)
Consumer(builder: (context, ref, child) { Consumer(builder: (context, ref, child) {
@ -206,6 +192,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
), ),
), ),
), ),
),
); );
} }

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
class InputHandler extends StatefulWidget {
final bool autoFocus;
final KeyEventResult Function(FocusNode node, KeyEvent event)? onKeyEvent;
final Widget child;
const InputHandler({
required this.child,
this.autoFocus = true,
this.onKeyEvent,
super.key,
});
@override
State<InputHandler> createState() => _InputHandlerState();
}
class _InputHandlerState extends State<InputHandler> {
final focusNode = FocusNode();
@override
Widget build(BuildContext context) {
return Focus(
autofocus: widget.autoFocus,
focusNode: focusNode,
onFocusChange: (value) {
if (!focusNode.hasFocus) {
focusNode.requestFocus();
}
},
onKeyEvent: widget.onKeyEvent,
child: widget.child,
);
}
}