#560 video: action to lock viewer

This commit is contained in:
Thibault Deckers 2023-03-25 23:31:55 +01:00
parent 3eb1b30552
commit 2e0b15787f
12 changed files with 278 additions and 43 deletions

View file

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
### Added
- Collection: optional support for Samsung and Sony burst patterns
- Video: action to lock viewer
- Info: improved state/place display (requires rescan, limited to AU/GB/IN/US)
- Info: edit tags with state placeholder
- improved support for system font scale

View file

@ -125,6 +125,8 @@
"videoActionSetSpeed": "Playback speed",
"viewerActionSettings": "Settings",
"viewerActionLock": "Lock viewer",
"viewerActionUnlock": "Unlock viewer",
"slideshowActionResume": "Resume",
"slideshowActionShowInCollection": "Show in Collection",

View file

@ -23,6 +23,7 @@ enum EntryAction {
// vector
viewSource,
// video
lockViewer,
videoCaptureFrame,
videoSelectStreams,
videoSetSpeed,
@ -88,7 +89,7 @@ class EntryActions {
EntryAction.setAs,
];
static const pageActions = [
static const pageActions = {
EntryAction.videoCaptureFrame,
EntryAction.videoSelectStreams,
EntryAction.videoSetSpeed,
@ -100,7 +101,7 @@ class EntryActions {
EntryAction.rotateCCW,
EntryAction.rotateCW,
EntryAction.flip,
];
};
static const trashed = [
EntryAction.delete,
@ -114,6 +115,7 @@ class EntryActions {
EntryAction.videoSetSpeed,
EntryAction.videoSelectStreams,
EntryAction.videoSettings,
EntryAction.lockViewer,
];
static const videoPlayback = [
@ -178,6 +180,8 @@ extension ExtraEntryAction on EntryAction {
case EntryAction.viewSource:
return context.l10n.entryActionViewSource;
// video
case EntryAction.lockViewer:
return context.l10n.viewerActionLock;
case EntryAction.videoCaptureFrame:
return context.l10n.videoActionCaptureFrame;
case EntryAction.videoToggleMute:
@ -290,6 +294,8 @@ extension ExtraEntryAction on EntryAction {
case EntryAction.viewSource:
return AIcons.vector;
// video
case EntryAction.lockViewer:
return AIcons.viewerLock;
case EntryAction.videoCaptureFrame:
return AIcons.captureFrame;
case EntryAction.videoToggleMute:

View file

@ -0,0 +1,26 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/services/accessibility_service.dart';
import 'package:aves/theme/durations.dart';
extension ExtraAccessibilityTimeout on AccessibilityTimeout {
Future<Duration> getSnackBarDuration(bool hasAction) async {
switch (this) {
case AccessibilityTimeout.system:
if (hasAction) {
return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToTakeAction(Durations.opToastActionDisplay)));
} else {
return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToRead(Durations.opToastTextDisplay)));
}
case AccessibilityTimeout.s1:
return const Duration(seconds: 1);
case AccessibilityTimeout.s3:
return const Duration(seconds: 3);
case AccessibilityTimeout.s5:
return const Duration(seconds: 5);
case AccessibilityTimeout.s10:
return const Duration(seconds: 10);
case AccessibilityTimeout.s30:
return const Duration(seconds: 30);
}
}
}

View file

@ -139,6 +139,8 @@ class AIcons {
static const IconData vaultConfigure = MdiIcons.shieldLockOutline;
static const IconData videoSettings = Icons.video_settings_outlined;
static const IconData view = Icons.grid_view_outlined;
static const IconData viewerLock = Icons.lock_outline;
static const IconData viewerUnlock = Icons.lock_open_outlined;
static const IconData zoomIn = Icons.add_outlined;
static const IconData zoomOut = Icons.remove_outlined;
static const IconData collapse = Icons.expand_less_outlined;

View file

@ -2,9 +2,8 @@ import 'dart:async';
import 'dart:math';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/accessibility_timeout.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/accessibility_service.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/action_mixins/overlay_snack_bar.dart';
@ -38,7 +37,7 @@ mixin FeedbackMixin {
// provide the messenger if feedback happens as the widget is disposed
void showFeedbackWithMessenger(BuildContext context, ScaffoldMessengerState messenger, String message, [SnackBarAction? action]) {
_getSnackBarDuration(action != null).then((duration) {
settings.timeToTakeAction.getSnackBarDuration(action != null).then((duration) {
final start = DateTime.now();
final theme = Theme.of(context);
final snackBarTheme = theme.snackBarTheme;
@ -107,27 +106,6 @@ mixin FeedbackMixin {
return horizontalPadding;
}
Future<Duration> _getSnackBarDuration(bool hasAction) async {
switch (settings.timeToTakeAction) {
case AccessibilityTimeout.system:
if (hasAction) {
return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToTakeAction(Durations.opToastActionDisplay)));
} else {
return Duration(milliseconds: await (AccessibilityService.getRecommendedTimeToRead(Durations.opToastTextDisplay)));
}
case AccessibilityTimeout.s1:
return const Duration(seconds: 1);
case AccessibilityTimeout.s3:
return const Duration(seconds: 3);
case AccessibilityTimeout.s5:
return const Duration(seconds: 5);
case AccessibilityTimeout.s10:
return const Duration(seconds: 10);
case AccessibilityTimeout.s30:
return const Duration(seconds: 30);
}
}
// report overlay for multiple operations
Future<void> showOpReport<T>({

View file

@ -2,6 +2,7 @@ import 'package:aves/model/actions/entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/quick_actions/editor_page.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
class ViewerActionEditorPage extends StatelessWidget {
@ -9,7 +10,7 @@ class ViewerActionEditorPage extends StatelessWidget {
const ViewerActionEditorPage({super.key});
static const allAvailableActions = [
static final allAvailableActions = [
[
EntryAction.share,
EntryAction.edit,
@ -26,10 +27,7 @@ class ViewerActionEditorPage extends StatelessWidget {
],
[
...EntryActions.exportInternal,
EntryAction.videoCaptureFrame,
EntryAction.videoToggleMute,
EntryAction.videoSetSpeed,
EntryAction.videoSelectStreams,
...EntryActions.video.whereNot((v) => v == EntryAction.videoSettings),
],
EntryActions.commonMetadataActions,
];

View file

@ -92,6 +92,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
return targetEntry.isSvg;
case EntryAction.videoCaptureFrame:
return canWrite && targetEntry.isVideo;
case EntryAction.lockViewer:
case EntryAction.videoToggleMute:
return !settings.useTvLayout && targetEntry.isVideo;
case EntryAction.videoSelectStreams:
@ -235,6 +236,9 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
case EntryAction.viewSource:
_goToSourceViewer(context, targetEntry);
break;
case EntryAction.lockViewer:
const LockViewNotification(locked: true).dispatch(context);
break;
// video
case EntryAction.videoCaptureFrame:
case EntryAction.videoToggleMute:

View file

@ -6,6 +6,13 @@ import 'package:aves_video/aves_video.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
@immutable
class LockViewNotification extends Notification {
final bool locked;
const LockViewNotification({required this.locked});
}
@immutable
class PopVisualNotification extends Notification {}

View file

@ -11,6 +11,7 @@ import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/settings/enums/accessibility_timeout.dart';
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
@ -27,6 +28,7 @@ import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart';
import 'package:aves/widgets/viewer/overlay/locked.dart';
import 'package:aves/widgets/viewer/overlay/panorama.dart';
import 'package:aves/widgets/viewer/overlay/slideshow_buttons.dart';
import 'package:aves/widgets/viewer/overlay/top.dart';
@ -70,6 +72,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
final AChangeNotifier _verticalScrollNotifier = AChangeNotifier();
bool _overlayInitialized = false;
final ValueNotifier<bool> _overlayVisible = ValueNotifier(true);
final ValueNotifier<bool> _viewLocked = ValueNotifier(false);
final ValueNotifier<bool> _overlayExpandedNotifier = ValueNotifier(false);
late AnimationController _overlayAnimationController;
late Animation<double> _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity;
@ -78,6 +81,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
late VideoActionDelegate _videoActionDelegate;
final ValueNotifier<HeroInfo?> _heroInfoNotifier = ValueNotifier(null);
bool _isEntryTracked = true;
Timer? _overlayHidingTimer;
@override
bool get isViewingImage => _currentVerticalPage.value == imagePage;
@ -147,6 +151,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
));
_overlayVisible.value = settings.showOverlayOnOpening && !viewerController.autopilot;
_overlayVisible.addListener(_onOverlayVisibleChanged);
_viewLocked.addListener(_onViewLockedChanged);
_videoActionDelegate = VideoActionDelegate(
collection: collection,
);
@ -170,9 +175,11 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
_videoActionDelegate.dispose();
_overlayAnimationController.dispose();
_overlayVisible.dispose();
_viewLocked.dispose();
_overlayExpandedNotifier.dispose();
_verticalPager.dispose();
_heroInfoNotifier.dispose();
_stopOverlayHidingTimer();
WidgetsBinding.instance.removeObserver(this);
_unregisterWidget(widget);
super.dispose();
@ -250,10 +257,24 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
stream: device.supportPictureInPicture ? _floating.pipStatus$ : Stream.value(PiPStatus.disabled),
builder: (context, snapshot) {
var pipEnabled = snapshot.data == PiPStatus.enabled;
return ValueListenableBuilder<bool>(
valueListenable: _viewLocked,
builder: (context, locked, child) {
return Stack(
children: [
viewer,
child!,
if (!pipEnabled) ...[
if (locked) ...[
const Positioned.fill(
child: AbsorbPointer(),
),
Positioned.fill(
child: GestureDetector(
onTap: () => _overlayVisible.value = !_overlayVisible.value,
),
),
_buildViewerLockedBottomOverlay(),
] else
..._buildOverlays(availableSize).map(_decorateOverlay),
const TopGestureAreaProtector(),
const SideGestureAreaProtector(),
@ -262,6 +283,9 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
],
);
},
child: viewer,
);
},
);
},
),
@ -301,6 +325,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
}
}
Widget _buildViewerLockedBottomOverlay() {
return TooltipTheme(
data: TooltipTheme.of(context).copyWith(
preferBelow: false,
),
child: ViewerLockedOverlay(
animationController: _overlayAnimationController,
),
);
}
Widget _buildSlideshowBottomOverlay(Size availableSize) {
return SizedBox.fromSize(
size: availableSize,
@ -491,6 +526,8 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
}
} else if (notification is ToggleOverlayNotification) {
_overlayVisible.value = notification.visible ?? !_overlayVisible.value;
} else if (notification is LockViewNotification) {
_viewLocked.value = notification.locked;
} else if (notification is VideoActionNotification) {
_onVideoAction(
context: context,
@ -809,8 +846,12 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
Future<void> _onOverlayVisibleChanged({bool animate = true}) async {
if (!mounted) return;
if (_overlayVisible.value) {
if (_viewLocked.value) {
await _startOverlayHidingTimer();
} else {
await AvesApp.showSystemUI();
AvesApp.setSystemUIStyle(Theme.of(context));
}
if (animate) {
await _overlayAnimationController.forward();
} else {
@ -835,4 +876,24 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
});
}
}
Future<void> _onViewLockedChanged() async {
if (_viewLocked.value) {
await AvesApp.hideSystemUI();
await _startOverlayHidingTimer();
} else {
await AvesApp.showSystemUI();
AvesApp.setSystemUIStyle(Theme.of(context));
_stopOverlayHidingTimer();
_overlayVisible.value = true;
}
}
Future<void> _startOverlayHidingTimer() async {
_stopOverlayHidingTimer();
final duration = await settings.timeToTakeAction.getSnackBarDuration(true);
_overlayHidingTimer = Timer(duration, () => _overlayVisible.value = false);
}
void _stopOverlayHidingTimer() => _overlayHidingTimer?.cancel();
}

View file

@ -0,0 +1,90 @@
import 'dart:math';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ViewerLockedOverlay extends StatefulWidget {
final AnimationController animationController;
final EdgeInsets? viewInsets, viewPadding;
const ViewerLockedOverlay({
super.key,
required this.animationController,
this.viewInsets,
this.viewPadding,
});
@override
State<StatefulWidget> createState() => _ViewerLockedOverlayState();
}
class _ViewerLockedOverlayState extends State<ViewerLockedOverlay> {
late Animation<double> _buttonScale;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant ViewerLockedOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(ViewerLockedOverlay widget) {
_buttonScale = CurvedAnimation(
parent: widget.animationController,
// a little bounce at the top
curve: Curves.easeOutBack,
);
}
void _unregisterWidget(ViewerLockedOverlay widget) {
// nothing
}
@override
Widget build(BuildContext context) {
return Selector<MediaQueryData, double>(
selector: (context, mq) => max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom),
builder: (context, mqPaddingBottom, child) {
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
return Container(
alignment: AlignmentDirectional.bottomEnd,
padding: EdgeInsets.only(bottom: mqPaddingBottom) + const EdgeInsets.all(ViewerButtonRowContent.padding),
child: SafeArea(
top: false,
bottom: false,
minimum: EdgeInsets.only(
left: viewInsetsPadding.left,
right: viewInsetsPadding.right,
),
child: OverlayButton(
scale: _buttonScale,
child: IconButton(
icon: const Icon(AIcons.viewerUnlock),
onPressed: () => const LockViewNotification(locked: false).dispatch(context),
tooltip: context.l10n.viewerActionUnlock,
),
),
),
);
},
);
}
}

View file

@ -62,6 +62,8 @@
"videoActionSelectStreams",
"videoActionSetSpeed",
"viewerActionSettings",
"viewerActionLock",
"viewerActionUnlock",
"slideshowActionResume",
"slideshowActionShowInCollection",
"entryInfoActionEditDate",
@ -623,6 +625,8 @@
"videoActionSelectStreams",
"videoActionSetSpeed",
"viewerActionSettings",
"viewerActionLock",
"viewerActionUnlock",
"slideshowActionResume",
"slideshowActionShowInCollection",
"entryInfoActionEditDate",
@ -1181,6 +1185,8 @@
],
"cs": [
"viewerActionLock",
"viewerActionUnlock",
"settingsVideoEnablePip",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
@ -1194,6 +1200,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
@ -1230,6 +1238,8 @@
"el": [
"chipActionGoToPlacePage",
"viewerActionLock",
"viewerActionUnlock",
"vaultLockTypePattern",
"settingsVideoEnablePip",
"patternDialogEnter",
@ -1246,12 +1256,16 @@
],
"es": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"tagPlaceholderState"
],
"eu": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"tagPlaceholderState"
@ -1266,6 +1280,8 @@
"videoActionPause",
"videoActionPlay",
"videoActionSelectStreams",
"viewerActionLock",
"viewerActionUnlock",
"slideshowActionResume",
"filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel",
@ -1735,6 +1751,8 @@
],
"fr": [
"viewerActionLock",
"viewerActionUnlock",
"tagPlaceholderState"
],
@ -1746,6 +1764,8 @@
"chipActionConfigureVault",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"viewerActionLock",
"viewerActionUnlock",
"entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel",
@ -2328,6 +2348,8 @@
"videoActionSelectStreams",
"videoActionSetSpeed",
"viewerActionSettings",
"viewerActionLock",
"viewerActionUnlock",
"slideshowActionResume",
"slideshowActionShowInCollection",
"entryInfoActionEditDate",
@ -2946,6 +2968,8 @@
"videoActionSelectStreams",
"videoActionSetSpeed",
"viewerActionSettings",
"viewerActionLock",
"viewerActionUnlock",
"slideshowActionResume",
"slideshowActionShowInCollection",
"entryInfoActionEditDate",
@ -3504,12 +3528,16 @@
],
"id": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"tagPlaceholderState"
],
"it": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"tagPlaceholderState"
@ -3522,6 +3550,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"filterAspectRatioLandscapeLabel",
"filterAspectRatioPortraitLabel",
"filterNoAddressLabel",
@ -3572,6 +3602,8 @@
],
"ko": [
"viewerActionLock",
"viewerActionUnlock",
"tagPlaceholderState"
],
@ -3581,6 +3613,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"filterLocatedLabel",
"filterTaggedLabel",
"albumTierVaults",
@ -3625,6 +3659,8 @@
],
"nb": [
"viewerActionLock",
"viewerActionUnlock",
"vaultLockTypePattern",
"settingsVideoEnablePip",
"patternDialogEnter",
@ -3644,6 +3680,8 @@
"chipActionConfigureVault",
"entryActionShareImageOnly",
"entryActionShareVideoOnly",
"viewerActionLock",
"viewerActionUnlock",
"entryInfoActionExportMetadata",
"entryInfoActionRemoveLocation",
"filterAspectRatioLandscapeLabel",
@ -3707,6 +3745,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"entryInfoActionRemoveLocation",
"filterLocatedLabel",
"filterNoLocationLabel",
@ -4024,12 +4064,16 @@
],
"pl": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"tagPlaceholderState"
],
"pt": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"settingsVideoBackgroundModeDialogTitle",
@ -4037,6 +4081,8 @@
],
"ro": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"tagPlaceholderState"
@ -4044,6 +4090,8 @@
"ru": [
"chipActionLock",
"viewerActionLock",
"viewerActionUnlock",
"vaultLockTypePattern",
"settingsVideoEnablePip",
"patternDialogEnter",
@ -4071,6 +4119,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"filterLocatedLabel",
"filterNoLocationLabel",
"albumTierVaults",
@ -4501,6 +4551,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
@ -4855,6 +4907,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"albumTierVaults",
"lengthUnitPixel",
"lengthUnitPercent",
@ -4890,6 +4944,8 @@
],
"uk": [
"viewerActionLock",
"viewerActionUnlock",
"settingsCollectionBurstPatternsTile",
"settingsCollectionBurstPatternsNone",
"tagPlaceholderState"
@ -4900,6 +4956,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"filterLocatedLabel",
"filterTaggedLabel",
"albumTierVaults",
@ -4948,6 +5006,8 @@
"chipActionLock",
"chipActionCreateVault",
"chipActionConfigureVault",
"viewerActionLock",
"viewerActionUnlock",
"filterLocatedLabel",
"filterTaggedLabel",
"albumTierVaults",