diff --git a/lib/screens/book_viewer/book_viewer_controls.dart b/lib/screens/book_viewer/book_viewer_controls.dart index 2ced41f..0e08049 100644 --- a/lib/screens/book_viewer/book_viewer_controls.dart +++ b/lib/screens/book_viewer/book_viewer_controls.dart @@ -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/fladder_snackbar.dart'; import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/throttler.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; @@ -75,12 +76,10 @@ class _BookViewerControlsState extends ConsumerState { viewController.visibilityChanged.addListener(() { toggleControls(value: viewController.controlsVisible); }); - ServicesBinding.instance.keyboard.addHandler(_onKey); } @override void dispose() { - ServicesBinding.instance.keyboard.removeHandler(_onKey); WakelockPlus.disable(); ScreenBrightness().resetScreenBrightness(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []); @@ -134,230 +133,233 @@ class _BookViewerControlsState extends ConsumerState { return MediaQuery.removePadding( context: context, - child: Stack( - children: [ - IgnorePointer( - ignoring: !showControls, - child: AnimatedOpacity( - duration: const Duration(milliseconds: 500), - opacity: showControls ? 1 : 0, - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - overlayColor.withOpacity(1), - overlayColor.withOpacity(0.65), - overlayColor.withOpacity(0), - ], + child: InputHandler( + onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, + child: Stack( + children: [ + IgnorePointer( + ignoring: !showControls, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 500), + opacity: showControls ? 1 : 0, + child: Stack( + children: [ + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + overlayColor.withOpacity(1), + overlayColor.withOpacity(0.65), + overlayColor.withOpacity(0), + ], + ), ), - ), - child: Padding( - padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (AdaptiveLayout.of(context).isDesktop) - const Flexible( - child: DefaultTitleBar( - height: 50, - 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, + child: Padding( + padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (AdaptiveLayout.of(context).isDesktop) + const Flexible( + child: DefaultTitleBar( + height: 50, + brightness: Brightness.dark, ), - ) - ], - ), - const SizedBox(height: 16), - ], - ), - ), - ), - if (!bookViewerDetails.loading) ...{ - if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{ - Align( - alignment: Alignment.bottomCenter, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - overlayColor.withOpacity(0), - overlayColor.withOpacity(0.65), - overlayColor.withOpacity(1), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const BackButton(), + const SizedBox( + width: 16, + ), + Flexible( + child: Text( + bookViewerDetails.book?.name ?? "None", + style: Theme.of(context).textTheme.titleLarge, + ), + ) ], ), - ), - child: Padding( - padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 30), - Row( - children: [ - const SizedBox(width: 8), - Tooltip( - message: bookViewerSettings.readDirection == ReadDirection.leftToRight - ? previousChapter?.name != null - ? "Load ${previousChapter?.name}" - : "" - : 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), - ), - ), - const SizedBox(width: 8), - Flexible( - child: Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), - borderRadius: BorderRadius.circular(60), + const SizedBox(height: 16), + ], + ), + ), + ), + if (!bookViewerDetails.loading) ...{ + if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{ + Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + overlayColor.withOpacity(0), + overlayColor.withOpacity(0.65), + overlayColor.withOpacity(1), + ], + ), + ), + child: Padding( + padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 30), + Row( + children: [ + const SizedBox(width: 8), + Tooltip( + message: bookViewerSettings.readDirection == ReadDirection.leftToRight + ? previousChapter?.name != null + ? "Load ${previousChapter?.name}" + : "" + : 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), - child: Row( - children: [ - if (bookViewerSettings.readDirection == ReadDirection.leftToRight) - ...controls(currentPage, bookViewerSettings, bookViewerDetails) - else - ...controls(currentPage, bookViewerSettings, bookViewerDetails) - .reversed, - ], + ), + const SizedBox(width: 8), + Flexible( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(60), + ), + 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), - Tooltip( - message: bookViewerSettings.readDirection == ReadDirection.leftToRight - ? nextChapter?.name != null - ? "Load ${nextChapter?.name}" - : "" - : previousChapter?.name != null - ? "Load ${previousChapter?.name}" - : "", - child: IconButton.filled( - onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight - ? nextChapter != null - ? () async => await loadNextBook(nextChapter) - : null - : previousChapter != null - ? () async => await loadNextBook(previousChapter) - : null, - icon: const Icon(IconsaxOutline.forward), + const SizedBox(width: 8), + Tooltip( + message: bookViewerSettings.readDirection == ReadDirection.leftToRight + ? nextChapter?.name != null + ? "Load ${nextChapter?.name}" + : "" + : previousChapter?.name != null + ? "Load ${previousChapter?.name}" + : "", + child: IconButton.filled( + onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight + ? nextChapter != null + ? () async => await loadNextBook(nextChapter) + : null + : previousChapter != null + ? () async => await loadNextBook(previousChapter) + : null, + icon: const Icon(IconsaxOutline.forward), + ), ), - ), - const SizedBox(width: 8), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Transform.flip( - flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft, - child: IconButton( - onPressed: () => widget.controller - .animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve), - icon: const Icon(IconsaxOutline.backward)), - ), - IconButton( - onPressed: () { - showBookViewerSettings(context); - }, - icon: const Icon(IconsaxOutline.setting_2), - ), - IconButton( - onPressed: chapters.length > 1 - ? () { - showBookViewerChapters( - context, - widget.provider, - onPressed: (book) async { - Navigator.of(context).pop(); - loadNextBook(book); - }, - ); - } - : () => fladderSnackbar(context, title: "No other chapters"), - icon: const Icon(IconsaxOutline.bookmark_2), - ) - ], - ), - ], + const SizedBox(width: 8), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Transform.flip( + flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft, + child: IconButton( + onPressed: () => widget.controller + .animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve), + icon: const Icon(IconsaxOutline.backward)), + ), + IconButton( + onPressed: () { + showBookViewerSettings(context); + }, + icon: const Icon(IconsaxOutline.setting_2), + ), + IconButton( + onPressed: chapters.length > 1 + ? () { + showBookViewerChapters( + context, + widget.provider, + onPressed: (book) async { + Navigator.of(context).pop(); + loadNextBook(book); + }, + ); + } + : () => fladderSnackbar(context, title: "No other chapters"), + icon: const Icon(IconsaxOutline.bookmark_2), + ) + ], + ), + ], + ), ), ), ), - ), - } else - const Center( - child: Card( - child: Padding( - padding: EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.menu_book_rounded), - SizedBox(width: 8), - Text("Unable to load book"), - ], + } else + const Center( + child: Card( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.menu_book_rounded), + SizedBox(width: 8), + 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), + ], + ), + ), + ), + ) + ], + ), ), ); } diff --git a/lib/screens/photo_viewer/photo_viewer_controls.dart b/lib/screens/photo_viewer/photo_viewer_controls.dart index 4fe5a76..8543227 100644 --- a/lib/screens/photo_viewer/photo_viewer_controls.dart +++ b/lib/screens/photo_viewer/photo_viewer_controls.dart @@ -16,6 +16,7 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/input_fields.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/localization_helper.dart'; import 'package:fladder/util/throttler.dart'; @@ -99,7 +100,7 @@ class _PhotoViewerControllsState extends ConsumerState with timerController.playPause(); return true; } - if (value.logicalKey == LogicalKeyboardKey.keyF) { + if (value.logicalKey == LogicalKeyboardKey.space) { widget.toggleOverlay?.call(null); return true; } @@ -117,12 +118,10 @@ class _PhotoViewerControllsState extends ConsumerState with timerController.reset(); }, ); - ServicesBinding.instance.keyboard.addHandler(_onKey); } @override void onWindowMinimize() { - ServicesBinding.instance.keyboard.removeHandler(_onKey); timerController.cancel(); super.onWindowMinimize(); } @@ -145,192 +144,197 @@ class _PhotoViewerControllsState extends ConsumerState with final padding = MediaQuery.of(context).padding; return PopScope( - onPopInvokedWithResult: (didPop, result) async { - await WakelockPlus.disable(); - }, - child: Stack( - children: [ - Align( - alignment: Alignment.topCenter, - widthFactor: 1, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: gradient, + onPopInvokedWithResult: (didPop, result) async => await WakelockPlus.disable(), + child: InputHandler( + onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, + child: Stack( + children: [ + Align( + alignment: Alignment.topCenter, + widthFactor: 1, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: gradient, + ), ), - ), - child: Padding( - padding: EdgeInsets.only(top: widget.padding.top), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25), - Padding( - padding: const EdgeInsets.symmetric(vertical: 12) - .add(EdgeInsets.only(left: padding.left, right: padding.right)), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - ElevatedIconButton( - onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()), - icon: getBackIcon(context), - ), - const SizedBox(width: 8), - Expanded( - child: Tooltip( - message: widget.photo.name, - child: Text( - widget.photo.name, - maxLines: 2, - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.bold, shadows: [ - BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)), - BoxShadow(blurRadius: 4, spreadRadius: 4, color: Colors.black.withOpacity(0.4)), - BoxShadow(blurRadius: 20, spreadRadius: 6, color: Colors.black.withOpacity(0.2)), - ]), + child: Padding( + padding: EdgeInsets.only(top: widget.padding.top), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 25), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12) + .add(EdgeInsets.only(left: padding.left, right: padding.right)), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + ElevatedIconButton( + onPressed: () => Navigator.of(context).pop(widget.pageController.page?.toInt()), + icon: getBackIcon(context), + ), + const SizedBox(width: 8), + Expanded( + child: Tooltip( + message: widget.photo.name, + child: Text( + widget.photo.name, + maxLines: 2, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.bold, shadows: [ + BoxShadow(blurRadius: 1, spreadRadius: 1, color: Colors.black.withOpacity(0.7)), + BoxShadow(blurRadius: 4, spreadRadius: 4, color: Colors.black.withOpacity(0.4)), + BoxShadow(blurRadius: 20, spreadRadius: 6, color: Colors.black.withOpacity(0.2)), + ]), + ), ), ), - ), - const SizedBox(width: 8), - Stack( - children: [ - Positioned.fill( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.onPrimary), - child: SquareProgressIndicator( - value: widget.currentIndex / (widget.itemCount - 1), - borderRadius: 7, - clockwise: false, - color: Theme.of(context).colorScheme.primary, + const SizedBox(width: 8), + Stack( + children: [ + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.onPrimary), + child: SquareProgressIndicator( + value: widget.currentIndex / (widget.itemCount - 1), + borderRadius: 7, + clockwise: false, + color: Theme.of(context).colorScheme.primary, + ), ), ), - ), - Padding( - padding: const EdgeInsets.all(9), - child: Row( - children: [ - Text( - "${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ", - style: - Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold), - ), - if (widget.loadingMoreItems) - const SizedBox.square( - dimension: 16, - child: CircularProgressIndicator.adaptive( - strokeCap: StrokeCap.round, - ), + Padding( + padding: const EdgeInsets.all(9), + child: Row( + children: [ + Text( + "${widget.currentIndex + 1} / ${widget.loadingMoreItems ? "-" : "${widget.itemCount}"} ", + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), ), - ].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( - child: FlatButton( - borderRadiusGeometry: BorderRadius.circular(8), - onTap: () async { - showDialog( - context: context, - builder: (context) => Dialog( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - child: SizedBox( - width: 125, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.localized.goTo, - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith(fontWeight: FontWeight.bold), - ), - const SizedBox(height: 5), - IntInputField( - controller: - TextEditingController(text: (widget.currentIndex + 1).toString()), - onSubmitted: (value) { - final position = ((value ?? 0) - 1).clamp(0, widget.itemCount - 1); - widget.pageController.jumpToPage(position); - Navigator.of(context).pop(); - }, - ), - ], + Positioned.fill( + child: FlatButton( + borderRadiusGeometry: BorderRadius.circular(8), + onTap: () async { + showDialog( + context: context, + builder: (context) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + child: SizedBox( + width: 125, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.localized.goTo, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 5), + IntInputField( + controller: TextEditingController( + text: (widget.currentIndex + 1).toString()), + onSubmitted: (value) { + final position = + ((value ?? 0) - 1).clamp(0, widget.itemCount - 1); + widget.pageController.jumpToPage(position); + Navigator.of(context).pop(); + }, + ), + ], + ), ), ), ), - ), - ); - }, - ), - ) - ], - ), - const SizedBox(width: 12), - ], + ); + }, + ), + ) + ], + ), + const SizedBox(width: 12), + ], + ), ), - ), - ], + ], + ), ), ), ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: gradient.reversed.toList(), + Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: gradient.reversed.toList(), + ), ), - ), - width: double.infinity, - child: Padding( - padding: EdgeInsets.only(bottom: widget.padding.bottom), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)), - child: Row( - children: [ - ElevatedIconButton( - onPressed: widget.openOptions, - icon: IconsaxOutline.more_2, - ), - const Spacer(), - ElevatedIconButton( - onPressed: markAsFavourite, - color: widget.photo.userData.isFavourite ? Colors.red : null, - icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart, - ), - ProgressFloatingButton( - controller: timerController, - onLongPress: (duration) { - if (duration != null) { - ref - .read(photoViewSettingsProvider.notifier) - .update((state) => state.copyWith(timer: duration)); - } - }, - ), - ].addPadding(const EdgeInsets.symmetric(horizontal: 8)), - ), - ) - ], + width: double.infinity, + child: Padding( + padding: EdgeInsets.only(bottom: widget.padding.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: + const EdgeInsets.all(8.0).add(EdgeInsets.only(left: padding.left, right: padding.right)), + child: Row( + children: [ + ElevatedIconButton( + onPressed: widget.openOptions, + icon: IconsaxOutline.more_2, + ), + const Spacer(), + ElevatedIconButton( + onPressed: markAsFavourite, + color: widget.photo.userData.isFavourite ? Colors.red : null, + icon: widget.photo.userData.isFavourite ? IconsaxBold.heart : IconsaxOutline.heart, + ), + ProgressFloatingButton( + controller: timerController, + onLongPress: (duration) { + if (duration != null) { + ref + .read(photoViewSettingsProvider.notifier) + .update((state) => state.copyWith(timer: duration)); + } + }, + ), + ].addPadding(const EdgeInsets.symmetric(horizontal: 8)), + ), + ) + ], + ), ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/screens/shared/passcode_input.dart b/lib/screens/shared/passcode_input.dart index a6d4baf..42731e3 100644 --- a/lib/screens/shared/passcode_input.dart +++ b/lib/screens/shared/passcode_input.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; +import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/list_padding.dart'; class PassCodeInput extends ConsumerStatefulWidget { @@ -19,18 +20,6 @@ class _PassCodeInputState extends ConsumerState { final passCodeLength = 4; 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) { if (value is KeyDownEvent) { final keyInt = int.tryParse(value.logicalKey.keyLabel); @@ -48,60 +37,63 @@ class _PassCodeInputState extends ConsumerState { @override Widget build(BuildContext context) { - return AlertDialog( - scrollable: true, - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate( - passCodeLength, - (index) => Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: SizedBox( - height: iconSize * 1.2, - width: iconSize * 1.2, - child: Card( - child: Transform.translate( - offset: const Offset(0, 5), - child: AnimatedFadeSize( - child: Text( - currentPasscode.length > index ? "*" : "", - style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60), + return InputHandler( + onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored, + child: AlertDialog( + scrollable: true, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate( + passCodeLength, + (index) => Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: SizedBox( + height: iconSize * 1.2, + width: iconSize * 1.2, + child: Card( + child: Transform.translate( + offset: const Offset(0, 5), + child: AnimatedFadeSize( + child: Text( + currentPasscode.length > index ? "*" : "", + style: Theme.of(context).textTheme.displayLarge?.copyWith(fontSize: 60), + ), ), ), ), ), ), ), - ), - ).toList(), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - backSpaceButton, - passCodeNumber(0), - clearAllButton, - ], - ) - ].addPadding(const EdgeInsets.symmetric(vertical: 8)), + ).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.of([1, 2, 3]).map((e) => passCodeNumber(e)).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.of([4, 5, 6]).map((e) => passCodeNumber(e)).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.of([7, 8, 9]).map((e) => passCodeNumber(e)).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + backSpaceButton, + passCodeNumber(0), + clearAllButton, + ], + ) + ].addPadding(const EdgeInsets.symmetric(vertical: 8)), + ), ), ); } diff --git a/lib/screens/video_player/components/video_player_seek_indicator.dart b/lib/screens/video_player/components/video_player_seek_indicator.dart index 14f3ba5..365cbf9 100644 --- a/lib/screens/video_player/components/video_player_seek_indicator.dart +++ b/lib/screens/video_player/components/video_player_seek_indicator.dart @@ -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 onSeekEnd()); + timer = RestartableTimer(const Duration(milliseconds: 500), () => onSeekEnd()); setState(() { seekPosition = 0; }); @@ -85,28 +74,32 @@ class _VideoPlayerSeekIndicatorState extends ConsumerState 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, + ) + ], + ), ), ), ), diff --git a/lib/screens/video_player/components/video_subtitles.dart b/lib/screens/video_player/components/video_subtitles.dart index 8a56d84..a7fad31 100644 --- a/lib/screens/video_player/components/video_subtitles.dart +++ b/lib/screens/video_player/components/video_subtitles.dart @@ -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 { return SubtitleText( subModel: settings, padding: padding, - offset: (widget.overlayed ? 0.5 : settings.verticalOffset), + offset: (widget.overLayed ? 0.5 : settings.verticalOffset), text: text, ); } diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 8701272..9679caa 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -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 { ); final fadeDuration = const Duration(milliseconds: 350); - final focusNode = FocusNode(); bool showOverlay = true; bool wasPlaying = false; @@ -79,7 +79,6 @@ class _DesktopControlsState extends ConsumerState { } else if (showCreditSkipButton) { skipCredits(introSkipModel); } - focusNode.requestFocus(); } if (value.logicalKey == LogicalKeyboardKey.escape) { disableFullscreen(); @@ -103,105 +102,93 @@ class _DesktopControlsState extends ConsumerState { 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), + ), ), - ), - ) - ], - ); - }, - ), - ], + ) + ], + ); + }, + ), + ], + ), ), ), ), diff --git a/lib/util/input_handler.dart b/lib/util/input_handler.dart new file mode 100644 index 0000000..161ca73 --- /dev/null +++ b/lib/util/input_handler.dart @@ -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 createState() => _InputHandlerState(); +} + +class _InputHandlerState extends State { + 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, + ); + } +}