diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8de3150aa..94b7b0a1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+### Added
+
+- Video: optional gestures to adjust brightness/volume
+
## [v1.7.9] - 2023-01-15
### Added
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 52c9ab346..1141b9bfb 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -781,6 +781,7 @@
"settingsVideoButtonsTile": "Buttons",
"settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause",
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
+ "settingsVideoGestureVerticalDragBrightnessVolume": "Swipe up or down to adjust brightness/volume",
"settingsPrivacySectionTitle": "Privacy",
"settingsAllowInstalledAppAccess": "Allow access to app inventory",
diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart
index 1dbcc1a18..fb2273e6d 100644
--- a/lib/model/settings/defaults.dart
+++ b/lib/model/settings/defaults.dart
@@ -97,6 +97,7 @@ class SettingsDefaults {
static const videoControls = VideoControls.play;
static const videoGestureDoubleTapTogglePlay = false;
static const videoGestureSideDoubleTapSeek = true;
+ static const videoGestureVerticalDragBrightnessVolume = false;
// subtitles
static const subtitleFontSize = 20.0;
diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart
index 27c944d82..f2595ffae 100644
--- a/lib/model/settings/settings.dart
+++ b/lib/model/settings/settings.dart
@@ -41,7 +41,6 @@ class Settings extends ChangeNotifier {
static const Set _internalKeys = {
hasAcceptedTermsKey,
catalogTimeZoneKey,
- videoShowRawTimedTextKey,
searchHistoryKey,
platformAccelerometerRotationKey,
platformTransitionAnimationScaleKey,
@@ -131,10 +130,10 @@ class Settings extends ChangeNotifier {
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
static const videoAutoPlayModeKey = 'video_auto_play_mode';
static const videoLoopModeKey = 'video_loop';
- static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
static const videoControlsKey = 'video_controls';
static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play';
static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip';
+ static const videoGestureVerticalDragBrightnessVolumeKey = 'video_gesture_vertical_drag_brightness_volume';
// subtitles
static const subtitleFontSizeKey = 'subtitle_font_size';
@@ -637,10 +636,6 @@ class Settings extends ChangeNotifier {
set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString());
- bool get videoShowRawTimedText => getBool(videoShowRawTimedTextKey) ?? SettingsDefaults.videoShowRawTimedText;
-
- set videoShowRawTimedText(bool newValue) => _set(videoShowRawTimedTextKey, newValue);
-
VideoControls get videoControls => getEnumOrDefault(videoControlsKey, SettingsDefaults.videoControls, VideoControls.values);
set videoControls(VideoControls newValue) => _set(videoControlsKey, newValue.toString());
@@ -653,6 +648,10 @@ class Settings extends ChangeNotifier {
set videoGestureSideDoubleTapSeek(bool newValue) => _set(videoGestureSideDoubleTapSeekKey, newValue);
+ bool get videoGestureVerticalDragBrightnessVolume => getBool(videoGestureVerticalDragBrightnessVolumeKey) ?? SettingsDefaults.videoGestureVerticalDragBrightnessVolume;
+
+ set videoGestureVerticalDragBrightnessVolume(bool newValue) => _set(videoGestureVerticalDragBrightnessVolumeKey, newValue);
+
// subtitles
double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize;
@@ -1039,6 +1038,7 @@ class Settings extends ChangeNotifier {
case enableVideoHardwareAccelerationKey:
case videoGestureDoubleTapTogglePlayKey:
case videoGestureSideDoubleTapSeekKey:
+ case videoGestureVerticalDragBrightnessVolumeKey:
case subtitleShowOutlineKey:
case tagEditorCurrentFilterSectionExpandedKey:
case saveSearchHistoryKey:
diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart
index 50185d2b2..cd05e0c89 100644
--- a/lib/theme/icons.dart
+++ b/lib/theme/icons.dart
@@ -14,6 +14,8 @@ class AIcons {
static const IconData aspectRatio = Icons.aspect_ratio_outlined;
static const IconData bin = Icons.delete_outlined;
static const IconData broken = Icons.broken_image_outlined;
+ static const IconData brightnessMin = Icons.brightness_low_outlined;
+ static const IconData brightnessMax = Icons.brightness_high_outlined;
static const IconData checked = Icons.done_outlined;
static const IconData count = MdiIcons.counter;
static const IconData counter = Icons.plus_one_outlined;
@@ -52,6 +54,8 @@ class AIcons {
static const IconData text = Icons.format_quote_outlined;
static const IconData tag = Icons.local_offer_outlined;
static const IconData tagUntagged = MdiIcons.tagOffOutline;
+ static const IconData volumeMin = Icons.volume_mute_outlined;
+ static const IconData volumeMax = Icons.volume_up_outlined;
// view
static const IconData group = Icons.group_work_outlined;
diff --git a/lib/utils/dependencies.dart b/lib/utils/dependencies.dart
index 9bb4babd0..27a0c94c9 100644
--- a/lib/utils/dependencies.dart
+++ b/lib/utils/dependencies.dart
@@ -123,6 +123,11 @@ class Dependencies {
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE',
sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher',
),
+ Dependency(
+ name: 'Volume Controller',
+ license: mit,
+ sourceUrl: 'https://github.com/kurenai7968/volume_controller',
+ ),
];
static const List _googleMobileServices = [
diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart
index a4bbe7c98..d4fc6dc75 100644
--- a/lib/widgets/debug/settings.dart
+++ b/lib/widgets/debug/settings.dart
@@ -43,11 +43,6 @@ class DebugSettingsSection extends StatelessWidget {
onChanged: (v) => settings.canUseAnalysisService = v,
title: const Text('canUseAnalysisService'),
),
- SwitchListTile(
- value: settings.videoShowRawTimedText,
- onChanged: (v) => settings.videoShowRawTimedText = v,
- title: const Text('videoShowRawTimedText'),
- ),
Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: InfoRowGroup(
diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart
index e57b8ef18..d7743500c 100644
--- a/lib/widgets/settings/video/controls.dart
+++ b/lib/widgets/settings/video/controls.dart
@@ -36,6 +36,11 @@ class VideoControlsPage extends StatelessWidget {
onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v,
title: context.l10n.settingsVideoGestureSideDoubleTapSeek,
),
+ SettingsSwitchListTile(
+ selector: (context, s) => s.videoGestureVerticalDragBrightnessVolume,
+ onChanged: (v) => settings.videoGestureVerticalDragBrightnessVolume = v,
+ title: context.l10n.settingsVideoGestureVerticalDragBrightnessVolume,
+ ),
],
),
),
diff --git a/lib/widgets/settings/video/subtitle_sample.dart b/lib/widgets/settings/video/subtitle_sample.dart
index e41ab0848..c8ae248e4 100644
--- a/lib/widgets/settings/video/subtitle_sample.dart
+++ b/lib/widgets/settings/video/subtitle_sample.dart
@@ -5,7 +5,7 @@ import 'package:aves/widgets/common/basic/text/background_painter.dart';
import 'package:aves/widgets/common/basic/text/outlined.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart';
-import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart
index 5dc2c3d74..871f1175f 100644
--- a/lib/widgets/viewer/entry_viewer_stack.dart
+++ b/lib/widgets/viewer/entry_viewer_stack.dart
@@ -680,9 +680,7 @@ class _EntryViewerStackState extends State with EntryViewContr
}
Future _onLeave() async {
- if (settings.viewerMaxBrightness) {
- await ScreenBrightness().resetScreenBrightness();
- }
+ await ScreenBrightness().resetScreenBrightness();
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
await windowService.keepScreenOn(false);
}
diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart
index 287a8caea..2257879b9 100644
--- a/lib/widgets/viewer/visual/entry_page_view.dart
+++ b/lib/widgets/viewer/visual/entry_page_view.dart
@@ -3,30 +3,27 @@ import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/entry.dart';
-import 'package:aves/model/entry_images.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/media_session_service.dart';
-import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/action_mixins/feedback.dart';
import 'package:aves/widgets/common/basic/insets.dart';
-import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
-import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:aves/widgets/viewer/visual/error.dart';
import 'package:aves/widgets/viewer/visual/raster.dart';
import 'package:aves/widgets/viewer/visual/state.dart';
-import 'package:aves/widgets/viewer/visual/subtitle/subtitle.dart';
import 'package:aves/widgets/viewer/visual/vector.dart';
-import 'package:aves/widgets/viewer/visual/video.dart';
+import 'package:aves/widgets/viewer/visual/video/cover.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/subtitle.dart';
+import 'package:aves/widgets/viewer/visual/video/swipe_action.dart';
+import 'package:aves/widgets/viewer/visual/video/video_view.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
-import 'package:collection/collection.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -55,17 +52,8 @@ class _EntryPageViewState extends State with SingleTickerProvider
late ValueNotifier _viewStateNotifier;
late AvesMagnifierController _magnifierController;
final List _subscriptions = [];
- ImageStream? _videoCoverStream;
- late ImageStreamListener _videoCoverStreamListener;
- final ValueNotifier _videoCoverInfoNotifier = ValueNotifier(null);
final ValueNotifier _actionFeedbackChildNotifier = ValueNotifier(null);
-
- AvesMagnifierController? _dismissedCoverMagnifierController;
-
- AvesMagnifierController get dismissedCoverMagnifierController {
- _dismissedCoverMagnifierController ??= AvesMagnifierController();
- return _dismissedCoverMagnifierController!;
- }
+ OverlayEntry? _actionFeedbackOverlayEntry;
AvesEntry get mainEntry => widget.mainEntry;
@@ -73,9 +61,6 @@ class _EntryPageViewState extends State with SingleTickerProvider
ViewerController get viewerController => widget.viewerController;
- // use the high res photo as cover for the video part of a motion photo
- ImageProvider get videoCoverUriImage => mainEntry.isMotionPhoto ? mainEntry.uriImage : entry.uriImage;
-
static const rasterMaxScale = ScaleLevel(factor: 5);
static const vectorMaxScale = ScaleLevel(factor: 25);
@@ -110,9 +95,6 @@ class _EntryPageViewState extends State with SingleTickerProvider
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
if (entry.isVideo) {
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
- _videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image);
- _videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty);
- _videoCoverStream!.addListener(_videoCoverStreamListener);
}
viewerController.startAutopilotAnimation(
vsync: this,
@@ -127,9 +109,6 @@ class _EntryPageViewState extends State with SingleTickerProvider
void _unregisterWidget(EntryPageView oldWidget) {
viewerController.stopAutopilotAnimation(vsync: this);
- _videoCoverStream?.removeListener(_videoCoverStreamListener);
- _videoCoverStream = null;
- _videoCoverInfoNotifier.value = null;
_magnifierController.dispose();
_subscriptions
..forEach((sub) => sub.cancel())
@@ -222,169 +201,169 @@ class _EntryPageViewState extends State with SingleTickerProvider
builder: (context, sar, child) {
final videoDisplaySize = entry.videoDisplaySize(sar);
- return Selector>(
- selector: (context, s) => Tuple2(s.videoGestureDoubleTapTogglePlay, s.videoGestureSideDoubleTapSeek),
+ return Selector>(
+ selector: (context, s) => Tuple3(
+ s.videoGestureDoubleTapTogglePlay,
+ s.videoGestureSideDoubleTapSeek,
+ s.videoGestureVerticalDragBrightnessVolume,
+ ),
builder: (context, s, child) {
final playGesture = s.item1;
final seekGesture = s.item2;
- final useActionGesture = playGesture || seekGesture;
+ final useVerticalDragGesture = s.item3;
+ final useTapGesture = playGesture || seekGesture;
- void _applyAction(EntryAction action, {IconData? Function()? icon}) {
- _actionFeedbackChildNotifier.value = DecoratedIcon(
- icon?.call() ?? action.getIconData(),
- size: 48,
- color: Colors.white,
- shadows: const [
- Shadow(
- color: Colors.black,
- blurRadius: 4,
- )
- ],
- );
- VideoActionNotification(
- controller: videoController,
- action: action,
- ).dispatch(context);
+ MagnifierDoubleTapCallback? onDoubleTap;
+ MagnifierGestureScaleStartCallback? onScaleStart;
+ MagnifierGestureScaleUpdateCallback? onScaleUpdate;
+ MagnifierGestureScaleEndCallback? onScaleEnd;
+
+ if (useTapGesture) {
+ void _applyAction(EntryAction action, {IconData? Function()? icon}) {
+ _actionFeedbackChildNotifier.value = DecoratedIcon(
+ icon?.call() ?? action.getIconData(),
+ size: 48,
+ color: Colors.white,
+ shadows: const [
+ Shadow(
+ color: Colors.black,
+ blurRadius: 4,
+ )
+ ],
+ );
+ VideoActionNotification(
+ controller: videoController,
+ action: action,
+ ).dispatch(context);
+ }
+
+ onDoubleTap = (alignment) {
+ final x = alignment.x;
+ if (seekGesture) {
+ if (x < sideRatio) {
+ _applyAction(EntryAction.videoReplay10);
+ return true;
+ } else if (x > 1 - sideRatio) {
+ _applyAction(EntryAction.videoSkip10);
+ return true;
+ }
+ }
+ if (playGesture) {
+ _applyAction(
+ EntryAction.videoTogglePlay,
+ icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
+ );
+ return true;
+ }
+ return false;
+ };
}
- MagnifierDoubleTapCallback? _onDoubleTap = useActionGesture
- ? (alignment) {
- final x = alignment.x;
- if (seekGesture) {
- if (x < sideRatio) {
- _applyAction(EntryAction.videoReplay10);
- return true;
- } else if (x > 1 - sideRatio) {
- _applyAction(EntryAction.videoSkip10);
- return true;
- }
- }
- if (playGesture) {
- _applyAction(
- EntryAction.videoTogglePlay,
- icon: () => videoController.isPlaying ? AIcons.pause : AIcons.play,
- );
- return true;
- }
- return false;
- }
- : null;
+ if (useVerticalDragGesture) {
+ SwipeAction? swipeAction;
+ var move = Offset.zero;
+ var dropped = false;
+ double? startValue;
+ final valueNotifier = ValueNotifier(null);
+ onScaleStart = (details, doubleTap, boundaries) {
+ dropped = details.pointerCount > 1 || doubleTap;
+ if (dropped) return;
+
+ startValue = null;
+ valueNotifier.value = null;
+ final alignmentX = details.focalPoint.dx / boundaries.viewportSize.width;
+ final action = alignmentX > .5 ? SwipeAction.volume : SwipeAction.brightness;
+ action.get().then((v) => startValue = v);
+ swipeAction = action;
+ move = Offset.zero;
+ _actionFeedbackOverlayEntry = OverlayEntry(
+ builder: (context) => SwipeActionFeedback(
+ action: action,
+ valueNotifier: valueNotifier,
+ ),
+ );
+ Overlay.of(context)!.insert(_actionFeedbackOverlayEntry!);
+ };
+ onScaleUpdate = (details) {
+ move += details.focalPointDelta;
+ dropped |= details.pointerCount > 1;
+ if (valueNotifier.value == null) {
+ dropped |= MagnifierGestureRecognizer.isXPan(move);
+ }
+ if (dropped) return false;
+
+ final _startValue = startValue;
+ if (_startValue != null) {
+ final double value = (_startValue - move.dy / SwipeActionFeedback.height).clamp(0, 1);
+ valueNotifier.value = value;
+ swipeAction?.set(value);
+ }
+ return true;
+ };
+ onScaleEnd = (details) {
+ if (_actionFeedbackOverlayEntry != null) {
+ _actionFeedbackOverlayEntry!.remove();
+ _actionFeedbackOverlayEntry = null;
+ }
+ };
+ }
+
+ Widget videoChild = Stack(
+ children: [
+ _buildMagnifier(
+ displaySize: videoDisplaySize,
+ onScaleStart: onScaleStart,
+ onScaleUpdate: onScaleUpdate,
+ onScaleEnd: onScaleEnd,
+ onDoubleTap: onDoubleTap,
+ child: VideoView(
+ entry: entry,
+ controller: videoController,
+ ),
+ ),
+ VideoSubtitles(
+ controller: videoController,
+ viewStateNotifier: _viewStateNotifier,
+ ),
+ if (useTapGesture)
+ ValueListenableBuilder(
+ valueListenable: _actionFeedbackChildNotifier,
+ builder: (context, feedbackChild, child) => ActionFeedback(
+ child: feedbackChild,
+ ),
+ ),
+ ],
+ );
+ if (useVerticalDragGesture) {
+ videoChild = MagnifierGestureDetectorScope.of(context)!.copyWith(
+ acceptPointerEvent: MagnifierGestureRecognizer.isYPan,
+ child: videoChild,
+ );
+ }
return Stack(
fit: StackFit.expand,
children: [
- Stack(
- children: [
- _buildMagnifier(
- displaySize: videoDisplaySize,
- onDoubleTap: _onDoubleTap,
- child: VideoView(
- entry: entry,
- controller: videoController,
- ),
- ),
- VideoSubtitles(
- controller: videoController,
- viewStateNotifier: _viewStateNotifier,
- ),
- if (settings.videoShowRawTimedText)
- VideoSubtitles(
- controller: videoController,
- viewStateNotifier: _viewStateNotifier,
- debugMode: true,
- ),
- if (useActionGesture)
- ValueListenableBuilder(
- valueListenable: _actionFeedbackChildNotifier,
- builder: (context, feedbackChild, child) => ActionFeedback(
- child: feedbackChild,
- ),
- ),
- ],
- ),
- _buildVideoCover(
+ videoChild,
+ VideoCover(
+ mainEntry: mainEntry,
+ pageEntry: entry,
+ magnifierController: _magnifierController,
videoController: videoController,
videoDisplaySize: videoDisplaySize,
- onDoubleTap: _onDoubleTap,
- ),
- ],
- );
- },
- );
- },
- );
- }
-
- StreamBuilder _buildVideoCover({
- required AvesVideoController videoController,
- required Size videoDisplaySize,
- required MagnifierDoubleTapCallback? onDoubleTap,
- }) {
- // fade out image to ease transition with the player
- return StreamBuilder(
- stream: videoController.statusStream,
- builder: (context, snapshot) {
- final showCover = !videoController.isReady;
- return IgnorePointer(
- ignoring: !showCover,
- child: AnimatedOpacity(
- opacity: showCover ? 1 : 0,
- curve: Curves.easeInCirc,
- duration: Durations.viewerVideoPlayerTransition,
- onEnd: () {
- // while cover is fading out, the same controller is used for both the cover and the video,
- // and both fire scale boundaries events, so we make sure that in the end
- // the scale boundaries from the video are used after the cover is gone
- final boundaries = _magnifierController.scaleBoundaries;
- if (boundaries != null) {
- _magnifierController.setScaleBoundaries(
- boundaries.copyWith(
- childSize: videoDisplaySize,
- ),
- );
- }
- },
- child: ValueListenableBuilder(
- valueListenable: _videoCoverInfoNotifier,
- builder: (context, videoCoverInfo, child) {
- if (videoCoverInfo != null) {
- // full cover image may have a different size and different aspect ratio
- final coverSize = Size(
- videoCoverInfo.image.width.toDouble(),
- videoCoverInfo.image.height.toDouble(),
- );
- // when the cover is the same size as the video itself
- // (which is often the case when the cover is not embedded but just a frame),
- // we can reuse the same magnifier and preserve its state when switching from cover to video
- final coverController = showCover || coverSize == videoDisplaySize ? _magnifierController : dismissedCoverMagnifierController;
- return _buildMagnifier(
+ onTap: _onTap,
+ magnifierBuilder: (coverController, coverSize, videoCoverUriImage) => _buildMagnifier(
controller: coverController,
displaySize: coverSize,
onDoubleTap: onDoubleTap,
child: Image(
image: videoCoverUriImage,
),
- );
- }
-
- // default to cached thumbnail, if any
- final extent = entry.cachedThumbnails.firstOrNull?.key.extent;
- if (extent != null && extent > 0) {
- return GestureDetector(
- onTap: _onTap,
- child: ThumbnailImage(
- entry: entry,
- extent: extent,
- fit: BoxFit.contain,
- showLoadingBackground: false,
- ),
- );
- }
-
- return const SizedBox();
- },
- ),
- ),
+ ),
+ ),
+ ],
+ );
+ },
);
},
);
@@ -396,6 +375,9 @@ class _EntryPageViewState extends State with SingleTickerProvider
ScaleLevel maxScale = rasterMaxScale,
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
bool applyScale = true,
+ MagnifierGestureScaleStartCallback? onScaleStart,
+ MagnifierGestureScaleUpdateCallback? onScaleUpdate,
+ MagnifierGestureScaleEndCallback? onScaleEnd,
MagnifierDoubleTapCallback? onDoubleTap,
required Widget child,
}) {
@@ -413,6 +395,9 @@ class _EntryPageViewState extends State with SingleTickerProvider
initialScale: viewerController.initialScale,
scaleStateCycle: scaleStateCycle,
applyScale: applyScale,
+ onScaleStart: onScaleStart,
+ onScaleUpdate: onScaleUpdate,
+ onScaleEnd: onScaleEnd,
onTap: (c, s, a, p) => _onTap(alignment: a),
onDoubleTap: onDoubleTap,
child: child,
@@ -487,5 +472,3 @@ class _EntryPageViewState extends State with SingleTickerProvider
}
}
}
-
-typedef MagnifierTapCallback = void Function(Offset childPosition);
diff --git a/lib/widgets/viewer/visual/video/cover.dart b/lib/widgets/viewer/visual/video/cover.dart
new file mode 100644
index 000000000..e9a3a81e1
--- /dev/null
+++ b/lib/widgets/viewer/visual/video/cover.dart
@@ -0,0 +1,160 @@
+import 'package:aves/model/entry.dart';
+import 'package:aves/model/entry_images.dart';
+import 'package:aves/theme/durations.dart';
+import 'package:aves/widgets/common/thumbnail/image.dart';
+import 'package:aves/widgets/viewer/video/controller.dart';
+import 'package:aves_magnifier/aves_magnifier.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+
+class VideoCover extends StatefulWidget {
+ final AvesEntry mainEntry, pageEntry;
+ final AvesMagnifierController magnifierController;
+ final AvesVideoController videoController;
+ final Size videoDisplaySize;
+ final void Function({Alignment? alignment}) onTap;
+ final Widget Function(
+ AvesMagnifierController coverController,
+ Size coverSize,
+ ImageProvider videoCoverUriImage,
+ ) magnifierBuilder;
+
+ const VideoCover({
+ super.key,
+ required this.mainEntry,
+ required this.pageEntry,
+ required this.magnifierController,
+ required this.videoController,
+ required this.videoDisplaySize,
+ required this.onTap,
+ required this.magnifierBuilder,
+ });
+
+ @override
+ State createState() => _VideoCoverState();
+}
+
+class _VideoCoverState extends State {
+ ImageStream? _videoCoverStream;
+ late ImageStreamListener _videoCoverStreamListener;
+ final ValueNotifier _videoCoverInfoNotifier = ValueNotifier(null);
+
+ AvesMagnifierController? _dismissedCoverMagnifierController;
+
+ AvesMagnifierController get dismissedCoverMagnifierController {
+ _dismissedCoverMagnifierController ??= AvesMagnifierController();
+ return _dismissedCoverMagnifierController!;
+ }
+
+ AvesEntry get mainEntry => widget.mainEntry;
+
+ AvesEntry get entry => widget.pageEntry;
+
+ AvesMagnifierController get magnifierController => widget.magnifierController;
+
+ AvesVideoController get videoController => widget.videoController;
+
+ Size get videoDisplaySize => widget.videoDisplaySize;
+
+ // use the high res photo as cover for the video part of a motion photo
+ ImageProvider get videoCoverUriImage => mainEntry.isMotionPhoto ? mainEntry.uriImage : entry.uriImage;
+
+ @override
+ void initState() {
+ super.initState();
+ _registerWidget(widget);
+ }
+
+ @override
+ void didUpdateWidget(covariant VideoCover oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (oldWidget.pageEntry != widget.pageEntry) {
+ _unregisterWidget(oldWidget);
+ _registerWidget(widget);
+ }
+ }
+
+ @override
+ void dispose() {
+ _unregisterWidget(widget);
+ super.dispose();
+ }
+
+ void _registerWidget(VideoCover widget) {
+ _videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image);
+ _videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty);
+ _videoCoverStream!.addListener(_videoCoverStreamListener);
+ }
+
+ void _unregisterWidget(VideoCover oldWidget) {
+ _videoCoverStream?.removeListener(_videoCoverStreamListener);
+ _videoCoverStream = null;
+ _videoCoverInfoNotifier.value = null;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // fade out image to ease transition with the player
+ return StreamBuilder(
+ stream: videoController.statusStream,
+ builder: (context, snapshot) {
+ final showCover = !videoController.isReady;
+ return IgnorePointer(
+ ignoring: !showCover,
+ child: AnimatedOpacity(
+ opacity: showCover ? 1 : 0,
+ curve: Curves.easeInCirc,
+ duration: Durations.viewerVideoPlayerTransition,
+ onEnd: () {
+ // while cover is fading out, the same controller is used for both the cover and the video,
+ // and both fire scale boundaries events, so we make sure that in the end
+ // the scale boundaries from the video are used after the cover is gone
+ final boundaries = magnifierController.scaleBoundaries;
+ if (boundaries != null) {
+ magnifierController.setScaleBoundaries(
+ boundaries.copyWith(
+ childSize: videoDisplaySize,
+ ),
+ );
+ }
+ },
+ child: ValueListenableBuilder(
+ valueListenable: _videoCoverInfoNotifier,
+ builder: (context, videoCoverInfo, child) {
+ if (videoCoverInfo != null) {
+ // full cover image may have a different size and different aspect ratio
+ final coverSize = Size(
+ videoCoverInfo.image.width.toDouble(),
+ videoCoverInfo.image.height.toDouble(),
+ );
+ // when the cover is the same size as the video itself
+ // (which is often the case when the cover is not embedded but just a frame),
+ // we can reuse the same magnifier and preserve its state when switching from cover to video
+ final coverController = showCover || coverSize == videoDisplaySize ? magnifierController : dismissedCoverMagnifierController;
+ return widget.magnifierBuilder(coverController, coverSize, videoCoverUriImage);
+ }
+
+ // default to cached thumbnail, if any
+ final extent = entry.cachedThumbnails.firstOrNull?.key.extent;
+ if (extent != null && extent > 0) {
+ return GestureDetector(
+ onTap: widget.onTap,
+ child: ThumbnailImage(
+ entry: entry,
+ extent: extent,
+ fit: BoxFit.contain,
+ showLoadingBackground: false,
+ ),
+ );
+ }
+
+ return const SizedBox();
+ },
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/widgets/viewer/visual/subtitle/ass_parser.dart b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart
similarity index 98%
rename from lib/widgets/viewer/visual/subtitle/ass_parser.dart
rename to lib/widgets/viewer/visual/video/subtitle/ass_parser.dart
index b15414474..3e9f24cba 100644
--- a/lib/widgets/viewer/visual/subtitle/ass_parser.dart
+++ b/lib/widgets/viewer/visual/video/subtitle/ass_parser.dart
@@ -1,6 +1,6 @@
-import 'package:aves/widgets/viewer/visual/subtitle/line.dart';
-import 'package:aves/widgets/viewer/visual/subtitle/span.dart';
-import 'package:aves/widgets/viewer/visual/subtitle/style.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/line.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
diff --git a/lib/widgets/viewer/visual/subtitle/line.dart b/lib/widgets/viewer/visual/video/subtitle/line.dart
similarity index 93%
rename from lib/widgets/viewer/visual/subtitle/line.dart
rename to lib/widgets/viewer/visual/video/subtitle/line.dart
index dd44cb49c..f7a213b80 100644
--- a/lib/widgets/viewer/visual/subtitle/line.dart
+++ b/lib/widgets/viewer/visual/video/subtitle/line.dart
@@ -1,4 +1,4 @@
-import 'package:aves/widgets/viewer/visual/subtitle/span.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
diff --git a/lib/widgets/viewer/visual/subtitle/span.dart b/lib/widgets/viewer/visual/video/subtitle/span.dart
similarity index 92%
rename from lib/widgets/viewer/visual/subtitle/span.dart
rename to lib/widgets/viewer/visual/video/subtitle/span.dart
index 8cee296a0..0f5870582 100644
--- a/lib/widgets/viewer/visual/subtitle/span.dart
+++ b/lib/widgets/viewer/visual/video/subtitle/span.dart
@@ -1,4 +1,4 @@
-import 'package:aves/widgets/viewer/visual/subtitle/style.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
diff --git a/lib/widgets/viewer/visual/subtitle/style.dart b/lib/widgets/viewer/visual/video/subtitle/style.dart
similarity index 100%
rename from lib/widgets/viewer/visual/subtitle/style.dart
rename to lib/widgets/viewer/visual/video/subtitle/style.dart
diff --git a/lib/widgets/viewer/visual/subtitle/subtitle.dart b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart
similarity index 98%
rename from lib/widgets/viewer/visual/subtitle/subtitle.dart
rename to lib/widgets/viewer/visual/video/subtitle/subtitle.dart
index dbb32113a..c83f9db6f 100644
--- a/lib/widgets/viewer/visual/subtitle/subtitle.dart
+++ b/lib/widgets/viewer/visual/video/subtitle/subtitle.dart
@@ -4,9 +4,9 @@ import 'package:aves/widgets/common/basic/text/background_painter.dart';
import 'package:aves/widgets/common/basic/text/outlined.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/visual/state.dart';
-import 'package:aves/widgets/viewer/visual/subtitle/ass_parser.dart';
-import 'package:aves/widgets/viewer/visual/subtitle/span.dart';
-import 'package:aves/widgets/viewer/visual/subtitle/style.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/ass_parser.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart';
+import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
diff --git a/lib/widgets/viewer/visual/video/swipe_action.dart b/lib/widgets/viewer/visual/video/swipe_action.dart
new file mode 100644
index 000000000..a921cca38
--- /dev/null
+++ b/lib/widgets/viewer/visual/video/swipe_action.dart
@@ -0,0 +1,138 @@
+import 'dart:async';
+
+import 'package:aves/theme/icons.dart';
+import 'package:decorated_icon/decorated_icon.dart';
+import 'package:flutter/material.dart';
+import 'package:screen_brightness/screen_brightness.dart';
+import 'package:volume_controller/volume_controller.dart';
+
+enum SwipeAction { brightness, volume }
+
+extension ExtraSwipeAction on SwipeAction {
+ Future get() {
+ switch (this) {
+ case SwipeAction.brightness:
+ return ScreenBrightness().current;
+ case SwipeAction.volume:
+ return VolumeController().getVolume();
+ }
+ }
+
+ Future set(double value) async {
+ switch (this) {
+ case SwipeAction.brightness:
+ await ScreenBrightness().setScreenBrightness(value);
+ break;
+ case SwipeAction.volume:
+ VolumeController().setVolume(value, showSystemUI: false);
+ break;
+ }
+ }
+}
+
+class SwipeActionFeedback extends StatelessWidget {
+ final SwipeAction action;
+ final ValueNotifier valueNotifier;
+
+ const SwipeActionFeedback({
+ super.key,
+ required this.action,
+ required this.valueNotifier,
+ });
+
+ static const double width = 32;
+ static const double height = 160;
+ static const Radius radius = Radius.circular(width / 2);
+ static const double borderWidth = 2;
+ static const Color borderColor = Colors.white;
+ static final Color fillColor = Colors.white.withOpacity(.8);
+ static final Color backgroundColor = Colors.black.withOpacity(.2);
+ static final Color innerBorderColor = Colors.black.withOpacity(.5);
+ static const Color iconColor = Colors.white;
+ static const Color shadowColor = Colors.black;
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: ValueListenableBuilder(
+ valueListenable: valueNotifier,
+ builder: (context, value, child) {
+ if (value == null) return const SizedBox();
+
+ return Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ _buildIcon(_getMaxIcon()),
+ Container(
+ decoration: BoxDecoration(
+ color: backgroundColor,
+ border: Border.all(
+ width: borderWidth * 2,
+ color: innerBorderColor,
+ ),
+ borderRadius: const BorderRadius.all(radius),
+ ),
+ foregroundDecoration: BoxDecoration(
+ border: Border.all(
+ color: borderColor,
+ width: borderWidth,
+ ),
+ borderRadius: const BorderRadius.all(radius),
+ ),
+ width: width,
+ height: height,
+ child: ClipRRect(
+ borderRadius: const BorderRadius.all(radius),
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ color: fillColor,
+ width: width,
+ height: height * value,
+ ),
+ ),
+ ),
+ ),
+ _buildIcon(_getMinIcon()),
+ ],
+ );
+ },
+ ),
+ );
+ }
+
+ Widget _buildIcon(IconData icon) {
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: DecoratedIcon(
+ icon,
+ size: width,
+ color: iconColor,
+ shadows: const [
+ Shadow(
+ color: shadowColor,
+ blurRadius: 4,
+ )
+ ],
+ ),
+ );
+ }
+
+ IconData _getMinIcon() {
+ switch (action) {
+ case SwipeAction.brightness:
+ return AIcons.brightnessMin;
+ case SwipeAction.volume:
+ return AIcons.volumeMin;
+ }
+ }
+
+ IconData _getMaxIcon() {
+ switch (action) {
+ case SwipeAction.brightness:
+ return AIcons.brightnessMax;
+ case SwipeAction.volume:
+ return AIcons.volumeMax;
+ }
+ }
+}
diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video/video_view.dart
similarity index 100%
rename from lib/widgets/viewer/visual/video.dart
rename to lib/widgets/viewer/visual/video/video_view.dart
diff --git a/plugins/aves_magnifier/lib/aves_magnifier.dart b/plugins/aves_magnifier/lib/aves_magnifier.dart
index bda2e655b..dae50972d 100644
--- a/plugins/aves_magnifier/lib/aves_magnifier.dart
+++ b/plugins/aves_magnifier/lib/aves_magnifier.dart
@@ -2,6 +2,7 @@ library aves_magnifier;
export 'src/controller/controller.dart';
export 'src/controller/state.dart';
+export 'src/core/scale_gesture_recognizer.dart';
export 'src/magnifier.dart';
export 'src/pan/gesture_detector_scope.dart';
export 'src/pan/scroll_physics.dart';
diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart
index eb24275ba..0ce553290 100644
--- a/plugins/aves_magnifier/lib/src/core/core.dart
+++ b/plugins/aves_magnifier/lib/src/core/core.dart
@@ -18,6 +18,9 @@ class MagnifierCore extends StatefulWidget {
final ScaleStateCycle scaleStateCycle;
final bool applyScale;
final double panInertia;
+ final MagnifierGestureScaleStartCallback? onScaleStart;
+ final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
+ final MagnifierGestureScaleEndCallback? onScaleEnd;
final MagnifierTapCallback? onTap;
final MagnifierDoubleTapCallback? onDoubleTap;
final Widget child;
@@ -28,6 +31,9 @@ class MagnifierCore extends StatefulWidget {
required this.scaleStateCycle,
required this.applyScale,
this.panInertia = .2,
+ this.onScaleStart,
+ this.onScaleUpdate,
+ this.onScaleEnd,
this.onTap,
this.onDoubleTap,
required this.child,
@@ -40,7 +46,7 @@ class MagnifierCore extends StatefulWidget {
class _MagnifierCoreState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector {
Offset? _startFocalPoint, _lastViewportFocalPosition;
double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
- late bool _doubleTap, _quickScaleMoved;
+ late bool _dropped, _doubleTap, _quickScaleMoved;
DateTime _lastScaleGestureDate = DateTime.now();
late AnimationController _scaleAnimationController;
@@ -99,9 +105,15 @@ class _MagnifierCoreState extends State with TickerProviderStateM
}
void onScaleStart(ScaleStartDetails details, bool doubleTap) {
+ final boundaries = scaleBoundaries;
+ if (boundaries == null) return;
+
+ widget.onScaleStart?.call(details, doubleTap, boundaries);
+
_startScale = scale;
_startFocalPoint = details.localFocalPoint;
_lastViewportFocalPosition = _startFocalPoint;
+ _dropped = false;
_doubleTap = doubleTap;
_quickScaleLastDistance = null;
_quickScaleLastY = _startFocalPoint!.dy;
@@ -115,6 +127,9 @@ class _MagnifierCoreState extends State with TickerProviderStateM
final boundaries = scaleBoundaries;
if (boundaries == null) return;
+ _dropped |= widget.onScaleUpdate?.call(details) ?? false;
+ if (_dropped) return;
+
double newScale;
if (_doubleTap) {
// quick scale, aka one finger zoom
@@ -151,6 +166,8 @@ class _MagnifierCoreState extends State with TickerProviderStateM
final boundaries = scaleBoundaries;
if (boundaries == null) return;
+ widget.onScaleEnd?.call(details);
+
final _position = controller.position;
final _scale = controller.scale!;
final maxScale = boundaries.maxScale;
@@ -228,7 +245,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM
if (onDoubleTap != null) {
final viewportSize = boundaries.viewportSize;
final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height);
- if (onDoubleTap.call(alignment) == true) return;
+ if (onDoubleTap(alignment) == true) return;
}
final childTapPosition = boundaries.viewportToChildPosition(controller, viewportTapPosition);
@@ -307,12 +324,12 @@ class _MagnifierCoreState extends State with TickerProviderStateM
);
return MagnifierGestureDetector(
- onDoubleTap: onDoubleTap,
+ hitDetector: this,
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
- hitDetector: this,
onTapUp: widget.onTap == null ? null : onTap,
+ onDoubleTap: onDoubleTap,
child: child,
);
},
diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart
index 7d27b623b..e9ae6017b 100644
--- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart
+++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart
@@ -60,8 +60,7 @@ class _MagnifierGestureDetectorState extends State {
() => MagnifierGestureRecognizer(
debugOwner: this,
hitDetector: widget.hitDetector,
- validateAxis: scope.axis,
- touchSlopFactor: scope.touchSlopFactor,
+ scope: scope,
doubleTapDetails: doubleTapDetails,
),
(instance) {
diff --git a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart
index 0c35ca666..eb95393b9 100644
--- a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart
+++ b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart
@@ -1,20 +1,19 @@
import 'dart:math';
+import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:aves_magnifier/src/pan/corner_hit_detector.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
final CornerHitDetector hitDetector;
- final List validateAxis;
- final double touchSlopFactor;
+ final MagnifierGestureDetectorScope scope;
final ValueNotifier doubleTapDetails;
MagnifierGestureRecognizer({
super.debugOwner,
required this.hitDetector,
- required this.validateAxis,
- this.touchSlopFactor = 2,
+ required this.scope,
required this.doubleTapDetails,
});
@@ -46,7 +45,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
@override
void handleEvent(PointerEvent event) {
- if (validateAxis.isNotEmpty) {
+ if (scope.axis.isNotEmpty) {
var didChangeConfiguration = false;
if (event is PointerMoveEvent) {
if (!event.synthesized) {
@@ -104,26 +103,27 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
return;
}
+ final validateAxis = scope.axis;
final move = _initialFocalPoint! - _currentFocalPoint!;
- var shouldMove = false;
- if (validateAxis.length == 2) {
- // the image is the descendant of gesture detector(s) handling drag in both directions
- final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move);
- final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move);
- if (shouldMoveX == shouldMoveY) {
- // consistently can/cannot pan the image in both direction the same way
- shouldMove = shouldMoveX;
+ bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false;
+
+ if (!shouldMove) {
+ if (validateAxis.length == 2) {
+ // the image is the descendant of gesture detector(s) handling drag in both directions
+ final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move);
+ final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move);
+ if (shouldMoveX == shouldMoveY) {
+ // consistently can/cannot pan the image in both direction the same way
+ shouldMove = shouldMoveX;
+ } else {
+ // can pan the image in one direction, but should yield to an ascendant gesture detector in the other one
+ // the gesture direction angle is in ]-pi, pi], cf `Offset` doc for details
+ shouldMove = (isXPan(move) && shouldMoveX) || (isYPan(move) && shouldMoveY);
+ }
} else {
- // can pan the image in one direction, but should yield to an ascendant gesture detector in the other one
- final d = move.direction;
- // the gesture direction angle is in ]-pi, pi], cf `Offset` doc for details
- final xPan = (-pi / 4 < d && d < pi / 4) || (3 / 4 * pi < d && d <= pi) || (-pi < d && d < -3 / 4 * pi);
- final yPan = (pi / 4 < d && d < 3 / 4 * pi) || (-3 / 4 * pi < d && d < -pi / 4);
- shouldMove = (xPan && shouldMoveX) || (yPan && shouldMoveY);
+ // the image is the descendant of a gesture detector handling drag in one direction
+ shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move);
}
- } else {
- // the image is the descendant of a gesture detector handling drag in one direction
- shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move);
}
final doubleTap = doubleTapDetails.value != null;
@@ -137,9 +137,19 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
// and the magnifier recognizer may compete with the `HorizontalDragGestureRecognizer` from a containing `PageView`
// setting `touchSlopFactor` to 2 restores default `ScaleGestureRecognizer` behaviour as `kPanSlop = kTouchSlop * 2.0`
// setting `touchSlopFactor` in [0, 1] will allow this recognizer to accept the gesture before the one from `PageView`
- if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computeHitSlop(pointerDeviceKind, gestureSettings) * touchSlopFactor) {
+ if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computeHitSlop(pointerDeviceKind, gestureSettings) * scope.touchSlopFactor) {
acceptGesture(event.pointer);
}
}
}
+
+ static bool isXPan(Offset move) {
+ final d = move.direction;
+ return (-pi / 4 < d && d < pi / 4) || (3 / 4 * pi < d && d <= pi) || (-pi < d && d < -3 / 4 * pi);
+ }
+
+ static bool isYPan(Offset move) {
+ final d = move.direction;
+ return (pi / 4 < d && d < 3 / 4 * pi) || (-3 / 4 * pi < d && d < -pi / 4);
+ }
}
diff --git a/plugins/aves_magnifier/lib/src/magnifier.dart b/plugins/aves_magnifier/lib/src/magnifier.dart
index f93a07cad..88c3f7586 100644
--- a/plugins/aves_magnifier/lib/src/magnifier.dart
+++ b/plugins/aves_magnifier/lib/src/magnifier.dart
@@ -29,6 +29,9 @@ class AvesMagnifier extends StatelessWidget {
this.initialScale = const ScaleLevel(ref: ScaleReference.contained),
this.scaleStateCycle = defaultScaleStateCycle,
this.applyScale = true,
+ this.onScaleStart,
+ this.onScaleUpdate,
+ this.onScaleEnd,
this.onTap,
this.onDoubleTap,
required this.child,
@@ -52,6 +55,9 @@ class AvesMagnifier extends StatelessWidget {
final ScaleStateCycle scaleStateCycle;
final bool applyScale;
+ final MagnifierGestureScaleStartCallback? onScaleStart;
+ final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
+ final MagnifierGestureScaleEndCallback? onScaleEnd;
final MagnifierTapCallback? onTap;
final MagnifierDoubleTapCallback? onDoubleTap;
final Widget child;
@@ -73,6 +79,9 @@ class AvesMagnifier extends StatelessWidget {
controller: controller,
scaleStateCycle: scaleStateCycle,
applyScale: applyScale,
+ onScaleStart: onScaleStart,
+ onScaleUpdate: onScaleUpdate,
+ onScaleEnd: onScaleEnd,
onTap: onTap,
onDoubleTap: onDoubleTap,
child: child,
@@ -88,7 +97,7 @@ typedef MagnifierTapCallback = Function(
Alignment alignment,
Offset childTapPosition,
);
-
-typedef MagnifierDoubleTapCallback = bool Function(
- Alignment alignment,
-);
+typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment);
+typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
+typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details);
+typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details);
diff --git a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart
index e7a597baf..a9b52c9fe 100644
--- a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart
+++ b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart
@@ -7,18 +7,6 @@ import 'package:flutter/widgets.dart';
/// Useful when placing Magnifier inside a gesture sensitive context,
/// such as [PageView], [Dismissible], [BottomSheet].
class MagnifierGestureDetectorScope extends InheritedWidget {
- const MagnifierGestureDetectorScope({
- super.key,
- required this.axis,
- this.touchSlopFactor = .8,
- required Widget child,
- }) : super(child: child);
-
- static MagnifierGestureDetectorScope? of(BuildContext context) {
- final scope = context.dependOnInheritedWidgetOfExactType();
- return scope;
- }
-
final List axis;
// in [0, 1[
@@ -26,9 +14,36 @@ class MagnifierGestureDetectorScope extends InheritedWidget {
// <1: less reactive but gives the most leeway to other recognizers
// 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree
final double touchSlopFactor;
+ final bool? Function(Offset move)? acceptPointerEvent;
+
+ const MagnifierGestureDetectorScope({
+ super.key,
+ required this.axis,
+ this.touchSlopFactor = .8,
+ this.acceptPointerEvent,
+ required Widget child,
+ }) : super(child: child);
+
+ static MagnifierGestureDetectorScope? of(BuildContext context) {
+ return context.dependOnInheritedWidgetOfExactType();
+ }
+
+ MagnifierGestureDetectorScope copyWith({
+ List? axis,
+ double? touchSlopFactor,
+ bool? Function(Offset move)? acceptPointerEvent,
+ required Widget child,
+ }) {
+ return MagnifierGestureDetectorScope(
+ axis: axis ?? this.axis,
+ touchSlopFactor: touchSlopFactor ?? this.touchSlopFactor,
+ acceptPointerEvent: acceptPointerEvent ?? this.acceptPointerEvent,
+ child: child,
+ );
+ }
@override
bool updateShouldNotify(MagnifierGestureDetectorScope oldWidget) {
- return axis != oldWidget.axis && touchSlopFactor != oldWidget.touchSlopFactor;
+ return axis != oldWidget.axis || touchSlopFactor != oldWidget.touchSlopFactor || acceptPointerEvent != oldWidget.acceptPointerEvent;
}
}
diff --git a/pubspec.lock b/pubspec.lock
index af51869ef..1c7170c2e 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1196,6 +1196,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "9.0.0"
+ volume_controller:
+ dependency: "direct main"
+ description:
+ name: volume_controller
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.6"
watcher:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 0e42057ba..aa6948a4e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -95,6 +95,7 @@ dependencies:
transparent_image:
tuple:
url_launcher:
+ volume_controller:
xml:
dev_dependencies:
diff --git a/untranslated.json b/untranslated.json
index 4301f7282..2fefa0641 100644
--- a/untranslated.json
+++ b/untranslated.json
@@ -475,6 +475,7 @@
"settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle",
@@ -576,6 +577,7 @@
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisplayUseTvInterface"
],
@@ -586,16 +588,19 @@
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface"
],
"el": [
- "tooManyItemsErrorDialogMessage"
+ "tooManyItemsErrorDialogMessage",
+ "settingsVideoGestureVerticalDragBrightnessVolume"
],
"es": [
- "tooManyItemsErrorDialogMessage"
+ "tooManyItemsErrorDialogMessage",
+ "settingsVideoGestureVerticalDragBrightnessVolume"
],
"fa": [
@@ -933,6 +938,7 @@
"settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle",
@@ -1039,6 +1045,10 @@
"filePickerUseThisFolder"
],
+ "fr": [
+ "settingsVideoGestureVerticalDragBrightnessVolume"
+ ],
+
"gl": [
"columnCount",
"entryActionShareImageOnly",
@@ -1404,6 +1414,7 @@
"settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle",
@@ -1512,6 +1523,14 @@
"filePickerUseThisFolder"
],
+ "id": [
+ "settingsVideoGestureVerticalDragBrightnessVolume"
+ ],
+
+ "it": [
+ "settingsVideoGestureVerticalDragBrightnessVolume"
+ ],
+
"ja": [
"columnCount",
"chipActionFilterIn",
@@ -1526,11 +1545,16 @@
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface",
"settingsWidgetDisplayedItem"
],
+ "ko": [
+ "settingsVideoGestureVerticalDragBrightnessVolume"
+ ],
+
"lt": [
"columnCount",
"filterLocatedLabel",
@@ -1539,6 +1563,7 @@
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface"
],
@@ -1554,6 +1579,7 @@
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface"
],
@@ -1580,6 +1606,7 @@
"settingsViewerShowDescription",
"settingsSubtitleThemeTextPositionTile",
"settingsSubtitleThemeTextPositionDialogTitle",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface",
"settingsWidgetDisplayedItem"
@@ -1828,6 +1855,7 @@
"settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle",
@@ -1873,16 +1901,19 @@
],
"pl": [
- "tooManyItemsErrorDialogMessage"
+ "tooManyItemsErrorDialogMessage",
+ "settingsVideoGestureVerticalDragBrightnessVolume"
],
"pt": [
"columnCount",
- "tooManyItemsErrorDialogMessage"
+ "tooManyItemsErrorDialogMessage",
+ "settingsVideoGestureVerticalDragBrightnessVolume"
],
"ro": [
- "tooManyItemsErrorDialogMessage"
+ "tooManyItemsErrorDialogMessage",
+ "settingsVideoGestureVerticalDragBrightnessVolume"
],
"ru": [
@@ -1890,6 +1921,7 @@
"filterTaggedLabel",
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsDisplayUseTvInterface"
],
@@ -2133,6 +2165,7 @@
"settingsVideoButtonsTile",
"settingsVideoGestureDoubleTapTogglePlay",
"settingsVideoGestureSideDoubleTapSeek",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsPrivacySectionTitle",
"settingsAllowInstalledAppAccess",
"settingsAllowInstalledAppAccessSubtitle",
@@ -2241,8 +2274,13 @@
"filePickerUseThisFolder"
],
+ "tr": [
+ "settingsVideoGestureVerticalDragBrightnessVolume"
+ ],
+
"uk": [
- "tooManyItemsErrorDialogMessage"
+ "tooManyItemsErrorDialogMessage",
+ "settingsVideoGestureVerticalDragBrightnessVolume"
],
"zh": [
@@ -2251,6 +2289,7 @@
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface"
],
@@ -2262,6 +2301,7 @@
"tooManyItemsErrorDialogMessage",
"settingsModificationWarningDialogMessage",
"settingsViewerShowDescription",
+ "settingsVideoGestureVerticalDragBrightnessVolume",
"settingsAccessibilityShowPinchGestureAlternatives",
"settingsDisplayUseTvInterface"
]