#180 video: double tap seek gesture
This commit is contained in:
parent
064f20bb3a
commit
7e54b91aa4
17 changed files with 231 additions and 30 deletions
|
@ -92,8 +92,8 @@
|
|||
"entryActionSetAs": "Set as",
|
||||
"entryActionOpenMap": "Show in map app",
|
||||
"entryActionRotateScreen": "Rotate screen",
|
||||
"entryActionAddFavourite": "Add to favourites",
|
||||
"entryActionRemoveFavourite": "Remove from favourites",
|
||||
"entryActionAddFavourite": "Add to favorites",
|
||||
"entryActionRemoveFavourite": "Remove from favorites",
|
||||
|
||||
"videoActionCaptureFrame": "Capture frame",
|
||||
"videoActionPause": "Pause",
|
||||
|
@ -111,7 +111,7 @@
|
|||
"entryInfoActionRemoveMetadata": "Remove metadata",
|
||||
|
||||
"filterBinLabel": "Recycle bin",
|
||||
"filterFavouriteLabel": "Favourite",
|
||||
"filterFavouriteLabel": "Favorite",
|
||||
"filterLocationEmptyLabel": "Unlocated",
|
||||
"filterTagEmptyLabel": "Untagged",
|
||||
"filterRatingUnratedLabel": "Unrated",
|
||||
|
@ -490,7 +490,7 @@
|
|||
}
|
||||
},
|
||||
|
||||
"collectionEmptyFavourites": "No favourites",
|
||||
"collectionEmptyFavourites": "No favorites",
|
||||
"collectionEmptyVideos": "No videos",
|
||||
"collectionEmptyImages": "No images",
|
||||
|
||||
|
@ -498,7 +498,7 @@
|
|||
"collectionDeselectSectionTooltip": "Deselect section",
|
||||
|
||||
"drawerCollectionAll": "All collection",
|
||||
"drawerCollectionFavourites": "Favourites",
|
||||
"drawerCollectionFavourites": "Favorites",
|
||||
"drawerCollectionImages": "Images",
|
||||
"drawerCollectionVideos": "Videos",
|
||||
"drawerCollectionAnimated": "Animated",
|
||||
|
@ -556,7 +556,7 @@
|
|||
"settingsActionImport": "Import",
|
||||
|
||||
"appExportCovers": "Covers",
|
||||
"appExportFavourites": "Favourites",
|
||||
"appExportFavourites": "Favorites",
|
||||
"appExportSettings": "Settings",
|
||||
|
||||
"settingsSectionNavigation": "Navigation",
|
||||
|
@ -579,7 +579,7 @@
|
|||
"settingsNavigationDrawerAddAlbum": "Add album",
|
||||
|
||||
"settingsSectionThumbnails": "Thumbnails",
|
||||
"settingsThumbnailShowFavouriteIcon": "Show favourite icon",
|
||||
"settingsThumbnailShowFavouriteIcon": "Show favorite icon",
|
||||
"settingsThumbnailShowLocationIcon": "Show location icon",
|
||||
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
|
||||
"settingsThumbnailShowRating": "Show rating",
|
||||
|
@ -641,6 +641,10 @@
|
|||
"settingsSubtitleThemeTextAlignmentCenter": "Center",
|
||||
"settingsSubtitleThemeTextAlignmentRight": "Right",
|
||||
|
||||
"settingsGesturesTile": "Gestures",
|
||||
"settingsGesturesTitle": "Gestures",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
|
||||
|
||||
"settingsSectionPrivacy": "Privacy",
|
||||
"settingsAllowInstalledAppAccess": "Allow access to app inventory",
|
||||
"settingsAllowInstalledAppAccessSubtitle": "Used to improve album display",
|
||||
|
|
|
@ -23,9 +23,7 @@ extension ExtraChipAction on ChipAction {
|
|||
}
|
||||
}
|
||||
|
||||
Widget getIcon() {
|
||||
return Icon(_getIconData());
|
||||
}
|
||||
Widget getIcon() => Icon(_getIconData());
|
||||
|
||||
IconData _getIconData() {
|
||||
switch (this) {
|
||||
|
|
|
@ -90,9 +90,7 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
}
|
||||
}
|
||||
|
||||
Widget getIcon() {
|
||||
return Icon(_getIconData());
|
||||
}
|
||||
Widget getIcon() => Icon(_getIconData());
|
||||
|
||||
IconData _getIconData() {
|
||||
switch (this) {
|
||||
|
|
|
@ -159,9 +159,7 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
}
|
||||
}
|
||||
|
||||
Widget getIcon() {
|
||||
return Icon(_getIconData());
|
||||
}
|
||||
Widget getIcon() => Icon(_getIconData());
|
||||
|
||||
IconData _getIconData() {
|
||||
switch (this) {
|
||||
|
|
|
@ -50,11 +50,9 @@ extension ExtraVideoAction on VideoAction {
|
|||
}
|
||||
}
|
||||
|
||||
Widget getIcon() {
|
||||
return Icon(_getIconData());
|
||||
}
|
||||
Widget getIcon() => Icon(getIconData());
|
||||
|
||||
IconData _getIconData() {
|
||||
IconData getIconData() {
|
||||
switch (this) {
|
||||
case VideoAction.captureFrame:
|
||||
return AIcons.captureFrame;
|
||||
|
|
|
@ -82,6 +82,7 @@ class SettingsDefaults {
|
|||
static const enableVideoAutoPlay = false;
|
||||
static const videoLoopMode = VideoLoopMode.shortOnly;
|
||||
static const videoShowRawTimedText = false;
|
||||
static const videoGestureSideDoubleTapSeek = true;
|
||||
|
||||
// subtitles
|
||||
static const subtitleFontSize = 20.0;
|
||||
|
|
|
@ -95,6 +95,7 @@ class Settings extends ChangeNotifier {
|
|||
static const enableVideoAutoPlayKey = 'video_auto_play';
|
||||
static const videoLoopModeKey = 'video_loop';
|
||||
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
|
||||
static const videoGestureSideDoubleTapSeekKey = 'video_gesture_side_double_tap_skip';
|
||||
|
||||
// subtitles
|
||||
static const subtitleFontSizeKey = 'subtitle_font_size';
|
||||
|
@ -436,6 +437,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue);
|
||||
|
||||
bool get videoGestureSideDoubleTapSeek => getBoolOrDefault(videoGestureSideDoubleTapSeekKey, SettingsDefaults.videoGestureSideDoubleTapSeek);
|
||||
|
||||
set videoGestureSideDoubleTapSeek(bool newValue) => setAndNotify(videoGestureSideDoubleTapSeekKey, newValue);
|
||||
|
||||
// subtitles
|
||||
|
||||
double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize;
|
||||
|
@ -654,6 +659,7 @@ class Settings extends ChangeNotifier {
|
|||
case enableMotionPhotoAutoPlayKey:
|
||||
case enableVideoHardwareAccelerationKey:
|
||||
case enableVideoAutoPlayKey:
|
||||
case videoGestureSideDoubleTapSeekKey:
|
||||
case subtitleShowOutlineKey:
|
||||
case saveSearchHistoryKey:
|
||||
case filePickerShowHiddenFilesKey:
|
||||
|
|
|
@ -39,6 +39,7 @@ class Durations {
|
|||
static const thumbnailScrollerScrollAnimation = Duration(milliseconds: 200);
|
||||
static const thumbnailScrollerShadeAnimation = Duration(milliseconds: 150);
|
||||
static const viewerVideoPlayerTransition = Duration(milliseconds: 500);
|
||||
static const viewerActionFeedbackAnimation = Duration(milliseconds: 800);
|
||||
|
||||
// info animations
|
||||
static const mapStyleSwitchAnimation = Duration(milliseconds: 300);
|
||||
|
|
|
@ -294,3 +294,70 @@ class _FeedbackMessageState extends State<_FeedbackMessage> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionFeedback extends StatefulWidget {
|
||||
final Widget? child;
|
||||
|
||||
const ActionFeedback({
|
||||
Key? key,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ActionFeedbackState createState() => _ActionFeedbackState();
|
||||
}
|
||||
|
||||
class _ActionFeedbackState extends State<ActionFeedback> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_animationController = AnimationController(
|
||||
duration: Durations.viewerActionFeedbackAnimation,
|
||||
vsync: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ActionFeedback oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.child != widget.child) {
|
||||
_animationController.reset();
|
||||
if (widget.child != null) {
|
||||
_animationController.forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IgnorePointer(
|
||||
child: Center(
|
||||
child: AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, child) {
|
||||
final t = _animationController.value;
|
||||
final opacity = Curves.easeOutQuad.transform(t > .5 ? (1 - t) * 2 : t * 2);
|
||||
final scale = Curves.slowMiddle.transform(t) * 2;
|
||||
return Opacity(
|
||||
opacity: opacity,
|
||||
child: Transform.scale(
|
||||
scale: scale,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
53
lib/widgets/settings/video/gestures.dart
Normal file
53
lib/widgets/settings/video/gestures.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class VideoGesturesTile extends StatelessWidget {
|
||||
const VideoGesturesTile({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(context.l10n.settingsGesturesTile),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoGesturesPage.routeName),
|
||||
builder: (context) => const VideoGesturesPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VideoGesturesPage extends StatelessWidget {
|
||||
static const routeName = '/settings/video/gestures';
|
||||
|
||||
const VideoGesturesPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.settingsGesturesTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
Selector<Settings, bool>(
|
||||
selector: (context, s) => s.videoGestureSideDoubleTapSeek,
|
||||
builder: (context, current, child) => SwitchListTile(
|
||||
value: current,
|
||||
onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v,
|
||||
title: Text(context.l10n.settingsVideoGestureSideDoubleTapSeek),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ class SubtitleThemeTile extends StatelessWidget {
|
|||
}
|
||||
|
||||
class SubtitleThemePage extends StatelessWidget {
|
||||
static const routeName = '/settings/subtitle_theme';
|
||||
static const routeName = '/settings/video/subtitle_theme';
|
||||
|
||||
static const textAlignOptions = [TextAlign.left, TextAlign.center, TextAlign.right];
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
|||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/video/gestures.dart';
|
||||
import 'package:aves/widgets/settings/video/subtitle_theme.dart';
|
||||
import 'package:aves/widgets/settings/video/video_actions_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -73,6 +74,7 @@ class VideoSection extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
),
|
||||
const VideoGesturesTile(),
|
||||
const SubtitleThemeTile(),
|
||||
];
|
||||
|
||||
|
|
|
@ -209,6 +209,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
if (_currentHorizontalPage != index) {
|
||||
_horizontalPager.jumpToPage(index);
|
||||
}
|
||||
} else if (notification is VideoGestureNotification) {
|
||||
final controller = notification.controller;
|
||||
final action = notification.action;
|
||||
_videoActionDelegate.onActionSelected(context, controller, action);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:aves/model/actions/video_actions.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
|
@ -13,3 +15,14 @@ class ViewEntryNotification extends Notification {
|
|||
|
||||
const ViewEntryNotification({required this.index});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class VideoGestureNotification extends Notification {
|
||||
final AvesVideoController controller;
|
||||
final VideoAction action;
|
||||
|
||||
const VideoGestureNotification({
|
||||
required this.controller,
|
||||
required this.action,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
|
@ -301,7 +300,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
|
||||
@override
|
||||
Future<void> seekTo(int targetMillis) async {
|
||||
targetMillis = max(0, targetMillis);
|
||||
targetMillis = targetMillis.clamp(0, duration);
|
||||
if (isReady) {
|
||||
await _instance.seekTo(targetMillis);
|
||||
} else {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/actions/video_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/theme/durations.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
import 'package:aves/widgets/common/magnifier/controller/controller.dart';
|
||||
import 'package:aves/widgets/common/magnifier/controller/state.dart';
|
||||
import 'package:aves/widgets/common/magnifier/magnifier.dart';
|
||||
|
@ -24,6 +27,7 @@ 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:collection/collection.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -51,6 +55,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
ImageStream? _videoCoverStream;
|
||||
late ImageStreamListener _videoCoverStreamListener;
|
||||
final ValueNotifier<ImageInfo?> _videoCoverInfoNotifier = ValueNotifier(null);
|
||||
final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null);
|
||||
|
||||
MagnifierController? _dismissedCoverMagnifierController;
|
||||
|
||||
|
@ -187,6 +192,29 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
Widget _buildVideoView() {
|
||||
final videoController = context.read<VideoConductor>().getController(entry);
|
||||
if (videoController == null) return const SizedBox();
|
||||
|
||||
Positioned _buildDoubleTapDetector(AlignmentGeometry alignment, VideoAction action) {
|
||||
return Positioned.fill(
|
||||
child: FractionallySizedBox(
|
||||
alignment: alignment,
|
||||
widthFactor: .25,
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () {
|
||||
_actionFeedbackChildNotifier.value = DecoratedIcon(
|
||||
action.getIconData(),
|
||||
shadows: Constants.embossShadows,
|
||||
size: 48,
|
||||
);
|
||||
VideoGestureNotification(
|
||||
controller: videoController,
|
||||
action: action,
|
||||
).dispatch(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ValueListenableBuilder<double>(
|
||||
valueListenable: videoController.sarNotifier,
|
||||
builder: (context, sar, child) {
|
||||
|
@ -213,6 +241,16 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
viewStateNotifier: _viewStateNotifier,
|
||||
debugMode: true,
|
||||
),
|
||||
if (settings.videoGestureSideDoubleTapSeek) ...[
|
||||
_buildDoubleTapDetector(Alignment.centerLeft, VideoAction.replay10),
|
||||
_buildDoubleTapDetector(Alignment.centerRight, VideoAction.skip10),
|
||||
ValueListenableBuilder<Widget?>(
|
||||
valueListenable: _actionFeedbackChildNotifier,
|
||||
builder: (context, feedbackChild, child) => ActionFeedback(
|
||||
child: feedbackChild,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
_buildVideoCover(videoController, videoDisplaySize),
|
||||
|
|
|
@ -1,31 +1,52 @@
|
|||
{
|
||||
"de": [
|
||||
"entryActionConvert",
|
||||
"settingsViewerShowOverlayThumbnails"
|
||||
"settingsViewerShowOverlayThumbnails",
|
||||
"settingsGesturesTile",
|
||||
"settingsGesturesTitle",
|
||||
"settingsVideoGestureSideDoubleTapSeek"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"settingsViewerShowOverlayThumbnails"
|
||||
"settingsViewerShowOverlayThumbnails",
|
||||
"settingsGesturesTile",
|
||||
"settingsGesturesTitle",
|
||||
"settingsVideoGestureSideDoubleTapSeek"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"settingsViewerShowOverlayThumbnails"
|
||||
"settingsViewerShowOverlayThumbnails",
|
||||
"settingsGesturesTile",
|
||||
"settingsGesturesTitle",
|
||||
"settingsVideoGestureSideDoubleTapSeek"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"settingsViewerShowOverlayThumbnails"
|
||||
"settingsViewerShowOverlayThumbnails",
|
||||
"settingsGesturesTile",
|
||||
"settingsGesturesTitle",
|
||||
"settingsVideoGestureSideDoubleTapSeek"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"settingsViewerShowOverlayThumbnails"
|
||||
"settingsViewerShowOverlayThumbnails",
|
||||
"settingsGesturesTile",
|
||||
"settingsGesturesTitle",
|
||||
"settingsVideoGestureSideDoubleTapSeek"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"settingsViewerShowOverlayThumbnails"
|
||||
"settingsViewerShowOverlayThumbnails",
|
||||
"settingsGesturesTile",
|
||||
"settingsGesturesTitle",
|
||||
"settingsVideoGestureSideDoubleTapSeek"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"entryActionConvert",
|
||||
"settingsViewerShowOverlayThumbnails"
|
||||
"settingsViewerShowOverlayThumbnails",
|
||||
"settingsGesturesTile",
|
||||
"settingsGesturesTitle",
|
||||
"settingsVideoGestureSideDoubleTapSeek"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue