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,230 +133,233 @@ class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
return MediaQuery.removePadding( return MediaQuery.removePadding(
context: context, context: context,
child: Stack( child: InputHandler(
children: [ onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
IgnorePointer( child: Stack(
ignoring: !showControls, children: [
child: AnimatedOpacity( IgnorePointer(
duration: const Duration(milliseconds: 500), ignoring: !showControls,
opacity: showControls ? 1 : 0, child: AnimatedOpacity(
child: Stack( duration: const Duration(milliseconds: 500),
children: [ opacity: showControls ? 1 : 0,
Container( child: Stack(
decoration: BoxDecoration( children: [
gradient: LinearGradient( Container(
begin: Alignment.topCenter, decoration: BoxDecoration(
end: Alignment.bottomCenter, gradient: LinearGradient(
colors: [ begin: Alignment.topCenter,
overlayColor.withOpacity(1), end: Alignment.bottomCenter,
overlayColor.withOpacity(0.65), colors: [
overlayColor.withOpacity(0), overlayColor.withOpacity(1),
], overlayColor.withOpacity(0.65),
overlayColor.withOpacity(0),
],
),
), ),
), child: Padding(
child: Padding( padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8),
padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ if (AdaptiveLayout.of(context).isDesktop)
if (AdaptiveLayout.of(context).isDesktop) const Flexible(
const Flexible( child: DefaultTitleBar(
child: DefaultTitleBar( height: 50,
height: 50, brightness: Brightness.dark,
brightness: Brightness.dark,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const BackButton(),
const SizedBox(
width: 16,
),
Flexible(
child: Text(
bookViewerDetails.book?.name ?? "None",
style: Theme.of(context).textTheme.titleLarge,
), ),
) ),
], Row(
), crossAxisAlignment: CrossAxisAlignment.center,
const SizedBox(height: 16), children: [
], const BackButton(),
), const SizedBox(
), width: 16,
), ),
if (!bookViewerDetails.loading) ...{ Flexible(
if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{ child: Text(
Align( bookViewerDetails.book?.name ?? "None",
alignment: Alignment.bottomCenter, style: Theme.of(context).textTheme.titleLarge,
child: Container( ),
decoration: BoxDecoration( )
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
overlayColor.withOpacity(0),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(1),
], ],
), ),
), const SizedBox(height: 16),
child: Padding( ],
padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16), ),
child: Column( ),
mainAxisSize: MainAxisSize.min, ),
children: [ if (!bookViewerDetails.loading) ...{
const SizedBox(height: 30), if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{
Row( Align(
children: [ alignment: Alignment.bottomCenter,
const SizedBox(width: 8), child: Container(
Tooltip( decoration: BoxDecoration(
message: bookViewerSettings.readDirection == ReadDirection.leftToRight gradient: LinearGradient(
? previousChapter?.name != null begin: Alignment.topCenter,
? "Load ${previousChapter?.name}" end: Alignment.bottomCenter,
: "" colors: [
: nextChapter?.name != null overlayColor.withOpacity(0),
? "Load ${nextChapter?.name}" overlayColor.withOpacity(0.65),
: "", overlayColor.withOpacity(1),
child: IconButton.filled( ],
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight ),
? previousChapter != null ),
? () async => await loadNextBook(previousChapter) child: Padding(
: null padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16),
: nextChapter != null child: Column(
? () async => await loadNextBook(nextChapter) mainAxisSize: MainAxisSize.min,
: null, children: [
icon: const Icon(IconsaxOutline.backward), const SizedBox(height: 30),
), Row(
), children: [
const SizedBox(width: 8), const SizedBox(width: 8),
Flexible( Tooltip(
child: Container( message: bookViewerSettings.readDirection == ReadDirection.leftToRight
decoration: BoxDecoration( ? previousChapter?.name != null
color: Colors.black.withOpacity(0.7), ? "Load ${previousChapter?.name}"
borderRadius: BorderRadius.circular(60), : ""
: nextChapter?.name != null
? "Load ${nextChapter?.name}"
: "",
child: IconButton.filled(
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
? previousChapter != null
? () async => await loadNextBook(previousChapter)
: null
: nextChapter != null
? () async => await loadNextBook(nextChapter)
: null,
icon: const Icon(IconsaxOutline.backward),
), ),
child: Padding( ),
padding: const EdgeInsets.symmetric(horizontal: 24), const SizedBox(width: 8),
child: Row( Flexible(
children: [ child: Container(
if (bookViewerSettings.readDirection == ReadDirection.leftToRight) decoration: BoxDecoration(
...controls(currentPage, bookViewerSettings, bookViewerDetails) color: Colors.black.withOpacity(0.7),
else borderRadius: BorderRadius.circular(60),
...controls(currentPage, bookViewerSettings, bookViewerDetails) ),
.reversed, child: Padding(
], padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
if (bookViewerSettings.readDirection == ReadDirection.leftToRight)
...controls(currentPage, bookViewerSettings, bookViewerDetails)
else
...controls(currentPage, bookViewerSettings, bookViewerDetails)
.reversed,
],
),
), ),
), ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), Tooltip(
Tooltip( message: bookViewerSettings.readDirection == ReadDirection.leftToRight
message: bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextChapter?.name != null
? nextChapter?.name != null ? "Load ${nextChapter?.name}"
? "Load ${nextChapter?.name}" : ""
: "" : previousChapter?.name != null
: previousChapter?.name != null ? "Load ${previousChapter?.name}"
? "Load ${previousChapter?.name}" : "",
: "", child: IconButton.filled(
child: IconButton.filled( onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextChapter != null
? nextChapter != null ? () async => await loadNextBook(nextChapter)
? () async => await loadNextBook(nextChapter) : null
: null : previousChapter != null
: previousChapter != null ? () async => await loadNextBook(previousChapter)
? () async => await loadNextBook(previousChapter) : null,
: null, icon: const Icon(IconsaxOutline.forward),
icon: const Icon(IconsaxOutline.forward), ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), ],
], ),
), const SizedBox(height: 16),
const SizedBox(height: 16), Row(
Row( mainAxisSize: MainAxisSize.max,
mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
children: [ Transform.flip(
Transform.flip( flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft,
flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft, child: IconButton(
child: IconButton( onPressed: () => widget.controller
onPressed: () => widget.controller .animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve),
.animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve), icon: const Icon(IconsaxOutline.backward)),
icon: const Icon(IconsaxOutline.backward)), ),
), IconButton(
IconButton( onPressed: () {
onPressed: () { showBookViewerSettings(context);
showBookViewerSettings(context); },
}, icon: const Icon(IconsaxOutline.setting_2),
icon: const Icon(IconsaxOutline.setting_2), ),
), IconButton(
IconButton( onPressed: chapters.length > 1
onPressed: chapters.length > 1 ? () {
? () { showBookViewerChapters(
showBookViewerChapters( context,
context, widget.provider,
widget.provider, onPressed: (book) async {
onPressed: (book) async { Navigator.of(context).pop();
Navigator.of(context).pop(); loadNextBook(book);
loadNextBook(book); },
}, );
); }
} : () => fladderSnackbar(context, title: "No other chapters"),
: () => fladderSnackbar(context, title: "No other chapters"), icon: const Icon(IconsaxOutline.bookmark_2),
icon: const Icon(IconsaxOutline.bookmark_2), )
) ],
], ),
), ],
], ),
), ),
), ),
), ),
), } else
} else const Center(
const Center( child: Card(
child: Card( child: Padding(
child: Padding( padding: EdgeInsets.all(8.0),
padding: EdgeInsets.all(8.0), child: Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Icon(Icons.menu_book_rounded),
Icon(Icons.menu_book_rounded), SizedBox(width: 8),
SizedBox(width: 8), Text("Unable to load book"),
Text("Unable to load book"), ],
], ),
), ),
), ),
), )
) },
}, ],
],
),
),
),
if (bookViewerDetails.loading)
Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (bookViewerDetails.book != null) ...{
Flexible(
child: Text("Loading ${bookViewerDetails.book?.name}",
style: Theme.of(context).textTheme.titleMedium),
),
const SizedBox(width: 16),
},
const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round),
],
),
), ),
), ),
) ),
], if (bookViewerDetails.loading)
Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (bookViewerDetails.book != null) ...{
Flexible(
child: Text("Loading ${bookViewerDetails.book?.name}",
style: Theme.of(context).textTheme.titleMedium),
),
const SizedBox(width: 16),
},
const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round),
],
),
),
),
)
],
),
), ),
); );
} }

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,192 +144,197 @@ 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(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
widthFactor: 1, widthFactor: 1,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: gradient, colors: gradient,
),
), ),
), child: Padding(
child: Padding( padding: EdgeInsets.only(top: widget.padding.top),
padding: EdgeInsets.only(top: widget.padding.top), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25),
if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25), Padding(
Padding( padding: const EdgeInsets.symmetric(vertical: 12)
padding: const EdgeInsets.symmetric(vertical: 12) .add(EdgeInsets.only(left: padding.left, right: padding.right)),
.add(EdgeInsets.only(left: padding.left, right: padding.right)), child: Row(
child: Row( mainAxisSize: MainAxisSize.max,
mainAxisSize: MainAxisSize.max, children: [
children: [ ElevatedIconButton(
ElevatedIconButton( onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()),
onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()), icon: getBackIcon(context),
icon: getBackIcon(context), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), Expanded(
Expanded( child: Tooltip(
child: Tooltip( message: widget.photo.name,
message: widget.photo.name, child: Text(
child: Text( widget.photo.name,
widget.photo.name, maxLines: 2,
maxLines: 2, style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .titleMedium
.titleMedium ?.copyWith(fontWeight: FontWeight.bold, shadows: [
?.copyWith(fontWeight: FontWeight.bold, shadows: [ BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)),
BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)), BoxShadow(blurRadius: 4, spreadRadius: 4, color: Colors.black.withOpacity(0.4)),
BoxShadow(blurRadius: 4, spreadRadius: 4, color: Colors.black.withOpacity(0.4)), BoxShadow(blurRadius: 20, spreadRadius: 6, color: Colors.black.withOpacity(0.2)),
BoxShadow(blurRadius: 20, spreadRadius: 6, color: Colors.black.withOpacity(0.2)), ]),
]), ),
), ),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), Stack(
Stack( children: [
children: [ Positioned.fill(
Positioned.fill( child: Container(
child: Container( decoration: BoxDecoration(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.onPrimary),
color: Theme.of(context).colorScheme.onPrimary), child: SquareProgressIndicator(
child: SquareProgressIndicator( value: widget.currentIndex / (widget.itemCount - 1),
value: widget.currentIndex / (widget.itemCount - 1), borderRadius: 7,
borderRadius: 7, clockwise: false,
clockwise: false, color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.primary, ),
), ),
), ),
), Padding(
Padding( padding: const EdgeInsets.all(9),
padding: const EdgeInsets.all(9), child: Row(
child: Row( children: [
children: [ Text(
Text( "${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ",
"${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ", style: Theme.of(context)
style: .textTheme
Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), .bodyMedium
), ?.copyWith(fontWeight: FontWeight.bold),
if (widget.loadingMoreItems)
const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator.adaptive(
strokeCap: StrokeCap.round,
),
), ),
].addInBetween(const SizedBox(width: 6)), if (widget.loadingMoreItems)
const SizedBox.square(
dimension: 16,
child: CircularProgressIndicator.adaptive(
strokeCap: StrokeCap.round,
),
),
].addInBetween(const SizedBox(width: 6)),
),
), ),
), Positioned.fill(
Positioned.fill( child: FlatButton(
child: FlatButton( borderRadiusGeometry: BorderRadius.circular(8),
borderRadiusGeometry: BorderRadius.circular(8), onTap: () async {
onTap: () async { showDialog(
showDialog( context: context,
context: context, builder: (context) => Dialog(
builder: (context) => Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: SizedBox(
child: SizedBox( width: 125,
width: 125, child: Padding(
child: Padding( padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(8.0), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Text(
Text( context.localized.goTo,
context.localized.goTo, style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .bodyLarge
.bodyLarge ?.copyWith(fontWeight: FontWeight.bold),
?.copyWith(fontWeight: FontWeight.bold), ),
), const SizedBox(height: 5),
const SizedBox(height: 5), IntInputField(
IntInputField( controller: TextEditingController(
controller: text: (widget.currentIndex + 1).toString()),
TextEditingController(text: (widget.currentIndex + 1).toString()), onSubmitted: (value) {
onSubmitted: (value) { final position =
final position = ((value ?? 0) - 1).clamp(0, widget.itemCount - 1); ((value ?? 0) - 1).clamp(0, widget.itemCount - 1);
widget.pageController.jumpToPage(position); widget.pageController.jumpToPage(position);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
], ],
),
), ),
), ),
), ),
), );
); },
}, ),
), )
) ],
], ),
), const SizedBox(width: 12),
const SizedBox(width: 12), ],
], ),
), ),
), ],
], ),
), ),
), ),
), ),
), Align(
Align( alignment: Alignment.bottomCenter,
alignment: Alignment.bottomCenter, child: Container(
child: Container( decoration: BoxDecoration(
decoration: BoxDecoration( gradient: LinearGradient(
gradient: LinearGradient( begin: Alignment.topCenter,
begin: Alignment.topCenter, end: Alignment.bottomCenter,
end: Alignment.bottomCenter, colors: gradient.reversed.toList(),
colors: gradient.reversed.toList(), ),
), ),
), width: double.infinity,
width: double.infinity, child: Padding(
child: Padding( padding: EdgeInsets.only(bottom: widget.padding.bottom),
padding: EdgeInsets.only(bottom: widget.padding.bottom), child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ Padding(
Padding( padding:
padding: const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)), const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)),
child: Row( child: Row(
children: [ children: [
ElevatedIconButton( ElevatedIconButton(
onPressed: widget.openOptions, onPressed: widget.openOptions,
icon: IconsaxOutline.more_2, icon: IconsaxOutline.more_2,
), ),
const Spacer(), const Spacer(),
ElevatedIconButton( ElevatedIconButton(
onPressed: markAsFavourite, onPressed: markAsFavourite,
color: widget.photo.userData.isFavourite ? Colors.red : null, color: widget.photo.userData.isFavourite ? Colors.red : null,
icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart, icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart,
), ),
ProgressFloatingButton( ProgressFloatingButton(
controller: timerController, controller: timerController,
onLongPress: (duration) { onLongPress: (duration) {
if (duration != null) { if (duration != null) {
ref ref
.read(photoViewSettingsProvider.notifier) .read(photoViewSettingsProvider.notifier)
.update((state) => state.copyWith(timer: duration)); .update((state) => state.copyWith(timer: duration));
} }
}, },
), ),
].addPadding(const EdgeInsets.symmetric(horizontal: 8)), ].addPadding(const EdgeInsets.symmetric(horizontal: 8)),
), ),
) )
], ],
),
), ),
), ),
), ),
), ],
], ),
), ),
); );
} }

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,60 +37,63 @@ class _PassCodeInputState extends ConsumerState<PassCodeInput> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return InputHandler(
scrollable: true, onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
content: Column( child: AlertDialog(
mainAxisSize: MainAxisSize.min, scrollable: true,
crossAxisAlignment: CrossAxisAlignment.stretch, content: Column(
children: [ mainAxisSize: MainAxisSize.min,
Row( crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max, children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: List.generate( mainAxisSize: MainAxisSize.max,
passCodeLength, mainAxisAlignment: MainAxisAlignment.spaceBetween,
(index) => Expanded( children: List.generate(
child: Padding( passCodeLength,
padding: const EdgeInsets.symmetric(horizontal: 4), (index) => Expanded(
child: SizedBox( child: Padding(
height: iconSize * 1.2, padding: const EdgeInsets.symmetric(horizontal: 4),
width: iconSize * 1.2, child: SizedBox(
child: Card( height: iconSize * 1.2,
child: Transform.translate( width: iconSize * 1.2,
offset: const Offset(0, 5), child: Card(
child: AnimatedFadeSize( child: Transform.translate(
child: Text( offset: const Offset(0, 5),
currentPasscode.length > index ? "*" : "", child: AnimatedFadeSize(
style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60), child: Text(
currentPasscode.length > index ? "*" : "",
style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60),
),
), ),
), ),
), ),
), ),
), ),
), ),
), ).toList(),
).toList(), ),
), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(),
children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(), ),
), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(),
children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(), ),
), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(),
children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(), ),
), Row(
Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ backSpaceButton,
backSpaceButton, passCodeNumber(0),
passCodeNumber(0), clearAllButton,
clearAllButton, ],
], )
) ].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,28 +74,32 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState<VideoPlayerSeekIndica
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IgnorePointer( return InputHandler(
child: AnimatedOpacity( autoFocus: true,
duration: const Duration(milliseconds: 500), onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
opacity: (visible && seekPosition != 0) ? 1 : 0, child: IgnorePointer(
child: Center( child: AnimatedOpacity(
child: Container( duration: const Duration(milliseconds: 500),
decoration: BoxDecoration( opacity: (visible && seekPosition != 0) ? 1 : 0,
color: Colors.black.withOpacity(0.85), child: Center(
borderRadius: BorderRadius.circular(16), child: Container(
), decoration: BoxDecoration(
child: Padding( color: Colors.black.withOpacity(0.85),
padding: const EdgeInsets.all(16.0), borderRadius: BorderRadius.circular(16),
child: Row( ),
mainAxisSize: MainAxisSize.min, child: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Text( child: Row(
seekPosition > 0 mainAxisSize: MainAxisSize.min,
? "+$seekPosition ${context.localized.seconds(seekPosition)}" children: [
: "$seekPosition ${context.localized.seconds(seekPosition)}", Text(
style: Theme.of(context).textTheme.bodyMedium, 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 { 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,105 +102,93 @@ 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,
onPopInvokedWithResult: (didPop, result) { onPopInvokedWithResult: (didPop, result) {
if (!didPop) { if (!didPop) {
closePlayer(); closePlayer();
} }
}, },
child: GestureDetector( child: GestureDetector(
onTap: () => toggleOverlay(), onTap: () => toggleOverlay(),
child: MouseRegion( child: MouseRegion(
cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none, cursor: showOverlay ? SystemMouseCursors.basic : SystemMouseCursors.none,
onEnter: (event) => toggleOverlay(value: true), onEnter: (event) => toggleOverlay(value: true),
onExit: (event) => toggleOverlay(value: false), onExit: (event) => toggleOverlay(value: false),
onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null, onHover: AdaptiveLayout.of(context).isDesktop || kIsWeb ? (event) => toggleOverlay(value: true) : null,
child: Stack( child: Stack(
children: [ children: [
if (player != null) if (player != null)
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) {
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing)); final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering)); final buffering = ref.watch(mediaPlaybackProvider.select((value) => value.buffering));
return playButton(playing, buffering); return playButton(playing, buffering);
}), }),
IgnorePointer( IgnorePointer(
ignoring: !showOverlay, ignoring: !showOverlay,
child: AnimatedOpacity( child: AnimatedOpacity(
duration: fadeDuration, duration: fadeDuration,
opacity: showOverlay ? 1 : 0, opacity: showOverlay ? 1 : 0,
child: Column( child: Column(
children: [ children: [
topButtons(context), topButtons(context),
const Spacer(), const Spacer(),
bottomButtons(context), bottomButtons(context),
], ],
),
), ),
), ),
), const VideoPlayerSeekIndicator(),
const VideoPlayerSeekIndicator(), Consumer(
Consumer( builder: (context, ref, child) {
builder: (context, ref, child) { final position = ref.watch(mediaPlaybackProvider.select((value) => value.position));
final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false;
bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false; bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false;
bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false; return Stack(
return Stack( children: [
children: [ if (showIntroSkipButton)
if (showIntroSkipButton) Align(
Align( alignment: Alignment.centerRight,
alignment: Alignment.centerRight, child: Padding(
child: Padding( padding: const EdgeInsets.all(32),
padding: const EdgeInsets.all(32), child: IntroSkipButton(
child: IntroSkipButton( isOverlayVisible: showOverlay,
isOverlayVisible: showOverlay, skipIntro: () => skipIntro(introSkipModel),
skipIntro: () => skipIntro(introSkipModel), ),
), ),
), ),
), if (showCreditSkipButton)
if (showCreditSkipButton) Align(
Align( alignment: Alignment.centerRight,
alignment: Alignment.centerRight, child: Padding(
child: Padding( padding: const EdgeInsets.all(32),
padding: const EdgeInsets.all(32), child: CreditsSkipButton(
child: CreditsSkipButton( isOverlayVisible: showOverlay,
isOverlayVisible: showOverlay, skipCredits: () => skipCredits(introSkipModel),
skipCredits: () => skipCredits(introSkipModel), ),
), ),
), )
) ],
], );
); },
}, ),
), ],
], ),
), ),
), ),
), ),

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,
);
}
}