From 054910f7b316393d5845416a4a57f9757edec0b0 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 2 Mar 2022 11:51:14 +0900 Subject: [PATCH] video: control buttons --- lib/l10n/app_en.arb | 11 +- lib/model/actions/video_actions.dart | 12 +- lib/model/settings/defaults.dart | 1 + lib/model/settings/enums/enums.dart | 2 + lib/model/settings/enums/video_controls.dart | 19 ++ lib/model/settings/settings.dart | 6 + .../common/action_mixins/entry_storage.dart | 14 +- lib/widgets/common/fx/blurred.dart | 30 ++- lib/widgets/common/map/buttons.dart | 23 +- .../dialogs/aves_selection_dialog.dart | 18 ++ .../accessibility/remove_animations.dart | 26 +- .../accessibility/time_to_take_action.dart | 22 +- lib/widgets/settings/language/language.dart | 46 ++-- lib/widgets/settings/language/locale.dart | 26 +- .../settings/navigation/navigation.dart | 44 ++-- lib/widgets/settings/video/controls.dart | 80 +++++++ lib/widgets/settings/video/gestures.dart | 61 ----- .../settings/video/subtitle_theme.dart | 22 +- lib/widgets/settings/video/video.dart | 26 +- .../settings/video/video_actions_editor.dart | 2 +- lib/widgets/viewer/entry_viewer_stack.dart | 2 +- .../viewer/overlay/bottom/video/controls.dart | 203 ++++++++++++++++ .../overlay/bottom/video/progress_bar.dart | 135 +++++++++++ .../overlay/bottom/{ => video}/video.dart | 225 ++---------------- lib/widgets/viewer/overlay/common.dart | 47 ++-- untranslated.json | 70 ++++-- 26 files changed, 709 insertions(+), 464 deletions(-) create mode 100644 lib/model/settings/enums/video_controls.dart create mode 100644 lib/widgets/settings/video/controls.dart delete mode 100644 lib/widgets/settings/video/gestures.dart create mode 100644 lib/widgets/viewer/overlay/bottom/video/controls.dart create mode 100644 lib/widgets/viewer/overlay/bottom/video/progress_bar.dart rename lib/widgets/viewer/overlay/bottom/{ => video}/video.dart (55%) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7e9f4f8e5..35df162c7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -152,6 +152,11 @@ "videoLoopModeShortOnly": "Short videos only", "videoLoopModeAlways": "Always", + "videoControlsNone": "None", + "videoControlsPlay": "Play", + "videoControlsPlaySeek": "Play & seek", + "videoControlsPlayOutside": "Play outside", + "mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleTerrain": "Google Maps (Terrain)", @@ -641,8 +646,10 @@ "settingsSubtitleThemeTextAlignmentCenter": "Center", "settingsSubtitleThemeTextAlignmentRight": "Right", - "settingsGesturesTile": "Gestures", - "settingsGesturesTitle": "Gestures", + "settingsVideoControlsTile": "Controls", + "settingsVideoControlsTitle": "Controls", + "settingsVideoButtonsTile": "Buttons", + "settingsVideoButtonsTitle": "Buttons", "settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause", "settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward", diff --git a/lib/model/actions/video_actions.dart b/lib/model/actions/video_actions.dart index 411d974ce..f0eff3676 100644 --- a/lib/model/actions/video_actions.dart +++ b/lib/model/actions/video_actions.dart @@ -3,26 +3,24 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/widgets.dart'; enum VideoAction { - captureFrame, + // controls playOutside, replay10, skip10, + togglePlay, + // menu + captureFrame, selectStreams, setSpeed, settings, - togglePlay, // TODO TLAD [video] toggle mute } class VideoActions { - static const all = [ - VideoAction.togglePlay, + static const menu = [ VideoAction.captureFrame, VideoAction.setSpeed, VideoAction.selectStreams, - VideoAction.replay10, - VideoAction.skip10, - VideoAction.playOutside, VideoAction.settings, ]; } diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index e07bb0adf..bbee26066 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -82,6 +82,7 @@ class SettingsDefaults { static const enableVideoAutoPlay = false; static const videoLoopMode = VideoLoopMode.shortOnly; static const videoShowRawTimedText = false; + static const videoControls = VideoControls.play; static const videoGestureDoubleTapTogglePlay = false; static const videoGestureSideDoubleTapSeek = true; diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index 8957d0141..c4cef7957 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -18,3 +18,5 @@ enum KeepScreenOn { never, viewerOnly, always } enum UnitSystem { metric, imperial } enum VideoLoopMode { never, shortOnly, always } + +enum VideoControls { none, play, playSeek, playOutside } diff --git a/lib/model/settings/enums/video_controls.dart b/lib/model/settings/enums/video_controls.dart new file mode 100644 index 000000000..48c59b577 --- /dev/null +++ b/lib/model/settings/enums/video_controls.dart @@ -0,0 +1,19 @@ +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +import 'enums.dart'; + +extension ExtraVideoControls on VideoControls { + String getName(BuildContext context) { + switch (this) { + case VideoControls.none: + return context.l10n.videoControlsNone; + case VideoControls.play: + return context.l10n.videoControlsPlay; + case VideoControls.playSeek: + return context.l10n.videoControlsPlaySeek; + case VideoControls.playOutside: + return context.l10n.videoControlsPlayOutside; + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 9eea828f4..3774b3441 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -95,6 +95,7 @@ class Settings extends ChangeNotifier { static const enableVideoAutoPlayKey = 'video_auto_play'; static const videoLoopModeKey = 'video_loop'; static const videoShowRawTimedTextKey = 'video_show_raw_timed_text'; + static const videoControlsKey = 'video_controls'; static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play'; static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip'; @@ -438,6 +439,10 @@ class Settings extends ChangeNotifier { set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue); + VideoControls get videoControls => getEnumOrDefault(videoControlsKey, SettingsDefaults.videoControls, VideoControls.values); + + set videoControls(VideoControls newValue) => setAndNotify(videoControlsKey, newValue.toString()); + bool get videoGestureDoubleTapTogglePlay => getBoolOrDefault(videoGestureDoubleTapTogglePlayKey, SettingsDefaults.videoGestureDoubleTapTogglePlay); set videoGestureDoubleTapTogglePlay(bool newValue) => setAndNotify(videoGestureDoubleTapTogglePlayKey, newValue); @@ -686,6 +691,7 @@ class Settings extends ChangeNotifier { case tagSortFactorKey: case imageBackgroundKey: case videoLoopModeKey: + case videoControlsKey: case subtitleTextAlignmentKey: case infoMapStyleKey: case coordinateFormatKey: diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index d25e09f0c..702a2d9c6 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -96,14 +96,12 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { if (uniqueNames.length < names.length) { final value = await showDialog( context: context, - builder: (context) { - return AvesSelectionDialog( - initialValue: nameConflictStrategy, - options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))), - message: originAlbums.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage, - confirmationButtonLabel: l10n.continueButtonLabel, - ); - }, + builder: (context) => AvesSelectionDialog( + initialValue: nameConflictStrategy, + options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))), + message: originAlbums.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage, + confirmationButtonLabel: l10n.continueButtonLabel, + ), ); if (value == null) return; nameConflictStrategy = value; diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index f98b7c63e..91131ac39 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -29,7 +29,7 @@ class BlurredRect extends StatelessWidget { class BlurredRRect extends StatelessWidget { final bool enabled; - final double borderRadius; + final BorderRadius? borderRadius; final Widget child; const BlurredRRect({ @@ -39,17 +39,31 @@ class BlurredRRect extends StatelessWidget { required this.child, }) : super(key: key); + factory BlurredRRect.all({ + Key? key, + bool enabled = true, + required double borderRadius, + required Widget child, + }) { + return BlurredRRect( + key: key, + enabled: enabled, + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + child: child, + ); + } + @override Widget build(BuildContext context) { - return enabled - ? ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), - child: BackdropFilter( + return ClipRRect( + borderRadius: borderRadius, + child: enabled + ? BackdropFilter( filter: _filter, child: child, - ), - ) - : child; + ) + : child, + ); } } diff --git a/lib/widgets/common/map/buttons.dart b/lib/widgets/common/map/buttons.dart index 1622ea8cb..e470a0b96 100644 --- a/lib/widgets/common/map/buttons.dart +++ b/lib/widgets/common/map/buttons.dart @@ -17,7 +17,6 @@ import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -139,21 +138,15 @@ class MapButtonPanel extends StatelessWidget { final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || canUseGoogleMaps); final preferredStyle = settings.infoMapStyle; final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first; - final style = await showDialog( + await showSelectionDialog( context: context, - builder: (context) { - return AvesSelectionDialog( - initialValue: initialStyle, - options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.mapStyleTitle, - ); - }, + builder: (context) => AvesSelectionDialog( + initialValue: initialStyle, + options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.mapStyleTitle, + ), + onSelection: (v) => settings.infoMapStyle = v, ); - // wait for the dialog to hide as applying the change may block the UI - await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); - if (style != null && style != settings.infoMapStyle) { - settings.infoMapStyle = style; - } }, tooltip: context.l10n.mapStyleTooltip, ), @@ -313,7 +306,7 @@ class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterCh ); return Padding( padding: EdgeInsets.all(widget.padding), - child: BlurredRRect( + child: BlurredRRect.all( enabled: blurred, borderRadius: AvesFilterChip.defaultRadius, child: AvesFilterChip( diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index ab6c0b626..7c7b09aef 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -1,8 +1,26 @@ +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'aves_dialog.dart'; +Future showSelectionDialog({ + required BuildContext context, + required WidgetBuilder builder, + required void Function(T value) onSelection, +}) async { + final value = await showDialog( + context: context, + builder: builder, + ); + // wait for the dialog to hide as applying the change may block the UI + await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); + if (value != null) { + onSelection(value); + } +} + typedef TextBuilder = String Function(T value); class AvesSelectionDialog extends StatefulWidget { diff --git a/lib/widgets/settings/accessibility/remove_animations.dart b/lib/widgets/settings/accessibility/remove_animations.dart index bf3095cef..62ea40095 100644 --- a/lib/widgets/settings/accessibility/remove_animations.dart +++ b/lib/widgets/settings/accessibility/remove_animations.dart @@ -1,11 +1,9 @@ import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; class RemoveAnimationsTile extends StatelessWidget { @@ -18,21 +16,15 @@ class RemoveAnimationsTile extends StatelessWidget { return ListTile( title: Text(context.l10n.settingsRemoveAnimationsTile), subtitle: Text(currentAnimations.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentAnimations, - options: Map.fromEntries(AccessibilityAnimations.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsRemoveAnimationsTitle, - ), - ); - // wait for the dialog to hide as applying the change may block the UI - await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); - if (value != null) { - settings.accessibilityAnimations = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentAnimations, + options: Map.fromEntries(AccessibilityAnimations.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsRemoveAnimationsTitle, + ), + onSelection: (v) => settings.accessibilityAnimations = v, + ), ); } } diff --git a/lib/widgets/settings/accessibility/time_to_take_action.dart b/lib/widgets/settings/accessibility/time_to_take_action.dart index 7de96d3f8..d1777b134 100644 --- a/lib/widgets/settings/accessibility/time_to_take_action.dart +++ b/lib/widgets/settings/accessibility/time_to_take_action.dart @@ -36,19 +36,15 @@ class _TimeToTakeActionTileState extends State { return ListTile( title: Text(context.l10n.settingsTimeToTakeActionTile), subtitle: Text(currentTimeToTakeAction.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentTimeToTakeAction, - options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsTimeToTakeActionTitle, - ), - ); - if (value != null) { - settings.timeToTakeAction = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentTimeToTakeAction, + options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsTimeToTakeActionTitle, + ), + onSelection: (v) => settings.timeToTakeAction = v, + ), ); }, ); diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart index b6f72e5e4..adcd05e2d 100644 --- a/lib/widgets/settings/language/language.dart +++ b/lib/widgets/settings/language/language.dart @@ -45,37 +45,29 @@ class LanguageSection extends StatelessWidget { ListTile( title: Text(l10n.settingsCoordinateFormatTile), subtitle: Text(currentCoordinateFormat.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentCoordinateFormat, - options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))), - optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo), - title: l10n.settingsCoordinateFormatTitle, - ), - ); - if (value != null) { - settings.coordinateFormat = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentCoordinateFormat, + options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))), + optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo), + title: l10n.settingsCoordinateFormatTitle, + ), + onSelection: (v) => settings.coordinateFormat = v, + ), ), ListTile( title: Text(l10n.settingsUnitSystemTile), subtitle: Text(currentUnitSystem.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentUnitSystem, - options: Map.fromEntries(UnitSystem.values.map((v) => MapEntry(v, v.getName(context)))), - title: l10n.settingsUnitSystemTitle, - ), - ); - if (value != null) { - settings.unitSystem = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentUnitSystem, + options: Map.fromEntries(UnitSystem.values.map((v) => MapEntry(v, v.getName(context)))), + title: l10n.settingsUnitSystemTitle, + ), + onSelection: (v) => settings.unitSystem = v, + ), ), ], ); diff --git a/lib/widgets/settings/language/locale.dart b/lib/widgets/settings/language/locale.dart index 1279b1b24..59622db66 100644 --- a/lib/widgets/settings/language/locale.dart +++ b/lib/widgets/settings/language/locale.dart @@ -2,13 +2,11 @@ import 'dart:collection'; import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/language/locales.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; class LocaleTile extends StatelessWidget { @@ -28,21 +26,15 @@ class LocaleTile extends StatelessWidget { return Text(locale == null ? context.l10n.settingsSystemDefault : _getLocaleName(locale)); }, ), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.locale ?? _systemLocaleOption, - options: _getLocaleOptions(context), - title: context.l10n.settingsLanguage, - ), - ); - // wait for the dialog to hide as applying the change may block the UI - await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); - if (value != null) { - settings.locale = value == _systemLocaleOption ? null : value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.locale ?? _systemLocaleOption, + options: _getLocaleOptions(context), + title: context.l10n.settingsLanguage, + ), + onSelection: (v) => settings.locale = v == _systemLocaleOption ? null : v, + ), ); } diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index a7466c80c..0737957ca 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -39,38 +39,30 @@ class NavigationSection extends StatelessWidget { ListTile( title: Text(context.l10n.settingsHome), subtitle: Text(currentHomePage.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentHomePage, - options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsHome, - ), - ); - if (value != null) { - settings.homePage = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentHomePage, + options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsHome, + ), + onSelection: (v) => settings.homePage = v, + ), ), const NavigationDrawerTile(), const ConfirmationDialogTile(), ListTile( title: Text(context.l10n.settingsKeepScreenOnTile), subtitle: Text(currentKeepScreenOn.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentKeepScreenOn, - options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsKeepScreenOnTitle, - ), - ); - if (value != null) { - settings.keepScreenOn = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: currentKeepScreenOn, + options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsKeepScreenOnTitle, + ), + onSelection: (v) => settings.keepScreenOn = v, + ), ), SwitchListTile( value: currentMustBackTwiceToExit, diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart new file mode 100644 index 000000000..895148960 --- /dev/null +++ b/lib/widgets/settings/video/controls.dart @@ -0,0 +1,80 @@ +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/video_controls.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class VideoControlsTile extends StatelessWidget { + const VideoControlsTile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(context.l10n.settingsVideoControlsTile), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: VideoControlsPage.routeName), + builder: (context) => const VideoControlsPage(), + ), + ); + }, + ); + } +} + +class VideoControlsPage extends StatelessWidget { + static const routeName = '/settings/video/controls'; + + const VideoControlsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.settingsVideoControlsTitle), + ), + body: SafeArea( + child: ListView( + children: [ + Selector( + selector: (context, s) => s.videoControls, + builder: (context, current, child) => ListTile( + title: Text(context.l10n.settingsVideoButtonsTile), + subtitle: Text(current.getName(context)), + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: current, + options: Map.fromEntries(VideoControls.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsVideoButtonsTitle, + ), + onSelection: (v) => settings.videoControls = v, + ), + ), + ), + Selector( + selector: (context, s) => s.videoGestureDoubleTapTogglePlay, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.videoGestureDoubleTapTogglePlay = v, + title: Text(context.l10n.settingsVideoGestureDoubleTapTogglePlay), + ), + ), + Selector( + selector: (context, s) => s.videoGestureSideDoubleTapSeek, + builder: (context, current, child) => SwitchListTile( + value: current, + onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v, + title: Text(context.l10n.settingsVideoGestureSideDoubleTapSeek), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/settings/video/gestures.dart b/lib/widgets/settings/video/gestures.dart deleted file mode 100644 index 18184118c..000000000 --- a/lib/widgets/settings/video/gestures.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class VideoGesturesTile extends StatelessWidget { - const VideoGesturesTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(context.l10n.settingsGesturesTile), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: VideoGesturesPage.routeName), - builder: (context) => const VideoGesturesPage(), - ), - ); - }, - ); - } -} - -class VideoGesturesPage extends StatelessWidget { - static const routeName = '/settings/video/gestures'; - - const VideoGesturesPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(context.l10n.settingsGesturesTitle), - ), - body: SafeArea( - child: ListView( - children: [ - Selector( - selector: (context, s) => s.videoGestureDoubleTapTogglePlay, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.videoGestureDoubleTapTogglePlay = v, - title: Text(context.l10n.settingsVideoGestureDoubleTapTogglePlay), - ), - ), - Selector( - selector: (context, s) => s.videoGestureSideDoubleTapSeek, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v, - title: Text(context.l10n.settingsVideoGestureSideDoubleTapSeek), - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index fbf5bec73..31f2b6ba7 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -57,19 +57,15 @@ class SubtitleThemePage extends StatelessWidget { ListTile( title: Text(context.l10n.settingsSubtitleThemeTextAlignmentTile), subtitle: Text(_getTextAlignName(context, settings.subtitleTextAlignment)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.subtitleTextAlignment, - options: Map.fromEntries(textAlignOptions.map((v) => MapEntry(v, _getTextAlignName(context, v)))), - title: context.l10n.settingsSubtitleThemeTextAlignmentTitle, - ), - ); - if (value != null) { - settings.subtitleTextAlignment = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.subtitleTextAlignment, + options: Map.fromEntries(textAlignOptions.map((v) => MapEntry(v, _getTextAlignName(context, v)))), + title: context.l10n.settingsSubtitleThemeTextAlignmentTitle, + ), + onSelection: (v) => settings.subtitleTextAlignment = v, + ), ), SliderListTile( title: context.l10n.settingsSubtitleThemeTextSize, diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index f51bff418..ffebd5bdf 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -9,7 +9,7 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; -import 'package:aves/widgets/settings/video/gestures.dart'; +import 'package:aves/widgets/settings/video/controls.dart'; import 'package:aves/widgets/settings/video/subtitle_theme.dart'; import 'package:aves/widgets/settings/video/video_actions_editor.dart'; import 'package:flutter/material.dart'; @@ -59,22 +59,18 @@ class VideoSection extends StatelessWidget { builder: (context, current, child) => ListTile( title: Text(context.l10n.settingsVideoLoopModeTile), subtitle: Text(current.getName(context)), - onTap: () async { - final value = await showDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: current, - options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsVideoLoopModeTitle, - ), - ); - if (value != null) { - settings.videoLoopMode = value; - } - }, + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: current, + options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsVideoLoopModeTitle, + ), + onSelection: (v) => settings.videoLoopMode = v, + ), ), ), - const VideoGesturesTile(), + const VideoControlsTile(), const SubtitleThemeTile(), ]; diff --git a/lib/widgets/settings/video/video_actions_editor.dart b/lib/widgets/settings/video/video_actions_editor.dart index f52cd5ab4..4dce80402 100644 --- a/lib/widgets/settings/video/video_actions_editor.dart +++ b/lib/widgets/settings/video/video_actions_editor.dart @@ -34,7 +34,7 @@ class VideoActionEditorPage extends StatelessWidget { return QuickActionEditorPage( title: context.l10n.settingsVideoQuickActionEditorTitle, bannerText: context.l10n.settingsViewerQuickActionEditorBanner, - allAvailableActions: VideoActions.all, + allAvailableActions: VideoActions.menu, actionIcon: (action) => action.getIcon(), actionText: (context, action) => action.getText(context), load: () => settings.videoQuickActions, diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index a5836deaf..9f6f88385 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -22,7 +22,7 @@ import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/bottom/common.dart'; import 'package:aves/widgets/viewer/overlay/bottom/panorama.dart'; -import 'package:aves/widgets/viewer/overlay/bottom/video.dart'; +import 'package:aves/widgets/viewer/overlay/bottom/video/video.dart'; import 'package:aves/widgets/viewer/overlay/notifications.dart'; import 'package:aves/widgets/viewer/overlay/top.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; diff --git a/lib/widgets/viewer/overlay/bottom/video/controls.dart b/lib/widgets/viewer/overlay/bottom/video/controls.dart new file mode 100644 index 000000000..941d5e437 --- /dev/null +++ b/lib/widgets/viewer/overlay/bottom/video/controls.dart @@ -0,0 +1,203 @@ +import 'dart:async'; + +import 'package:aves/model/actions/video_actions.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/menu.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/viewer/overlay/common.dart'; +import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class VideoControlRow extends StatelessWidget { + final AvesVideoController? controller; + final Animation scale; + final Function(VideoAction value) onActionSelected; + + static const double padding = 8; + static const Radius radius = Radius.circular(123); + + const VideoControlRow({ + Key? key, + required this.controller, + required this.scale, + required this.onActionSelected, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (context, s) => s.videoControls, + builder: (context, videoControls, child) { + switch (videoControls) { + case VideoControls.none: + return const SizedBox(); + case VideoControls.play: + return Padding( + padding: const EdgeInsetsDirectional.only(start: padding), + child: _buildOverlayButton( + child: PlayToggler( + controller: controller, + onPressed: () => onActionSelected(VideoAction.togglePlay), + ), + ), + ); + case VideoControls.playSeek: + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(width: padding), + _buildIconButton( + context, + VideoAction.replay10, + borderRadius: const BorderRadius.only(topLeft: radius, bottomLeft: radius), + ), + _buildOverlayButton( + child: PlayToggler( + controller: controller, + onPressed: () => onActionSelected(VideoAction.togglePlay), + ), + borderRadius: const BorderRadius.all(Radius.zero), + ), + _buildIconButton( + context, + VideoAction.skip10, + borderRadius: const BorderRadius.only(topRight: radius, bottomRight: radius), + ), + ], + ); + case VideoControls.playOutside: + return Padding( + padding: const EdgeInsetsDirectional.only(start: padding), + child: _buildIconButton( + context, + VideoAction.playOutside, + ), + ); + } + }, + ); + } + + Widget _buildOverlayButton({ + BorderRadius? borderRadius, + required Widget child, + }) => + OverlayButton( + scale: scale, + borderRadius: borderRadius, + child: child, + ); + + Widget _buildIconButton( + BuildContext context, + VideoAction action, { + BorderRadius? borderRadius, + }) => + _buildOverlayButton( + borderRadius: borderRadius, + child: IconButton( + icon: action.getIcon(), + onPressed: () => onActionSelected(action), + tooltip: action.getText(context), + ), + ); +} + +class PlayToggler extends StatefulWidget { + final AvesVideoController? controller; + final bool isMenuItem; + final VoidCallback? onPressed; + + const PlayToggler({ + Key? key, + required this.controller, + this.isMenuItem = false, + this.onPressed, + }) : super(key: key); + + @override + State createState() => _PlayTogglerState(); +} + +class _PlayTogglerState extends State with SingleTickerProviderStateMixin { + final List _subscriptions = []; + late AnimationController _playPauseAnimation; + + AvesVideoController? get controller => widget.controller; + + bool get isPlaying => controller?.isPlaying ?? false; + + @override + void initState() { + super.initState(); + _playPauseAnimation = AnimationController( + duration: context.read().iconAnimation, + vsync: this, + ); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant PlayToggler oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + _playPauseAnimation.dispose(); + super.dispose(); + } + + void _registerWidget(PlayToggler widget) { + final controller = widget.controller; + if (controller != null) { + _subscriptions.add(controller.statusStream.listen(_onStatusChange)); + _onStatusChange(controller.status); + } + } + + void _unregisterWidget(PlayToggler widget) { + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + + @override + Widget build(BuildContext context) { + if (widget.isMenuItem) { + return isPlaying + ? MenuRow( + text: context.l10n.videoActionPause, + icon: const Icon(AIcons.pause), + ) + : MenuRow( + text: context.l10n.videoActionPlay, + icon: const Icon(AIcons.play), + ); + } + return IconButton( + icon: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: _playPauseAnimation, + ), + onPressed: widget.onPressed, + tooltip: isPlaying ? context.l10n.videoActionPause : context.l10n.videoActionPlay, + ); + } + + void _onStatusChange(VideoStatus status) { + final status = _playPauseAnimation.status; + if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) { + _playPauseAnimation.forward(); + } else if (!isPlaying && status != AnimationStatus.reverse && status != AnimationStatus.dismissed) { + _playPauseAnimation.reverse(); + } + } +} diff --git a/lib/widgets/viewer/overlay/bottom/video/progress_bar.dart b/lib/widgets/viewer/overlay/bottom/video/progress_bar.dart new file mode 100644 index 000000000..6138b37f2 --- /dev/null +++ b/lib/widgets/viewer/overlay/bottom/video/progress_bar.dart @@ -0,0 +1,135 @@ +import 'dart:async'; + +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/format.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/viewer/overlay/common.dart'; +import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:flutter/material.dart'; + +class VideoProgressBar extends StatefulWidget { + final AvesVideoController? controller; + final Animation scale; + + const VideoProgressBar({ + Key? key, + required this.controller, + required this.scale, + }) : super(key: key); + + @override + State createState() => _VideoProgressBarState(); +} + +class _VideoProgressBarState extends State { + final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar'); + bool _playingOnDragStart = false; + + static const double radius = 123; + + AvesVideoController? get controller => widget.controller; + + Stream get positionStream => controller?.positionStream ?? Stream.value(0); + + bool get isPlaying => controller?.isPlaying ?? false; + + @override + Widget build(BuildContext context) { + final blurred = settings.enableOverlayBlurEffect; + const textStyle = TextStyle(shadows: Constants.embossShadows); + return SizeTransition( + sizeFactor: widget.scale, + child: BlurredRRect.all( + enabled: blurred, + borderRadius: radius, + child: GestureDetector( + onTapDown: (details) { + _seekFromTap(details.globalPosition); + }, + onHorizontalDragStart: (details) { + _playingOnDragStart = isPlaying; + if (_playingOnDragStart) controller!.pause(); + }, + onHorizontalDragUpdate: (details) { + _seekFromTap(details.globalPosition); + }, + onHorizontalDragEnd: (details) { + if (_playingOnDragStart) controller!.play(); + }, + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: kMinInteractiveDimension, + ), + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), + decoration: BoxDecoration( + color: overlayBackgroundColor(blurred: blurred), + border: AvesBorder.border, + borderRadius: const BorderRadius.all(Radius.circular(radius)), + ), + child: Column( + key: _progressBarKey, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + StreamBuilder( + stream: positionStream, + builder: (context, snapshot) { + // do not use stream snapshot because it is obsolete when switching between videos + final position = controller?.currentPosition.floor() ?? 0; + return Text( + formatFriendlyDuration(Duration(milliseconds: position)), + style: textStyle, + ); + }), + const Spacer(), + Text( + formatFriendlyDuration(Duration(milliseconds: controller?.duration ?? 0)), + style: textStyle, + ), + ], + ), + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(4)), + child: Directionality( + // force directionality for `LinearProgressIndicator` + textDirection: TextDirection.ltr, + child: StreamBuilder( + stream: positionStream, + builder: (context, snapshot) { + // do not use stream snapshot because it is obsolete when switching between videos + var progress = controller?.progress ?? 0.0; + if (!progress.isFinite) progress = 0.0; + return LinearProgressIndicator( + value: progress, + backgroundColor: Colors.grey.shade700, + ); + }), + ), + ), + const Text( + // fake text below to match the height of the text above and center the whole thing + '', + style: textStyle, + ), + ], + ), + ), + ), + ), + ), + ); + } + + void _seekFromTap(Offset globalPosition) async { + if (controller == null) return; + final keyContext = _progressBarKey.currentContext!; + final box = keyContext.findRenderObject() as RenderBox; + final localPosition = box.globalToLocal(globalPosition); + await controller!.seekToProgress(localPosition.dx / box.size.width); + } +} diff --git a/lib/widgets/viewer/overlay/bottom/video.dart b/lib/widgets/viewer/overlay/bottom/video/video.dart similarity index 55% rename from lib/widgets/viewer/overlay/bottom/video.dart rename to lib/widgets/viewer/overlay/bottom/video/video.dart index edfafaa41..f7d5187fd 100644 --- a/lib/widgets/viewer/overlay/bottom/video.dart +++ b/lib/widgets/viewer/overlay/bottom/video/video.dart @@ -4,14 +4,10 @@ import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/theme/format.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/popup_menu_button.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/common/fx/blurred.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/viewer/overlay/bottom/video/controls.dart'; +import 'package:aves/widgets/viewer/overlay/bottom/video/progress_bar.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:flutter/foundation.dart'; @@ -40,9 +36,6 @@ class VideoControlOverlay extends StatefulWidget { } class _VideoControlOverlayState extends State with SingleTickerProviderStateMixin { - final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar'); - bool _playingOnDragStart = false; - AvesEntry get entry => widget.entry; Animation get scale => widget.scale; @@ -89,7 +82,7 @@ class _VideoControlOverlayState extends State with SingleTi selector: (context, s) => s.videoQuickActions, builder: (context, videoQuickActions, child) { final quickActions = videoQuickActions.take(availableCount - 1).toList(); - final menuActions = VideoActions.all.where((action) => !quickActions.contains(action)).toList(); + final menuActions = VideoActions.menu.where((action) => !quickActions.contains(action)).toList(); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, @@ -103,7 +96,21 @@ class _VideoControlOverlayState extends State with SingleTi onActionMenuOpened: widget.onActionMenuOpened, ), const SizedBox(height: 8), - _buildProgressBar(), + Row( + children: [ + Expanded( + child: VideoProgressBar( + controller: controller, + scale: scale, + ), + ), + VideoControlRow( + controller: controller, + scale: scale, + onActionSelected: widget.onActionSelected, + ), + ], + ), ], ); }, @@ -120,104 +127,6 @@ class _VideoControlOverlayState extends State with SingleTi ); }); } - - Widget _buildProgressBar() { - const progressBarBorderRadius = 123.0; - final blurred = settings.enableOverlayBlurEffect; - const textStyle = TextStyle(shadows: Constants.embossShadows); - return SizeTransition( - sizeFactor: scale, - child: BlurredRRect( - enabled: blurred, - borderRadius: progressBarBorderRadius, - child: GestureDetector( - onTapDown: (details) { - _seekFromTap(details.globalPosition); - }, - onHorizontalDragStart: (details) { - _playingOnDragStart = isPlaying; - if (_playingOnDragStart) controller!.pause(); - }, - onHorizontalDragUpdate: (details) { - _seekFromTap(details.globalPosition); - }, - onHorizontalDragEnd: (details) { - if (_playingOnDragStart) controller!.play(); - }, - child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: kMinInteractiveDimension, - ), - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), - decoration: BoxDecoration( - color: overlayBackgroundColor(blurred: blurred), - border: AvesBorder.border, - borderRadius: const BorderRadius.all(Radius.circular(progressBarBorderRadius)), - ), - child: Column( - key: _progressBarKey, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - StreamBuilder( - stream: positionStream, - builder: (context, snapshot) { - // do not use stream snapshot because it is obsolete when switching between videos - final position = controller?.currentPosition.floor() ?? 0; - return Text( - formatFriendlyDuration(Duration(milliseconds: position)), - style: textStyle, - ); - }), - const Spacer(), - Text( - entry.durationText, - style: textStyle, - ), - ], - ), - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: Directionality( - // force directionality for `LinearProgressIndicator` - textDirection: TextDirection.ltr, - child: StreamBuilder( - stream: positionStream, - builder: (context, snapshot) { - // do not use stream snapshot because it is obsolete when switching between videos - var progress = controller?.progress ?? 0.0; - if (!progress.isFinite) progress = 0.0; - return LinearProgressIndicator( - value: progress, - backgroundColor: Colors.grey.shade700, - ); - }), - ), - ), - const Text( - // fake text below to match the height of the text above and center the whole thing - '', - style: textStyle, - ), - ], - ), - ), - ), - ), - ), - ); - } - - void _seekFromTap(Offset globalPosition) async { - if (controller == null) return; - final keyContext = _progressBarKey.currentContext!; - final box = keyContext.findRenderObject() as RenderBox; - final localPosition = box.globalToLocal(globalPosition); - await controller!.seekToProgress(localPosition.dx / box.size.width); - } } class _ButtonRow extends StatelessWidget { @@ -296,7 +205,7 @@ class _ButtonRow extends StatelessWidget { child = _buildFromListenable(controller?.canSetSpeedNotifier); break; case VideoAction.togglePlay: - child = _PlayToggler( + child = PlayToggler( controller: controller, onPressed: onPressed, ); @@ -347,7 +256,7 @@ class _ButtonRow extends StatelessWidget { Widget? child; switch (action) { case VideoAction.togglePlay: - child = _PlayToggler( + child = PlayToggler( controller: controller, isMenuItem: true, ); @@ -370,97 +279,3 @@ class _ButtonRow extends StatelessWidget { ); } } - -class _PlayToggler extends StatefulWidget { - final AvesVideoController? controller; - final bool isMenuItem; - final VoidCallback? onPressed; - - const _PlayToggler({ - required this.controller, - this.isMenuItem = false, - this.onPressed, - }); - - @override - State<_PlayToggler> createState() => _PlayTogglerState(); -} - -class _PlayTogglerState extends State<_PlayToggler> with SingleTickerProviderStateMixin { - final List _subscriptions = []; - late AnimationController _playPauseAnimation; - - AvesVideoController? get controller => widget.controller; - - bool get isPlaying => controller?.isPlaying ?? false; - - @override - void initState() { - super.initState(); - _playPauseAnimation = AnimationController( - duration: context.read().iconAnimation, - vsync: this, - ); - _registerWidget(widget); - } - - @override - void didUpdateWidget(covariant _PlayToggler oldWidget) { - super.didUpdateWidget(oldWidget); - _unregisterWidget(oldWidget); - _registerWidget(widget); - } - - @override - void dispose() { - _unregisterWidget(widget); - _playPauseAnimation.dispose(); - super.dispose(); - } - - void _registerWidget(_PlayToggler widget) { - final controller = widget.controller; - if (controller != null) { - _subscriptions.add(controller.statusStream.listen(_onStatusChange)); - _onStatusChange(controller.status); - } - } - - void _unregisterWidget(_PlayToggler widget) { - _subscriptions - ..forEach((sub) => sub.cancel()) - ..clear(); - } - - @override - Widget build(BuildContext context) { - if (widget.isMenuItem) { - return isPlaying - ? MenuRow( - text: context.l10n.videoActionPause, - icon: const Icon(AIcons.pause), - ) - : MenuRow( - text: context.l10n.videoActionPlay, - icon: const Icon(AIcons.play), - ); - } - return IconButton( - icon: AnimatedIcon( - icon: AnimatedIcons.play_pause, - progress: _playPauseAnimation, - ), - onPressed: widget.onPressed, - tooltip: isPlaying ? context.l10n.videoActionPause : context.l10n.videoActionPlay, - ); - } - - void _onStatusChange(VideoStatus status) { - final status = _playPauseAnimation.status; - if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) { - _playPauseAnimation.forward(); - } else if (!isPlaying && status != AnimationStatus.reverse && status != AnimationStatus.dismissed) { - _playPauseAnimation.reverse(); - } - } -} diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index 7caf6446d..4aa7331d1 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -7,11 +7,13 @@ Color overlayBackgroundColor({required bool blurred}) => blurred ? Colors.black2 class OverlayButton extends StatelessWidget { final Animation scale; + final BorderRadius? borderRadius; final Widget child; const OverlayButton({ Key? key, this.scale = kAlwaysCompleteAnimation, + this.borderRadius, required this.child, }) : super(key: key); @@ -20,20 +22,37 @@ class OverlayButton extends StatelessWidget { final blurred = settings.enableOverlayBlurEffect; return ScaleTransition( scale: scale, - child: BlurredOval( - enabled: blurred, - child: Material( - type: MaterialType.circle, - color: overlayBackgroundColor(blurred: blurred), - child: Ink( - decoration: BoxDecoration( - border: AvesBorder.border, - shape: BoxShape.circle, + child: borderRadius != null + ? BlurredRRect( + enabled: blurred, + borderRadius: borderRadius, + child: Material( + type: MaterialType.button, + borderRadius: borderRadius, + color: overlayBackgroundColor(blurred: blurred), + child: Ink( + decoration: BoxDecoration( + border: AvesBorder.border, + borderRadius: borderRadius, + ), + child: child, + ), + ), + ) + : BlurredOval( + enabled: blurred, + child: Material( + type: MaterialType.circle, + color: overlayBackgroundColor(blurred: blurred), + child: Ink( + decoration: BoxDecoration( + border: AvesBorder.border, + shape: BoxShape.circle, + ), + child: child, + ), + ), ), - child: child, - ), - ), - ), ); } @@ -61,7 +80,7 @@ class OverlayTextButton extends StatelessWidget { final blurred = settings.enableOverlayBlurEffect; return SizeTransition( sizeFactor: scale, - child: BlurredRRect( + child: BlurredRRect.all( enabled: blurred, borderRadius: _borderRadius, child: OutlinedButton( diff --git a/untranslated.json b/untranslated.json index e90c29dc4..cbd776a7b 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,58 +1,100 @@ { "de": [ "entryActionConvert", + "videoControlsNone", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", "settingsViewerShowOverlayThumbnails", - "settingsGesturesTile", - "settingsGesturesTitle", + "settingsVideoControlsTile", + "settingsVideoControlsTitle", + "settingsVideoButtonsTile", + "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek" ], "es": [ + "videoControlsNone", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", "settingsViewerShowOverlayThumbnails", - "settingsGesturesTile", - "settingsGesturesTitle", + "settingsVideoControlsTile", + "settingsVideoControlsTitle", + "settingsVideoButtonsTile", + "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek" ], "fr": [ + "videoControlsNone", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", "settingsViewerShowOverlayThumbnails", - "settingsGesturesTile", - "settingsGesturesTitle", + "settingsVideoControlsTile", + "settingsVideoControlsTitle", + "settingsVideoButtonsTile", + "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek" ], "id": [ + "videoControlsNone", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", "settingsViewerShowOverlayThumbnails", - "settingsGesturesTile", - "settingsGesturesTitle", + "settingsVideoControlsTile", + "settingsVideoControlsTitle", + "settingsVideoButtonsTile", + "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek" ], "ko": [ + "videoControlsNone", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", "settingsViewerShowOverlayThumbnails", - "settingsGesturesTile", - "settingsGesturesTitle", + "settingsVideoControlsTile", + "settingsVideoControlsTitle", + "settingsVideoButtonsTile", + "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek" ], "pt": [ + "videoControlsNone", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", "settingsViewerShowOverlayThumbnails", - "settingsGesturesTile", - "settingsGesturesTitle", + "settingsVideoControlsTile", + "settingsVideoControlsTitle", + "settingsVideoButtonsTile", + "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek" ], "ru": [ "entryActionConvert", + "videoControlsNone", + "videoControlsPlay", + "videoControlsPlaySeek", + "videoControlsPlayOutside", "settingsViewerShowOverlayThumbnails", - "settingsGesturesTile", - "settingsGesturesTitle", + "settingsVideoControlsTile", + "settingsVideoControlsTitle", + "settingsVideoButtonsTile", + "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", "settingsVideoGestureSideDoubleTapSeek" ]