From 2e0b15787f3795a96fa6980963c65ef964616cde Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 25 Mar 2023 23:31:55 +0100 Subject: [PATCH] #560 video: action to lock viewer --- CHANGELOG.md | 1 + lib/l10n/app_en.arb | 2 + lib/model/actions/entry.dart | 10 ++- .../settings/enums/accessibility_timeout.dart | 26 ++++++ lib/theme/icons.dart | 2 + .../common/action_mixins/feedback.dart | 26 +----- .../viewer/viewer_actions_editor.dart | 8 +- .../viewer/action/entry_action_delegate.dart | 4 + .../viewer/controls/notifications.dart | 7 ++ lib/widgets/viewer/entry_viewer_stack.dart | 85 +++++++++++++++--- lib/widgets/viewer/overlay/locked.dart | 90 +++++++++++++++++++ untranslated.json | 60 +++++++++++++ 12 files changed, 278 insertions(+), 43 deletions(-) create mode 100644 lib/model/settings/enums/accessibility_timeout.dart create mode 100644 lib/widgets/viewer/overlay/locked.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b8e4fdad0..4d866c405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 50156a649..cd2b1a034 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -125,6 +125,8 @@ "videoActionSetSpeed": "Playback speed", "viewerActionSettings": "Settings", + "viewerActionLock": "Lock viewer", + "viewerActionUnlock": "Unlock viewer", "slideshowActionResume": "Resume", "slideshowActionShowInCollection": "Show in Collection", diff --git a/lib/model/actions/entry.dart b/lib/model/actions/entry.dart index 2153f3ac7..66e57637a 100644 --- a/lib/model/actions/entry.dart +++ b/lib/model/actions/entry.dart @@ -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: diff --git a/lib/model/settings/enums/accessibility_timeout.dart b/lib/model/settings/enums/accessibility_timeout.dart new file mode 100644 index 000000000..33c36f749 --- /dev/null +++ b/lib/model/settings/enums/accessibility_timeout.dart @@ -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 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); + } + } +} diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 40112de86..4c683e908 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -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; diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 2ba8fd1c0..ec3ae653d 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -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 _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 showOpReport({ diff --git a/lib/widgets/settings/viewer/viewer_actions_editor.dart b/lib/widgets/settings/viewer/viewer_actions_editor.dart index c184fa598..770acf547 100644 --- a/lib/widgets/settings/viewer/viewer_actions_editor.dart +++ b/lib/widgets/settings/viewer/viewer_actions_editor.dart @@ -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, ]; diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index b00d6e856..0cf330fd1 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -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: diff --git a/lib/widgets/viewer/controls/notifications.dart b/lib/widgets/viewer/controls/notifications.dart index 8ed5068ca..3cbac5929 100644 --- a/lib/widgets/viewer/controls/notifications.dart +++ b/lib/widgets/viewer/controls/notifications.dart @@ -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 {} diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 076908d50..5c97537c9 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -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 with EntryViewContr final AChangeNotifier _verticalScrollNotifier = AChangeNotifier(); bool _overlayInitialized = false; final ValueNotifier _overlayVisible = ValueNotifier(true); + final ValueNotifier _viewLocked = ValueNotifier(false); final ValueNotifier _overlayExpandedNotifier = ValueNotifier(false); late AnimationController _overlayAnimationController; late Animation _overlayButtonScale, _overlayVideoControlScale, _overlayOpacity; @@ -78,6 +81,7 @@ class _EntryViewerStackState extends State with EntryViewContr late VideoActionDelegate _videoActionDelegate; final ValueNotifier _heroInfoNotifier = ValueNotifier(null); bool _isEntryTracked = true; + Timer? _overlayHidingTimer; @override bool get isViewingImage => _currentVerticalPage.value == imagePage; @@ -147,6 +151,7 @@ class _EntryViewerStackState extends State 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 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,16 +257,33 @@ class _EntryViewerStackState extends State with EntryViewContr stream: device.supportPictureInPicture ? _floating.pipStatus$ : Stream.value(PiPStatus.disabled), builder: (context, snapshot) { var pipEnabled = snapshot.data == PiPStatus.enabled; - return Stack( - children: [ - viewer, - if (!pipEnabled) ...[ - ..._buildOverlays(availableSize).map(_decorateOverlay), - const TopGestureAreaProtector(), - const SideGestureAreaProtector(), - const BottomGestureAreaProtector(), - ], - ], + return ValueListenableBuilder( + valueListenable: _viewLocked, + builder: (context, locked, child) { + return Stack( + children: [ + 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(), + const BottomGestureAreaProtector(), + ], + ], + ); + }, + child: viewer, ); }, ); @@ -301,6 +325,17 @@ class _EntryViewerStackState extends State 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 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 with EntryViewContr Future _onOverlayVisibleChanged({bool animate = true}) async { if (!mounted) return; if (_overlayVisible.value) { - await AvesApp.showSystemUI(); - AvesApp.setSystemUIStyle(Theme.of(context)); + 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 with EntryViewContr }); } } + + Future _onViewLockedChanged() async { + if (_viewLocked.value) { + await AvesApp.hideSystemUI(); + await _startOverlayHidingTimer(); + } else { + await AvesApp.showSystemUI(); + AvesApp.setSystemUIStyle(Theme.of(context)); + _stopOverlayHidingTimer(); + _overlayVisible.value = true; + } + } + + Future _startOverlayHidingTimer() async { + _stopOverlayHidingTimer(); + final duration = await settings.timeToTakeAction.getSnackBarDuration(true); + _overlayHidingTimer = Timer(duration, () => _overlayVisible.value = false); + } + + void _stopOverlayHidingTimer() => _overlayHidingTimer?.cancel(); } diff --git a/lib/widgets/viewer/overlay/locked.dart b/lib/widgets/viewer/overlay/locked.dart new file mode 100644 index 000000000..57ec2484e --- /dev/null +++ b/lib/widgets/viewer/overlay/locked.dart @@ -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 createState() => _ViewerLockedOverlayState(); +} + +class _ViewerLockedOverlayState extends State { + late Animation _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( + 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, + ), + ), + ), + ); + }, + ); + } +} diff --git a/untranslated.json b/untranslated.json index 548016db9..228df0282 100644 --- a/untranslated.json +++ b/untranslated.json @@ -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",