From 1babf05834fb1d7ec901e2221fad863d925d8933 Mon Sep 17 00:00:00 2001 From: PartyDonut <42371342+PartyDonut@users.noreply.github.com> Date: Sat, 26 Oct 2024 14:00:15 +0200 Subject: [PATCH] fix: Keyboard controls and translations (#79) Co-authored-by: PartyDonut --- lib/l10n/app_en.arb | 9 +- lib/models/items/media_streams_model.dart | 19 +- lib/models/media_playback_model.dart | 4 + .../book_viewer/book_viewer_controls.dart | 496 +++++++++--------- .../photo_viewer/photo_viewer_controls.dart | 414 +++++++-------- lib/screens/shared/passcode_input.dart | 146 +++--- .../video_player_options_sheet.dart | 51 +- .../components/video_subtitle_controls.dart | 30 +- .../video_player/video_player_controls.dart | 275 +++++----- lib/widgets/shared/full_screen_button.dart | 41 +- .../shared/full_screen_button_web.dart | 56 +- 11 files changed, 796 insertions(+), 745 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a3c2283..225a838 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1048,5 +1048,12 @@ }, "aboutCreatedBy": "Created by DonutWare", "aboutSocials": "Socials", - "aboutLicenses": "Licenses" + "aboutLicenses": "Licenses", + "subtitle": "Subtitle", + "subtitleConfiguration": "Subtitle configuration", + "off": "Off", + "screenBrightness": "Screen brightness", + "scale":"Scale", + "playBackSettings": "Playback Settings" + } diff --git a/lib/models/items/media_streams_model.dart b/lib/models/items/media_streams_model.dart index 5f72277..28e2281 100644 --- a/lib/models/items/media_streams_model.dart +++ b/lib/models/items/media_streams_model.dart @@ -1,14 +1,15 @@ import 'dart:convert'; -import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:flutter/material.dart'; // ignore_for_file: public_member_api_docs, sort_constructors_first import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; import 'package:fladder/providers/user_provider.dart'; +import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/video_properties.dart'; class MediaStreamsModel { @@ -233,6 +234,14 @@ class AudioStreamModel extends StreamModel { ); } + String label(BuildContext context) { + if (index == -1) { + return context.localized.off; + } else { + return displayTitle; + } + } + String get title => [name, language, codec, channelLayout].whereNotNull().where((element) => element.isNotEmpty).join(' - '); @@ -283,6 +292,14 @@ class SubStreamModel extends StreamModel { this.supportsExternalStream = false, }); + String label(BuildContext context) { + if (index == -1) { + return context.localized.off; + } else { + return displayTitle; + } + } + factory SubStreamModel.fromMediaStream(dto.MediaStream stream, Ref ref) { return SubStreamModel( name: stream.title ?? "", diff --git a/lib/models/media_playback_model.dart b/lib/models/media_playback_model.dart index 4b38e97..24c4ffc 100644 --- a/lib/models/media_playback_model.dart +++ b/lib/models/media_playback_model.dart @@ -14,6 +14,7 @@ class MediaPlaybackModel { final bool completed; final bool errorPlaying; final bool buffering; + final bool fullScreen; MediaPlaybackModel({ this.state = VideoPlayerState.disposed, this.playing = false, @@ -24,6 +25,7 @@ class MediaPlaybackModel { this.completed = false, this.errorPlaying = false, this.buffering = false, + this.fullScreen = false, }); MediaPlaybackModel copyWith({ @@ -36,6 +38,7 @@ class MediaPlaybackModel { bool? completed, bool? errorPlaying, bool? buffering, + bool? fullScreen, }) { return MediaPlaybackModel( state: state ?? this.state, @@ -47,6 +50,7 @@ class MediaPlaybackModel { completed: completed ?? this.completed, errorPlaying: errorPlaying ?? this.errorPlaying, buffering: buffering ?? this.buffering, + fullScreen: fullScreen ?? this.fullScreen, ); } } diff --git a/lib/screens/book_viewer/book_viewer_controls.dart b/lib/screens/book_viewer/book_viewer_controls.dart index 9d6bac3..2ced41f 100644 --- a/lib/screens/book_viewer/book_viewer_controls.dart +++ b/lib/screens/book_viewer/book_viewer_controls.dart @@ -1,5 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:extended_image/extended_image.dart'; import 'package:ficonsax/ficonsax.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:screen_brightness/screen_brightness.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + import 'package:fladder/models/book_model.dart'; import 'package:fladder/providers/book_viewer_provider.dart'; import 'package:fladder/providers/items/book_details_provider.dart'; @@ -11,11 +18,6 @@ import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/throttler.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:screen_brightness/screen_brightness.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; class BookViewController { bool controlsVisible = true; @@ -45,7 +47,6 @@ class BookViewerControls extends ConsumerStatefulWidget { } class _BookViewerControlsState extends ConsumerState { - final FocusNode focusNode = FocusNode(); final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130)); final Duration pageAnimDuration = const Duration(milliseconds: 125); final Curve pageAnimCurve = Curves.easeInCubic; @@ -74,11 +75,12 @@ class _BookViewerControlsState extends ConsumerState { viewController.visibilityChanged.addListener(() { toggleControls(value: viewController.controlsVisible); }); + ServicesBinding.instance.keyboard.addHandler(_onKey); } @override void dispose() { - super.dispose(); + ServicesBinding.instance.keyboard.removeHandler(_onKey); WakelockPlus.disable(); ScreenBrightness().resetScreenBrightness(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []); @@ -87,6 +89,36 @@ class _BookViewerControlsState extends ConsumerState { systemNavigationBarColor: Colors.transparent, systemNavigationBarDividerColor: Colors.transparent, )); + super.dispose(); + } + + bool _onKey(KeyEvent value) { + final bookViewerSettings = ref.read(bookViewerSettingsProvider); + if (value is KeyRepeatEvent) { + if (value.logicalKey == LogicalKeyboardKey.arrowLeft || value.logicalKey == LogicalKeyboardKey.keyA) { + bookViewerSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage(); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.arrowRight || value.logicalKey == LogicalKeyboardKey.keyD) { + bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage(); + return true; + } + } + if (value is KeyDownEvent) { + if (value.logicalKey == LogicalKeyboardKey.arrowLeft || value.logicalKey == LogicalKeyboardKey.keyA) { + bookViewerSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage(); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.arrowRight || value.logicalKey == LogicalKeyboardKey.keyD) { + bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage(); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.keyF) { + toggleControls(); + return true; + } + } + return false; } @override @@ -100,252 +132,232 @@ class _BookViewerControlsState extends ConsumerState { final previousChapter = details.previousChapter(bookViewerDetails.book); final nextChapter = details.nextChapter(bookViewerDetails.book); - if (AdaptiveLayout.of(context).isDesktop) { - FocusScope.of(context).requestFocus(focusNode); - } return MediaQuery.removePadding( context: context, - child: KeyboardListener( - focusNode: focusNode, - autofocus: AdaptiveLayout.of(context).isDesktop, - onKeyEvent: (value) { - if (value is KeyDownEvent) { - if (value.logicalKey == LogicalKeyboardKey.arrowLeft || value.logicalKey == LogicalKeyboardKey.keyA) { - bookViewerSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage(); - } - if (value.logicalKey == LogicalKeyboardKey.arrowRight || value.logicalKey == LogicalKeyboardKey.keyD) { - bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage(); - } - if (value.logicalKey == LogicalKeyboardKey.space) { - toggleControls(); - } - } - }, - 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, - ), - ) - ], - ), - const SizedBox(height: 16), - ], - ), + 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), + ], ), ), - 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(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, ), ), - 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), - ), - 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), - ], - ), - 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), - ) - ], - ), - ], + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const BackButton(), + const SizedBox( + width: 16, ), - ), + Flexible( + child: Text( + bookViewerDetails.book?.name ?? "None", + style: Theme.of(context).textTheme.titleLarge, + ), + ) + ], ), - ), - } 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), - ], + 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), + ), + ), + 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), + ], + ), + 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"), + ], + ), + ), + ), + ) + }, + ], + ), + ), + ), + 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 a0aacb0..620acb4 100644 --- a/lib/screens/photo_viewer/photo_viewer_controls.dart +++ b/lib/screens/photo_viewer/photo_viewer_controls.dart @@ -1,8 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:extended_image/extended_image.dart'; import 'package:ficonsax/ficonsax.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:square_progress_indicator/square_progress_indicator.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:window_manager/window_manager.dart'; + import 'package:fladder/models/items/photos_model.dart'; -import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/settings/photo_view_settings_provider.dart'; +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'; @@ -11,14 +21,6 @@ import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/throttler.dart'; import 'package:fladder/widgets/shared/elevated_icon.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:square_progress_indicator/square_progress_indicator.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:window_manager/window_manager.dart'; class PhotoViewerControls extends ConsumerStatefulWidget { final EdgeInsets padding; @@ -48,7 +50,6 @@ class PhotoViewerControls extends ConsumerStatefulWidget { } class _PhotoViewerControllsState extends ConsumerState with WindowListener { - final FocusNode focusNode = FocusNode(); final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130)); late int currentPage = widget.pageController.page?.round() ?? 0; double dragUpDelta = 0.0; @@ -69,13 +70,46 @@ class _PhotoViewerControllsState extends ConsumerState with } } + bool _onKey(KeyEvent value) { + if (value is KeyRepeatEvent) { + if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { + throttler.run(() => + widget.pageController.previousPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut)); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.arrowRight) { + throttler.run( + () => widget.pageController.nextPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut)); + return true; + } + } + if (value is KeyDownEvent) { + if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { + throttler.run(() => + widget.pageController.previousPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut)); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.arrowRight) { + throttler.run( + () => widget.pageController.nextPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut)); + return true; + } + + if (value.logicalKey == LogicalKeyboardKey.keyK) { + timerController.playPause(); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.keyF) { + widget.toggleOverlay?.call(null); + return true; + } + } + return false; + } + @override void initState() { super.initState(); - Future.microtask(() => () { - if (AdaptiveLayout.of(context).isDesktop) focusNode.requestFocus(); - }); - windowManager.addListener(this); widget.pageController.addListener( () { @@ -83,10 +117,12 @@ class _PhotoViewerControllsState extends ConsumerState with timerController.reset(); }, ); + ServicesBinding.instance.keyboard.addHandler(_onKey); } @override void onWindowMinimize() { + ServicesBinding.instance.keyboard.removeHandler(_onKey); timerController.cancel(); super.onWindowMinimize(); } @@ -100,8 +136,6 @@ class _PhotoViewerControllsState extends ConsumerState with @override Widget build(BuildContext context) { - if (AdaptiveLayout.of(context).isDesktop) focusNode.requestFocus(); - final gradient = [ Colors.black.withOpacity(0.6), Colors.black.withOpacity(0.3), @@ -109,217 +143,187 @@ class _PhotoViewerControllsState extends ConsumerState with Colors.black.withOpacity(0.0), ]; - if (AdaptiveLayout.of(context).isDesktop) { - focusNode.requestFocus(); - } - final padding = MediaQuery.of(context).padding; return PopScope( onPopInvokedWithResult: (didPop, result) async { await WakelockPlus.disable(); }, - child: KeyboardListener( - focusNode: focusNode, - autofocus: true, - onKeyEvent: (value) { - if (value is KeyDownEvent) { - if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { - throttler.run(() => widget.pageController - .previousPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut)); - } - if (value.logicalKey == LogicalKeyboardKey.arrowRight) { - throttler.run(() => - widget.pageController.nextPage(duration: const Duration(milliseconds: 125), curve: Curves.easeInOut)); - } - if (value.logicalKey == LogicalKeyboardKey.keyK) { - timerController.playPause(); - } - if (value.logicalKey == LogicalKeyboardKey.space) { - widget.toggleOverlay?.call(null); - } - } - }, - child: Stack( - children: [ - Align( - alignment: Alignment.topCenter, - widthFactor: 1, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: gradient, - ), + 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), ), - ), - ), - 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, - ), + if (widget.loadingMoreItems) + const SizedBox.square( + dimension: 16, + child: CircularProgressIndicator.adaptive( + strokeCap: StrokeCap.round, ), - ].addInBetween(const SizedBox(width: 6)), - ), + ), + ].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, - ), - ].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, + ), + ].addPadding(const EdgeInsets.symmetric(horizontal: 8)), + ), + ) + ], ), ), ), - ], - ), + ), + ], ), ); } diff --git a/lib/screens/shared/passcode_input.dart b/lib/screens/shared/passcode_input.dart index b90a420..a6d4baf 100644 --- a/lib/screens/shared/passcode_input.dart +++ b/lib/screens/shared/passcode_input.dart @@ -1,9 +1,11 @@ -import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:flutter/material.dart'; 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/list_padding.dart'; + class PassCodeInput extends ConsumerStatefulWidget { final ValueChanged passCode; const PassCodeInput({required this.passCode, super.key}); @@ -16,80 +18,90 @@ class _PassCodeInputState extends ConsumerState { final iconSize = 45.0; final passCodeLength = 4; var currentPasscode = ""; - final focusNode = FocusNode(); + + @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); + if (keyInt != null) { + addToPassCode(value.logicalKey.keyLabel); + return true; + } + if (value.logicalKey == LogicalKeyboardKey.backspace) { + backSpace(); + return true; + } + } + return false; + } @override Widget build(BuildContext context) { - focusNode.requestFocus(); - return KeyboardListener( - focusNode: focusNode, - autofocus: true, - onKeyEvent: (value) { - if (value is KeyDownEvent) { - final keyInt = int.tryParse(value.logicalKey.keyLabel); - if (keyInt != null) { - addToPassCode(value.logicalKey.keyLabel); - } - if (value.logicalKey == LogicalKeyboardKey.backspace) { - backSpace(); - } - } - }, - 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), - ), + 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), ), ), ), ), ), ), - ).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_options_sheet.dart b/lib/screens/video_player/components/video_player_options_sheet.dart index fcb0cf0..912f2d3 100644 --- a/lib/screens/video_player/components/video_player_options_sheet.dart +++ b/lib/screens/video_player/components/video_player_options_sheet.dart @@ -19,6 +19,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/video_player/components/video_player_queue.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; @@ -91,7 +92,7 @@ class _VideoOptionsMobileState extends ConsumerState { title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Flexible(flex: 1, child: Text("Screen Brightness")), + Flexible(flex: 1, child: Text(context.localized.screenBrightness)), Flexible( child: Row( children: [ @@ -124,13 +125,13 @@ class _VideoOptionsMobileState extends ConsumerState { ), ), SpacedListTile( - title: const Text("Subtitles"), - content: Text(currentMediaStreams?.currentSubStream?.displayTitle ?? "Off"), + title: Text(context.localized.subtitles), + content: Text(currentMediaStreams?.currentSubStream?.label(context) ?? context.localized.off), onTap: currentMediaStreams?.subStreams.isNotEmpty == true ? () => showSubSelection(context) : null, ), SpacedListTile( - title: const Text("Audio"), - content: Text(currentMediaStreams?.currentAudioStream?.displayTitle ?? "Off"), + title: Text(context.localized.audio), + content: Text(currentMediaStreams?.currentAudioStream?.label(context) ?? context.localized.off), onTap: currentMediaStreams?.audioStreams.isNotEmpty == true ? () => showAudioSelection(context) : null, ), ListTile( @@ -139,7 +140,7 @@ class _VideoOptionsMobileState extends ConsumerState { children: [ Flexible( child: EnumSelection( - label: const Text("Scale"), + label: Text(context.localized.scale), current: videoSettings.videoFit.name.toUpperCaseSplit(), itemBuilder: (context) => BoxFit.values .map((value) => PopupMenuItem( @@ -159,9 +160,9 @@ class _VideoOptionsMobileState extends ConsumerState { title: Row( mainAxisSize: MainAxisSize.min, children: [ - const Expanded( + Expanded( flex: 3, - child: Text("Fill-screen"), + child: Text(context.localized.videoScalingFill), ), const Spacer(), Switch.adaptive( @@ -235,7 +236,7 @@ class _VideoOptionsMobileState extends ConsumerState { widget.minimizePlayer(); (this as EpisodeModel).parentBaseModel.navigateTo(context); }, - title: const Text("Open show"), + title: Text(context.localized.openShow), ), ListTile( onTap: () async { @@ -243,7 +244,7 @@ class _VideoOptionsMobileState extends ConsumerState { widget.minimizePlayer(); await currentItem.navigateTo(context); }, - title: const Text("Show details"), + title: Text(context.localized.showDetails), ), if (currentItem.type != FladderItemType.boxset) ListTile( @@ -253,7 +254,7 @@ class _VideoOptionsMobileState extends ConsumerState { context.refreshData(); } }, - title: const Text("Add to collection"), + title: Text(context.localized.addToCollection), ), if (currentItem.type != FladderItemType.playlist) ListTile( @@ -263,7 +264,7 @@ class _VideoOptionsMobileState extends ConsumerState { context.refreshData(); } }, - title: const Text("Add to playlist"), + title: Text(context.localized.addToPlaylist), ), ListTile( onTap: () async { @@ -280,14 +281,16 @@ class _VideoOptionsMobileState extends ConsumerState { ref.read(playBackModel.notifier).update((state) => playbackModel); Navigator.of(context).pop(); }, - title: Text(currentItem.userData.isFavourite == true ? "Remove from favorites" : "Add to favourites"), + title: Text(currentItem.userData.isFavourite == true + ? context.localized.removeAsFavorite + : context.localized.addAsFavorite), ), ListTile( onTap: () { Navigator.of(context).pop(); showInfoScreen(context, currentItem); }, - title: const Text('Media info'), + title: Text(context.localized.info), ), } ], @@ -340,14 +343,14 @@ Future showSubSelection(BuildContext context) { contentPadding: const EdgeInsets.only(top: 8, bottom: 24), title: Row( children: [ - const Text("Subtitle"), + Text(context.localized.subtitle), const Spacer(), IconButton.outlined( onPressed: () { Navigator.pop(context); showSubtitleControls( context: context, - label: 'Subtitle configuration', + label: context.localized.subtitleConfiguration, ); }, icon: const Icon(Icons.display_settings_rounded)) @@ -357,7 +360,7 @@ Future showSubSelection(BuildContext context) { (index, subModel) { final selected = playbackModel.mediaStreams?.defaultSubStreamIndex == subModel.index; return ListTile( - title: Text(subModel.displayTitle), + title: Text(subModel.label(context)), tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null, subtitle: subModel.language.isNotEmpty ? Opacity(opacity: 0.6, child: Text(subModel.language.capitalize())) @@ -391,24 +394,14 @@ Future showAudioSelection(BuildContext context) { contentPadding: const EdgeInsets.only(top: 8, bottom: 24), title: Row( children: [ - const Text("Subtitle"), - const Spacer(), - IconButton.outlined( - onPressed: () { - Navigator.pop(context); - showSubtitleControls( - context: context, - label: 'Subtitle configuration', - ); - }, - icon: const Icon(Icons.display_settings_rounded)) + Text(context.localized.audio), ], ), children: playbackModel?.audioStreams?.mapIndexed( (index, audioStream) { final selected = playbackModel.mediaStreams?.defaultAudioStreamIndex == audioStream.index; return ListTile( - title: Text(audioStream.displayTitle), + title: Text(audioStream.label(context)), tileColor: selected ? Theme.of(context).colorScheme.primary.withOpacity(0.3) : null, subtitle: audioStream.language.isNotEmpty ? Opacity(opacity: 0.6, child: Text(audioStream.language.capitalize())) diff --git a/lib/screens/video_player/components/video_subtitle_controls.dart b/lib/screens/video_player/components/video_subtitle_controls.dart index b0e5641..a9c1837 100644 --- a/lib/screens/video_player/components/video_subtitle_controls.dart +++ b/lib/screens/video_player/components/video_subtitle_controls.dart @@ -1,11 +1,13 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/widget_extensions.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; Future showSubtitleControls({ required BuildContext context, @@ -18,7 +20,8 @@ Future showSubtitleControls({ backgroundColor: Colors.transparent, elevation: 0, content: ConstrainedBox( - constraints: BoxConstraints(minWidth: MediaQuery.sizeOf(context).width * 0.75), child: VideoSubtitleControls(label: label)), + constraints: BoxConstraints(minWidth: MediaQuery.sizeOf(context).width * 0.75), + child: VideoSubtitleControls(label: label)), ), ); return; @@ -52,6 +55,8 @@ class _VideoSubtitleControlsState extends ConsumerState { final provider = ref.read(subtitleSettingsProvider.notifier); final controlsHidden = hideControls ? false : showPartial; return AnimatedContainer( + height: MediaQuery.sizeOf(context).width * 0.85, + width: MediaQuery.sizeOf(context).height * 0.7, duration: const Duration(milliseconds: 250), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), @@ -90,7 +95,9 @@ class _VideoSubtitleControlsState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( - onPressed: subSettings != lastSettings ? () => provider.resetSettings(value: lastSettings) : null, + onPressed: subSettings != lastSettings + ? () => provider.resetSettings(value: lastSettings) + : null, child: Text(context.localized.clearChanges), ), const SizedBox(width: 32), @@ -105,15 +112,18 @@ class _VideoSubtitleControlsState extends ConsumerState { multiSelectionEnabled: false, segments: [ ButtonSegment( - label: Text(context.localized.light, style: const TextStyle(fontWeight: FontWeight.w100)), + label: + Text(context.localized.light, style: const TextStyle(fontWeight: FontWeight.w100)), value: FontWeight.w100, ), ButtonSegment( - label: Text(context.localized.normal, style: const TextStyle(fontWeight: FontWeight.w500)), + label: + Text(context.localized.normal, style: const TextStyle(fontWeight: FontWeight.w500)), value: FontWeight.normal, ), ButtonSegment( - label: Text(context.localized.bold, style: const TextStyle(fontWeight: FontWeight.w900)), + label: + Text(context.localized.bold, style: const TextStyle(fontWeight: FontWeight.w900)), value: FontWeight.bold, ), ], @@ -210,7 +220,8 @@ class _VideoSubtitleControlsState extends ConsumerState { const Icon(Icons.border_color_rounded), ...[Colors.white, Colors.yellow, Colors.black, Colors.grey, Colors.transparent].map( (e) => FlatButton( - onTap: () => provider.setOutlineColor(e == Colors.transparent ? e : e.withOpacity(0.85)), + onTap: () => + provider.setOutlineColor(e == Colors.transparent ? e : e.withOpacity(0.85)), borderRadiusGeometry: BorderRadius.circular(5), clipBehavior: Clip.antiAlias, child: Container( @@ -290,7 +301,8 @@ class _VideoSubtitleControlsState extends ConsumerState { ), Text(context.localized.backgroundOpacity), ], - ).addVisiblity(activeKey == null ? controlsHidden : activeKey == const Key('backGroundOpacity')), + ).addVisiblity( + activeKey == null ? controlsHidden : activeKey == const Key('backGroundOpacity')), Column( children: [ Row( diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 09641d1..32ef24c 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -42,6 +42,11 @@ class DesktopControls extends ConsumerStatefulWidget { } class _DesktopControlsState extends ConsumerState { + late RestartableTimer timer = RestartableTimer( + const Duration(seconds: 5), + () => mounted ? toggleOverlay(value: false) : null, + ); + final fadeDuration = const Duration(milliseconds: 350); final focusNode = FocusNode(); bool showOverlay = true; @@ -50,6 +55,72 @@ class _DesktopControlsState extends ConsumerState { late final double topPadding = MediaQuery.of(context).viewPadding.top; late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom; + bool _onKey(KeyEvent value) { + final introSkipModel = ref.read(playBackModel.select((value) => value?.introSkipModel)); + final position = ref.read(mediaPlaybackProvider).position; + bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false; + bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false; + if (value is KeyRepeatEvent) { + if (value.logicalKey == LogicalKeyboardKey.arrowUp) { + resetTimer(); + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); + } + if (value.logicalKey == LogicalKeyboardKey.arrowDown) { + resetTimer(); + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); + } + return true; + } + if (value is KeyDownEvent) { + if (value.logicalKey == LogicalKeyboardKey.keyS) { + if (showIntroSkipButton) { + skipIntro(introSkipModel); + } else if (showCreditSkipButton) { + skipCredits(introSkipModel); + } + focusNode.requestFocus(); + } + if (value.logicalKey == LogicalKeyboardKey.escape) { + disableFullscreen(); + } + if (value.logicalKey == LogicalKeyboardKey.space) { + ref.read(videoPlayerProvider).playOrPause(); + } + if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { + seekBack(ref); + } + if (value.logicalKey == LogicalKeyboardKey.arrowRight) { + seekForward(ref); + } + if (value.logicalKey == LogicalKeyboardKey.keyF) { + toggleFullScreen(ref); + } + if (value.logicalKey == LogicalKeyboardKey.arrowUp) { + resetTimer(); + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); + } + if (value.logicalKey == LogicalKeyboardKey.arrowDown) { + resetTimer(); + ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); + } + return true; + } + 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)); @@ -66,121 +137,75 @@ class _DesktopControlsState extends ConsumerState { closePlayer(); } }, - child: KeyboardListener( - focusNode: focusNode, - autofocus: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer, - onKeyEvent: (value) { - final position = ref.read(mediaPlaybackProvider).position; - bool showIntroSkipButton = introSkipModel?.introInRange(position) ?? false; - bool showCreditSkipButton = introSkipModel?.creditsInRange(position) ?? false; - if (value is KeyRepeatEvent) {} - if (value is KeyDownEvent) { - if (value.logicalKey == LogicalKeyboardKey.keyS) { - if (showIntroSkipButton) { - skipIntro(introSkipModel); - } else if (showCreditSkipButton) { - skipCredits(introSkipModel); - } - focusNode.requestFocus(); - } - if (value.logicalKey == LogicalKeyboardKey.escape) { - disableFullscreen(); - } - if (value.logicalKey == LogicalKeyboardKey.space) { - ref.read(videoPlayerProvider).playOrPause(); - } - if (value.logicalKey == LogicalKeyboardKey.arrowLeft) { - seekBack(ref); - } - if (value.logicalKey == LogicalKeyboardKey.arrowRight) { - seekForward(ref); - } - if (value.logicalKey == LogicalKeyboardKey.keyF) { - toggleFullScreen(); - } - if (AdaptiveLayout.of(context).isDesktop || kIsWeb) { - if (value.logicalKey == LogicalKeyboardKey.arrowUp) { - resetTimer(); - ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(5); - } - if (value.logicalKey == LogicalKeyboardKey.arrowDown) { - resetTimer(); - ref.read(videoPlayerSettingsProvider.notifier).steppedVolume(-5); - } - } - focusNode.requestFocus(); - } - }, - 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), - ], - ), + 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), + ], ), ), - 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), - ), + ), + 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), ), - ) - ], - ); - }, - ), - ], - ), + ), + ) + ], + ); + }, + ), + ], ), ), ), @@ -317,11 +342,11 @@ class _DesktopControlsState extends ConsumerState { onPressed: () => showSubSelection(context), icon: const Icon(IconsaxOutline.subtitle), label: Text( - ref - .watch(playBackModel.select((value) => value?.mediaStreams?.currentSubStream)) - ?.language - .capitalize() ?? - "Off", + ref.watch(playBackModel.select((value) { + final language = value?.mediaStreams?.currentSubStream?.language; + return language?.isEmpty == true ? context.localized.off : language; + }))?.capitalize() ?? + "", maxLines: 1, ), ), @@ -331,11 +356,11 @@ class _DesktopControlsState extends ConsumerState { onPressed: () => showAudioSelection(context), icon: const Icon(IconsaxOutline.audio_square), label: Text( - ref - .watch(playBackModel.select((value) => value?.mediaStreams?.currentAudioStream)) - ?.language - .capitalize() ?? - "Off", + ref.watch(playBackModel.select((value) { + final language = value?.mediaStreams?.currentAudioStream?.language; + return language?.isEmpty == true ? context.localized.off : language; + }))?.capitalize() ?? + "", maxLines: 1, ), ), @@ -591,11 +616,6 @@ class _DesktopControlsState extends ConsumerState { ref.read(videoPlayerProvider).seek(Duration(seconds: newPosition)); } - late RestartableTimer timer = RestartableTimer( - const Duration(seconds: 5), - () => mounted ? toggleOverlay(value: false) : null, - ); - void toggleOverlay({bool? value}) { if (showOverlay == (value ?? !showOverlay)) return; setState(() => showOverlay = (value ?? !showOverlay)); @@ -652,9 +672,4 @@ class _DesktopControlsState extends ConsumerState { } } } - - Future toggleFullScreen() async { - final isFullScreen = await windowManager.isFullScreen(); - await windowManager.setFullScreen(!isFullScreen); - } } diff --git a/lib/widgets/shared/full_screen_button.dart b/lib/widgets/shared/full_screen_button.dart index 4bd25a5..05a11be 100644 --- a/lib/widgets/shared/full_screen_button.dart +++ b/lib/widgets/shared/full_screen_button.dart @@ -1,40 +1,27 @@ import 'package:flutter/material.dart'; import 'package:ficonsax/ficonsax.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart'; -class FullScreenButton extends StatefulWidget { +import 'package:fladder/providers/video_player_provider.dart'; + +Future toggleFullScreen(WidgetRef ref) async { + final isFullScreen = await windowManager.isFullScreen(); + await windowManager.setFullScreen(!isFullScreen); + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: !isFullScreen)); +} + +class FullScreenButton extends ConsumerWidget { const FullScreenButton({super.key}); @override - State createState() => _FullScreenButtonState(); -} - -class _FullScreenButtonState extends State { - bool isFullScreen = false; - - @override - void initState() { - super.initState(); - Future.microtask(checkFullScreen); - } - - void checkFullScreen() async { - final fullScreen = await windowManager.isFullScreen(); - setState(() { - isFullScreen = fullScreen; - }); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); return IconButton( - onPressed: () async { - await windowManager.setFullScreen(!isFullScreen); - checkFullScreen(); - }, + onPressed: () => toggleFullScreen(ref), icon: Icon( - isFullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, + fullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, ), ); } diff --git a/lib/widgets/shared/full_screen_button_web.dart b/lib/widgets/shared/full_screen_button_web.dart index af6ec6f..bf3879b 100644 --- a/lib/widgets/shared/full_screen_button_web.dart +++ b/lib/widgets/shared/full_screen_button_web.dart @@ -1,46 +1,34 @@ import 'package:flutter/material.dart'; import 'package:ficonsax/ficonsax.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:universal_html/html.dart' as html; -class FullScreenButton extends StatefulWidget { +import 'package:fladder/providers/video_player_provider.dart'; + +Future toggleFullScreen(WidgetRef ref) async { + final isFullScreen = html.document.fullscreenElement != null; + + if (isFullScreen) { + html.document.exitFullscreen(); + //Wait for 1 second + await Future.delayed(const Duration(seconds: 1)); + } else { + await html.document.documentElement?.requestFullscreen(); + } + ref + .read(mediaPlaybackProvider.notifier) + .update((state) => state.copyWith(fullScreen: html.document.fullscreenElement != null)); +} + +class FullScreenButton extends ConsumerWidget { const FullScreenButton({super.key}); @override - State createState() => _FullScreenButtonState(); -} - -class _FullScreenButtonState extends State { - bool fullScreen = false; - - @override - void initState() { - super.initState(); - _updateFullScreenStatus(); - } - - void _updateFullScreenStatus() { - setState(() { - fullScreen = html.document.fullscreenElement != null; - }); - } - - void toggleFullScreen() async { - if (fullScreen) { - html.document.exitFullscreen(); - //Wait for 1 second - await Future.delayed(const Duration(seconds: 1)); - } else { - await html.document.documentElement?.requestFullscreen(); - } - - _updateFullScreenStatus(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); return IconButton( - onPressed: toggleFullScreen, + onPressed: () => toggleFullScreen(ref), icon: Icon( fullScreen ? IconsaxOutline.close_square : IconsaxOutline.maximize_4, ),