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

@ -5,6 +5,7 @@ import 'package:async/async.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/input_handler.dart';
import 'package:fladder/util/localization_helper.dart';
class VideoPlayerSeekIndicator extends ConsumerStatefulWidget {
@ -20,18 +21,6 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
bool visible = false;
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() {
setState(() {
visible = false;
@ -46,7 +35,7 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
void onSeekStart(int value) {
if (timer == null) {
timer = RestartableTimer(const Duration(seconds: 2), () => onSeekEnd());
timer = RestartableTimer(const Duration(milliseconds: 500), () => onSeekEnd());
setState(() {
seekPosition = 0;
});
@ -85,28 +74,32 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: (visible && seekPosition != 0) ? 1 : 0,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.85),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
seekPosition > 0
? "+$seekPosition ${context.localized.seconds(seekPosition)}"
: "$seekPosition ${context.localized.seconds(seekPosition)}",
style: Theme.of(context).textTheme.bodyMedium,
)
],
return InputHandler(
autoFocus: true,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: (visible && seekPosition != 0) ? 1 : 0,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.85),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
seekPosition > 0
? "+$seekPosition ${context.localized.seconds(seekPosition)}"
: "$seekPosition ${context.localized.seconds(seekPosition)}",
style: Theme.of(context).textTheme.bodyMedium,
)
],
),
),
),
),

View file

@ -10,10 +10,10 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart';
class VideoSubtitles extends ConsumerStatefulWidget {
final VideoController controller;
final bool overlayed;
final bool overLayed;
const VideoSubtitles({
required this.controller,
this.overlayed = false,
this.overLayed = false,
super.key,
});
@ -56,7 +56,7 @@ class _VideoSubtitlesState extends ConsumerState<VideoSubtitles> {
return SubtitleText(
subModel: settings,
padding: padding,
offset: (widget.overlayed ? 0.5 : settings.verticalOffset),
offset: (widget.overLayed ? 0.5 : settings.verticalOffset),
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/util/adaptive_layout.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/localization_helper.dart';
import 'package:fladder/util/string_extensions.dart';
@ -49,7 +50,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
);
final fadeDuration = const Duration(milliseconds: 350);
final focusNode = FocusNode();
bool showOverlay = true;
bool wasPlaying = false;
@ -79,7 +79,6 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
} else if (showCreditSkipButton) {
skipCredits(introSkipModel);
}
focusNode.requestFocus();
}
if (value.logicalKey == LogicalKeyboardKey.escape) {
disableFullscreen();
@ -103,105 +102,93 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
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
Widget build(BuildContext context) {
final introSkipModel = ref.watch(playBackModel.select((value) => value?.introSkipModel));
final player = ref.watch(videoPlayerProvider.select((value) => value.controller));
if (AdaptiveLayout.of(context).isDesktop) {
focusNode.requestFocus();
}
return Listener(
onPointerSignal: (event) => resetTimer(),
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) {
closePlayer();
}
},
child: GestureDetector(
onTap: () => toggleOverlay(),
child: MouseRegion(
cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
onEnter: (event) => toggleOverlay(value: true),
onExit: (event) => toggleOverlay(value: false),
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
child: Stack(
children: [
if (player != null)
VideoSubtitles(
key: const Key('subtitles'),
controller: player,
overlayed: showOverlay,
),
if (AdaptiveLayout.of(context).isDesktop)
Consumer(builder: (context, ref, child) {
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
return playButton(playing, buffering);
}),
IgnorePointer(
ignoring: !showOverlay,
child: AnimatedOpacity(
duration: fadeDuration,
opacity: showOverlay ? 1 : 0,
child: Column(
children: [
topButtons(context),
const Spacer(),
bottomButtons(context),
],
return InputHandler(
autoFocus: false,
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
child: Listener(
onPointerSignal: (event) => resetTimer(),
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) {
if (!didPop) {
closePlayer();
}
},
child: GestureDetector(
onTap: () => toggleOverlay(),
child: MouseRegion(
cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
onEnter: (event) => toggleOverlay(value: true),
onExit: (event) => toggleOverlay(value: false),
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
child: Stack(
children: [
if (player != null)
VideoSubtitles(
key: const Key('subtitles'),
controller: player,
overLayed: showOverlay,
),
if (AdaptiveLayout.of(context).isDesktop)
Consumer(builder: (context, ref, child) {
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
return playButton(playing, buffering);
}),
IgnorePointer(
ignoring: !showOverlay,
child: AnimatedOpacity(
duration: fadeDuration,
opacity: showOverlay ? 1 : 0,
child: Column(
children: [
topButtons(context),
const Spacer(),
bottomButtons(context),
],
),
),
),
),
const VideoPlayerSeekIndicator(),
Consumer(
builder: (context, ref, child) {
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
return Stack(
children: [
if (showIntroSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: IntroSkipButton(
isOverlayVisible: showOverlay,
skipIntro: () => skipIntro(introSkipModel),
const VideoPlayerSeekIndicator(),
Consumer(
builder: (context, ref, child) {
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
return Stack(
children: [
if (showIntroSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: IntroSkipButton(
isOverlayVisible: showOverlay,
skipIntro: () => skipIntro(introSkipModel),
),
),
),
),
if (showCreditSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: CreditsSkipButton(
isOverlayVisible: showOverlay,
skipCredits: () => skipCredits(introSkipModel),
if (showCreditSkipButton)
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.all(32),
child: CreditsSkipButton(
isOverlayVisible: showOverlay,
skipCredits: () => skipCredits(introSkipModel),
),
),
),
)
],
);
},
),
],
)
],
);
},
),
],
),
),
),
),