video: control buttons
This commit is contained in:
parent
6b62806ddb
commit
054910f7b3
26 changed files with 709 additions and 464 deletions
|
@ -152,6 +152,11 @@
|
||||||
"videoLoopModeShortOnly": "Short videos only",
|
"videoLoopModeShortOnly": "Short videos only",
|
||||||
"videoLoopModeAlways": "Always",
|
"videoLoopModeAlways": "Always",
|
||||||
|
|
||||||
|
"videoControlsNone": "None",
|
||||||
|
"videoControlsPlay": "Play",
|
||||||
|
"videoControlsPlaySeek": "Play & seek",
|
||||||
|
"videoControlsPlayOutside": "Play outside",
|
||||||
|
|
||||||
"mapStyleGoogleNormal": "Google Maps",
|
"mapStyleGoogleNormal": "Google Maps",
|
||||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||||
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
|
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
|
||||||
|
@ -641,8 +646,10 @@
|
||||||
"settingsSubtitleThemeTextAlignmentCenter": "Center",
|
"settingsSubtitleThemeTextAlignmentCenter": "Center",
|
||||||
"settingsSubtitleThemeTextAlignmentRight": "Right",
|
"settingsSubtitleThemeTextAlignmentRight": "Right",
|
||||||
|
|
||||||
"settingsGesturesTile": "Gestures",
|
"settingsVideoControlsTile": "Controls",
|
||||||
"settingsGesturesTitle": "Gestures",
|
"settingsVideoControlsTitle": "Controls",
|
||||||
|
"settingsVideoButtonsTile": "Buttons",
|
||||||
|
"settingsVideoButtonsTitle": "Buttons",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause",
|
"settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause",
|
||||||
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
|
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
|
||||||
|
|
||||||
|
|
|
@ -3,26 +3,24 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
enum VideoAction {
|
enum VideoAction {
|
||||||
captureFrame,
|
// controls
|
||||||
playOutside,
|
playOutside,
|
||||||
replay10,
|
replay10,
|
||||||
skip10,
|
skip10,
|
||||||
|
togglePlay,
|
||||||
|
// menu
|
||||||
|
captureFrame,
|
||||||
selectStreams,
|
selectStreams,
|
||||||
setSpeed,
|
setSpeed,
|
||||||
settings,
|
settings,
|
||||||
togglePlay,
|
|
||||||
// TODO TLAD [video] toggle mute
|
// TODO TLAD [video] toggle mute
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoActions {
|
class VideoActions {
|
||||||
static const all = [
|
static const menu = [
|
||||||
VideoAction.togglePlay,
|
|
||||||
VideoAction.captureFrame,
|
VideoAction.captureFrame,
|
||||||
VideoAction.setSpeed,
|
VideoAction.setSpeed,
|
||||||
VideoAction.selectStreams,
|
VideoAction.selectStreams,
|
||||||
VideoAction.replay10,
|
|
||||||
VideoAction.skip10,
|
|
||||||
VideoAction.playOutside,
|
|
||||||
VideoAction.settings,
|
VideoAction.settings,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ class SettingsDefaults {
|
||||||
static const enableVideoAutoPlay = false;
|
static const enableVideoAutoPlay = false;
|
||||||
static const videoLoopMode = VideoLoopMode.shortOnly;
|
static const videoLoopMode = VideoLoopMode.shortOnly;
|
||||||
static const videoShowRawTimedText = false;
|
static const videoShowRawTimedText = false;
|
||||||
|
static const videoControls = VideoControls.play;
|
||||||
static const videoGestureDoubleTapTogglePlay = false;
|
static const videoGestureDoubleTapTogglePlay = false;
|
||||||
static const videoGestureSideDoubleTapSeek = true;
|
static const videoGestureSideDoubleTapSeek = true;
|
||||||
|
|
||||||
|
|
|
@ -18,3 +18,5 @@ enum KeepScreenOn { never, viewerOnly, always }
|
||||||
enum UnitSystem { metric, imperial }
|
enum UnitSystem { metric, imperial }
|
||||||
|
|
||||||
enum VideoLoopMode { never, shortOnly, always }
|
enum VideoLoopMode { never, shortOnly, always }
|
||||||
|
|
||||||
|
enum VideoControls { none, play, playSeek, playOutside }
|
||||||
|
|
19
lib/model/settings/enums/video_controls.dart
Normal file
19
lib/model/settings/enums/video_controls.dart
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -95,6 +95,7 @@ class Settings extends ChangeNotifier {
|
||||||
static const enableVideoAutoPlayKey = 'video_auto_play';
|
static const enableVideoAutoPlayKey = 'video_auto_play';
|
||||||
static const videoLoopModeKey = 'video_loop';
|
static const videoLoopModeKey = 'video_loop';
|
||||||
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
|
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
|
||||||
|
static const videoControlsKey = 'video_controls';
|
||||||
static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play';
|
static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play';
|
||||||
static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip';
|
static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip';
|
||||||
|
|
||||||
|
@ -438,6 +439,10 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue);
|
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);
|
bool get videoGestureDoubleTapTogglePlay => getBoolOrDefault(videoGestureDoubleTapTogglePlayKey, SettingsDefaults.videoGestureDoubleTapTogglePlay);
|
||||||
|
|
||||||
set videoGestureDoubleTapTogglePlay(bool newValue) => setAndNotify(videoGestureDoubleTapTogglePlayKey, newValue);
|
set videoGestureDoubleTapTogglePlay(bool newValue) => setAndNotify(videoGestureDoubleTapTogglePlayKey, newValue);
|
||||||
|
@ -686,6 +691,7 @@ class Settings extends ChangeNotifier {
|
||||||
case tagSortFactorKey:
|
case tagSortFactorKey:
|
||||||
case imageBackgroundKey:
|
case imageBackgroundKey:
|
||||||
case videoLoopModeKey:
|
case videoLoopModeKey:
|
||||||
|
case videoControlsKey:
|
||||||
case subtitleTextAlignmentKey:
|
case subtitleTextAlignmentKey:
|
||||||
case infoMapStyleKey:
|
case infoMapStyleKey:
|
||||||
case coordinateFormatKey:
|
case coordinateFormatKey:
|
||||||
|
|
|
@ -96,14 +96,12 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||||
if (uniqueNames.length < names.length) {
|
if (uniqueNames.length < names.length) {
|
||||||
final value = await showDialog<NameConflictStrategy>(
|
final value = await showDialog<NameConflictStrategy>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) => AvesSelectionDialog<NameConflictStrategy>(
|
||||||
return AvesSelectionDialog<NameConflictStrategy>(
|
initialValue: nameConflictStrategy,
|
||||||
initialValue: nameConflictStrategy,
|
options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))),
|
message: originAlbums.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage,
|
||||||
message: originAlbums.length == 1 ? l10n.nameConflictDialogSingleSourceMessage : l10n.nameConflictDialogMultipleSourceMessage,
|
confirmationButtonLabel: l10n.continueButtonLabel,
|
||||||
confirmationButtonLabel: l10n.continueButtonLabel,
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
nameConflictStrategy = value;
|
nameConflictStrategy = value;
|
||||||
|
|
|
@ -29,7 +29,7 @@ class BlurredRect extends StatelessWidget {
|
||||||
|
|
||||||
class BlurredRRect extends StatelessWidget {
|
class BlurredRRect extends StatelessWidget {
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final double borderRadius;
|
final BorderRadius? borderRadius;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const BlurredRRect({
|
const BlurredRRect({
|
||||||
|
@ -39,17 +39,31 @@ class BlurredRRect extends StatelessWidget {
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
}) : 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return enabled
|
return ClipRRect(
|
||||||
? ClipRRect(
|
borderRadius: borderRadius,
|
||||||
borderRadius: BorderRadius.all(Radius.circular(borderRadius)),
|
child: enabled
|
||||||
child: BackdropFilter(
|
? BackdropFilter(
|
||||||
filter: _filter,
|
filter: _filter,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
)
|
||||||
)
|
: child,
|
||||||
: child;
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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/info/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -139,21 +138,15 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || canUseGoogleMaps);
|
final availableStyles = EntryMapStyle.values.where((style) => !style.isGoogleMaps || canUseGoogleMaps);
|
||||||
final preferredStyle = settings.infoMapStyle;
|
final preferredStyle = settings.infoMapStyle;
|
||||||
final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first;
|
final initialStyle = availableStyles.contains(preferredStyle) ? preferredStyle : availableStyles.first;
|
||||||
final style = await showDialog<EntryMapStyle>(
|
await showSelectionDialog<EntryMapStyle>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) => AvesSelectionDialog<EntryMapStyle>(
|
||||||
return AvesSelectionDialog<EntryMapStyle>(
|
initialValue: initialStyle,
|
||||||
initialValue: initialStyle,
|
options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))),
|
title: context.l10n.mapStyleTitle,
|
||||||
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,
|
tooltip: context.l10n.mapStyleTooltip,
|
||||||
),
|
),
|
||||||
|
@ -313,7 +306,7 @@ class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterCh
|
||||||
);
|
);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.all(widget.padding),
|
padding: EdgeInsets.all(widget.padding),
|
||||||
child: BlurredRRect(
|
child: BlurredRRect.all(
|
||||||
enabled: blurred,
|
enabled: blurred,
|
||||||
borderRadius: AvesFilterChip.defaultRadius,
|
borderRadius: AvesFilterChip.defaultRadius,
|
||||||
child: AvesFilterChip(
|
child: AvesFilterChip(
|
||||||
|
|
|
@ -1,8 +1,26 @@
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
import 'aves_dialog.dart';
|
import 'aves_dialog.dart';
|
||||||
|
|
||||||
|
Future<void> showSelectionDialog<T>({
|
||||||
|
required BuildContext context,
|
||||||
|
required WidgetBuilder builder,
|
||||||
|
required void Function(T value) onSelection,
|
||||||
|
}) async {
|
||||||
|
final value = await showDialog<T>(
|
||||||
|
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<T> = String Function(T value);
|
typedef TextBuilder<T> = String Function(T value);
|
||||||
|
|
||||||
class AvesSelectionDialog<T> extends StatefulWidget {
|
class AvesSelectionDialog<T> extends StatefulWidget {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||||
import 'package:aves/model/settings/enums/enums.dart';
|
import 'package:aves/model/settings/enums/enums.dart';
|
||||||
import 'package:aves/model/settings/settings.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/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class RemoveAnimationsTile extends StatelessWidget {
|
class RemoveAnimationsTile extends StatelessWidget {
|
||||||
|
@ -18,21 +16,15 @@ class RemoveAnimationsTile extends StatelessWidget {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(context.l10n.settingsRemoveAnimationsTile),
|
title: Text(context.l10n.settingsRemoveAnimationsTile),
|
||||||
subtitle: Text(currentAnimations.getName(context)),
|
subtitle: Text(currentAnimations.getName(context)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<AccessibilityAnimations>(
|
||||||
final value = await showDialog<AccessibilityAnimations>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<AccessibilityAnimations>(
|
||||||
builder: (context) => AvesSelectionDialog<AccessibilityAnimations>(
|
initialValue: currentAnimations,
|
||||||
initialValue: currentAnimations,
|
options: Map.fromEntries(AccessibilityAnimations.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(AccessibilityAnimations.values.map((v) => MapEntry(v, v.getName(context)))),
|
title: context.l10n.settingsRemoveAnimationsTitle,
|
||||||
title: context.l10n.settingsRemoveAnimationsTitle,
|
),
|
||||||
),
|
onSelection: (v) => settings.accessibilityAnimations = v,
|
||||||
);
|
),
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,19 +36,15 @@ class _TimeToTakeActionTileState extends State<TimeToTakeActionTile> {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(context.l10n.settingsTimeToTakeActionTile),
|
title: Text(context.l10n.settingsTimeToTakeActionTile),
|
||||||
subtitle: Text(currentTimeToTakeAction.getName(context)),
|
subtitle: Text(currentTimeToTakeAction.getName(context)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<AccessibilityTimeout>(
|
||||||
final value = await showDialog<AccessibilityTimeout>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<AccessibilityTimeout>(
|
||||||
builder: (context) => AvesSelectionDialog<AccessibilityTimeout>(
|
initialValue: currentTimeToTakeAction,
|
||||||
initialValue: currentTimeToTakeAction,
|
options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))),
|
title: context.l10n.settingsTimeToTakeActionTitle,
|
||||||
title: context.l10n.settingsTimeToTakeActionTitle,
|
),
|
||||||
),
|
onSelection: (v) => settings.timeToTakeAction = v,
|
||||||
);
|
),
|
||||||
if (value != null) {
|
|
||||||
settings.timeToTakeAction = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,37 +45,29 @@ class LanguageSection extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(l10n.settingsCoordinateFormatTile),
|
title: Text(l10n.settingsCoordinateFormatTile),
|
||||||
subtitle: Text(currentCoordinateFormat.getName(context)),
|
subtitle: Text(currentCoordinateFormat.getName(context)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<CoordinateFormat>(
|
||||||
final value = await showDialog<CoordinateFormat>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
|
||||||
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
|
initialValue: currentCoordinateFormat,
|
||||||
initialValue: currentCoordinateFormat,
|
options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))),
|
optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo),
|
||||||
optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo),
|
title: l10n.settingsCoordinateFormatTitle,
|
||||||
title: l10n.settingsCoordinateFormatTitle,
|
),
|
||||||
),
|
onSelection: (v) => settings.coordinateFormat = v,
|
||||||
);
|
),
|
||||||
if (value != null) {
|
|
||||||
settings.coordinateFormat = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(l10n.settingsUnitSystemTile),
|
title: Text(l10n.settingsUnitSystemTile),
|
||||||
subtitle: Text(currentUnitSystem.getName(context)),
|
subtitle: Text(currentUnitSystem.getName(context)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<UnitSystem>(
|
||||||
final value = await showDialog<UnitSystem>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<UnitSystem>(
|
||||||
builder: (context) => AvesSelectionDialog<UnitSystem>(
|
initialValue: currentUnitSystem,
|
||||||
initialValue: currentUnitSystem,
|
options: Map.fromEntries(UnitSystem.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(UnitSystem.values.map((v) => MapEntry(v, v.getName(context)))),
|
title: l10n.settingsUnitSystemTitle,
|
||||||
title: l10n.settingsUnitSystemTitle,
|
),
|
||||||
),
|
onSelection: (v) => settings.unitSystem = v,
|
||||||
);
|
),
|
||||||
if (value != null) {
|
|
||||||
settings.unitSystem = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,13 +2,11 @@ import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/l10n/l10n.dart';
|
import 'package:aves/l10n/l10n.dart';
|
||||||
import 'package:aves/model/settings/settings.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/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||||
import 'package:aves/widgets/settings/language/locales.dart';
|
import 'package:aves/widgets/settings/language/locales.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class LocaleTile extends StatelessWidget {
|
class LocaleTile extends StatelessWidget {
|
||||||
|
@ -28,21 +26,15 @@ class LocaleTile extends StatelessWidget {
|
||||||
return Text(locale == null ? context.l10n.settingsSystemDefault : _getLocaleName(locale));
|
return Text(locale == null ? context.l10n.settingsSystemDefault : _getLocaleName(locale));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<Locale>(
|
||||||
final value = await showDialog<Locale>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<Locale>(
|
||||||
builder: (context) => AvesSelectionDialog<Locale>(
|
initialValue: settings.locale ?? _systemLocaleOption,
|
||||||
initialValue: settings.locale ?? _systemLocaleOption,
|
options: _getLocaleOptions(context),
|
||||||
options: _getLocaleOptions(context),
|
title: context.l10n.settingsLanguage,
|
||||||
title: context.l10n.settingsLanguage,
|
),
|
||||||
),
|
onSelection: (v) => settings.locale = v == _systemLocaleOption ? null : v,
|
||||||
);
|
),
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,38 +39,30 @@ class NavigationSection extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.settingsHome),
|
title: Text(context.l10n.settingsHome),
|
||||||
subtitle: Text(currentHomePage.getName(context)),
|
subtitle: Text(currentHomePage.getName(context)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<HomePageSetting>(
|
||||||
final value = await showDialog<HomePageSetting>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<HomePageSetting>(
|
||||||
builder: (context) => AvesSelectionDialog<HomePageSetting>(
|
initialValue: currentHomePage,
|
||||||
initialValue: currentHomePage,
|
options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))),
|
title: context.l10n.settingsHome,
|
||||||
title: context.l10n.settingsHome,
|
),
|
||||||
),
|
onSelection: (v) => settings.homePage = v,
|
||||||
);
|
),
|
||||||
if (value != null) {
|
|
||||||
settings.homePage = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const NavigationDrawerTile(),
|
const NavigationDrawerTile(),
|
||||||
const ConfirmationDialogTile(),
|
const ConfirmationDialogTile(),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.settingsKeepScreenOnTile),
|
title: Text(context.l10n.settingsKeepScreenOnTile),
|
||||||
subtitle: Text(currentKeepScreenOn.getName(context)),
|
subtitle: Text(currentKeepScreenOn.getName(context)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<KeepScreenOn>(
|
||||||
final value = await showDialog<KeepScreenOn>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<KeepScreenOn>(
|
||||||
builder: (context) => AvesSelectionDialog<KeepScreenOn>(
|
initialValue: currentKeepScreenOn,
|
||||||
initialValue: currentKeepScreenOn,
|
options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))),
|
title: context.l10n.settingsKeepScreenOnTitle,
|
||||||
title: context.l10n.settingsKeepScreenOnTitle,
|
),
|
||||||
),
|
onSelection: (v) => settings.keepScreenOn = v,
|
||||||
);
|
),
|
||||||
if (value != null) {
|
|
||||||
settings.keepScreenOn = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: currentMustBackTwiceToExit,
|
value: currentMustBackTwiceToExit,
|
||||||
|
|
80
lib/widgets/settings/video/controls.dart
Normal file
80
lib/widgets/settings/video/controls.dart
Normal file
|
@ -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<Settings, VideoControls>(
|
||||||
|
selector: (context, s) => s.videoControls,
|
||||||
|
builder: (context, current, child) => ListTile(
|
||||||
|
title: Text(context.l10n.settingsVideoButtonsTile),
|
||||||
|
subtitle: Text(current.getName(context)),
|
||||||
|
onTap: () => showSelectionDialog<VideoControls>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AvesSelectionDialog<VideoControls>(
|
||||||
|
initialValue: current,
|
||||||
|
options: Map.fromEntries(VideoControls.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
|
title: context.l10n.settingsVideoButtonsTitle,
|
||||||
|
),
|
||||||
|
onSelection: (v) => settings.videoControls = v,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Selector<Settings, bool>(
|
||||||
|
selector: (context, s) => s.videoGestureDoubleTapTogglePlay,
|
||||||
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
value: current,
|
||||||
|
onChanged: (v) => settings.videoGestureDoubleTapTogglePlay = v,
|
||||||
|
title: Text(context.l10n.settingsVideoGestureDoubleTapTogglePlay),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Selector<Settings, bool>(
|
||||||
|
selector: (context, s) => s.videoGestureSideDoubleTapSeek,
|
||||||
|
builder: (context, current, child) => SwitchListTile(
|
||||||
|
value: current,
|
||||||
|
onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v,
|
||||||
|
title: Text(context.l10n.settingsVideoGestureSideDoubleTapSeek),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Settings, bool>(
|
|
||||||
selector: (context, s) => s.videoGestureDoubleTapTogglePlay,
|
|
||||||
builder: (context, current, child) => SwitchListTile(
|
|
||||||
value: current,
|
|
||||||
onChanged: (v) => settings.videoGestureDoubleTapTogglePlay = v,
|
|
||||||
title: Text(context.l10n.settingsVideoGestureDoubleTapTogglePlay),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Selector<Settings, bool>(
|
|
||||||
selector: (context, s) => s.videoGestureSideDoubleTapSeek,
|
|
||||||
builder: (context, current, child) => SwitchListTile(
|
|
||||||
value: current,
|
|
||||||
onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v,
|
|
||||||
title: Text(context.l10n.settingsVideoGestureSideDoubleTapSeek),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -57,19 +57,15 @@ class SubtitleThemePage extends StatelessWidget {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(context.l10n.settingsSubtitleThemeTextAlignmentTile),
|
title: Text(context.l10n.settingsSubtitleThemeTextAlignmentTile),
|
||||||
subtitle: Text(_getTextAlignName(context, settings.subtitleTextAlignment)),
|
subtitle: Text(_getTextAlignName(context, settings.subtitleTextAlignment)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<TextAlign>(
|
||||||
final value = await showDialog<TextAlign>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<TextAlign>(
|
||||||
builder: (context) => AvesSelectionDialog<TextAlign>(
|
initialValue: settings.subtitleTextAlignment,
|
||||||
initialValue: settings.subtitleTextAlignment,
|
options: Map.fromEntries(textAlignOptions.map((v) => MapEntry(v, _getTextAlignName(context, v)))),
|
||||||
options: Map.fromEntries(textAlignOptions.map((v) => MapEntry(v, _getTextAlignName(context, v)))),
|
title: context.l10n.settingsSubtitleThemeTextAlignmentTitle,
|
||||||
title: context.l10n.settingsSubtitleThemeTextAlignmentTitle,
|
),
|
||||||
),
|
onSelection: (v) => settings.subtitleTextAlignment = v,
|
||||||
);
|
),
|
||||||
if (value != null) {
|
|
||||||
settings.subtitleTextAlignment = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SliderListTile(
|
SliderListTile(
|
||||||
title: context.l10n.settingsSubtitleThemeTextSize,
|
title: context.l10n.settingsSubtitleThemeTextSize,
|
||||||
|
|
|
@ -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/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||||
import 'package:aves/widgets/settings/common/tile_leading.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/subtitle_theme.dart';
|
||||||
import 'package:aves/widgets/settings/video/video_actions_editor.dart';
|
import 'package:aves/widgets/settings/video/video_actions_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -59,22 +59,18 @@ class VideoSection extends StatelessWidget {
|
||||||
builder: (context, current, child) => ListTile(
|
builder: (context, current, child) => ListTile(
|
||||||
title: Text(context.l10n.settingsVideoLoopModeTile),
|
title: Text(context.l10n.settingsVideoLoopModeTile),
|
||||||
subtitle: Text(current.getName(context)),
|
subtitle: Text(current.getName(context)),
|
||||||
onTap: () async {
|
onTap: () => showSelectionDialog<VideoLoopMode>(
|
||||||
final value = await showDialog<VideoLoopMode>(
|
context: context,
|
||||||
context: context,
|
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
|
||||||
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
|
initialValue: current,
|
||||||
initialValue: current,
|
options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||||
options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))),
|
title: context.l10n.settingsVideoLoopModeTitle,
|
||||||
title: context.l10n.settingsVideoLoopModeTitle,
|
),
|
||||||
),
|
onSelection: (v) => settings.videoLoopMode = v,
|
||||||
);
|
),
|
||||||
if (value != null) {
|
|
||||||
settings.videoLoopMode = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VideoGesturesTile(),
|
const VideoControlsTile(),
|
||||||
const SubtitleThemeTile(),
|
const SubtitleThemeTile(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class VideoActionEditorPage extends StatelessWidget {
|
||||||
return QuickActionEditorPage<VideoAction>(
|
return QuickActionEditorPage<VideoAction>(
|
||||||
title: context.l10n.settingsVideoQuickActionEditorTitle,
|
title: context.l10n.settingsVideoQuickActionEditorTitle,
|
||||||
bannerText: context.l10n.settingsViewerQuickActionEditorBanner,
|
bannerText: context.l10n.settingsViewerQuickActionEditorBanner,
|
||||||
allAvailableActions: VideoActions.all,
|
allAvailableActions: VideoActions.menu,
|
||||||
actionIcon: (action) => action.getIcon(),
|
actionIcon: (action) => action.getIcon(),
|
||||||
actionText: (context, action) => action.getText(context),
|
actionText: (context, action) => action.getText(context),
|
||||||
load: () => settings.videoQuickActions,
|
load: () => settings.videoQuickActions,
|
||||||
|
|
|
@ -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/multipage/controller.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/bottom/common.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/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/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/top.dart';
|
import 'package:aves/widgets/viewer/overlay/top.dart';
|
||||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||||
|
|
203
lib/widgets/viewer/overlay/bottom/video/controls.dart
Normal file
203
lib/widgets/viewer/overlay/bottom/video/controls.dart
Normal file
|
@ -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<double> 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<Settings, VideoControls>(
|
||||||
|
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<PlayToggler> createState() => _PlayTogglerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStateMixin {
|
||||||
|
final List<StreamSubscription> _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<DurationsData>().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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
lib/widgets/viewer/overlay/bottom/video/progress_bar.dart
Normal file
135
lib/widgets/viewer/overlay/bottom/video/progress_bar.dart
Normal file
|
@ -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<double> scale;
|
||||||
|
|
||||||
|
const VideoProgressBar({
|
||||||
|
Key? key,
|
||||||
|
required this.controller,
|
||||||
|
required this.scale,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VideoProgressBar> createState() => _VideoProgressBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoProgressBarState extends State<VideoProgressBar> {
|
||||||
|
final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar');
|
||||||
|
bool _playingOnDragStart = false;
|
||||||
|
|
||||||
|
static const double radius = 123;
|
||||||
|
|
||||||
|
AvesVideoController? get controller => widget.controller;
|
||||||
|
|
||||||
|
Stream<int> 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<int>(
|
||||||
|
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<int>(
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,14 +4,10 @@ import 'package:aves/model/actions/video_actions.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/durations.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/menu.dart';
|
||||||
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/viewer/overlay/bottom/video/controls.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/viewer/overlay/bottom/video/progress_bar.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
|
||||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
@ -40,9 +36,6 @@ class VideoControlOverlay extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTickerProviderStateMixin {
|
class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTickerProviderStateMixin {
|
||||||
final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar');
|
|
||||||
bool _playingOnDragStart = false;
|
|
||||||
|
|
||||||
AvesEntry get entry => widget.entry;
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
||||||
Animation<double> get scale => widget.scale;
|
Animation<double> get scale => widget.scale;
|
||||||
|
@ -89,7 +82,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
selector: (context, s) => s.videoQuickActions,
|
selector: (context, s) => s.videoQuickActions,
|
||||||
builder: (context, videoQuickActions, child) {
|
builder: (context, videoQuickActions, child) {
|
||||||
final quickActions = videoQuickActions.take(availableCount - 1).toList();
|
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(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
@ -103,7 +96,21 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
onActionMenuOpened: widget.onActionMenuOpened,
|
onActionMenuOpened: widget.onActionMenuOpened,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
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<VideoControlOverlay> 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<int>(
|
|
||||||
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<int>(
|
|
||||||
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 {
|
class _ButtonRow extends StatelessWidget {
|
||||||
|
@ -296,7 +205,7 @@ class _ButtonRow extends StatelessWidget {
|
||||||
child = _buildFromListenable(controller?.canSetSpeedNotifier);
|
child = _buildFromListenable(controller?.canSetSpeedNotifier);
|
||||||
break;
|
break;
|
||||||
case VideoAction.togglePlay:
|
case VideoAction.togglePlay:
|
||||||
child = _PlayToggler(
|
child = PlayToggler(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
);
|
);
|
||||||
|
@ -347,7 +256,7 @@ class _ButtonRow extends StatelessWidget {
|
||||||
Widget? child;
|
Widget? child;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case VideoAction.togglePlay:
|
case VideoAction.togglePlay:
|
||||||
child = _PlayToggler(
|
child = PlayToggler(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
isMenuItem: true,
|
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<StreamSubscription> _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<DurationsData>().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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,11 +7,13 @@ Color overlayBackgroundColor({required bool blurred}) => blurred ? Colors.black2
|
||||||
|
|
||||||
class OverlayButton extends StatelessWidget {
|
class OverlayButton extends StatelessWidget {
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
|
final BorderRadius? borderRadius;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const OverlayButton({
|
const OverlayButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
this.scale = kAlwaysCompleteAnimation,
|
this.scale = kAlwaysCompleteAnimation,
|
||||||
|
this.borderRadius,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@ -20,20 +22,37 @@ class OverlayButton extends StatelessWidget {
|
||||||
final blurred = settings.enableOverlayBlurEffect;
|
final blurred = settings.enableOverlayBlurEffect;
|
||||||
return ScaleTransition(
|
return ScaleTransition(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
child: BlurredOval(
|
child: borderRadius != null
|
||||||
enabled: blurred,
|
? BlurredRRect(
|
||||||
child: Material(
|
enabled: blurred,
|
||||||
type: MaterialType.circle,
|
borderRadius: borderRadius,
|
||||||
color: overlayBackgroundColor(blurred: blurred),
|
child: Material(
|
||||||
child: Ink(
|
type: MaterialType.button,
|
||||||
decoration: BoxDecoration(
|
borderRadius: borderRadius,
|
||||||
border: AvesBorder.border,
|
color: overlayBackgroundColor(blurred: blurred),
|
||||||
shape: BoxShape.circle,
|
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;
|
final blurred = settings.enableOverlayBlurEffect;
|
||||||
return SizeTransition(
|
return SizeTransition(
|
||||||
sizeFactor: scale,
|
sizeFactor: scale,
|
||||||
child: BlurredRRect(
|
child: BlurredRRect.all(
|
||||||
enabled: blurred,
|
enabled: blurred,
|
||||||
borderRadius: _borderRadius,
|
borderRadius: _borderRadius,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
|
|
|
@ -1,58 +1,100 @@
|
||||||
{
|
{
|
||||||
"de": [
|
"de": [
|
||||||
"entryActionConvert",
|
"entryActionConvert",
|
||||||
|
"videoControlsNone",
|
||||||
|
"videoControlsPlay",
|
||||||
|
"videoControlsPlaySeek",
|
||||||
|
"videoControlsPlayOutside",
|
||||||
"settingsViewerShowOverlayThumbnails",
|
"settingsViewerShowOverlayThumbnails",
|
||||||
"settingsGesturesTile",
|
"settingsVideoControlsTile",
|
||||||
"settingsGesturesTitle",
|
"settingsVideoControlsTitle",
|
||||||
|
"settingsVideoButtonsTile",
|
||||||
|
"settingsVideoButtonsTitle",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek"
|
"settingsVideoGestureSideDoubleTapSeek"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
|
"videoControlsNone",
|
||||||
|
"videoControlsPlay",
|
||||||
|
"videoControlsPlaySeek",
|
||||||
|
"videoControlsPlayOutside",
|
||||||
"settingsViewerShowOverlayThumbnails",
|
"settingsViewerShowOverlayThumbnails",
|
||||||
"settingsGesturesTile",
|
"settingsVideoControlsTile",
|
||||||
"settingsGesturesTitle",
|
"settingsVideoControlsTitle",
|
||||||
|
"settingsVideoButtonsTile",
|
||||||
|
"settingsVideoButtonsTitle",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek"
|
"settingsVideoGestureSideDoubleTapSeek"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
|
"videoControlsNone",
|
||||||
|
"videoControlsPlay",
|
||||||
|
"videoControlsPlaySeek",
|
||||||
|
"videoControlsPlayOutside",
|
||||||
"settingsViewerShowOverlayThumbnails",
|
"settingsViewerShowOverlayThumbnails",
|
||||||
"settingsGesturesTile",
|
"settingsVideoControlsTile",
|
||||||
"settingsGesturesTitle",
|
"settingsVideoControlsTitle",
|
||||||
|
"settingsVideoButtonsTile",
|
||||||
|
"settingsVideoButtonsTitle",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek"
|
"settingsVideoGestureSideDoubleTapSeek"
|
||||||
],
|
],
|
||||||
|
|
||||||
"id": [
|
"id": [
|
||||||
|
"videoControlsNone",
|
||||||
|
"videoControlsPlay",
|
||||||
|
"videoControlsPlaySeek",
|
||||||
|
"videoControlsPlayOutside",
|
||||||
"settingsViewerShowOverlayThumbnails",
|
"settingsViewerShowOverlayThumbnails",
|
||||||
"settingsGesturesTile",
|
"settingsVideoControlsTile",
|
||||||
"settingsGesturesTitle",
|
"settingsVideoControlsTitle",
|
||||||
|
"settingsVideoButtonsTile",
|
||||||
|
"settingsVideoButtonsTitle",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek"
|
"settingsVideoGestureSideDoubleTapSeek"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ko": [
|
"ko": [
|
||||||
|
"videoControlsNone",
|
||||||
|
"videoControlsPlay",
|
||||||
|
"videoControlsPlaySeek",
|
||||||
|
"videoControlsPlayOutside",
|
||||||
"settingsViewerShowOverlayThumbnails",
|
"settingsViewerShowOverlayThumbnails",
|
||||||
"settingsGesturesTile",
|
"settingsVideoControlsTile",
|
||||||
"settingsGesturesTitle",
|
"settingsVideoControlsTitle",
|
||||||
|
"settingsVideoButtonsTile",
|
||||||
|
"settingsVideoButtonsTitle",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek"
|
"settingsVideoGestureSideDoubleTapSeek"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
|
"videoControlsNone",
|
||||||
|
"videoControlsPlay",
|
||||||
|
"videoControlsPlaySeek",
|
||||||
|
"videoControlsPlayOutside",
|
||||||
"settingsViewerShowOverlayThumbnails",
|
"settingsViewerShowOverlayThumbnails",
|
||||||
"settingsGesturesTile",
|
"settingsVideoControlsTile",
|
||||||
"settingsGesturesTitle",
|
"settingsVideoControlsTitle",
|
||||||
|
"settingsVideoButtonsTile",
|
||||||
|
"settingsVideoButtonsTitle",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek"
|
"settingsVideoGestureSideDoubleTapSeek"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
"entryActionConvert",
|
"entryActionConvert",
|
||||||
|
"videoControlsNone",
|
||||||
|
"videoControlsPlay",
|
||||||
|
"videoControlsPlaySeek",
|
||||||
|
"videoControlsPlayOutside",
|
||||||
"settingsViewerShowOverlayThumbnails",
|
"settingsViewerShowOverlayThumbnails",
|
||||||
"settingsGesturesTile",
|
"settingsVideoControlsTile",
|
||||||
"settingsGesturesTitle",
|
"settingsVideoControlsTitle",
|
||||||
|
"settingsVideoButtonsTile",
|
||||||
|
"settingsVideoButtonsTitle",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek"
|
"settingsVideoGestureSideDoubleTapSeek"
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue