#478 picture-in-picture
This commit is contained in:
parent
2183ff7cd7
commit
655d251890
24 changed files with 341 additions and 90 deletions
|
@ -7,6 +7,8 @@ All notable changes to this project will be documented in this file.
|
|||
### Added
|
||||
|
||||
- Vaults: custom pattern lock
|
||||
- Video: picture-in-picture
|
||||
- Video: handle skip next/previous media buttons
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -96,6 +96,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
android:exported="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:launchMode="singleTop"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:theme="@style/NormalTheme"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
|
|
|
@ -65,11 +65,13 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
|||
val stateString = call.argument<String>("state")
|
||||
val positionMillis = call.argument<Number>("positionMillis")?.toLong()
|
||||
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(
|
||||
"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
|
||||
}
|
||||
|
@ -90,6 +92,12 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
|
|||
} else {
|
||||
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()
|
||||
.setState(
|
||||
|
|
|
@ -46,6 +46,16 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
|
|||
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() {
|
||||
super.onStop()
|
||||
success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
|
||||
|
@ -70,6 +80,8 @@ class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat
|
|||
|
||||
const val COMMAND_PLAY = "play"
|
||||
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_SEEK = "seek"
|
||||
}
|
||||
|
|
|
@ -793,6 +793,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Show videos",
|
||||
"settingsVideoEnableHardwareAcceleration": "Hardware acceleration",
|
||||
"settingsVideoEnablePip": "Picture-in-picture",
|
||||
"settingsVideoAutoPlay": "Auto play",
|
||||
"settingsVideoLoopModeTile": "Loop mode",
|
||||
"settingsVideoLoopModeDialogTitle": "Loop Mode",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
|
@ -9,7 +10,7 @@ class Device {
|
|||
late final String _userAgent;
|
||||
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut, _canPrint;
|
||||
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;
|
||||
|
||||
|
@ -41,6 +42,8 @@ class Device {
|
|||
|
||||
bool get supportEdgeToEdgeUIMode => _supportEdgeToEdgeUIMode;
|
||||
|
||||
bool get supportPictureInPicture => _supportPictureInPicture;
|
||||
|
||||
Device._private();
|
||||
|
||||
Future<void> init() async {
|
||||
|
@ -53,6 +56,10 @@ class Device {
|
|||
final auth = LocalAuthentication();
|
||||
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
|
||||
|
||||
final floating = Floating();
|
||||
_supportPictureInPicture = await floating.isPipAvailable;
|
||||
floating.dispose();
|
||||
|
||||
final capabilities = await deviceService.getCapabilities();
|
||||
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
|
||||
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
|
||||
|
|
|
@ -90,6 +90,7 @@ class SettingsDefaults {
|
|||
|
||||
// video
|
||||
static const enableVideoHardwareAcceleration = true;
|
||||
static const enableVideoPip = false;
|
||||
static const videoAutoPlayMode = VideoAutoPlayMode.disabled;
|
||||
static const videoLoopMode = VideoLoopMode.shortOnly;
|
||||
static const videoShowRawTimedText = false;
|
||||
|
|
|
@ -133,6 +133,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// video
|
||||
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
||||
static const enableVideoPipKey = 'video_pip';
|
||||
static const videoAutoPlayModeKey = 'video_auto_play_mode';
|
||||
static const videoLoopModeKey = 'video_loop';
|
||||
static const videoControlsKey = 'video_controls';
|
||||
|
@ -284,6 +285,7 @@ class Settings extends ChangeNotifier {
|
|||
viewerGestureSideTapNext = false;
|
||||
viewerUseCutout = true;
|
||||
viewerMaxBrightness = false;
|
||||
enableVideoPip = false;
|
||||
videoControls = VideoControls.none;
|
||||
videoGestureDoubleTapTogglePlay = false;
|
||||
videoGestureSideDoubleTapSeek = false;
|
||||
|
@ -298,6 +300,9 @@ class Settings extends ChangeNotifier {
|
|||
if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !await windowService.isCutoutAware()) {
|
||||
_set(viewerUseCutoutKey, null);
|
||||
}
|
||||
if (enableVideoPip && !device.supportPictureInPicture) {
|
||||
_set(enableVideoPipKey, null);
|
||||
}
|
||||
}
|
||||
|
||||
// app
|
||||
|
@ -655,6 +660,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
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);
|
||||
|
||||
set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString());
|
||||
|
@ -1078,6 +1087,7 @@ class Settings extends ChangeNotifier {
|
|||
case viewerUseCutoutKey:
|
||||
case viewerMaxBrightnessKey:
|
||||
case enableMotionPhotoAutoPlayKey:
|
||||
case enableVideoPipKey:
|
||||
case enableVideoHardwareAccelerationKey:
|
||||
case videoGestureDoubleTapTogglePlayKey:
|
||||
case videoGestureSideDoubleTapSeekKey:
|
||||
|
|
|
@ -11,7 +11,11 @@ import 'package:get_it/get_it.dart';
|
|||
abstract class MediaSessionService {
|
||||
Stream<MediaCommandEvent> get mediaCommands;
|
||||
|
||||
Future<void> update(AvesVideoController controller);
|
||||
Future<void> update({
|
||||
required AvesVideoController controller,
|
||||
required bool canSkipToNext,
|
||||
required bool canSkipToPrevious,
|
||||
});
|
||||
|
||||
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>();
|
||||
|
||||
@override
|
||||
Future<void> update(AvesVideoController controller) async {
|
||||
Future<void> update({
|
||||
required AvesVideoController controller,
|
||||
required bool canSkipToNext,
|
||||
required bool canSkipToPrevious,
|
||||
}) async {
|
||||
final entry = controller.entry;
|
||||
try {
|
||||
await _platformObject.invokeMethod('update', <String, dynamic>{
|
||||
|
@ -48,6 +56,8 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
|||
'state': _toPlatformState(controller.status),
|
||||
'positionMillis': controller.currentPosition,
|
||||
'playbackSpeed': controller.speed,
|
||||
'canSkipToNext': canSkipToNext,
|
||||
'canSkipToPrevious': canSkipToPrevious,
|
||||
});
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
|
@ -88,6 +98,12 @@ class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
|||
case 'pause':
|
||||
event = const MediaCommandEvent(MediaCommand.pause);
|
||||
break;
|
||||
case 'skip_to_next':
|
||||
event = const MediaCommandEvent(MediaCommand.skipToNext);
|
||||
break;
|
||||
case 'skip_to_previous':
|
||||
event = const MediaCommandEvent(MediaCommand.skipToPrevious);
|
||||
break;
|
||||
case 'stop':
|
||||
event = const MediaCommandEvent(MediaCommand.stop);
|
||||
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
|
||||
class MediaCommandEvent extends Equatable {
|
||||
|
|
|
@ -75,6 +75,11 @@ class Dependencies {
|
|||
license: mit,
|
||||
sourceUrl: 'https://github.com/deckerst/fijkplayer',
|
||||
),
|
||||
Dependency(
|
||||
name: 'Floating',
|
||||
license: mit,
|
||||
sourceUrl: 'https://github.com/wrbl606/floating',
|
||||
),
|
||||
Dependency(
|
||||
name: 'Flutter Display Mode',
|
||||
license: mit,
|
||||
|
|
|
@ -362,7 +362,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
debugPrint('$runtimeType lifecycle ${state.name}');
|
||||
reportService.log('Lifecycle ${state.name}');
|
||||
switch (state) {
|
||||
case AppLifecycleState.inactive:
|
||||
|
|
|
@ -87,14 +87,14 @@ class TileExtentController {
|
|||
int _effectiveColumnCountForExtent(double extent) {
|
||||
if (extent > 0) {
|
||||
final columnCount = _columnCountForExtent(extent);
|
||||
final countMin = _effectiveColumnCountMin();
|
||||
final countMax = _effectiveColumnCountMax();
|
||||
final countMin = min(_effectiveColumnCountMin(), countMax);
|
||||
return columnCount.round().clamp(countMin, countMax);
|
||||
}
|
||||
return columnCountDefault;
|
||||
}
|
||||
|
||||
double get effectiveExtentMin => _extentForColumnCount(_effectiveColumnCountMax());
|
||||
double get effectiveExtentMin => min(_extentForColumnCount(_effectiveColumnCountMax()), effectiveExtentMax);
|
||||
|
||||
double get effectiveExtentMax => _extentForColumnCount(_effectiveColumnCountMin());
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/enums/video_auto_play_mode.dart';
|
||||
|
@ -40,6 +41,7 @@ class VideoSection extends SettingsSection {
|
|||
return [
|
||||
if (!standalonePage) SettingsTileVideoShowVideos(),
|
||||
SettingsTileVideoEnableHardwareAcceleration(),
|
||||
if (!settings.useTvLayout && device.supportPictureInPicture) SettingsTileVideoEnablePip(),
|
||||
SettingsTileVideoEnableAutoPlay(),
|
||||
SettingsTileVideoLoopMode(),
|
||||
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 {
|
||||
@override
|
||||
String title(BuildContext context) => context.l10n.settingsVideoAutoPlay;
|
||||
|
|
|
@ -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/settings.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/multipage/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.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/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -45,40 +47,43 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
|||
|
||||
return MagnifierGestureDetectorScope(
|
||||
axis: const [Axis.horizontal, Axis.vertical],
|
||||
child: PageView.builder(
|
||||
// key is expected by test driver
|
||||
key: const Key('horizontal-pageview'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: pageController,
|
||||
physics: MagnifierScrollerPhysics(
|
||||
gestureSettings: context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings),
|
||||
parent: const BouncingScrollPhysics(),
|
||||
child: NotificationListener(
|
||||
onNotification: _handleNotification,
|
||||
child: PageView.builder(
|
||||
// key is expected by test driver
|
||||
key: const Key('horizontal-pageview'),
|
||||
scrollDirection: Axis.horizontal,
|
||||
controller: pageController,
|
||||
physics: MagnifierScrollerPhysics(
|
||||
gestureSettings: context.select<MediaQueryData, DeviceGestureSettings>((mq) => mq.gestureSettings),
|
||||
parent: const BouncingScrollPhysics(),
|
||||
),
|
||||
onPageChanged: widget.onPageChanged,
|
||||
itemBuilder: (context, index) {
|
||||
final mainEntry = entries[index % entries.length];
|
||||
|
||||
final child = mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildViewer(mainEntry, pageEntry: pageEntry),
|
||||
)
|
||||
: _buildViewer(mainEntry);
|
||||
|
||||
return Selector<Settings, bool>(
|
||||
selector: (context, s) => s.accessibilityAnimations.animate,
|
||||
builder: (context, animate, child) {
|
||||
if (!animate) return child!;
|
||||
return AnimatedBuilder(
|
||||
animation: pageController,
|
||||
builder: viewerController.transition.builder(pageController, index),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
itemCount: viewerController.repeat ? null : entries.length,
|
||||
),
|
||||
onPageChanged: widget.onPageChanged,
|
||||
itemBuilder: (context, index) {
|
||||
final mainEntry = entries[index % entries.length];
|
||||
|
||||
final child = mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildViewer(mainEntry, pageEntry: pageEntry),
|
||||
)
|
||||
: _buildViewer(mainEntry);
|
||||
|
||||
return Selector<Settings, bool>(
|
||||
selector: (context, s) => s.accessibilityAnimations.animate,
|
||||
builder: (context, animate, child) {
|
||||
if (!animate) return child!;
|
||||
return AnimatedBuilder(
|
||||
animation: pageController,
|
||||
builder: viewerController.transition.builder(pageController, index),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
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
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
|
@ -41,12 +41,14 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final collection = widget.collection;
|
||||
return AvesScaffold(
|
||||
body: ViewStateConductorProvider(
|
||||
child: VideoConductorProvider(
|
||||
collection: collection,
|
||||
child: MultiPageConductorProvider(
|
||||
child: EntryViewerStack(
|
||||
collection: widget.collection,
|
||||
collection: collection,
|
||||
initialEntry: widget.initialEntry,
|
||||
viewerController: _viewerController,
|
||||
),
|
||||
|
@ -86,17 +88,19 @@ class ViewStateConductorProvider extends StatelessWidget {
|
|||
}
|
||||
|
||||
class VideoConductorProvider extends StatelessWidget {
|
||||
final CollectionLens? collection;
|
||||
final Widget? child;
|
||||
|
||||
const VideoConductorProvider({
|
||||
super.key,
|
||||
this.child,
|
||||
this.collection,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider<VideoConductor>(
|
||||
create: (context) => VideoConductor(),
|
||||
create: (context) => VideoConductor(collection: collection),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: child,
|
||||
);
|
||||
|
@ -108,7 +112,7 @@ class MultiPageConductorProvider extends StatelessWidget {
|
|||
|
||||
const MultiPageConductorProvider({
|
||||
super.key,
|
||||
this.child,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
|
@ -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/controller_mixin.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:floating/floating.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -58,6 +59,7 @@ class EntryViewerStack extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||
final Floating _floating = Floating();
|
||||
late int _currentEntryIndex;
|
||||
late ValueNotifier<int> _currentVerticalPage;
|
||||
late PageController _horizontalPager, _verticalPager;
|
||||
|
@ -159,6 +161,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
_floating.dispose();
|
||||
cleanEntryControllers(entryNotifier.value);
|
||||
_videoActionDelegate.dispose();
|
||||
_overlayAnimationController.dispose();
|
||||
|
@ -183,9 +186,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
switch (state) {
|
||||
case AppLifecycleState.inactive:
|
||||
_onAppInactive();
|
||||
break;
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
viewerController.autopilot = false;
|
||||
pauseVideoControllers();
|
||||
break;
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final viewStateConductor = context.read<ViewStateConductor>();
|
||||
|
@ -209,29 +224,38 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableSize = Size(constraints.maxWidth, constraints.maxHeight);
|
||||
return Stack(
|
||||
children: [
|
||||
ViewerVerticalPageView(
|
||||
collection: collection,
|
||||
entryNotifier: entryNotifier,
|
||||
viewerController: viewerController,
|
||||
overlayOpacity: _overlayInitialized
|
||||
? _overlayOpacity
|
||||
: settings.showOverlayOnOpening
|
||||
? kAlwaysCompleteAnimation
|
||||
: kAlwaysDismissedAnimation,
|
||||
verticalPager: _verticalPager,
|
||||
horizontalPager: _horizontalPager,
|
||||
onVerticalPageChanged: _onVerticalPageChanged,
|
||||
onHorizontalPageChanged: _onHorizontalPageChanged,
|
||||
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
||||
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
|
||||
),
|
||||
..._buildOverlays(availableSize).map(_decorateOverlay),
|
||||
const TopGestureAreaProtector(),
|
||||
const SideGestureAreaProtector(),
|
||||
const BottomGestureAreaProtector(),
|
||||
],
|
||||
final viewer = ViewerVerticalPageView(
|
||||
collection: collection,
|
||||
entryNotifier: entryNotifier,
|
||||
viewerController: viewerController,
|
||||
overlayOpacity: _overlayInitialized
|
||||
? _overlayOpacity
|
||||
: settings.showOverlayOnOpening
|
||||
? kAlwaysCompleteAnimation
|
||||
: kAlwaysDismissedAnimation,
|
||||
verticalPager: _verticalPager,
|
||||
horizontalPager: _horizontalPager,
|
||||
onVerticalPageChanged: _onVerticalPageChanged,
|
||||
onHorizontalPageChanged: _onHorizontalPageChanged,
|
||||
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
||||
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),
|
||||
const TopGestureAreaProtector(),
|
||||
const SideGestureAreaProtector(),
|
||||
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
|
||||
|
||||
Future<void> _initOverlay() async {
|
||||
|
|
|
@ -40,6 +40,12 @@ class ShowEntryNotification extends Notification {
|
|||
});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ShowPreviousVideoNotification extends Notification {}
|
||||
|
||||
@immutable
|
||||
class ShowNextVideoNotification extends Notification {}
|
||||
|
||||
@immutable
|
||||
class ToggleOverlayNotification extends Notification {
|
||||
final bool? visible;
|
||||
|
|
|
@ -3,18 +3,20 @@ import 'dart:async';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/enums/enums.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/widgets/viewer/video/controller.dart';
|
||||
import 'package:aves/widgets/viewer/video/fijkplayer.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class VideoConductor {
|
||||
final CollectionLens? _collection;
|
||||
final List<AvesVideoController> _controllers = [];
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
static const _defaultMaxControllerCount = 3;
|
||||
|
||||
VideoConductor();
|
||||
VideoConductor({CollectionLens? collection}) : _collection = collection;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await disposeAll();
|
||||
|
@ -33,7 +35,7 @@ class VideoConductor {
|
|||
_controllers.remove(controller);
|
||||
} else {
|
||||
controller = IjkPlayerAvesVideoController(entry, persistPlayback: true);
|
||||
_subscriptions.add(controller.statusStream.listen(_onControllerStatusChanged));
|
||||
_subscriptions.add(controller.statusStream.listen((event) => _onControllerStatusChanged(controller!, event)));
|
||||
}
|
||||
_controllers.insert(0, controller);
|
||||
while (_controllers.length > (maxControllerCount ?? _defaultMaxControllerCount)) {
|
||||
|
@ -42,11 +44,29 @@ class VideoConductor {
|
|||
return controller;
|
||||
}
|
||||
|
||||
AvesVideoController? getPlayingController() => _controllers.firstWhereOrNull((c) => c.isPlaying);
|
||||
|
||||
AvesVideoController? getController(AvesEntry entry) {
|
||||
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) {
|
||||
await windowService.keepScreenOn(status == VideoStatus.playing);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
abstract class AvesVideoController {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final AvesEntry _entry;
|
||||
final bool persistPlayback;
|
||||
|
||||
|
@ -22,14 +21,10 @@ abstract class AvesVideoController {
|
|||
|
||||
AvesVideoController(AvesEntry entry, {required this.persistPlayback}) : _entry = entry {
|
||||
entry.visualChangeNotifier.addListener(onVisualChanged);
|
||||
_subscriptions.add(statusStream.listen((event) => mediaSessionService.update(this)));
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
Future<void> dispose() async {
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
entry.visualChangeNotifier.removeListener(onVisualChanged);
|
||||
await _savePlaybackState();
|
||||
}
|
||||
|
|
|
@ -450,6 +450,12 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
|||
case MediaCommand.pause:
|
||||
videoController.pause();
|
||||
break;
|
||||
case MediaCommand.skipToNext:
|
||||
ShowNextVideoNotification().dispatch(context);
|
||||
break;
|
||||
case MediaCommand.skipToPrevious:
|
||||
ShowPreviousVideoNotification().dispatch(context);
|
||||
break;
|
||||
case MediaCommand.stop:
|
||||
videoController.pause();
|
||||
break;
|
||||
|
|
|
@ -55,7 +55,10 @@ class PlatformReportService extends ReportService {
|
|||
}
|
||||
|
||||
@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
|
||||
Future<void> setCustomKey(String key, Object value) async => _instance?.setCustomKey(key, value);
|
||||
|
|
|
@ -373,6 +373,15 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -60,6 +60,10 @@ dependencies:
|
|||
url: https://github.com/deckerst/fijkplayer.git
|
||||
ref: aves
|
||||
flex_color_picker:
|
||||
floating:
|
||||
git:
|
||||
url: https://github.com/deckerst/floating.git
|
||||
ref: source-rect-hint
|
||||
fluster:
|
||||
flutter_displaymode:
|
||||
flutter_highlight:
|
||||
|
|
|
@ -479,6 +479,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -1034,6 +1035,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -1198,6 +1200,7 @@
|
|||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsDisablingBinWarningDialogMessage"
|
||||
],
|
||||
|
||||
|
@ -1231,6 +1234,7 @@
|
|||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsDisablingBinWarningDialogMessage"
|
||||
],
|
||||
|
||||
|
@ -1242,19 +1246,22 @@
|
|||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
"placeEmpty",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"vaultLockTypePattern",
|
||||
"patternDialogEnter",
|
||||
"patternDialogConfirm"
|
||||
"patternDialogConfirm",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
"vaultLockTypePattern",
|
||||
"patternDialogEnter",
|
||||
"patternDialogConfirm"
|
||||
"patternDialogConfirm",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
|
@ -1596,6 +1603,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -1731,7 +1739,8 @@
|
|||
"fr": [
|
||||
"vaultLockTypePattern",
|
||||
"patternDialogEnter",
|
||||
"patternDialogConfirm"
|
||||
"patternDialogConfirm",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"gl": [
|
||||
|
@ -2103,6 +2112,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -2735,6 +2745,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -2872,7 +2883,8 @@
|
|||
"id": [
|
||||
"vaultLockTypePattern",
|
||||
"patternDialogEnter",
|
||||
"patternDialogConfirm"
|
||||
"patternDialogConfirm",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"it": [
|
||||
|
@ -2885,7 +2897,8 @@
|
|||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
"placeEmpty",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
|
@ -2931,6 +2944,7 @@
|
|||
"settingsModificationWarningDialogMessage",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsViewerShowDescription",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||
"settingsDisablingBinWarningDialogMessage",
|
||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||
|
@ -2941,7 +2955,8 @@
|
|||
"ko": [
|
||||
"vaultLockTypePattern",
|
||||
"patternDialogEnter",
|
||||
"patternDialogConfirm"
|
||||
"patternDialogConfirm",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"lt": [
|
||||
|
@ -2981,6 +2996,7 @@
|
|||
"settingsModificationWarningDialogMessage",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsViewerShowDescription",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||
"settingsDisablingBinWarningDialogMessage",
|
||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||
|
@ -3017,6 +3033,7 @@
|
|||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsDisablingBinWarningDialogMessage"
|
||||
],
|
||||
|
||||
|
@ -3069,6 +3086,7 @@
|
|||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsViewerShowRatingTags",
|
||||
"settingsViewerShowDescription",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsSubtitleThemeTextPositionTile",
|
||||
"settingsSubtitleThemeTextPositionDialogTitle",
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||
|
@ -3325,6 +3343,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -3398,7 +3417,8 @@
|
|||
"pl": [
|
||||
"vaultLockTypePattern",
|
||||
"patternDialogEnter",
|
||||
"patternDialogConfirm"
|
||||
"patternDialogConfirm",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
|
@ -3424,6 +3444,7 @@
|
|||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||
"settingsDisablingBinWarningDialogMessage"
|
||||
],
|
||||
|
@ -3438,7 +3459,8 @@
|
|||
"exportEntryDialogWriteMetadata",
|
||||
"drawerPlacePage",
|
||||
"placePageTitle",
|
||||
"placeEmpty"
|
||||
"placeEmpty",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
|
@ -3459,6 +3481,7 @@
|
|||
"placeEmpty",
|
||||
"settingsModificationWarningDialogMessage",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||
"settingsDisablingBinWarningDialogMessage"
|
||||
],
|
||||
|
@ -3749,6 +3772,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -4104,6 +4128,7 @@
|
|||
"settingsVideoSectionTitle",
|
||||
"settingsVideoShowVideos",
|
||||
"settingsVideoEnableHardwareAcceleration",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoAutoPlay",
|
||||
"settingsVideoLoopModeTile",
|
||||
"settingsVideoLoopModeDialogTitle",
|
||||
|
@ -4268,13 +4293,15 @@
|
|||
"placePageTitle",
|
||||
"placeEmpty",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsDisablingBinWarningDialogMessage"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"vaultLockTypePattern",
|
||||
"patternDialogEnter",
|
||||
"patternDialogConfirm"
|
||||
"patternDialogConfirm",
|
||||
"settingsVideoEnablePip"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
|
@ -4312,6 +4339,7 @@
|
|||
"settingsModificationWarningDialogMessage",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsViewerShowDescription",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||
"settingsDisablingBinWarningDialogMessage",
|
||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||
|
@ -4354,6 +4382,7 @@
|
|||
"settingsModificationWarningDialogMessage",
|
||||
"settingsConfirmationVaultDataLoss",
|
||||
"settingsViewerShowDescription",
|
||||
"settingsVideoEnablePip",
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||
"settingsDisablingBinWarningDialogMessage",
|
||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||
|
|
Loading…
Reference in a new issue