#180 #289 tap on screen edge to show previous/next item

This commit is contained in:
Thibault Deckers 2022-08-03 11:43:19 +02:00
parent 94ed66c4be
commit daedd552e2
18 changed files with 143 additions and 51 deletions

View file

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
### Added
- Viewer: optional gesture to show previous/next item
## <a id="v1.6.11"></a>[v1.6.11] - 2022-07-26
### Added

View file

@ -658,6 +658,7 @@
"settingsCollectionSelectionQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when selecting items.",
"settingsSectionViewer": "Viewer",
"settingsViewerGestureSideTapNext": "Tap on screen edges to show previous/next item",
"settingsViewerUseCutout": "Use cutout area",
"settingsViewerMaximumBrightness": "Maximum brightness",
"settingsMotionPhotoAutoPlay": "Auto play motion photos",

View file

@ -83,6 +83,7 @@ class SettingsDefaults {
static const showOverlayInfo = true;
static const showOverlayShootingDetails = false;
static const showOverlayThumbnailPreview = false;
static const viewerGestureSideTapNext = false;
static const viewerUseCutout = true;
static const viewerMaxBrightness = false;
static const enableMotionPhotoAutoPlay = false;

View file

@ -98,6 +98,7 @@ class Settings extends ChangeNotifier {
static const showOverlayInfoKey = 'show_overlay_info';
static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details';
static const showOverlayThumbnailPreviewKey = 'show_overlay_thumbnail_preview';
static const viewerGestureSideTapNextKey = 'viewer_gesture_side_tap_next';
static const viewerUseCutoutKey = 'viewer_use_cutout';
static const viewerMaxBrightnessKey = 'viewer_max_brightness';
static const enableMotionPhotoAutoPlayKey = 'motion_photo_auto_play';
@ -480,6 +481,10 @@ class Settings extends ChangeNotifier {
set showOverlayThumbnailPreview(bool newValue) => setAndNotify(showOverlayThumbnailPreviewKey, newValue);
bool get viewerGestureSideTapNext => getBoolOrDefault(viewerGestureSideTapNextKey, SettingsDefaults.viewerGestureSideTapNext);
set viewerGestureSideTapNext(bool newValue) => setAndNotify(viewerGestureSideTapNextKey, newValue);
bool get viewerUseCutout => getBoolOrDefault(viewerUseCutoutKey, SettingsDefaults.viewerUseCutout);
set viewerUseCutout(bool newValue) => setAndNotify(viewerUseCutoutKey, newValue);
@ -819,6 +824,7 @@ class Settings extends ChangeNotifier {
case showOverlayInfoKey:
case showOverlayShootingDetailsKey:
case showOverlayThumbnailPreviewKey:
case viewerGestureSideTapNextKey:
case viewerUseCutoutKey:
case viewerMaxBrightnessKey:
case enableMotionPhotoAutoPlayKey:

View file

@ -203,8 +203,9 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
if (onTap == null) return;
final viewportTapPosition = details.localPosition;
final alignment = Alignment(viewportTapPosition.dx / viewportSize.width, viewportTapPosition.dy / viewportSize.height);
final childTapPosition = scaleBoundaries.viewportToChildPosition(controller, viewportTapPosition);
onTap(context, details, controller.currentState, childTapPosition);
onTap(context, controller.currentState, alignment, childTapPosition);
}
void onDoubleTap(TapDownDetails details) {

View file

@ -84,8 +84,8 @@ class Magnifier extends StatelessWidget {
typedef MagnifierTapCallback = Function(
BuildContext context,
TapUpDetails details,
MagnifierState state,
Alignment alignment,
Offset childTapPosition,
);

View file

@ -36,6 +36,7 @@ class ViewerSection extends SettingsSection {
SettingsTileViewerQuickActions(),
SettingsTileViewerOverlay(),
SettingsTileViewerSlideshow(),
SettingsTileViewerGestureSideTapNext(),
if (canSetCutoutMode) SettingsTileViewerCutoutMode(),
SettingsTileViewerMaxBrightness(),
SettingsTileViewerMotionPhotoAutoPlay(),
@ -74,10 +75,22 @@ class SettingsTileViewerSlideshow extends SettingsTile {
@override
Widget build(BuildContext context) => SettingsSubPageTile(
title: title(context),
routeName: ViewerSlideshowPage.routeName,
builder: (context) => const ViewerSlideshowPage(),
);
title: title(context),
routeName: ViewerSlideshowPage.routeName,
builder: (context) => const ViewerSlideshowPage(),
);
}
class SettingsTileViewerGestureSideTapNext extends SettingsTile {
@override
String title(BuildContext context) => context.l10n.settingsViewerGestureSideTapNext;
@override
Widget build(BuildContext context) => SettingsSwitchListTile(
selector: (context, s) => s.viewerGestureSideTapNext,
onChanged: (v) => settings.viewerGestureSideTapNext = v,
title: title(context),
);
}
class SettingsTileViewerCutoutMode extends SettingsTile {

View file

@ -33,7 +33,6 @@ import 'package:aves/widgets/viewer/action/printer.dart';
import 'package:aves/widgets/viewer/action/single_entry_editor.dart';
import 'package:aves/widgets/viewer/debug/debug_page.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/source_viewer_page.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:collection/collection.dart';

View file

@ -12,7 +12,6 @@ import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
import 'package:aves/widgets/viewer/info/info_page.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

View file

@ -22,7 +22,6 @@ import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/overlay/panorama.dart';
import 'package:aves/widgets/viewer/overlay/slideshow_buttons.dart';
import 'package:aves/widgets/viewer/overlay/top.dart';
@ -244,11 +243,12 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
// remove focus, if any, to prevent viewer shortcuts activation from the Info page
FocusManager.instance.primaryFocus?.unfocus();
_goToVerticalPage(infoPage);
} else if (notification is ViewEntryNotification) {
final index = notification.index;
if (_currentEntryIndex != index) {
_horizontalPager.jumpToPage(index);
}
} else if (notification is JumpToPreviousEntryNotification) {
_jumpToHorizontalPageByDelta(-1);
} else if (notification is JumpToNextEntryNotification) {
_jumpToHorizontalPageByDelta(1);
} else if (notification is JumpToEntryNotification) {
_jumpToHorizontalPageByIndex(notification.index);
} else if (notification is VideoActionNotification) {
final controller = notification.controller;
final action = notification.action;
@ -532,6 +532,25 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
}
}
void _jumpToHorizontalPageByDelta(int delta) {
final page = _horizontalPager.page?.round();
if (page != null) {
_jumpToHorizontalPageByIndex(page + delta);
}
}
void _jumpToHorizontalPageByIndex(int target) {
final _collection = collection;
if (_collection != null) {
if (!widget.viewerController.repeat) {
target = target.clamp(0, _collection.entryCount - 1);
}
if (_currentEntryIndex != target) {
_horizontalPager.jumpToPage(target);
}
}
}
void _onHorizontalPageChanged(int page) {
_currentEntryIndex = page;
if (viewerController.repeat) {

View file

@ -1,6 +1,8 @@
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
@ -10,6 +12,37 @@ class ShowImageNotification extends Notification {}
@immutable
class ShowInfoNotification extends Notification {}
@immutable
class ToggleOverlayNotification extends Notification {
final bool? visible;
const ToggleOverlayNotification({this.visible});
}
@immutable
class JumpToPreviousEntryNotification extends Notification {}
@immutable
class JumpToNextEntryNotification extends Notification {}
@immutable
class JumpToEntryNotification extends Notification {
final int index;
const JumpToEntryNotification({required this.index});
}
@immutable
class VideoActionNotification extends Notification {
final AvesVideoController controller;
final EntryAction action;
const VideoActionNotification({
required this.controller,
required this.action,
});
}
@immutable
class FilterSelectedNotification extends Notification with EquatableMixin {
final CollectionFilter filter;

View file

@ -1,28 +0,0 @@
import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:flutter/material.dart';
@immutable
class ToggleOverlayNotification extends Notification {
final bool? visible;
const ToggleOverlayNotification({this.visible});
}
@immutable
class ViewEntryNotification extends Notification {
final int index;
const ViewEntryNotification({required this.index});
}
@immutable
class VideoActionNotification extends Notification {
final AvesVideoController controller;
final EntryAction action;
const VideoActionNotification({
required this.controller,
required this.action,
});
}

View file

@ -2,7 +2,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/thumbnail/scroller.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:flutter/material.dart';
class ViewerThumbnailPreview extends StatefulWidget {
@ -60,13 +60,13 @@ class _ViewerThumbnailPreviewState extends State<ViewerThumbnailPreview> {
entryCount: entryCount,
entryBuilder: (index) => 0 <= index && index < entryCount ? entries[index] : null,
indexNotifier: _entryIndexNotifier,
onTap: (index) => ViewEntryNotification(index: index).dispatch(context),
onTap: (index) => JumpToEntryNotification(index: index).dispatch(context),
);
}
void _onScrollerIndexChange() => _debouncer(() {
if (mounted) {
ViewEntryNotification(index: _entryIndexNotifier.value).dispatch(context);
JumpToEntryNotification(index: _entryIndexNotifier.value).dispatch(context);
}
});
}

View file

@ -10,8 +10,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/favourite_toggler.dart';
import 'package:aves/widgets/viewer/action/entry_action_delegate.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/common.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/overlay/video/mute_toggler.dart';
import 'package:aves/widgets/viewer/overlay/video/play_toggler.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';

View file

@ -17,7 +17,7 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/video_speed_dialog.dart';
import 'package:aves/widgets/dialogs/video_stream_selection_dialog.dart';
import 'package:aves/widgets/settings/video/video_settings_page.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

View file

@ -17,7 +17,7 @@ import 'package:aves/widgets/common/magnifier/scale/scale_level.dart';
import 'package:aves/widgets/common/magnifier/scale/state.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/viewer/hero.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart';
@ -385,13 +385,25 @@ class _EntryPageViewState extends State<EntryPageView> {
initialScale: widget.initialScale,
scaleStateCycle: scaleStateCycle,
applyScale: applyScale,
onTap: (c, d, s, o) => _onTap(),
onTap: (c, s, a, p) => _onTap(alignment: a),
onDoubleTap: onDoubleTap,
child: child,
);
}
void _onTap() => const ToggleOverlayNotification().dispatch(context);
void _onTap({Alignment? alignment}) {
if (settings.viewerGestureSideTapNext && alignment != null) {
final x = alignment.x;
if (x < .25) {
JumpToPreviousEntryNotification().dispatch(context);
return;
} else if (x > .75) {
JumpToNextEntryNotification().dispatch(context);
return;
}
}
const ToggleOverlayNotification().dispatch(context);
}
void _onViewStateChanged(MagnifierState v) {
_viewStateNotifier.value = _viewStateNotifier.value.copyWith(

View file

@ -11,8 +11,8 @@ import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart';
import 'package:aves/widgets/viewer/overlay/notifications.dart';
import 'package:aves/widgets/viewer/overlay/video/video.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';

View file

@ -1,22 +1,49 @@
{
"de": [
"settingsViewerGestureSideTapNext"
],
"es": [
"filterOnThisDayLabel",
"settingsViewerGestureSideTapNext",
"settingsSlideshowFillScreen",
"settingsScreenSaverPageTitle",
"settingsWidgetPageTitle",
"settingsWidgetShowOutline"
],
"fr": [
"settingsViewerGestureSideTapNext"
],
"id": [
"filterOnThisDayLabel",
"settingsViewerGestureSideTapNext",
"settingsSlideshowFillScreen",
"settingsScreenSaverPageTitle",
"settingsWidgetPageTitle",
"settingsWidgetShowOutline"
],
"it": [
"settingsViewerGestureSideTapNext"
],
"ja": [
"settingsViewerGestureSideTapNext"
],
"ko": [
"settingsViewerGestureSideTapNext"
],
"pt": [
"settingsViewerGestureSideTapNext"
],
"ru": [
"filterOnThisDayLabel",
"settingsViewerGestureSideTapNext",
"settingsSlideshowFillScreen",
"settingsScreenSaverPageTitle",
"settingsWidgetPageTitle",
@ -38,6 +65,7 @@
"wallpaperTargetLock",
"wallpaperTargetHomeLock",
"menuActionSlideshow",
"settingsViewerGestureSideTapNext",
"settingsViewerSlideshowTile",
"settingsViewerSlideshowTitle",
"settingsSlideshowRepeat",
@ -53,5 +81,9 @@
"settingsWidgetPageTitle",
"settingsWidgetShowOutline",
"viewerSetWallpaperButtonLabel"
],
"zh": [
"settingsViewerGestureSideTapNext"
]
}