#478 picture-in-picture

This commit is contained in:
Thibault Deckers 2023-03-05 23:05:58 +01:00
parent 2183ff7cd7
commit 655d251890
24 changed files with 341 additions and 90 deletions

View file

@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Vaults: custom pattern lock - Vaults: custom pattern lock
- Video: picture-in-picture
- Video: handle skip next/previous media buttons
### Changed ### Changed

View file

@ -96,6 +96,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:supportsPictureInPicture="true"
android:theme="@style/NormalTheme" android:theme="@style/NormalTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>

View file

@ -65,11 +65,13 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
val stateString = call.argument<String>("state") val stateString = call.argument<String>("state")
val positionMillis = call.argument<Number>("positionMillis")?.toLong() val positionMillis = call.argument<Number>("positionMillis")?.toLong()
val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat() val playbackSpeed = call.argument<Number>("playbackSpeed")?.toFloat()
val canSkipToNext = call.argument<Boolean>("canSkipToNext")
val canSkipToPrevious = call.argument<Boolean>("canSkipToPrevious")
if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null) { if (uri == null || title == null || durationMillis == null || stateString == null || positionMillis == null || playbackSpeed == null || canSkipToNext == null || canSkipToPrevious == null) {
result.error( result.error(
"updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" + "updateSession-args", "missing arguments: uri=$uri, title=$title, durationMillis=$durationMillis" +
", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed", null ", stateString=$stateString, positionMillis=$positionMillis, playbackSpeed=$playbackSpeed, canSkipToNext=$canSkipToNext, canSkipToPrevious=$canSkipToPrevious", null
) )
return return
} }
@ -90,6 +92,12 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
} else { } else {
actions or PlaybackStateCompat.ACTION_PLAY actions or PlaybackStateCompat.ACTION_PLAY
} }
if (canSkipToNext) {
actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_NEXT
}
if (canSkipToPrevious) {
actions = actions or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
}
val playbackState = PlaybackStateCompat.Builder() val playbackState = PlaybackStateCompat.Builder()
.setState( .setState(

View file

@ -46,6 +46,16 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE)) success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE))
} }
override fun onSkipToNext() {
super.onSkipToNext()
success(hashMapOf(KEY_COMMAND to COMMAND_SKIP_TO_NEXT))
}
override fun onSkipToPrevious() {
super.onSkipToPrevious()
success(hashMapOf(KEY_COMMAND to COMMAND_SKIP_TO_PREVIOUS))
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
success(hashMapOf(KEY_COMMAND to COMMAND_STOP)) success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
@ -70,6 +80,8 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
const val COMMAND_PLAY = "play" const val COMMAND_PLAY = "play"
const val COMMAND_PAUSE = "pause" const val COMMAND_PAUSE = "pause"
const val COMMAND_SKIP_TO_NEXT = "skip_to_next"
const val COMMAND_SKIP_TO_PREVIOUS = "skip_to_previous"
const val COMMAND_STOP = "stop" const val COMMAND_STOP = "stop"
const val COMMAND_SEEK = "seek" const val COMMAND_SEEK = "seek"
} }

View file

@ -793,6 +793,7 @@
"settingsVideoSectionTitle": "Video", "settingsVideoSectionTitle": "Video",
"settingsVideoShowVideos": "Show videos", "settingsVideoShowVideos": "Show videos",
"settingsVideoEnableHardwareAcceleration": "Hardware acceleration", "settingsVideoEnableHardwareAcceleration": "Hardware acceleration",
"settingsVideoEnablePip": "Picture-in-picture",
"settingsVideoAutoPlay": "Auto play", "settingsVideoAutoPlay": "Auto play",
"settingsVideoLoopModeTile": "Loop mode", "settingsVideoLoopModeTile": "Loop mode",
"settingsVideoLoopModeDialogTitle": "Loop Mode", "settingsVideoLoopModeDialogTitle": "Loop Mode",

View file

@ -1,5 +1,6 @@
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:floating/floating.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
@ -9,7 +10,7 @@ class Device {
late final String _userAgent; late final String _userAgent;
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint; late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto; late final bool _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode; late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
String get userAgent => _userAgent; String get userAgent => _userAgent;
@ -41,6 +42,8 @@ class Device {
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode; bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
bool get supportPictureInPicture => _supportPictureInPicture;
Device._private(); Device._private();
Future<void> init() async { Future<void> init() async {
@ -53,6 +56,10 @@ class Device {
final auth = LocalAuthentication(); final auth = LocalAuthentication();
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported(); _canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
final floating = Floating();
_supportPictureInPicture = await floating.isPipAvailable;
floating.dispose();
final capabilities = await deviceService.getCapabilities(); final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false; _canPinShortcut = capabilities['canPinShortcut'] ?? false;

View file

@ -90,6 +90,7 @@ class SettingsDefaults {
// video // video
static const enableVideoHardwareAcceleration = true; static const enableVideoHardwareAcceleration = true;
static const enableVideoPip = false;
static const videoAutoPlayMode = VideoAutoPlayMode.disabled; static const videoAutoPlayMode = VideoAutoPlayMode.disabled;
static const videoLoopMode = VideoLoopMode.shortOnly; static const videoLoopMode = VideoLoopMode.shortOnly;
static const videoShowRawTimedText = false; static const videoShowRawTimedText = false;

View file

@ -133,6 +133,7 @@ class Settings extends ChangeNotifier {
// video // video
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec'; static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
static const enableVideoPipKey = 'video_pip';
static const videoAutoPlayModeKey = 'video_auto_play_mode'; static const videoAutoPlayModeKey = 'video_auto_play_mode';
static const videoLoopModeKey = 'video_loop'; static const videoLoopModeKey = 'video_loop';
static const videoControlsKey = 'video_controls'; static const videoControlsKey = 'video_controls';
@ -284,6 +285,7 @@ class Settings extends ChangeNotifier {
viewerGestureSideTapNext = false; viewerGestureSideTapNext = false;
viewerUseCutout = true; viewerUseCutout = true;
viewerMaxBrightness = false; viewerMaxBrightness = false;
enableVideoPip = false;
videoControls = VideoControls.none; videoControls = VideoControls.none;
videoGestureDoubleTapTogglePlay = false; videoGestureDoubleTapTogglePlay = false;
videoGestureSideDoubleTapSeek = false; videoGestureSideDoubleTapSeek = false;
@ -298,6 +300,9 @@ class Settings extends ChangeNotifier {
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) { if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) {
_set(viewerUseCutoutKey, null); _set(viewerUseCutoutKey, null);
} }
if (enableVideoPip && !device.supportPictureInPicture) {
_set(enableVideoPipKey, null);
}
} }
// app // app
@ -655,6 +660,10 @@ class Settings extends ChangeNotifier {
set enableVideoHardwareAcceleration(bool newValue) => _set(enableVideoHardwareAccelerationKey, newValue); set enableVideoHardwareAcceleration(bool newValue) => _set(enableVideoHardwareAccelerationKey, newValue);
bool get enableVideoPip => getBool(enableVideoPipKey) ?? SettingsDefaults.enableVideoPip;
set enableVideoPip(bool newValue) => _set(enableVideoPipKey, newValue);
VideoAutoPlayMode get videoAutoPlayMode => getEnumOrDefault(videoAutoPlayModeKey, SettingsDefaults.videoAutoPlayMode, VideoAutoPlayMode.values); VideoAutoPlayMode get videoAutoPlayMode => getEnumOrDefault(videoAutoPlayModeKey, SettingsDefaults.videoAutoPlayMode, VideoAutoPlayMode.values);
set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString()); set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString());
@ -1078,6 +1087,7 @@ class Settings extends ChangeNotifier {
case viewerUseCutoutKey: case viewerUseCutoutKey:
case viewerMaxBrightnessKey: case viewerMaxBrightnessKey:
case enableMotionPhotoAutoPlayKey: case enableMotionPhotoAutoPlayKey:
case enableVideoPipKey:
case enableVideoHardwareAccelerationKey: case enableVideoHardwareAccelerationKey:
case videoGestureDoubleTapTogglePlayKey: case videoGestureDoubleTapTogglePlayKey:
case videoGestureSideDoubleTapSeekKey: case videoGestureSideDoubleTapSeekKey:

View file

@ -11,7 +11,11 @@ import 'package:get_it/get_it.dart';
abstract class MediaSessionService { abstract class MediaSessionService {
Stream<MediaCommandEvent> get mediaCommands; Stream<MediaCommandEvent> get mediaCommands;
Future<void> update(AvesVideoController controller); Future<void> update({
required AvesVideoController controller,
required bool canSkipToNext,
required bool canSkipToPrevious,
});
Future<void> release(); Future<void> release();
} }
@ -38,7 +42,11 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
Stream<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>(); Stream<MediaCommandEvent> get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast<MediaCommandEvent>();
@override @override
Future<void> update(AvesVideoController controller) async { Future<void> update({
required AvesVideoController controller,
required bool canSkipToNext,
required bool canSkipToPrevious,
}) async {
final entry = controller.entry; final entry = controller.entry;
try { try {
await _platformObject.invokeMethod('update', <String, dynamic>{ await _platformObject.invokeMethod('update', <String, dynamic>{
@ -48,6 +56,8 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
'state': _toPlatformState(controller.status), 'state': _toPlatformState(controller.status),
'positionMillis': controller.currentPosition, 'positionMillis': controller.currentPosition,
'playbackSpeed': controller.speed, 'playbackSpeed': controller.speed,
'canSkipToNext': canSkipToNext,
'canSkipToPrevious': canSkipToPrevious,
}); });
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -88,6 +98,12 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
case 'pause': case 'pause':
event = const MediaCommandEvent(MediaCommand.pause); event = const MediaCommandEvent(MediaCommand.pause);
break; break;
case 'skip_to_next':
event = const MediaCommandEvent(MediaCommand.skipToNext);
break;
case 'skip_to_previous':
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
break;
case 'stop': case 'stop':
event = const MediaCommandEvent(MediaCommand.stop); event = const MediaCommandEvent(MediaCommand.stop);
break; break;
@ -104,7 +120,7 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
} }
} }
enum MediaCommand { play, pause, stop, seek } enum MediaCommand { play, pause, skipToNext, skipToPrevious, stop, seek }
@immutable @immutable
class MediaCommandEvent extends Equatable { class MediaCommandEvent extends Equatable {

View file

@ -75,6 +75,11 @@ class Dependencies {
license: mit, license: mit,
sourceUrl: 'https://github.com/deckerst/fijkplayer', sourceUrl: 'https://github.com/deckerst/fijkplayer',
), ),
Dependency(
name: 'Floating',
license: mit,
sourceUrl: 'https://github.com/wrbl606/floating',
),
Dependency( Dependency(
name: 'Flutter Display Mode', name: 'Flutter Display Mode',
license: mit, license: mit,

View file

@ -362,7 +362,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
debugPrint('$runtimeType lifecycle ${state.name}');
reportService.log('Lifecycle ${state.name}'); reportService.log('Lifecycle ${state.name}');
switch (state) { switch (state) {
case AppLifecycleState.inactive: case AppLifecycleState.inactive:

View file

@ -87,14 +87,14 @@ class TileExtentController {
int _effectiveColumnCountForExtent(double extent) { int _effectiveColumnCountForExtent(double extent) {
if (extent > 0) { if (extent > 0) {
final columnCount = _columnCountForExtent(extent); final columnCount = _columnCountForExtent(extent);
final countMin = _effectiveColumnCountMin();
final countMax = _effectiveColumnCountMax(); final countMax = _effectiveColumnCountMax();
final countMin = min(_effectiveColumnCountMin(), countMax);
return columnCount.round().clamp(countMin, countMax); return columnCount.round().clamp(countMin, countMax);
} }
return columnCountDefault; return columnCountDefault;
} }
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax()); double get effectiveExtentMin => min(_extentForColumnCount(_effectiveColumnCountMax()), effectiveExtentMax);
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin()); double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/video_auto_play_mode.dart'; import 'package:aves/model/settings/enums/video_auto_play_mode.dart';
@ -40,6 +41,7 @@ class VideoSection extends SettingsSection {
return [ return [
if (!standalonePage) SettingsTileVideoShowVideos(), if (!standalonePage) SettingsTileVideoShowVideos(),
SettingsTileVideoEnableHardwareAcceleration(), SettingsTileVideoEnableHardwareAcceleration(),
if (!settings.useTvLayout && device.supportPictureInPicture) SettingsTileVideoEnablePip(),
SettingsTileVideoEnableAutoPlay(), SettingsTileVideoEnableAutoPlay(),
SettingsTileVideoLoopMode(), SettingsTileVideoLoopMode(),
if (!settings.useTvLayout) SettingsTileVideoControls(), if (!settings.useTvLayout) SettingsTileVideoControls(),
@ -72,6 +74,18 @@ class SettingsTileVideoEnableHardwareAcceleration extends SettingsTile {
); );
} }
class SettingsTileVideoEnablePip extends SettingsTile {
@override
String title(BuildContext context) => context.l10n.settingsVideoEnablePip;
@override
Widget build(BuildContext context) => SettingsSwitchListTile(
selector: (context, s) => s.enableVideoPip,
onChanged: (v) => settings.enableVideoPip = v,
title: title(context),
);
}
class SettingsTileVideoEnableAutoPlay extends SettingsTile { class SettingsTileVideoEnableAutoPlay extends SettingsTile {
@override @override
String title(BuildContext context) => context.l10n.settingsVideoAutoPlay; String title(BuildContext context) => context.l10n.settingsVideoAutoPlay;

View file

@ -3,11 +3,13 @@ import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/viewer_transition.dart'; import 'package:aves/model/settings/enums/viewer_transition.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -45,6 +47,8 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
return MagnifierGestureDetectorScope( return MagnifierGestureDetectorScope(
axis: const [Axis.horizontal, Axis.vertical], axis: const [Axis.horizontal, Axis.vertical],
child: NotificationListener(
onNotification: _handleNotification,
child: PageView.builder( child: PageView.builder(
// key is expected by test driver // key is expected by test driver
key: const Key('horizontal-pageview'), key: const Key('horizontal-pageview'),
@ -80,6 +84,7 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
}, },
itemCount: viewerController.repeat ? null : entries.length, itemCount: viewerController.repeat ? null : entries.length,
), ),
),
); );
} }
@ -94,6 +99,43 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
); );
} }
bool _handleNotification(dynamic notification) {
if (notification is ShowPreviousVideoNotification) {
_showPreviousVideo();
} else if (notification is ShowNextVideoNotification) {
_showNextVideo();
} else {
return false;
}
return true;
}
void _showPreviousVideo() {
final currentIndex = pageController.page?.round();
if (currentIndex != null) {
final previousVideoEntry = entries.take(currentIndex).lastWhereOrNull((entry) => entry.isVideo);
if (previousVideoEntry != null) {
final previousIndex = entries.indexOf(previousVideoEntry);
if (previousIndex != -1) {
ShowEntryNotification(animate: false, index: previousIndex).dispatch(context);
}
}
}
}
void _showNextVideo() {
final currentIndex = pageController.page?.round();
if (currentIndex != null) {
final nextVideoEntry = entries.skip(currentIndex + 1).firstWhereOrNull((entry) => entry.isVideo);
if (nextVideoEntry != null) {
final nextIndex = entries.indexOf(nextVideoEntry);
if (nextIndex != -1) {
ShowEntryNotification(animate: false, index: nextIndex).dispatch(context);
}
}
}
}
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }

View file

@ -41,12 +41,14 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final collection = widget.collection;
return AvesScaffold( return AvesScaffold(
body: ViewStateConductorProvider( body: ViewStateConductorProvider(
child: VideoConductorProvider( child: VideoConductorProvider(
collection: collection,
child: MultiPageConductorProvider( child: MultiPageConductorProvider(
child: EntryViewerStack( child: EntryViewerStack(
collection: widget.collection, collection: collection,
initialEntry: widget.initialEntry, initialEntry: widget.initialEntry,
viewerController: _viewerController, viewerController: _viewerController,
), ),
@ -86,17 +88,19 @@ class ViewStateConductorProvider extends StatelessWidget {
} }
class VideoConductorProvider extends StatelessWidget { class VideoConductorProvider extends StatelessWidget {
final CollectionLens? collection;
final Widget? child; final Widget? child;
const VideoConductorProvider({ const VideoConductorProvider({
super.key, super.key,
this.child, this.collection,
required this.child,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Provider<VideoConductor>( return Provider<VideoConductor>(
create: (context) => VideoConductor(), create: (context) => VideoConductor(collection: collection),
dispose: (context, value) => value.dispose(), dispose: (context, value) => value.dispose(),
child: child, child: child,
); );
@ -108,7 +112,7 @@ class MultiPageConductorProvider extends StatelessWidget {
const MultiPageConductorProvider({ const MultiPageConductorProvider({
super.key, super.key,
this.child, required this.child,
}); });
@override @override

View file

@ -34,6 +34,7 @@ import 'package:aves/widgets/viewer/video_action_delegate.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart'; import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:aves/widgets/viewer/visual/controller_mixin.dart'; import 'package:aves/widgets/viewer/visual/controller_mixin.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:floating/floating.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -58,6 +59,7 @@ class EntryViewerStack extends StatefulWidget {
} }
class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver { class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver {
final Floating _floating = Floating();
late int _currentEntryIndex; late int _currentEntryIndex;
late ValueNotifier<int> _currentVerticalPage; late ValueNotifier<int> _currentVerticalPage;
late PageController _horizontalPager, _verticalPager; late PageController _horizontalPager, _verticalPager;
@ -159,6 +161,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
@override @override
void dispose() { void dispose() {
_floating.dispose();
cleanEntryControllers(entryNotifier.value); cleanEntryControllers(entryNotifier.value);
_videoActionDelegate.dispose(); _videoActionDelegate.dispose();
_overlayAnimationController.dispose(); _overlayAnimationController.dispose();
@ -183,9 +186,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) { switch (state) {
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
_onAppInactive();
break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
case AppLifecycleState.detached: case AppLifecycleState.detached:
viewerController.autopilot = false;
pauseVideoControllers(); pauseVideoControllers();
break; break;
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
@ -194,6 +198,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
} }
Future<void> _onAppInactive() async {
viewerController.autopilot = false;
bool enabledPip = false;
if (settings.enableVideoPip) {
enabledPip |= await _enablePictureInPicture();
}
if (!enabledPip) {
await pauseVideoControllers();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final viewStateConductor = context.read<ViewStateConductor>(); final viewStateConductor = context.read<ViewStateConductor>();
@ -209,9 +224,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final availableSize = Size(constraints.maxWidth, constraints.maxHeight); final availableSize = Size(constraints.maxWidth, constraints.maxHeight);
return Stack( final viewer = ViewerVerticalPageView(
children: [
ViewerVerticalPageView(
collection: collection, collection: collection,
entryNotifier: entryNotifier, entryNotifier: entryNotifier,
viewerController: viewerController, viewerController: viewerController,
@ -226,12 +239,23 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
onHorizontalPageChanged: _onHorizontalPageChanged, onHorizontalPageChanged: _onHorizontalPageChanged,
onImagePageRequested: () => _goToVerticalPage(imagePage), onImagePageRequested: () => _goToVerticalPage(imagePage),
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
), );
return StreamBuilder<PiPStatus>(
stream: _floating.pipStatus$,
builder: (context, snapshot) {
var pipEnabled = snapshot.data == PiPStatus.enabled;
return Stack(
children: [
viewer,
if (!pipEnabled) ...[
..._buildOverlays(availableSize).map(_decorateOverlay), ..._buildOverlays(availableSize).map(_decorateOverlay),
const TopGestureAreaProtector(), const TopGestureAreaProtector(),
const SideGestureAreaProtector(), const SideGestureAreaProtector(),
const BottomGestureAreaProtector(), const BottomGestureAreaProtector(),
], ],
],
);
},
); );
}, },
), ),
@ -708,6 +732,39 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
} }
} }
Future<bool> _enablePictureInPicture() async {
final videoController = context.read<VideoConductor>().getPlayingController();
if (videoController != null) {
final targetEntry = videoController.entry;
final entrySize = targetEntry.displaySize;
final aspectRatio = Rational(entrySize.width.round(), entrySize.height.round());
final mq = context.read<MediaQueryData>();
final viewSize = mq.size * mq.devicePixelRatio;
final fittedSize = applyBoxFit(BoxFit.contain, entrySize, viewSize).destination;
final sourceRectHint = Rectangle<int>(
((viewSize.width - fittedSize.width) / 2).round(),
((viewSize.height - fittedSize.height) / 2).round(),
fittedSize.width.round(),
fittedSize.height.round(),
);
try {
final status = await _floating.enable(
aspectRatio: aspectRatio,
sourceRectHint: sourceRectHint,
);
await reportService.log('Enabled picture-in-picture with status=$status');
return status == PiPStatus.enabled;
} on PlatformException catch (e, stack) {
if (e.message != 'Activity must be resumed to enter picture-in-picture') {
await reportService.recordError(e, stack);
}
}
}
return false;
}
// overlay // overlay
Future<void> _initOverlay() async { Future<void> _initOverlay() async {

View file

@ -40,6 +40,12 @@ class ShowEntryNotification extends Notification {
}); });
} }
@immutable
class ShowPreviousVideoNotification extends Notification {}
@immutable
class ShowNextVideoNotification extends Notification {}
@immutable @immutable
class ToggleOverlayNotification extends Notification { class ToggleOverlayNotification extends Notification {
final bool? visible; final bool? visible;

View file

@ -3,18 +3,20 @@ import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/video/fijkplayer.dart'; import 'package:aves/widgets/viewer/video/fijkplayer.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
class VideoConductor { class VideoConductor {
final CollectionLens? _collection;
final List<AvesVideoController> _controllers = []; final List<AvesVideoController> _controllers = [];
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
static const _defaultMaxControllerCount = 3; static const _defaultMaxControllerCount = 3;
VideoConductor(); VideoConductor({CollectionLens? collection}) : _collection = collection;
Future<void> dispose() async { Future<void> dispose() async {
await disposeAll(); await disposeAll();
@ -33,7 +35,7 @@ class VideoConductor {
_controllers.remove(controller); _controllers.remove(controller);
} else { } else {
controller = IjkPlayerAvesVideoController(entry, persistPlayback: true); controller = IjkPlayerAvesVideoController(entry, persistPlayback: true);
_subscriptions.add(controller.statusStream.listen(_onControllerStatusChanged)); _subscriptions.add(controller.statusStream.listen((event) => _onControllerStatusChanged(controller!, event)));
} }
_controllers.insert(0, controller); _controllers.insert(0, controller);
while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) { while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) {
@ -42,11 +44,29 @@ class VideoConductor {
return controller; return controller;
} }
AvesVideoController? getPlayingController() => _controllers.firstWhereOrNull((c) => c.isPlaying);
AvesVideoController? getController(AvesEntry entry) { AvesVideoController? getController(AvesEntry entry) {
return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId);
} }
Future<void> _onControllerStatusChanged(VideoStatus status) async { Future<void> _onControllerStatusChanged(AvesVideoController controller, VideoStatus status) async {
bool canSkipToNext = false, canSkipToPrevious = false;
final entries = _collection?.sortedEntries;
if (entries != null) {
final currentIndex = entries.indexOf(controller.entry);
if (currentIndex != -1) {
bool isVideo(AvesEntry entry) => entry.isVideo;
canSkipToPrevious = entries.take(currentIndex).lastWhereOrNull(isVideo) != null;
canSkipToNext = entries.skip(currentIndex + 1).firstWhereOrNull(isVideo) != null;
}
}
await mediaSessionService.update(
controller: controller,
canSkipToNext: canSkipToNext,
canSkipToPrevious: canSkipToPrevious,
);
if (settings.keepScreenOn == KeepScreenOn.videoPlayback) { if (settings.keepScreenOn == KeepScreenOn.videoPlayback) {
await windowService.keepScreenOn(status == VideoStatus.playing); await windowService.keepScreenOn(status == VideoStatus.playing);
} }

View file

@ -10,7 +10,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
abstract class AvesVideoController { abstract class AvesVideoController {
final List<StreamSubscription> _subscriptions = [];
final AvesEntry _entry; final AvesEntry _entry;
final bool persistPlayback; final bool persistPlayback;
@ -22,14 +21,10 @@ abstract class AvesVideoController {
AvesVideoController(AvesEntry entry, {required this.persistPlayback}) : _entry = entry { AvesVideoController(AvesEntry entry, {required this.persistPlayback}) : _entry = entry {
entry.visualChangeNotifier.addListener(onVisualChanged); entry.visualChangeNotifier.addListener(onVisualChanged);
_subscriptions.add(statusStream.listen((event) => mediaSessionService.update(this)));
} }
@mustCallSuper @mustCallSuper
Future<void> dispose() async { Future<void> dispose() async {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
entry.visualChangeNotifier.removeListener(onVisualChanged); entry.visualChangeNotifier.removeListener(onVisualChanged);
await _savePlaybackState(); await _savePlaybackState();
} }

View file

@ -450,6 +450,12 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
case MediaCommand.pause: case MediaCommand.pause:
videoController.pause(); videoController.pause();
break; break;
case MediaCommand.skipToNext:
ShowNextVideoNotification().dispatch(context);
break;
case MediaCommand.skipToPrevious:
ShowPreviousVideoNotification().dispatch(context);
break;
case MediaCommand.stop: case MediaCommand.stop:
videoController.pause(); videoController.pause();
break; break;

View file

@ -55,7 +55,10 @@ class PlatformReportService extends ReportService {
} }
@override @override
Future<void> log(String message) async => _instance?.log(message); Future<void> log(String message) async {
debugPrint('Report log=$message');
await _instance?.log(message);
}
@override @override
Future<void> setCustomKey(String key, Object value) async => _instance?.setCustomKey(key, value); Future<void> setCustomKey(String key, Object value) async => _instance?.setCustomKey(key, value);

View file

@ -373,6 +373,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.2" version: "1.2.2"
floating:
dependency: "direct main"
description:
path: "."
ref: source-rect-hint
resolved-ref: "84b2f7997da4393dedf3f2e09dd990e5d5492161"
url: "https://github.com/deckerst/floating.git"
source: git
version: "1.1.3"
fluster: fluster:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -60,6 +60,10 @@ dependencies:
url: https://github.com/deckerst/fijkplayer.git url: https://github.com/deckerst/fijkplayer.git
ref: aves ref: aves
flex_color_picker: flex_color_picker:
floating:
git:
url: https://github.com/deckerst/floating.git
ref: source-rect-hint
fluster: fluster:
flutter_displaymode: flutter_displaymode:
flutter_highlight: flutter_highlight:

View file

@ -479,6 +479,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -1034,6 +1035,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -1198,6 +1200,7 @@
"placePageTitle", "placePageTitle",
"placeEmpty", "placeEmpty",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsVideoEnablePip",
"settingsDisablingBinWarningDialogMessage" "settingsDisablingBinWarningDialogMessage"
], ],
@ -1231,6 +1234,7 @@
"placePageTitle", "placePageTitle",
"placeEmpty", "placeEmpty",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsVideoEnablePip",
"settingsDisablingBinWarningDialogMessage" "settingsDisablingBinWarningDialogMessage"
], ],
@ -1242,19 +1246,22 @@
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty",
"settingsVideoEnablePip"
], ],
"es": [ "es": [
"vaultLockTypePattern", "vaultLockTypePattern",
"patternDialogEnter", "patternDialogEnter",
"patternDialogConfirm" "patternDialogConfirm",
"settingsVideoEnablePip"
], ],
"eu": [ "eu": [
"vaultLockTypePattern", "vaultLockTypePattern",
"patternDialogEnter", "patternDialogEnter",
"patternDialogConfirm" "patternDialogConfirm",
"settingsVideoEnablePip"
], ],
"fa": [ "fa": [
@ -1596,6 +1603,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -1731,7 +1739,8 @@
"fr": [ "fr": [
"vaultLockTypePattern", "vaultLockTypePattern",
"patternDialogEnter", "patternDialogEnter",
"patternDialogConfirm" "patternDialogConfirm",
"settingsVideoEnablePip"
], ],
"gl": [ "gl": [
@ -2103,6 +2112,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -2735,6 +2745,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -2872,7 +2883,8 @@
"id": [ "id": [
"vaultLockTypePattern", "vaultLockTypePattern",
"patternDialogEnter", "patternDialogEnter",
"patternDialogConfirm" "patternDialogConfirm",
"settingsVideoEnablePip"
], ],
"it": [ "it": [
@ -2885,7 +2897,8 @@
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty",
"settingsVideoEnablePip"
], ],
"ja": [ "ja": [
@ -2931,6 +2944,7 @@
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoEnablePip",
"settingsVideoGestureVerticalDragBrightnessVolume", "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisablingBinWarningDialogMessage", "settingsDisablingBinWarningDialogMessage",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -2941,7 +2955,8 @@
"ko": [ "ko": [
"vaultLockTypePattern", "vaultLockTypePattern",
"patternDialogEnter", "patternDialogEnter",
"patternDialogConfirm" "patternDialogConfirm",
"settingsVideoEnablePip"
], ],
"lt": [ "lt": [
@ -2981,6 +2996,7 @@
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoEnablePip",
"settingsVideoGestureVerticalDragBrightnessVolume", "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisablingBinWarningDialogMessage", "settingsDisablingBinWarningDialogMessage",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -3017,6 +3033,7 @@
"placePageTitle", "placePageTitle",
"placeEmpty", "placeEmpty",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsVideoEnablePip",
"settingsDisablingBinWarningDialogMessage" "settingsDisablingBinWarningDialogMessage"
], ],
@ -3069,6 +3086,7 @@
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsViewerShowRatingTags", "settingsViewerShowRatingTags",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoEnablePip",
"settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionTile",
"settingsSubtitleThemeTextPositionDialogTitle", "settingsSubtitleThemeTextPositionDialogTitle",
"settingsVideoGestureVerticalDragBrightnessVolume", "settingsVideoGestureVerticalDragBrightnessVolume",
@ -3325,6 +3343,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -3398,7 +3417,8 @@
"pl": [ "pl": [
"vaultLockTypePattern", "vaultLockTypePattern",
"patternDialogEnter", "patternDialogEnter",
"patternDialogConfirm" "patternDialogConfirm",
"settingsVideoEnablePip"
], ],
"pt": [ "pt": [
@ -3424,6 +3444,7 @@
"placePageTitle", "placePageTitle",
"placeEmpty", "placeEmpty",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsVideoEnablePip",
"settingsVideoGestureVerticalDragBrightnessVolume", "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisablingBinWarningDialogMessage" "settingsDisablingBinWarningDialogMessage"
], ],
@ -3438,7 +3459,8 @@
"exportEntryDialogWriteMetadata", "exportEntryDialogWriteMetadata",
"drawerPlacePage", "drawerPlacePage",
"placePageTitle", "placePageTitle",
"placeEmpty" "placeEmpty",
"settingsVideoEnablePip"
], ],
"ru": [ "ru": [
@ -3459,6 +3481,7 @@
"placeEmpty", "placeEmpty",
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsVideoEnablePip",
"settingsVideoGestureVerticalDragBrightnessVolume", "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisablingBinWarningDialogMessage" "settingsDisablingBinWarningDialogMessage"
], ],
@ -3749,6 +3772,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -4104,6 +4128,7 @@
"settingsVideoSectionTitle", "settingsVideoSectionTitle",
"settingsVideoShowVideos", "settingsVideoShowVideos",
"settingsVideoEnableHardwareAcceleration", "settingsVideoEnableHardwareAcceleration",
"settingsVideoEnablePip",
"settingsVideoAutoPlay", "settingsVideoAutoPlay",
"settingsVideoLoopModeTile", "settingsVideoLoopModeTile",
"settingsVideoLoopModeDialogTitle", "settingsVideoLoopModeDialogTitle",
@ -4268,13 +4293,15 @@
"placePageTitle", "placePageTitle",
"placeEmpty", "placeEmpty",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsVideoEnablePip",
"settingsDisablingBinWarningDialogMessage" "settingsDisablingBinWarningDialogMessage"
], ],
"uk": [ "uk": [
"vaultLockTypePattern", "vaultLockTypePattern",
"patternDialogEnter", "patternDialogEnter",
"patternDialogConfirm" "patternDialogConfirm",
"settingsVideoEnablePip"
], ],
"zh": [ "zh": [
@ -4312,6 +4339,7 @@
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoEnablePip",
"settingsVideoGestureVerticalDragBrightnessVolume", "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisablingBinWarningDialogMessage", "settingsDisablingBinWarningDialogMessage",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",
@ -4354,6 +4382,7 @@
"settingsModificationWarningDialogMessage", "settingsModificationWarningDialogMessage",
"settingsConfirmationVaultDataLoss", "settingsConfirmationVaultDataLoss",
"settingsViewerShowDescription", "settingsViewerShowDescription",
"settingsVideoEnablePip",
"settingsVideoGestureVerticalDragBrightnessVolume", "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisablingBinWarningDialogMessage", "settingsDisablingBinWarningDialogMessage",
"settingsAccessibilityShowPinchGestureAlternatives", "settingsAccessibilityShowPinchGestureAlternatives",