#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
- Vaults: custom pattern lock
- Video: picture-in-picture
- Video: handle skip next/previous media buttons
### Changed

View file

@ -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>

View file

@ -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(

View file

@ -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"
}

View file

@ -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",

View file

@ -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;

View file

@ -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;

View file

@ -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:

View file

@ -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 {

View file

@ -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,

View file

@ -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:

View file

@ -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());

View file

@ -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;

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/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,6 +47,8 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
return MagnifierGestureDetectorScope(
axis: const [Axis.horizontal, Axis.vertical],
child: NotificationListener(
onNotification: _handleNotification,
child: PageView.builder(
// key is expected by test driver
key: const Key('horizontal-pageview'),
@ -80,6 +84,7 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
},
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;
}

View file

@ -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

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/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,9 +224,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
child: LayoutBuilder(
builder: (context, constraints) {
final availableSize = Size(constraints.maxWidth, constraints.maxHeight);
return Stack(
children: [
ViewerVerticalPageView(
final viewer = ViewerVerticalPageView(
collection: collection,
entryNotifier: entryNotifier,
viewerController: viewerController,
@ -226,12 +239,23 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
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 {

View file

@ -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;

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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);

View file

@ -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:

View file

@ -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:

View file

@ -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",