#180 video: brightness/volume swipe gestures
This commit is contained in:
parent
b39eaba3eb
commit
cbfba1c156
28 changed files with 641 additions and 248 deletions
|
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Video: optional gestures to adjust brightness/volume
|
||||||
|
|
||||||
## <a id="v1.7.9"></a>[v1.7.9] - 2023-01-15
|
## <a id="v1.7.9"></a>[v1.7.9] - 2023-01-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -781,6 +781,7 @@
|
||||||
"settingsVideoButtonsTile": "Buttons",
|
"settingsVideoButtonsTile": "Buttons",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause",
|
"settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause",
|
||||||
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
|
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume": "Swipe up or down to adjust brightness/volume",
|
||||||
|
|
||||||
"settingsPrivacySectionTitle": "Privacy",
|
"settingsPrivacySectionTitle": "Privacy",
|
||||||
"settingsAllowInstalledAppAccess": "Allow access to app inventory",
|
"settingsAllowInstalledAppAccess": "Allow access to app inventory",
|
||||||
|
|
|
@ -97,6 +97,7 @@ class SettingsDefaults {
|
||||||
static const videoControls = VideoControls.play;
|
static const videoControls = VideoControls.play;
|
||||||
static const videoGestureDoubleTapTogglePlay = false;
|
static const videoGestureDoubleTapTogglePlay = false;
|
||||||
static const videoGestureSideDoubleTapSeek = true;
|
static const videoGestureSideDoubleTapSeek = true;
|
||||||
|
static const videoGestureVerticalDragBrightnessVolume = false;
|
||||||
|
|
||||||
// subtitles
|
// subtitles
|
||||||
static const subtitleFontSize = 20.0;
|
static const subtitleFontSize = 20.0;
|
||||||
|
|
|
@ -41,7 +41,6 @@ class Settings extends ChangeNotifier {
|
||||||
static const Set<String> _internalKeys = {
|
static const Set<String> _internalKeys = {
|
||||||
hasAcceptedTermsKey,
|
hasAcceptedTermsKey,
|
||||||
catalogTimeZoneKey,
|
catalogTimeZoneKey,
|
||||||
videoShowRawTimedTextKey,
|
|
||||||
searchHistoryKey,
|
searchHistoryKey,
|
||||||
platformAccelerometerRotationKey,
|
platformAccelerometerRotationKey,
|
||||||
platformTransitionAnimationScaleKey,
|
platformTransitionAnimationScaleKey,
|
||||||
|
@ -131,10 +130,10 @@ class Settings extends ChangeNotifier {
|
||||||
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
||||||
static const videoAutoPlayModeKey = 'video_auto_play_mode';
|
static const videoAutoPlayModeKey = 'video_auto_play_mode';
|
||||||
static const videoLoopModeKey = 'video_loop';
|
static const videoLoopModeKey = 'video_loop';
|
||||||
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
|
|
||||||
static const videoControlsKey = 'video_controls';
|
static const videoControlsKey = 'video_controls';
|
||||||
static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play';
|
static const videoGestureDoubleTapTogglePlayKey = 'video_gesture_double_tap_toggle_play';
|
||||||
static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip';
|
static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip';
|
||||||
|
static const videoGestureVerticalDragBrightnessVolumeKey = 'video_gesture_vertical_drag_brightness_volume';
|
||||||
|
|
||||||
// subtitles
|
// subtitles
|
||||||
static const subtitleFontSizeKey = 'subtitle_font_size';
|
static const subtitleFontSizeKey = 'subtitle_font_size';
|
||||||
|
@ -637,10 +636,6 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString());
|
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);
|
VideoControls get videoControls => getEnumOrDefault(videoControlsKey, SettingsDefaults.videoControls, VideoControls.values);
|
||||||
|
|
||||||
set videoControls(VideoControls newValue) => _set(videoControlsKey, newValue.toString());
|
set videoControls(VideoControls newValue) => _set(videoControlsKey, newValue.toString());
|
||||||
|
@ -653,6 +648,10 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set videoGestureSideDoubleTapSeek(bool newValue) => _set(videoGestureSideDoubleTapSeekKey, newValue);
|
set videoGestureSideDoubleTapSeek(bool newValue) => _set(videoGestureSideDoubleTapSeekKey, newValue);
|
||||||
|
|
||||||
|
bool get videoGestureVerticalDragBrightnessVolume => getBool(videoGestureVerticalDragBrightnessVolumeKey) ?? SettingsDefaults.videoGestureVerticalDragBrightnessVolume;
|
||||||
|
|
||||||
|
set videoGestureVerticalDragBrightnessVolume(bool newValue) => _set(videoGestureVerticalDragBrightnessVolumeKey, newValue);
|
||||||
|
|
||||||
// subtitles
|
// subtitles
|
||||||
|
|
||||||
double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize;
|
double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize;
|
||||||
|
@ -1039,6 +1038,7 @@ class Settings extends ChangeNotifier {
|
||||||
case enableVideoHardwareAccelerationKey:
|
case enableVideoHardwareAccelerationKey:
|
||||||
case videoGestureDoubleTapTogglePlayKey:
|
case videoGestureDoubleTapTogglePlayKey:
|
||||||
case videoGestureSideDoubleTapSeekKey:
|
case videoGestureSideDoubleTapSeekKey:
|
||||||
|
case videoGestureVerticalDragBrightnessVolumeKey:
|
||||||
case subtitleShowOutlineKey:
|
case subtitleShowOutlineKey:
|
||||||
case tagEditorCurrentFilterSectionExpandedKey:
|
case tagEditorCurrentFilterSectionExpandedKey:
|
||||||
case saveSearchHistoryKey:
|
case saveSearchHistoryKey:
|
||||||
|
|
|
@ -14,6 +14,8 @@ class AIcons {
|
||||||
static const IconData aspectRatio = Icons.aspect_ratio_outlined;
|
static const IconData aspectRatio = Icons.aspect_ratio_outlined;
|
||||||
static const IconData bin = Icons.delete_outlined;
|
static const IconData bin = Icons.delete_outlined;
|
||||||
static const IconData broken = Icons.broken_image_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 checked = Icons.done_outlined;
|
||||||
static const IconData count = MdiIcons.counter;
|
static const IconData count = MdiIcons.counter;
|
||||||
static const IconData counter = Icons.plus_one_outlined;
|
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 text = Icons.format_quote_outlined;
|
||||||
static const IconData tag = Icons.local_offer_outlined;
|
static const IconData tag = Icons.local_offer_outlined;
|
||||||
static const IconData tagUntagged = MdiIcons.tagOffOutline;
|
static const IconData tagUntagged = MdiIcons.tagOffOutline;
|
||||||
|
static const IconData volumeMin = Icons.volume_mute_outlined;
|
||||||
|
static const IconData volumeMax = Icons.volume_up_outlined;
|
||||||
|
|
||||||
// view
|
// view
|
||||||
static const IconData group = Icons.group_work_outlined;
|
static const IconData group = Icons.group_work_outlined;
|
||||||
|
|
|
@ -123,6 +123,11 @@ class Dependencies {
|
||||||
licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE',
|
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',
|
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<Dependency> _googleMobileServices = [
|
static const List<Dependency> _googleMobileServices = [
|
||||||
|
|
|
@ -43,11 +43,6 @@ class DebugSettingsSection extends StatelessWidget {
|
||||||
onChanged: (v) => settings.canUseAnalysisService = v,
|
onChanged: (v) => settings.canUseAnalysisService = v,
|
||||||
title: const Text('canUseAnalysisService'),
|
title: const Text('canUseAnalysisService'),
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
|
||||||
value: settings.videoShowRawTimedText,
|
|
||||||
onChanged: (v) => settings.videoShowRawTimedText = v,
|
|
||||||
title: const Text('videoShowRawTimedText'),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
child: InfoRowGroup(
|
child: InfoRowGroup(
|
||||||
|
|
|
@ -36,6 +36,11 @@ class VideoControlsPage extends StatelessWidget {
|
||||||
onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v,
|
onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v,
|
||||||
title: context.l10n.settingsVideoGestureSideDoubleTapSeek,
|
title: context.l10n.settingsVideoGestureSideDoubleTapSeek,
|
||||||
),
|
),
|
||||||
|
SettingsSwitchListTile(
|
||||||
|
selector: (context, s) => s.videoGestureVerticalDragBrightnessVolume,
|
||||||
|
onChanged: (v) => settings.videoGestureVerticalDragBrightnessVolume = v,
|
||||||
|
title: context.l10n.settingsVideoGestureVerticalDragBrightnessVolume,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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/basic/text/outlined.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.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:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
|
@ -680,9 +680,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLeave() async {
|
Future<void> _onLeave() async {
|
||||||
if (settings.viewerMaxBrightness) {
|
await ScreenBrightness().resetScreenBrightness();
|
||||||
await ScreenBrightness().resetScreenBrightness();
|
|
||||||
}
|
|
||||||
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
|
if (settings.keepScreenOn == KeepScreenOn.viewerOnly) {
|
||||||
await windowService.keepScreenOn(false);
|
await windowService.keepScreenOn(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,30 +3,27 @@ import 'dart:async';
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/entry.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/enums/accessibility_animations.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/media/media_session_service.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/theme/icons.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/basic/insets.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/controller.dart';
|
||||||
import 'package:aves/widgets/viewer/hero.dart';
|
import 'package:aves/widgets/viewer/hero.dart';
|
||||||
import 'package:aves/widgets/viewer/notifications.dart';
|
import 'package:aves/widgets/viewer/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/video/conductor.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/conductor.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/error.dart';
|
import 'package:aves/widgets/viewer/visual/error.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/raster.dart';
|
import 'package:aves/widgets/viewer/visual/raster.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/state.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/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:aves_magnifier/aves_magnifier.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:decorated_icon/decorated_icon.dart';
|
import 'package:decorated_icon/decorated_icon.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -55,17 +52,8 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
late ValueNotifier<ViewState> _viewStateNotifier;
|
late ValueNotifier<ViewState> _viewStateNotifier;
|
||||||
late AvesMagnifierController _magnifierController;
|
late AvesMagnifierController _magnifierController;
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
ImageStream? _videoCoverStream;
|
|
||||||
late ImageStreamListener _videoCoverStreamListener;
|
|
||||||
final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null);
|
|
||||||
final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null);
|
final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null);
|
||||||
|
OverlayEntry? _actionFeedbackOverlayEntry;
|
||||||
AvesMagnifierController? _dismissedCoverMagnifierController;
|
|
||||||
|
|
||||||
AvesMagnifierController get dismissedCoverMagnifierController {
|
|
||||||
_dismissedCoverMagnifierController ??= AvesMagnifierController();
|
|
||||||
return _dismissedCoverMagnifierController!;
|
|
||||||
}
|
|
||||||
|
|
||||||
AvesEntry get mainEntry => widget.mainEntry;
|
AvesEntry get mainEntry => widget.mainEntry;
|
||||||
|
|
||||||
|
@ -73,9 +61,6 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
|
|
||||||
ViewerController get viewerController => widget.viewerController;
|
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 rasterMaxScale = ScaleLevel(factor: 5);
|
||||||
static const vectorMaxScale = ScaleLevel(factor: 25);
|
static const vectorMaxScale = ScaleLevel(factor: 25);
|
||||||
|
|
||||||
|
@ -110,9 +95,6 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
|
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
|
||||||
if (entry.isVideo) {
|
if (entry.isVideo) {
|
||||||
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
|
_subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand));
|
||||||
_videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image);
|
|
||||||
_videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty);
|
|
||||||
_videoCoverStream!.addListener(_videoCoverStreamListener);
|
|
||||||
}
|
}
|
||||||
viewerController.startAutopilotAnimation(
|
viewerController.startAutopilotAnimation(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
|
@ -127,9 +109,6 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
|
|
||||||
void _unregisterWidget(EntryPageView oldWidget) {
|
void _unregisterWidget(EntryPageView oldWidget) {
|
||||||
viewerController.stopAutopilotAnimation(vsync: this);
|
viewerController.stopAutopilotAnimation(vsync: this);
|
||||||
_videoCoverStream?.removeListener(_videoCoverStreamListener);
|
|
||||||
_videoCoverStream = null;
|
|
||||||
_videoCoverInfoNotifier.value = null;
|
|
||||||
_magnifierController.dispose();
|
_magnifierController.dispose();
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
|
@ -222,169 +201,169 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
builder: (context, sar, child) {
|
builder: (context, sar, child) {
|
||||||
final videoDisplaySize = entry.videoDisplaySize(sar);
|
final videoDisplaySize = entry.videoDisplaySize(sar);
|
||||||
|
|
||||||
return Selector<Settings, Tuple2<bool, bool>>(
|
return Selector<Settings, Tuple3<bool, bool, bool>>(
|
||||||
selector: (context, s) => Tuple2(s.videoGestureDoubleTapTogglePlay, s.videoGestureSideDoubleTapSeek),
|
selector: (context, s) => Tuple3(
|
||||||
|
s.videoGestureDoubleTapTogglePlay,
|
||||||
|
s.videoGestureSideDoubleTapSeek,
|
||||||
|
s.videoGestureVerticalDragBrightnessVolume,
|
||||||
|
),
|
||||||
builder: (context, s, child) {
|
builder: (context, s, child) {
|
||||||
final playGesture = s.item1;
|
final playGesture = s.item1;
|
||||||
final seekGesture = s.item2;
|
final seekGesture = s.item2;
|
||||||
final useActionGesture = playGesture || seekGesture;
|
final useVerticalDragGesture = s.item3;
|
||||||
|
final useTapGesture = playGesture || seekGesture;
|
||||||
|
|
||||||
void _applyAction(EntryAction action, {IconData? Function()? icon}) {
|
MagnifierDoubleTapCallback? onDoubleTap;
|
||||||
_actionFeedbackChildNotifier.value = DecoratedIcon(
|
MagnifierGestureScaleStartCallback? onScaleStart;
|
||||||
icon?.call() ?? action.getIconData(),
|
MagnifierGestureScaleUpdateCallback? onScaleUpdate;
|
||||||
size: 48,
|
MagnifierGestureScaleEndCallback? onScaleEnd;
|
||||||
color: Colors.white,
|
|
||||||
shadows: const [
|
if (useTapGesture) {
|
||||||
Shadow(
|
void _applyAction(EntryAction action, {IconData? Function()? icon}) {
|
||||||
color: Colors.black,
|
_actionFeedbackChildNotifier.value = DecoratedIcon(
|
||||||
blurRadius: 4,
|
icon?.call() ?? action.getIconData(),
|
||||||
)
|
size: 48,
|
||||||
],
|
color: Colors.white,
|
||||||
);
|
shadows: const [
|
||||||
VideoActionNotification(
|
Shadow(
|
||||||
controller: videoController,
|
color: Colors.black,
|
||||||
action: action,
|
blurRadius: 4,
|
||||||
).dispatch(context);
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
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
|
if (useVerticalDragGesture) {
|
||||||
? (alignment) {
|
SwipeAction? swipeAction;
|
||||||
final x = alignment.x;
|
var move = Offset.zero;
|
||||||
if (seekGesture) {
|
var dropped = false;
|
||||||
if (x < sideRatio) {
|
double? startValue;
|
||||||
_applyAction(EntryAction.videoReplay10);
|
final valueNotifier = ValueNotifier<double?>(null);
|
||||||
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;
|
|
||||||
|
|
||||||
|
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<Widget?>(
|
||||||
|
valueListenable: _actionFeedbackChildNotifier,
|
||||||
|
builder: (context, feedbackChild, child) => ActionFeedback(
|
||||||
|
child: feedbackChild,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (useVerticalDragGesture) {
|
||||||
|
videoChild = MagnifierGestureDetectorScope.of(context)!.copyWith(
|
||||||
|
acceptPointerEvent: MagnifierGestureRecognizer.isYPan,
|
||||||
|
child: videoChild,
|
||||||
|
);
|
||||||
|
}
|
||||||
return Stack(
|
return Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
Stack(
|
videoChild,
|
||||||
children: [
|
VideoCover(
|
||||||
_buildMagnifier(
|
mainEntry: mainEntry,
|
||||||
displaySize: videoDisplaySize,
|
pageEntry: entry,
|
||||||
onDoubleTap: _onDoubleTap,
|
magnifierController: _magnifierController,
|
||||||
child: VideoView(
|
|
||||||
entry: entry,
|
|
||||||
controller: videoController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VideoSubtitles(
|
|
||||||
controller: videoController,
|
|
||||||
viewStateNotifier: _viewStateNotifier,
|
|
||||||
),
|
|
||||||
if (settings.videoShowRawTimedText)
|
|
||||||
VideoSubtitles(
|
|
||||||
controller: videoController,
|
|
||||||
viewStateNotifier: _viewStateNotifier,
|
|
||||||
debugMode: true,
|
|
||||||
),
|
|
||||||
if (useActionGesture)
|
|
||||||
ValueListenableBuilder<Widget?>(
|
|
||||||
valueListenable: _actionFeedbackChildNotifier,
|
|
||||||
builder: (context, feedbackChild, child) => ActionFeedback(
|
|
||||||
child: feedbackChild,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_buildVideoCover(
|
|
||||||
videoController: videoController,
|
videoController: videoController,
|
||||||
videoDisplaySize: videoDisplaySize,
|
videoDisplaySize: videoDisplaySize,
|
||||||
onDoubleTap: _onDoubleTap,
|
onTap: _onTap,
|
||||||
),
|
magnifierBuilder: (coverController, coverSize, videoCoverUriImage) => _buildMagnifier(
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamBuilder<VideoStatus> _buildVideoCover({
|
|
||||||
required AvesVideoController videoController,
|
|
||||||
required Size videoDisplaySize,
|
|
||||||
required MagnifierDoubleTapCallback? onDoubleTap,
|
|
||||||
}) {
|
|
||||||
// fade out image to ease transition with the player
|
|
||||||
return StreamBuilder<VideoStatus>(
|
|
||||||
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<ImageInfo?>(
|
|
||||||
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(
|
|
||||||
controller: coverController,
|
controller: coverController,
|
||||||
displaySize: coverSize,
|
displaySize: coverSize,
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
child: Image(
|
child: Image(
|
||||||
image: videoCoverUriImage,
|
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<EntryPageView> with SingleTickerProvider
|
||||||
ScaleLevel maxScale = rasterMaxScale,
|
ScaleLevel maxScale = rasterMaxScale,
|
||||||
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
|
ScaleStateCycle scaleStateCycle = defaultScaleStateCycle,
|
||||||
bool applyScale = true,
|
bool applyScale = true,
|
||||||
|
MagnifierGestureScaleStartCallback? onScaleStart,
|
||||||
|
MagnifierGestureScaleUpdateCallback? onScaleUpdate,
|
||||||
|
MagnifierGestureScaleEndCallback? onScaleEnd,
|
||||||
MagnifierDoubleTapCallback? onDoubleTap,
|
MagnifierDoubleTapCallback? onDoubleTap,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) {
|
}) {
|
||||||
|
@ -413,6 +395,9 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
initialScale: viewerController.initialScale,
|
initialScale: viewerController.initialScale,
|
||||||
scaleStateCycle: scaleStateCycle,
|
scaleStateCycle: scaleStateCycle,
|
||||||
applyScale: applyScale,
|
applyScale: applyScale,
|
||||||
|
onScaleStart: onScaleStart,
|
||||||
|
onScaleUpdate: onScaleUpdate,
|
||||||
|
onScaleEnd: onScaleEnd,
|
||||||
onTap: (c, s, a, p) => _onTap(alignment: a),
|
onTap: (c, s, a, p) => _onTap(alignment: a),
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -487,5 +472,3 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef MagnifierTapCallback = void Function(Offset childPosition);
|
|
||||||
|
|
160
lib/widgets/viewer/visual/video/cover.dart
Normal file
160
lib/widgets/viewer/visual/video/cover.dart
Normal file
|
@ -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<VideoCover> createState() => _VideoCoverState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoCoverState extends State<VideoCover> {
|
||||||
|
ImageStream? _videoCoverStream;
|
||||||
|
late ImageStreamListener _videoCoverStreamListener;
|
||||||
|
final ValueNotifier<ImageInfo?> _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<VideoStatus>(
|
||||||
|
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<ImageInfo?>(
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:aves/widgets/viewer/visual/subtitle/line.dart';
|
import 'package:aves/widgets/viewer/visual/video/subtitle/line.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/subtitle/span.dart';
|
import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/subtitle/style.dart';
|
import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.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:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.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/common/basic/text/outlined.dart';
|
||||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/state.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/video/subtitle/ass_parser.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/subtitle/span.dart';
|
import 'package:aves/widgets/viewer/visual/video/subtitle/span.dart';
|
||||||
import 'package:aves/widgets/viewer/visual/subtitle/style.dart';
|
import 'package:aves/widgets/viewer/visual/video/subtitle/style.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
138
lib/widgets/viewer/visual/video/swipe_action.dart
Normal file
138
lib/widgets/viewer/visual/video/swipe_action.dart
Normal file
|
@ -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<double> get() {
|
||||||
|
switch (this) {
|
||||||
|
case SwipeAction.brightness:
|
||||||
|
return ScreenBrightness().current;
|
||||||
|
case SwipeAction.volume:
|
||||||
|
return VolumeController().getVolume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> 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<double?> 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<double?>(
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ library aves_magnifier;
|
||||||
|
|
||||||
export 'src/controller/controller.dart';
|
export 'src/controller/controller.dart';
|
||||||
export 'src/controller/state.dart';
|
export 'src/controller/state.dart';
|
||||||
|
export 'src/core/scale_gesture_recognizer.dart';
|
||||||
export 'src/magnifier.dart';
|
export 'src/magnifier.dart';
|
||||||
export 'src/pan/gesture_detector_scope.dart';
|
export 'src/pan/gesture_detector_scope.dart';
|
||||||
export 'src/pan/scroll_physics.dart';
|
export 'src/pan/scroll_physics.dart';
|
||||||
|
|
|
@ -18,6 +18,9 @@ class MagnifierCore extends StatefulWidget {
|
||||||
final ScaleStateCycle scaleStateCycle;
|
final ScaleStateCycle scaleStateCycle;
|
||||||
final bool applyScale;
|
final bool applyScale;
|
||||||
final double panInertia;
|
final double panInertia;
|
||||||
|
final MagnifierGestureScaleStartCallback? onScaleStart;
|
||||||
|
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
|
||||||
|
final MagnifierGestureScaleEndCallback? onScaleEnd;
|
||||||
final MagnifierTapCallback? onTap;
|
final MagnifierTapCallback? onTap;
|
||||||
final MagnifierDoubleTapCallback? onDoubleTap;
|
final MagnifierDoubleTapCallback? onDoubleTap;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -28,6 +31,9 @@ class MagnifierCore extends StatefulWidget {
|
||||||
required this.scaleStateCycle,
|
required this.scaleStateCycle,
|
||||||
required this.applyScale,
|
required this.applyScale,
|
||||||
this.panInertia = .2,
|
this.panInertia = .2,
|
||||||
|
this.onScaleStart,
|
||||||
|
this.onScaleUpdate,
|
||||||
|
this.onScaleEnd,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
@ -40,7 +46,7 @@ class MagnifierCore extends StatefulWidget {
|
||||||
class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector {
|
class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector {
|
||||||
Offset? _startFocalPoint, _lastViewportFocalPosition;
|
Offset? _startFocalPoint, _lastViewportFocalPosition;
|
||||||
double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
|
double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
|
||||||
late bool _doubleTap, _quickScaleMoved;
|
late bool _dropped, _doubleTap, _quickScaleMoved;
|
||||||
DateTime _lastScaleGestureDate = DateTime.now();
|
DateTime _lastScaleGestureDate = DateTime.now();
|
||||||
|
|
||||||
late AnimationController _scaleAnimationController;
|
late AnimationController _scaleAnimationController;
|
||||||
|
@ -99,9 +105,15 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
}
|
}
|
||||||
|
|
||||||
void onScaleStart(ScaleStartDetails details, bool doubleTap) {
|
void onScaleStart(ScaleStartDetails details, bool doubleTap) {
|
||||||
|
final boundaries = scaleBoundaries;
|
||||||
|
if (boundaries == null) return;
|
||||||
|
|
||||||
|
widget.onScaleStart?.call(details, doubleTap, boundaries);
|
||||||
|
|
||||||
_startScale = scale;
|
_startScale = scale;
|
||||||
_startFocalPoint = details.localFocalPoint;
|
_startFocalPoint = details.localFocalPoint;
|
||||||
_lastViewportFocalPosition = _startFocalPoint;
|
_lastViewportFocalPosition = _startFocalPoint;
|
||||||
|
_dropped = false;
|
||||||
_doubleTap = doubleTap;
|
_doubleTap = doubleTap;
|
||||||
_quickScaleLastDistance = null;
|
_quickScaleLastDistance = null;
|
||||||
_quickScaleLastY = _startFocalPoint!.dy;
|
_quickScaleLastY = _startFocalPoint!.dy;
|
||||||
|
@ -115,6 +127,9 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
final boundaries = scaleBoundaries;
|
final boundaries = scaleBoundaries;
|
||||||
if (boundaries == null) return;
|
if (boundaries == null) return;
|
||||||
|
|
||||||
|
_dropped |= widget.onScaleUpdate?.call(details) ?? false;
|
||||||
|
if (_dropped) return;
|
||||||
|
|
||||||
double newScale;
|
double newScale;
|
||||||
if (_doubleTap) {
|
if (_doubleTap) {
|
||||||
// quick scale, aka one finger zoom
|
// quick scale, aka one finger zoom
|
||||||
|
@ -151,6 +166,8 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
final boundaries = scaleBoundaries;
|
final boundaries = scaleBoundaries;
|
||||||
if (boundaries == null) return;
|
if (boundaries == null) return;
|
||||||
|
|
||||||
|
widget.onScaleEnd?.call(details);
|
||||||
|
|
||||||
final _position = controller.position;
|
final _position = controller.position;
|
||||||
final _scale = controller.scale!;
|
final _scale = controller.scale!;
|
||||||
final maxScale = boundaries.maxScale;
|
final maxScale = boundaries.maxScale;
|
||||||
|
@ -228,7 +245,7 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
if (onDoubleTap != null) {
|
if (onDoubleTap != null) {
|
||||||
final viewportSize = boundaries.viewportSize;
|
final viewportSize = boundaries.viewportSize;
|
||||||
final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height);
|
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);
|
final childTapPosition = boundaries.viewportToChildPosition(controller, viewportTapPosition);
|
||||||
|
@ -307,12 +324,12 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
);
|
);
|
||||||
|
|
||||||
return MagnifierGestureDetector(
|
return MagnifierGestureDetector(
|
||||||
onDoubleTap: onDoubleTap,
|
hitDetector: this,
|
||||||
onScaleStart: onScaleStart,
|
onScaleStart: onScaleStart,
|
||||||
onScaleUpdate: onScaleUpdate,
|
onScaleUpdate: onScaleUpdate,
|
||||||
onScaleEnd: onScaleEnd,
|
onScaleEnd: onScaleEnd,
|
||||||
hitDetector: this,
|
|
||||||
onTapUp: widget.onTap == null ? null : onTap,
|
onTapUp: widget.onTap == null ? null : onTap,
|
||||||
|
onDoubleTap: onDoubleTap,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,8 +60,7 @@ class _MagnifierGestureDetectorState extends State<MagnifierGestureDetector> {
|
||||||
() => MagnifierGestureRecognizer(
|
() => MagnifierGestureRecognizer(
|
||||||
debugOwner: this,
|
debugOwner: this,
|
||||||
hitDetector: widget.hitDetector,
|
hitDetector: widget.hitDetector,
|
||||||
validateAxis: scope.axis,
|
scope: scope,
|
||||||
touchSlopFactor: scope.touchSlopFactor,
|
|
||||||
doubleTapDetails: doubleTapDetails,
|
doubleTapDetails: doubleTapDetails,
|
||||||
),
|
),
|
||||||
(instance) {
|
(instance) {
|
||||||
|
|
|
@ -1,20 +1,19 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:aves_magnifier/aves_magnifier.dart';
|
||||||
import 'package:aves_magnifier/src/pan/corner_hit_detector.dart';
|
import 'package:aves_magnifier/src/pan/corner_hit_detector.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
final CornerHitDetector hitDetector;
|
final CornerHitDetector hitDetector;
|
||||||
final List<Axis> validateAxis;
|
final MagnifierGestureDetectorScope scope;
|
||||||
final double touchSlopFactor;
|
|
||||||
final ValueNotifier<TapDownDetails?> doubleTapDetails;
|
final ValueNotifier<TapDownDetails?> doubleTapDetails;
|
||||||
|
|
||||||
MagnifierGestureRecognizer({
|
MagnifierGestureRecognizer({
|
||||||
super.debugOwner,
|
super.debugOwner,
|
||||||
required this.hitDetector,
|
required this.hitDetector,
|
||||||
required this.validateAxis,
|
required this.scope,
|
||||||
this.touchSlopFactor = 2,
|
|
||||||
required this.doubleTapDetails,
|
required this.doubleTapDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -46,7 +45,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void handleEvent(PointerEvent event) {
|
void handleEvent(PointerEvent event) {
|
||||||
if (validateAxis.isNotEmpty) {
|
if (scope.axis.isNotEmpty) {
|
||||||
var didChangeConfiguration = false;
|
var didChangeConfiguration = false;
|
||||||
if (event is PointerMoveEvent) {
|
if (event is PointerMoveEvent) {
|
||||||
if (!event.synthesized) {
|
if (!event.synthesized) {
|
||||||
|
@ -104,26 +103,27 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final validateAxis = scope.axis;
|
||||||
final move = _initialFocalPoint! - _currentFocalPoint!;
|
final move = _initialFocalPoint! - _currentFocalPoint!;
|
||||||
var shouldMove = false;
|
bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false;
|
||||||
if (validateAxis.length == 2) {
|
|
||||||
// the image is the descendant of gesture detector(s) handling drag in both directions
|
if (!shouldMove) {
|
||||||
final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move);
|
if (validateAxis.length == 2) {
|
||||||
final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move);
|
// the image is the descendant of gesture detector(s) handling drag in both directions
|
||||||
if (shouldMoveX == shouldMoveY) {
|
final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move);
|
||||||
// consistently can/cannot pan the image in both direction the same way
|
final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move);
|
||||||
shouldMove = shouldMoveX;
|
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 {
|
} else {
|
||||||
// can pan the image in one direction, but should yield to an ascendant gesture detector in the other one
|
// the image is the descendant of a gesture detector handling drag in one direction
|
||||||
final d = move.direction;
|
shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move) : hitDetector.shouldMoveX(move);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
} 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;
|
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`
|
// 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` 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`
|
// 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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,9 @@ class AvesMagnifier extends StatelessWidget {
|
||||||
this.initialScale = const ScaleLevel(ref: ScaleReference.contained),
|
this.initialScale = const ScaleLevel(ref: ScaleReference.contained),
|
||||||
this.scaleStateCycle = defaultScaleStateCycle,
|
this.scaleStateCycle = defaultScaleStateCycle,
|
||||||
this.applyScale = true,
|
this.applyScale = true,
|
||||||
|
this.onScaleStart,
|
||||||
|
this.onScaleUpdate,
|
||||||
|
this.onScaleEnd,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
@ -52,6 +55,9 @@ class AvesMagnifier extends StatelessWidget {
|
||||||
|
|
||||||
final ScaleStateCycle scaleStateCycle;
|
final ScaleStateCycle scaleStateCycle;
|
||||||
final bool applyScale;
|
final bool applyScale;
|
||||||
|
final MagnifierGestureScaleStartCallback? onScaleStart;
|
||||||
|
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
|
||||||
|
final MagnifierGestureScaleEndCallback? onScaleEnd;
|
||||||
final MagnifierTapCallback? onTap;
|
final MagnifierTapCallback? onTap;
|
||||||
final MagnifierDoubleTapCallback? onDoubleTap;
|
final MagnifierDoubleTapCallback? onDoubleTap;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -73,6 +79,9 @@ class AvesMagnifier extends StatelessWidget {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
scaleStateCycle: scaleStateCycle,
|
scaleStateCycle: scaleStateCycle,
|
||||||
applyScale: applyScale,
|
applyScale: applyScale,
|
||||||
|
onScaleStart: onScaleStart,
|
||||||
|
onScaleUpdate: onScaleUpdate,
|
||||||
|
onScaleEnd: onScaleEnd,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -88,7 +97,7 @@ typedef MagnifierTapCallback = Function(
|
||||||
Alignment alignment,
|
Alignment alignment,
|
||||||
Offset childTapPosition,
|
Offset childTapPosition,
|
||||||
);
|
);
|
||||||
|
typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment);
|
||||||
typedef MagnifierDoubleTapCallback = bool Function(
|
typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
|
||||||
Alignment alignment,
|
typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details);
|
||||||
);
|
typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details);
|
||||||
|
|
|
@ -7,18 +7,6 @@ import 'package:flutter/widgets.dart';
|
||||||
/// Useful when placing Magnifier inside a gesture sensitive context,
|
/// Useful when placing Magnifier inside a gesture sensitive context,
|
||||||
/// such as [PageView], [Dismissible], [BottomSheet].
|
/// such as [PageView], [Dismissible], [BottomSheet].
|
||||||
class MagnifierGestureDetectorScope extends InheritedWidget {
|
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<MagnifierGestureDetectorScope>();
|
|
||||||
return scope;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Axis> axis;
|
final List<Axis> axis;
|
||||||
|
|
||||||
// in [0, 1[
|
// in [0, 1[
|
||||||
|
@ -26,9 +14,36 @@ class MagnifierGestureDetectorScope extends InheritedWidget {
|
||||||
// <1: less reactive but gives the most leeway to other recognizers
|
// <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
|
// 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree
|
||||||
final double touchSlopFactor;
|
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>();
|
||||||
|
}
|
||||||
|
|
||||||
|
MagnifierGestureDetectorScope copyWith({
|
||||||
|
List<Axis>? 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
|
@override
|
||||||
bool updateShouldNotify(MagnifierGestureDetectorScope oldWidget) {
|
bool updateShouldNotify(MagnifierGestureDetectorScope oldWidget) {
|
||||||
return axis != oldWidget.axis && touchSlopFactor != oldWidget.touchSlopFactor;
|
return axis != oldWidget.axis || touchSlopFactor != oldWidget.touchSlopFactor || acceptPointerEvent != oldWidget.acceptPointerEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1196,6 +1196,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.0.0"
|
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:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -95,6 +95,7 @@ dependencies:
|
||||||
transparent_image:
|
transparent_image:
|
||||||
tuple:
|
tuple:
|
||||||
url_launcher:
|
url_launcher:
|
||||||
|
volume_controller:
|
||||||
xml:
|
xml:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
@ -475,6 +475,7 @@
|
||||||
"settingsVideoButtonsTile",
|
"settingsVideoButtonsTile",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek",
|
"settingsVideoGestureSideDoubleTapSeek",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsPrivacySectionTitle",
|
"settingsPrivacySectionTitle",
|
||||||
"settingsAllowInstalledAppAccess",
|
"settingsAllowInstalledAppAccess",
|
||||||
"settingsAllowInstalledAppAccessSubtitle",
|
"settingsAllowInstalledAppAccessSubtitle",
|
||||||
|
@ -576,6 +577,7 @@
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsDisplayUseTvInterface"
|
"settingsDisplayUseTvInterface"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -586,16 +588,19 @@
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||||
"settingsDisplayUseTvInterface"
|
"settingsDisplayUseTvInterface"
|
||||||
],
|
],
|
||||||
|
|
||||||
"el": [
|
"el": [
|
||||||
"tooManyItemsErrorDialogMessage"
|
"tooManyItemsErrorDialogMessage",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
"tooManyItemsErrorDialogMessage"
|
"tooManyItemsErrorDialogMessage",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fa": [
|
"fa": [
|
||||||
|
@ -933,6 +938,7 @@
|
||||||
"settingsVideoButtonsTile",
|
"settingsVideoButtonsTile",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek",
|
"settingsVideoGestureSideDoubleTapSeek",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsPrivacySectionTitle",
|
"settingsPrivacySectionTitle",
|
||||||
"settingsAllowInstalledAppAccess",
|
"settingsAllowInstalledAppAccess",
|
||||||
"settingsAllowInstalledAppAccessSubtitle",
|
"settingsAllowInstalledAppAccessSubtitle",
|
||||||
|
@ -1039,6 +1045,10 @@
|
||||||
"filePickerUseThisFolder"
|
"filePickerUseThisFolder"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"fr": [
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
|
],
|
||||||
|
|
||||||
"gl": [
|
"gl": [
|
||||||
"columnCount",
|
"columnCount",
|
||||||
"entryActionShareImageOnly",
|
"entryActionShareImageOnly",
|
||||||
|
@ -1404,6 +1414,7 @@
|
||||||
"settingsVideoButtonsTile",
|
"settingsVideoButtonsTile",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek",
|
"settingsVideoGestureSideDoubleTapSeek",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsPrivacySectionTitle",
|
"settingsPrivacySectionTitle",
|
||||||
"settingsAllowInstalledAppAccess",
|
"settingsAllowInstalledAppAccess",
|
||||||
"settingsAllowInstalledAppAccessSubtitle",
|
"settingsAllowInstalledAppAccessSubtitle",
|
||||||
|
@ -1512,6 +1523,14 @@
|
||||||
"filePickerUseThisFolder"
|
"filePickerUseThisFolder"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"id": [
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
|
],
|
||||||
|
|
||||||
|
"it": [
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
"columnCount",
|
"columnCount",
|
||||||
"chipActionFilterIn",
|
"chipActionFilterIn",
|
||||||
|
@ -1526,11 +1545,16 @@
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||||
"settingsDisplayUseTvInterface",
|
"settingsDisplayUseTvInterface",
|
||||||
"settingsWidgetDisplayedItem"
|
"settingsWidgetDisplayedItem"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"ko": [
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
|
],
|
||||||
|
|
||||||
"lt": [
|
"lt": [
|
||||||
"columnCount",
|
"columnCount",
|
||||||
"filterLocatedLabel",
|
"filterLocatedLabel",
|
||||||
|
@ -1539,6 +1563,7 @@
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||||
"settingsDisplayUseTvInterface"
|
"settingsDisplayUseTvInterface"
|
||||||
],
|
],
|
||||||
|
@ -1554,6 +1579,7 @@
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||||
"settingsDisplayUseTvInterface"
|
"settingsDisplayUseTvInterface"
|
||||||
],
|
],
|
||||||
|
@ -1580,6 +1606,7 @@
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
"settingsSubtitleThemeTextPositionTile",
|
"settingsSubtitleThemeTextPositionTile",
|
||||||
"settingsSubtitleThemeTextPositionDialogTitle",
|
"settingsSubtitleThemeTextPositionDialogTitle",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||||
"settingsDisplayUseTvInterface",
|
"settingsDisplayUseTvInterface",
|
||||||
"settingsWidgetDisplayedItem"
|
"settingsWidgetDisplayedItem"
|
||||||
|
@ -1828,6 +1855,7 @@
|
||||||
"settingsVideoButtonsTile",
|
"settingsVideoButtonsTile",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek",
|
"settingsVideoGestureSideDoubleTapSeek",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsPrivacySectionTitle",
|
"settingsPrivacySectionTitle",
|
||||||
"settingsAllowInstalledAppAccess",
|
"settingsAllowInstalledAppAccess",
|
||||||
"settingsAllowInstalledAppAccessSubtitle",
|
"settingsAllowInstalledAppAccessSubtitle",
|
||||||
|
@ -1873,16 +1901,19 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
"tooManyItemsErrorDialogMessage"
|
"tooManyItemsErrorDialogMessage",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
"columnCount",
|
"columnCount",
|
||||||
"tooManyItemsErrorDialogMessage"
|
"tooManyItemsErrorDialogMessage",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ro": [
|
"ro": [
|
||||||
"tooManyItemsErrorDialogMessage"
|
"tooManyItemsErrorDialogMessage",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
|
@ -1890,6 +1921,7 @@
|
||||||
"filterTaggedLabel",
|
"filterTaggedLabel",
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsDisplayUseTvInterface"
|
"settingsDisplayUseTvInterface"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -2133,6 +2165,7 @@
|
||||||
"settingsVideoButtonsTile",
|
"settingsVideoButtonsTile",
|
||||||
"settingsVideoGestureDoubleTapTogglePlay",
|
"settingsVideoGestureDoubleTapTogglePlay",
|
||||||
"settingsVideoGestureSideDoubleTapSeek",
|
"settingsVideoGestureSideDoubleTapSeek",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsPrivacySectionTitle",
|
"settingsPrivacySectionTitle",
|
||||||
"settingsAllowInstalledAppAccess",
|
"settingsAllowInstalledAppAccess",
|
||||||
"settingsAllowInstalledAppAccessSubtitle",
|
"settingsAllowInstalledAppAccessSubtitle",
|
||||||
|
@ -2241,8 +2274,13 @@
|
||||||
"filePickerUseThisFolder"
|
"filePickerUseThisFolder"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"tr": [
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
|
],
|
||||||
|
|
||||||
"uk": [
|
"uk": [
|
||||||
"tooManyItemsErrorDialogMessage"
|
"tooManyItemsErrorDialogMessage",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
|
@ -2251,6 +2289,7 @@
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||||
"settingsDisplayUseTvInterface"
|
"settingsDisplayUseTvInterface"
|
||||||
],
|
],
|
||||||
|
@ -2262,6 +2301,7 @@
|
||||||
"tooManyItemsErrorDialogMessage",
|
"tooManyItemsErrorDialogMessage",
|
||||||
"settingsModificationWarningDialogMessage",
|
"settingsModificationWarningDialogMessage",
|
||||||
"settingsViewerShowDescription",
|
"settingsViewerShowDescription",
|
||||||
|
"settingsVideoGestureVerticalDragBrightnessVolume",
|
||||||
"settingsAccessibilityShowPinchGestureAlternatives",
|
"settingsAccessibilityShowPinchGestureAlternatives",
|
||||||
"settingsDisplayUseTvInterface"
|
"settingsDisplayUseTvInterface"
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in a new issue