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",
|
||||
"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",
|
||||
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -18,3 +18,5 @@ enum KeepScreenOn { never, viewerOnly, always }
|
|||
enum UnitSystem { metric, imperial }
|
||||
|
||||
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 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:
|
||||
|
|
|
@ -96,14 +96,12 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
if (uniqueNames.length < names.length) {
|
||||
final value = await showDialog<NameConflictStrategy>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AvesSelectionDialog<NameConflictStrategy>(
|
||||
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<NameConflictStrategy>(
|
||||
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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<EntryMapStyle>(
|
||||
await showSelectionDialog<EntryMapStyle>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AvesSelectionDialog<EntryMapStyle>(
|
||||
initialValue: initialStyle,
|
||||
options: Map.fromEntries(availableStyles.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.mapStyleTitle,
|
||||
);
|
||||
},
|
||||
builder: (context) => AvesSelectionDialog<EntryMapStyle>(
|
||||
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(
|
||||
|
|
|
@ -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<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);
|
||||
|
||||
class AvesSelectionDialog<T> extends StatefulWidget {
|
||||
|
|
|
@ -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<AccessibilityAnimations>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<AccessibilityAnimations>(
|
||||
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<AccessibilityAnimations>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<AccessibilityAnimations>(
|
||||
initialValue: currentAnimations,
|
||||
options: Map.fromEntries(AccessibilityAnimations.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsRemoveAnimationsTitle,
|
||||
),
|
||||
onSelection: (v) => settings.accessibilityAnimations = v,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,19 +36,15 @@ class _TimeToTakeActionTileState extends State<TimeToTakeActionTile> {
|
|||
return ListTile(
|
||||
title: Text(context.l10n.settingsTimeToTakeActionTile),
|
||||
subtitle: Text(currentTimeToTakeAction.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<AccessibilityTimeout>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<AccessibilityTimeout>(
|
||||
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<AccessibilityTimeout>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<AccessibilityTimeout>(
|
||||
initialValue: currentTimeToTakeAction,
|
||||
options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: context.l10n.settingsTimeToTakeActionTitle,
|
||||
),
|
||||
onSelection: (v) => settings.timeToTakeAction = v,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -45,37 +45,29 @@ class LanguageSection extends StatelessWidget {
|
|||
ListTile(
|
||||
title: Text(l10n.settingsCoordinateFormatTile),
|
||||
subtitle: Text(currentCoordinateFormat.getName(context)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<CoordinateFormat>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
|
||||
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<CoordinateFormat>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
|
||||
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<UnitSystem>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<UnitSystem>(
|
||||
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<UnitSystem>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<UnitSystem>(
|
||||
initialValue: currentUnitSystem,
|
||||
options: Map.fromEntries(UnitSystem.values.map((v) => MapEntry(v, v.getName(context)))),
|
||||
title: l10n.settingsUnitSystemTitle,
|
||||
),
|
||||
onSelection: (v) => settings.unitSystem = v,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -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<Locale>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<Locale>(
|
||||
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<Locale>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<Locale>(
|
||||
initialValue: settings.locale ?? _systemLocaleOption,
|
||||
options: _getLocaleOptions(context),
|
||||
title: context.l10n.settingsLanguage,
|
||||
),
|
||||
onSelection: (v) => settings.locale = v == _systemLocaleOption ? null : v,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<HomePageSetting>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<HomePageSetting>(
|
||||
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<HomePageSetting>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<HomePageSetting>(
|
||||
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<KeepScreenOn>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<KeepScreenOn>(
|
||||
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<KeepScreenOn>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<KeepScreenOn>(
|
||||
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,
|
||||
|
|
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(
|
||||
title: Text(context.l10n.settingsSubtitleThemeTextAlignmentTile),
|
||||
subtitle: Text(_getTextAlignName(context, settings.subtitleTextAlignment)),
|
||||
onTap: () async {
|
||||
final value = await showDialog<TextAlign>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<TextAlign>(
|
||||
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<TextAlign>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<TextAlign>(
|
||||
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,
|
||||
|
|
|
@ -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<VideoLoopMode>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
|
||||
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<VideoLoopMode>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<VideoLoopMode>(
|
||||
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(),
|
||||
];
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ class VideoActionEditorPage extends StatelessWidget {
|
|||
return QuickActionEditorPage<VideoAction>(
|
||||
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,
|
||||
|
|
|
@ -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';
|
||||
|
|
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/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<VideoControlOverlay> with SingleTickerProviderStateMixin {
|
||||
final GlobalKey _progressBarKey = GlobalKey(debugLabel: 'video-progress-bar');
|
||||
bool _playingOnDragStart = false;
|
||||
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
||||
Animation<double> get scale => widget.scale;
|
||||
|
@ -89,7 +82,7 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> 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<VideoControlOverlay> 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<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 {
|
||||
|
@ -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<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 {
|
||||
final Animation<double> 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(
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue