diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 14a8bc164..5c495c9a1 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clone the repository.
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Get packages for the Flutter project.
run: scripts/pub_get_all.sh
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d107a6cd9..f45de1a4c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -10,12 +10,13 @@ jobs:
name: Build and release artifacts.
runs-on: ubuntu-latest
steps:
- - uses: actions/setup-java@v1
+ - uses: actions/setup-java@v3
with:
- java-version: '11.x'
+ distribution: 'zulu'
+ java-version: '11'
- name: Clone the repository.
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Get packages for the Flutter project.
run: scripts/pub_get_all.sh
@@ -74,7 +75,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload app bundle
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v3
with:
name: appbundle
path: outputs/app-play-release.aab
@@ -84,15 +85,15 @@ jobs:
needs: [ build ]
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Get appbundle from artifacts.
- uses: actions/download-artifact@v2
+ uses: actions/download-artifact@v3
with:
name: appbundle
- name: Release app to beta channel.
- uses: r0adkll/upload-google-play@v1
+ uses: r0adkll/upload-google-play@v1.1.1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
packageName: deckers.thibault.aves
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b148456f6..3016c71ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
-## [v1.8.0] - 2023-02-20
+## [v1.8.1] - 2023-02-21
### Added
@@ -28,6 +28,8 @@ All notable changes to this project will be documented in this file.
- copying to SD card in some cases
- sharing SD card files referred by `file` URI
+## [v1.8.0] - 2023-02-20 [YANKED]
+
## [v1.7.10] - 2023-01-18
### Added
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt
index de0d7d78a..dce85d755 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt
@@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
+import java.util.*
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@@ -219,18 +220,20 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
entriesToNewName[AvesEntry(rawEntry)] = newName
}
- // assume same provider for all entries
- val firstEntry = entriesToNewName.keys.first()
- val provider = getProvider(firstEntry.uri)
- if (provider == null) {
- error("rename-provider", "failed to find provider for entry=$firstEntry", null)
- return
+ val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(kv.key.uri) }
+ for ((provider, entryList) in byProvider) {
+ if (provider == null) {
+ error("rename-provider", "failed to find provider for entry=${entryList.firstOrNull()}", null)
+ return
+ }
+
+ val entryMap = mapOf(*entryList.map { Pair(it.key, it.value) }.toTypedArray())
+ provider.renameMultiple(activity, entryMap, ::isCancelledOp, object : ImageOpCallback {
+ override fun onSuccess(fields: FieldMap) = success(fields)
+ override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
+ })
}
- provider.renameMultiple(activity, entriesToNewName, ::isCancelledOp, object : ImageOpCallback {
- override fun onSuccess(fields: FieldMap) = success(fields)
- override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
- })
endOfStream()
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt
index 6a1b0725e..7ef565d8b 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt
@@ -1,5 +1,6 @@
package deckers.thibault.aves.model.provider
+import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.net.Uri
@@ -53,6 +54,26 @@ internal class FileImageProvider : ImageProvider() {
throw Exception("failed to delete entry with uri=$uri path=$path")
}
+ override suspend fun renameSingle(
+ activity: Activity,
+ mimeType: String,
+ oldMediaUri: Uri,
+ oldPath: String,
+ newFile: File,
+ ): FieldMap {
+ Log.d(LOG_TAG, "rename file at path=$oldPath")
+ val renamed = File(oldPath).renameTo(newFile)
+ if (!renamed) {
+ throw Exception("failed to rename file at path=$oldPath")
+ }
+
+ return hashMapOf(
+ "uri" to Uri.fromFile(newFile).toString(),
+ "path" to newFile.path,
+ "dateModifiedSecs" to newFile.lastModified() / 1000,
+ )
+ }
+
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
try {
val file = File(path)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 4fb59c16c..8e973af43 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -68,13 +68,75 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider"))
}
- open suspend fun renameMultiple(
+ suspend fun renameMultiple(
activity: Activity,
entriesToNewName: Map,
isCancelledOp: CancelCheck,
callback: ImageOpCallback,
) {
- callback.onFailure(UnsupportedOperationException("`renameMultiple` is not supported by this image provider"))
+ for (kv in entriesToNewName) {
+ val entry = kv.key
+ val desiredName = kv.value
+
+ val sourceUri = entry.uri
+ val sourcePath = entry.path
+ val mimeType = entry.mimeType
+
+ val result: FieldMap = hashMapOf(
+ "uri" to sourceUri.toString(),
+ "success" to false,
+ )
+
+ // prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store
+ if (sourcePath != null && !desiredName.startsWith('.')) {
+ try {
+ var newFields: FieldMap = skippedFieldMap
+ if (!isCancelledOp()) {
+ val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
+
+ val oldFile = File(sourcePath)
+ if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
+ oldFile.parent?.let { dir ->
+ resolveTargetFileNameWithoutExtension(
+ contextWrapper = activity,
+ dir = dir,
+ desiredNameWithoutExtension = desiredNameWithoutExtension,
+ mimeType = mimeType,
+ conflictStrategy = NameConflictStrategy.RENAME,
+ )?.let { targetNameWithoutExtension ->
+ val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
+ val newFile = File(dir, targetFileName)
+ if (oldFile != newFile) {
+ newFields = renameSingle(
+ activity = activity,
+ mimeType = mimeType,
+ oldMediaUri = sourceUri,
+ oldPath = sourcePath,
+ newFile = newFile,
+ )
+ }
+ }
+ }
+ }
+ }
+ result["newFields"] = newFields
+ result["success"] = true
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to rename to newFileName=$desiredName entry with sourcePath=$sourcePath", e)
+ }
+ }
+ callback.onSuccess(result)
+ }
+ }
+
+ open suspend fun renameSingle(
+ activity: Activity,
+ mimeType: String,
+ oldMediaUri: Uri,
+ oldPath: String,
+ newFile: File,
+ ): FieldMap {
+ throw UnsupportedOperationException("`renameSingle` is not supported by this image provider")
}
open fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
index 5e568be50..c57872841 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
@@ -552,10 +552,10 @@ class MediaStoreImageProvider : ImageProvider() {
)
} else if (toVault) {
hashMapOf(
+ "origin" to SourceEntry.ORIGIN_VAULT,
"uri" to File(targetPath).toUri().toString(),
"contentId" to null,
"path" to targetPath,
- "origin" to SourceEntry.ORIGIN_VAULT,
)
} else {
scanNewPath(activity, targetPath, mimeType)
@@ -626,74 +626,16 @@ class MediaStoreImageProvider : ImageProvider() {
return targetDir + fileName
}
- override suspend fun renameMultiple(
- activity: Activity,
- entriesToNewName: Map,
- isCancelledOp: CancelCheck,
- callback: ImageOpCallback,
- ) {
- for (kv in entriesToNewName) {
- val entry = kv.key
- val desiredName = kv.value
-
- val sourceUri = entry.uri
- val sourcePath = entry.path
- val mimeType = entry.mimeType
-
- val result: FieldMap = hashMapOf(
- "uri" to sourceUri.toString(),
- "success" to false,
- )
-
- // prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store
- if (sourcePath != null && !desiredName.startsWith('.')) {
- try {
- val newFields = if (isCancelledOp()) skippedFieldMap else renameSingle(
- activity = activity,
- mimeType = mimeType,
- oldMediaUri = sourceUri,
- oldPath = sourcePath,
- desiredName = desiredName,
- )
- result["newFields"] = newFields
- result["success"] = true
- } catch (e: Exception) {
- Log.w(LOG_TAG, "failed to rename to newFileName=$desiredName entry with sourcePath=$sourcePath", e)
- }
- }
- callback.onSuccess(result)
- }
- }
-
- private suspend fun renameSingle(
+ override suspend fun renameSingle(
activity: Activity,
mimeType: String,
oldMediaUri: Uri,
oldPath: String,
- desiredName: String,
- ): FieldMap {
- val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
-
- val oldFile = File(oldPath)
- if (oldFile.nameWithoutExtension == desiredNameWithoutExtension) return skippedFieldMap
-
- val dir = oldFile.parent ?: return skippedFieldMap
- val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
- contextWrapper = activity,
- dir = dir,
- desiredNameWithoutExtension = desiredNameWithoutExtension,
- mimeType = mimeType,
- conflictStrategy = NameConflictStrategy.RENAME,
- ) ?: return skippedFieldMap
- val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
-
- val newFile = File(dir, targetFileName)
- return when {
- oldFile == newFile -> skippedFieldMap
- StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile)
- isMediaUriPermissionGranted(activity, oldMediaUri, mimeType) -> renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
- else -> renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
- }
+ newFile: File,
+ ): FieldMap = when {
+ StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile)
+ isMediaUriPermissionGranted(activity, oldMediaUri, mimeType) -> renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
+ else -> renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
}
private suspend fun renameSingleByMediaStore(
@@ -851,10 +793,12 @@ class MediaStoreImageProvider : ImageProvider() {
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
- val newFields = HashMap()
- newFields["uri"] = uri.toString()
- newFields["contentId"] = uri.tryParseId()
- newFields["path"] = path
+ val newFields = hashMapOf(
+ "origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
+ "uri" to uri.toString(),
+ "contentId" to uri.tryParseId(),
+ "path" to path,
+ )
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.close()
diff --git a/fastlane/metadata/android/en-US/changelogs/92.txt b/fastlane/metadata/android/en-US/changelogs/92.txt
new file mode 100644
index 000000000..aa868aeac
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/92.txt
@@ -0,0 +1,5 @@
+In v1.8.1:
+- Android TV support (cont'd)
+- hide your secrets in vaults
+- enjoy the app in Basque
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/9201.txt b/fastlane/metadata/android/en-US/changelogs/9201.txt
new file mode 100644
index 000000000..aa868aeac
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/9201.txt
@@ -0,0 +1,5 @@
+In v1.8.1:
+- Android TV support (cont'd)
+- hide your secrets in vaults
+- enjoy the app in Basque
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/lib/utils/dependencies.dart b/lib/utils/dependencies.dart
index 4076d7df9..9219ac084 100644
--- a/lib/utils/dependencies.dart
+++ b/lib/utils/dependencies.dart
@@ -266,9 +266,9 @@ class Dependencies {
sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator',
),
Dependency(
- name: 'Pinput',
+ name: 'Pin Code Fields',
license: mit,
- sourceUrl: 'https://github.com/Tkko/Flutter_PinPut',
+ sourceUrl: 'https://github.com/adar2378/pin_code_fields',
),
Dependency(
name: 'Provider',
diff --git a/lib/widgets/dialogs/filter_editors/password_dialog.dart b/lib/widgets/dialogs/filter_editors/password_dialog.dart
index 1bce02e90..80b075e6b 100644
--- a/lib/widgets/dialogs/filter_editors/password_dialog.dart
+++ b/lib/widgets/dialogs/filter_editors/password_dialog.dart
@@ -44,7 +44,18 @@ class _PasswordDialogState extends State {
onSubmitted: (password) {
if (widget.needConfirmation) {
if (_confirming) {
- Navigator.maybeOf(context)?.pop(_firstPassword == password ? password : null);
+ final match = _firstPassword == password;
+ Navigator.maybeOf(context)?.pop(match ? password : null);
+ if (!match) {
+ showDialog(
+ context: context,
+ builder: (context) => AvesDialog(
+ content: Text(context.l10n.genericFailureFeedback),
+ actions: const [OkButton()],
+ ),
+ routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
+ );
+ }
} else {
_firstPassword = password;
_controller.clear();
diff --git a/lib/widgets/dialogs/filter_editors/pin_dialog.dart b/lib/widgets/dialogs/filter_editors/pin_dialog.dart
index 6afd18420..7394061c0 100644
--- a/lib/widgets/dialogs/filter_editors/pin_dialog.dart
+++ b/lib/widgets/dialogs/filter_editors/pin_dialog.dart
@@ -1,7 +1,7 @@
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:flutter/material.dart';
-import 'package:pinput/pinput.dart';
+import 'package:pin_code_fields/pin_code_fields.dart';
class PinDialog extends StatefulWidget {
static const routeName = '/dialog/pin';
@@ -19,18 +19,12 @@ class PinDialog extends StatefulWidget {
class _PinDialogState extends State {
final _controller = TextEditingController();
- final _focusNode = FocusNode();
bool _confirming = false;
String? _firstPin;
- @override
- void initState() {
- super.initState();
- _focusNode.requestFocus();
- }
-
@override
Widget build(BuildContext context) {
+ final colorScheme = Theme.of(context).colorScheme;
return AvesDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
@@ -38,24 +32,48 @@ class _PinDialogState extends State {
Text(_confirming ? context.l10n.pinDialogConfirm : context.l10n.pinDialogEnter),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
- child: Pinput(
+ child: PinCodeTextField(
+ appContext: context,
+ length: 4,
+ controller: _controller,
+ obscureText: true,
+ onChanged: (v) {},
onCompleted: (pin) {
if (widget.needConfirmation) {
if (_confirming) {
- Navigator.maybeOf(context)?.pop(_firstPin == pin ? pin : null);
+ final match = _firstPin == pin;
+ Navigator.maybeOf(context)?.pop(match ? pin : null);
+ if (!match) {
+ showDialog(
+ context: context,
+ builder: (context) => AvesDialog(
+ content: Text(context.l10n.genericFailureFeedback),
+ actions: const [OkButton()],
+ ),
+ routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
+ );
+ }
} else {
_firstPin = pin;
_controller.clear();
setState(() => _confirming = true);
- WidgetsBinding.instance.addPostFrameCallback((_) => _focusNode.requestFocus());
}
} else {
Navigator.maybeOf(context)?.pop(pin);
}
},
- controller: _controller,
- focusNode: _focusNode,
- obscureText: true,
+ animationType: AnimationType.scale,
+ keyboardType: TextInputType.number,
+ autoFocus: true,
+ autoDismissKeyboard: !widget.needConfirmation || _confirming,
+ pinTheme: PinTheme(
+ activeColor: colorScheme.onBackground,
+ inactiveColor: colorScheme.onBackground,
+ selectedColor: colorScheme.secondary,
+ selectedFillColor: colorScheme.secondary,
+ borderRadius: BorderRadius.circular(8),
+ shape: PinCodeFieldShape.box,
+ ),
),
),
],
diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart
index c157b1ae8..b805798fa 100644
--- a/lib/widgets/viewer/entry_viewer_stack.dart
+++ b/lib/widgets/viewer/entry_viewer_stack.dart
@@ -466,14 +466,16 @@ class _EntryViewerStackState extends State with EntryViewContr
if (!_overlayVisible.value) {
_overlayVisible.value = true;
}
+ } else if (notification is PopVisualNotification) {
+ _popVisual();
} else if (notification is ShowInfoPageNotification) {
_goToVerticalPage(infoPage);
- } 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 ShowPreviousEntryNotification) {
+ _goToHorizontalPageByDelta(delta: -1, animate: notification.animate);
+ } else if (notification is ShowNextEntryNotification) {
+ _goToHorizontalPageByDelta(delta: 1, animate: notification.animate);
+ } else if (notification is ShowEntryNotification) {
+ _goToHorizontalPageByIndex(page: notification.index, animate: notification.animate);
} else if (notification is VideoActionNotification) {
final controller = notification.controller;
final action = notification.action;
@@ -545,23 +547,33 @@ class _EntryViewerStackState extends State with EntryViewContr
}
}
- void _jumpToHorizontalPageByDelta(int delta) {
+ void _goToHorizontalPageByDelta({required int delta, required bool animate}) {
if (_horizontalPager.positions.isEmpty) return;
final page = _horizontalPager.page?.round();
if (page != null) {
- _jumpToHorizontalPageByIndex(page + delta);
+ _goToHorizontalPageByIndex(page: page + delta, animate: animate);
}
}
- void _jumpToHorizontalPageByIndex(int target) {
+ Future _goToHorizontalPageByIndex({required int page, required bool animate}) async {
final _collection = collection;
if (_collection != null) {
if (!widget.viewerController.repeat) {
- target = target.clamp(0, _collection.entryCount - 1);
+ page = page.clamp(0, _collection.entryCount - 1);
}
- if (_currentEntryIndex != target) {
- _horizontalPager.jumpToPage(target);
+ if (_currentEntryIndex != page) {
+ final animationDuration = animate ? context.read().viewerVerticalPageScrollAnimation : Duration.zero;
+ if (animationDuration > Duration.zero) {
+ // duration & curve should feel similar to changing page by vertical fling
+ await _horizontalPager.animateToPage(
+ page,
+ duration: animationDuration,
+ curve: Curves.easeOutQuart,
+ );
+ } else {
+ _horizontalPager.jumpToPage(page);
+ }
}
}
}
diff --git a/lib/widgets/viewer/notifications.dart b/lib/widgets/viewer/notifications.dart
index 889a82127..a8d0736ed 100644
--- a/lib/widgets/viewer/notifications.dart
+++ b/lib/widgets/viewer/notifications.dart
@@ -6,6 +6,9 @@ import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/widgets.dart';
+@immutable
+class PopVisualNotification extends Notification {}
+
@immutable
class ShowImageNotification extends Notification {}
@@ -13,10 +16,29 @@ class ShowImageNotification extends Notification {}
class ShowInfoPageNotification extends Notification {}
@immutable
-class TvShowLessInfoNotification extends Notification {}
+class ShowPreviousEntryNotification extends Notification {
+ final bool animate;
+
+ const ShowPreviousEntryNotification({required this.animate});
+}
@immutable
-class TvShowMoreInfoNotification extends Notification {}
+class ShowNextEntryNotification extends Notification {
+ final bool animate;
+
+ const ShowNextEntryNotification({required this.animate});
+}
+
+@immutable
+class ShowEntryNotification extends Notification {
+ final bool animate;
+ final int index;
+
+ const ShowEntryNotification({
+ required this.animate,
+ required this.index,
+ });
+}
@immutable
class ToggleOverlayNotification extends Notification {
@@ -26,17 +48,10 @@ class ToggleOverlayNotification extends Notification {
}
@immutable
-class JumpToPreviousEntryNotification extends Notification {}
+class TvShowLessInfoNotification extends Notification {}
@immutable
-class JumpToNextEntryNotification extends Notification {}
-
-@immutable
-class JumpToEntryNotification extends Notification {
- final int index;
-
- const JumpToEntryNotification({required this.index});
-}
+class TvShowMoreInfoNotification extends Notification {}
@immutable
class VideoActionNotification extends Notification {
diff --git a/lib/widgets/viewer/overlay/thumbnail_preview.dart b/lib/widgets/viewer/overlay/thumbnail_preview.dart
index 962826fc9..5a0f27bb9 100644
--- a/lib/widgets/viewer/overlay/thumbnail_preview.dart
+++ b/lib/widgets/viewer/overlay/thumbnail_preview.dart
@@ -60,13 +60,13 @@ class _ViewerThumbnailPreviewState extends State {
entryCount: entryCount,
entryBuilder: (index) => 0 <= index && index < entryCount ? entries[index] : null,
indexNotifier: _entryIndexNotifier,
- onTap: (index) => JumpToEntryNotification(index: index).dispatch(context),
+ onTap: (index) => ShowEntryNotification(animate: false, index: index).dispatch(context),
);
}
void _onScrollerIndexChanged() => _debouncer(() {
if (mounted) {
- JumpToEntryNotification(index: _entryIndexNotifier.value).dispatch(context);
+ ShowEntryNotification(animate: false, index: _entryIndexNotifier.value).dispatch(context);
}
});
}
diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart
index 0b64cd867..e5712ed24 100644
--- a/lib/widgets/viewer/visual/entry_page_view.dart
+++ b/lib/widgets/viewer/visual/entry_page_view.dart
@@ -401,20 +401,38 @@ class _EntryPageViewState extends State with SingleTickerProvider
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
+ onFling: _onFling,
onTap: (c, s, a, p) => _onTap(alignment: a),
onDoubleTap: onDoubleTap,
child: child,
);
}
+ void _onFling(AxisDirection direction) {
+ switch (direction) {
+ case AxisDirection.left:
+ const ShowPreviousEntryNotification(animate: true).dispatch(context);
+ break;
+ case AxisDirection.right:
+ const ShowNextEntryNotification(animate: true).dispatch(context);
+ break;
+ case AxisDirection.up:
+ PopVisualNotification().dispatch(context);
+ break;
+ case AxisDirection.down:
+ ShowInfoPageNotification().dispatch(context);
+ break;
+ }
+ }
+
void _onTap({Alignment? alignment}) {
if (settings.viewerGestureSideTapNext && alignment != null) {
final x = alignment.x;
if (x < sideRatio) {
- JumpToPreviousEntryNotification().dispatch(context);
+ const ShowPreviousEntryNotification(animate: false).dispatch(context);
return;
} else if (x > 1 - sideRatio) {
- JumpToNextEntryNotification().dispatch(context);
+ const ShowNextEntryNotification(animate: false).dispatch(context);
return;
}
}
diff --git a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart
index 93a47d0f1..c2de57bbb 100644
--- a/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart
+++ b/plugins/aves_magnifier/lib/src/controller/controller_delegate.dart
@@ -138,9 +138,9 @@ mixin AvesMagnifierControllerDelegate on State {
controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint);
}
- CornersRange cornersX({double? scale}) {
+ EdgeRange getXEdges({double? scale}) {
final boundaries = scaleBoundaries;
- if (boundaries == null) return const CornersRange(0, 0);
+ if (boundaries == null) return const EdgeRange(0, 0);
final _scale = scale ?? this.scale!;
@@ -152,12 +152,12 @@ mixin AvesMagnifierControllerDelegate on State {
final minX = ((positionX - 1).abs() / 2) * widthDiff * -1;
final maxX = ((positionX + 1).abs() / 2) * widthDiff;
- return CornersRange(minX, maxX);
+ return EdgeRange(minX, maxX);
}
- CornersRange cornersY({double? scale}) {
+ EdgeRange getYEdges({double? scale}) {
final boundaries = scaleBoundaries;
- if (boundaries == null) return const CornersRange(0, 0);
+ if (boundaries == null) return const EdgeRange(0, 0);
final _scale = scale ?? this.scale!;
@@ -169,7 +169,7 @@ mixin AvesMagnifierControllerDelegate on State {
final minY = ((positionY - 1).abs() / 2) * heightDiff * -1;
final maxY = ((positionY + 1).abs() / 2) * heightDiff;
- return CornersRange(minY, maxY);
+ return EdgeRange(minY, maxY);
}
Offset clampPosition({Offset? position, double? scale}) {
@@ -187,14 +187,14 @@ mixin AvesMagnifierControllerDelegate on State {
var finalX = 0.0;
if (screenWidth < computedWidth) {
- final cornersX = this.cornersX(scale: _scale);
- finalX = _position.dx.clamp(cornersX.min, cornersX.max);
+ final range = getXEdges(scale: _scale);
+ finalX = _position.dx.clamp(range.min, range.max);
}
var finalY = 0.0;
if (screenHeight < computedHeight) {
- final cornersY = this.cornersY(scale: _scale);
- finalY = _position.dy.clamp(cornersY.min, cornersY.max);
+ final range = getYEdges(scale: _scale);
+ finalY = _position.dy.clamp(range.min, range.max);
}
return Offset(finalX, finalY);
@@ -202,8 +202,8 @@ mixin AvesMagnifierControllerDelegate on State {
}
/// Simple class to store a min and a max value
-class CornersRange {
- const CornersRange(this.min, this.max);
+class EdgeRange {
+ const EdgeRange(this.min, this.max);
final double min;
final double max;
diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart
index 0ce553290..531934cd6 100644
--- a/plugins/aves_magnifier/lib/src/core/core.dart
+++ b/plugins/aves_magnifier/lib/src/core/core.dart
@@ -5,11 +5,14 @@ import 'package:aves_magnifier/src/controller/controller_delegate.dart';
import 'package:aves_magnifier/src/controller/state.dart';
import 'package:aves_magnifier/src/core/gesture_detector.dart';
import 'package:aves_magnifier/src/magnifier.dart';
-import 'package:aves_magnifier/src/pan/corner_hit_detector.dart';
+import 'package:aves_magnifier/src/pan/edge_hit_detector.dart';
import 'package:aves_magnifier/src/scale/scale_boundaries.dart';
import 'package:aves_magnifier/src/scale/state.dart';
import 'package:equatable/equatable.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
+import 'package:provider/provider.dart';
+import 'package:tuple/tuple.dart';
/// Internal widget in which controls all animations lifecycle, core responses
/// to user gestures, updates to the controller state and mounts the entire Layout
@@ -21,6 +24,7 @@ class MagnifierCore extends StatefulWidget {
final MagnifierGestureScaleStartCallback? onScaleStart;
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
final MagnifierGestureScaleEndCallback? onScaleEnd;
+ final MagnifierGestureFlingCallback? onFling;
final MagnifierTapCallback? onTap;
final MagnifierDoubleTapCallback? onDoubleTap;
final Widget child;
@@ -34,6 +38,7 @@ class MagnifierCore extends StatefulWidget {
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
+ this.onFling,
this.onTap,
this.onDoubleTap,
required this.child,
@@ -43,7 +48,7 @@ class MagnifierCore extends StatefulWidget {
State createState() => _MagnifierCoreState();
}
-class _MagnifierCoreState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector {
+class _MagnifierCoreState extends State with TickerProviderStateMixin, AvesMagnifierControllerDelegate, EdgeHitDetector {
Offset? _startFocalPoint, _lastViewportFocalPosition;
double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
late bool _dropped, _doubleTap, _quickScaleMoved;
@@ -57,6 +62,8 @@ class _MagnifierCoreState extends State with TickerProviderStateM
ScaleBoundaries? cachedScaleBoundaries;
+ static const _flingPointerKind = PointerDeviceKind.unknown;
+
@override
void initState() {
super.initState();
@@ -104,12 +111,21 @@ class _MagnifierCoreState extends State with TickerProviderStateM
controller.update(position: _positionAnimation.value, source: ChangeSource.animation);
}
+ Stopwatch? _scaleStopwatch;
+ VelocityTracker? _velocityTracker;
+ var _mayFlingLTRB = const Tuple4(false, false, false, false);
+
void onScaleStart(ScaleStartDetails details, bool doubleTap) {
final boundaries = scaleBoundaries;
if (boundaries == null) return;
widget.onScaleStart?.call(details, doubleTap, boundaries);
+ _scaleStopwatch = Stopwatch()..start();
+ _velocityTracker = VelocityTracker.withKind(_flingPointerKind);
+ _mayFlingLTRB = const Tuple4(true, true, true, true);
+ _updateMayFling();
+
_startScale = scale;
_startFocalPoint = details.localFocalPoint;
_lastViewportFocalPosition = _startFocalPoint;
@@ -130,6 +146,12 @@ class _MagnifierCoreState extends State with TickerProviderStateM
_dropped |= widget.onScaleUpdate?.call(details) ?? false;
if (_dropped) return;
+ final elapsed = _scaleStopwatch?.elapsed;
+ if (elapsed != null) {
+ _velocityTracker?.addPosition(elapsed, details.focalPoint);
+ }
+ _updateMayFling();
+
double newScale;
if (_doubleTap) {
// quick scale, aka one finger zoom
@@ -168,6 +190,29 @@ class _MagnifierCoreState extends State with TickerProviderStateM
widget.onScaleEnd?.call(details);
+ _updateMayFling();
+ final estimate = _velocityTracker?.getVelocityEstimate();
+ final onFling = widget.onFling;
+ if (estimate != null && onFling != null) {
+ if (_isFlingGesture(estimate, _flingPointerKind, Axis.horizontal)) {
+ final left = _mayFlingLTRB.item1;
+ final right = _mayFlingLTRB.item3;
+ if (left) {
+ onFling(AxisDirection.left);
+ } else if (right) {
+ onFling(AxisDirection.right);
+ }
+ } else if (_isFlingGesture(estimate, _flingPointerKind, Axis.vertical)) {
+ final up = _mayFlingLTRB.item2;
+ final down = _mayFlingLTRB.item4;
+ if (up) {
+ onFling(AxisDirection.up);
+ } else if (down) {
+ onFling(AxisDirection.down);
+ }
+ }
+ }
+
final _position = controller.position;
final _scale = controller.scale!;
final maxScale = boundaries.maxScale;
@@ -208,6 +253,31 @@ class _MagnifierCoreState extends State with TickerProviderStateM
}
}
+ void _updateMayFling() {
+ final xHit = getXEdgeHit();
+ final yHit = getYEdgeHit();
+ _mayFlingLTRB = Tuple4(
+ _mayFlingLTRB.item1 && xHit.hasHitMin,
+ _mayFlingLTRB.item2 && yHit.hasHitMin,
+ _mayFlingLTRB.item3 && xHit.hasHitMax,
+ _mayFlingLTRB.item4 && yHit.hasHitMax,
+ );
+ }
+
+ bool _isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind, Axis axis) {
+ final gestureSettings = context.read().gestureSettings;
+ const minVelocity = kMinFlingVelocity;
+ final minDistance = computeHitSlop(kind, gestureSettings);
+
+ final pps = estimate.pixelsPerSecond;
+ final offset = estimate.offset;
+ if (axis == Axis.horizontal) {
+ return pps.dx.abs() > minVelocity && offset.dx.abs() > minDistance;
+ } else {
+ return pps.dy.abs() > minVelocity && offset.dy.abs() > minDistance;
+ }
+ }
+
Duration _getAnimationDurationForVelocity({
required Cubic curve,
required Tween tween,
diff --git a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart
index 76a6e1348..e18c4283f 100644
--- a/plugins/aves_magnifier/lib/src/core/gesture_detector.dart
+++ b/plugins/aves_magnifier/lib/src/core/gesture_detector.dart
@@ -1,5 +1,5 @@
import 'package:aves_magnifier/src/core/scale_gesture_recognizer.dart';
-import 'package:aves_magnifier/src/pan/corner_hit_detector.dart';
+import 'package:aves_magnifier/src/pan/edge_hit_detector.dart';
import 'package:aves_magnifier/src/pan/gesture_detector_scope.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
@@ -19,7 +19,7 @@ class MagnifierGestureDetector extends StatefulWidget {
this.child,
});
- final CornerHitDetector hitDetector;
+ final EdgeHitDetector hitDetector;
final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart;
final GestureScaleUpdateCallback? onScaleUpdate;
final GestureScaleEndCallback? onScaleEnd;
diff --git a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart
index eb95393b9..361db25d6 100644
--- a/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart
+++ b/plugins/aves_magnifier/lib/src/core/scale_gesture_recognizer.dart
@@ -1,12 +1,12 @@
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/edge_hit_detector.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
- final CornerHitDetector hitDetector;
+ final EdgeHitDetector hitDetector;
final MagnifierGestureDetectorScope scope;
final ValueNotifier doubleTapDetails;
@@ -104,14 +104,15 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
}
final validateAxis = scope.axis;
+ final canFling = scope.escapeByFling;
final move = _initialFocalPoint! - _currentFocalPoint!;
bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false;
if (!shouldMove) {
if (validateAxis.length == 2) {
// the image is the descendant of gesture detector(s) handling drag in both directions
- final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move);
- final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move);
+ final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move, canFling);
+ final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move, canFling);
if (shouldMoveX == shouldMoveY) {
// consistently can/cannot pan the image in both direction the same way
shouldMove = shouldMoveX;
@@ -122,7 +123,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
}
} 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);
+ shouldMove = validateAxis.contains(Axis.vertical) ? hitDetector.shouldMoveY(move, canFling) : hitDetector.shouldMoveX(move, canFling);
}
}
diff --git a/plugins/aves_magnifier/lib/src/magnifier.dart b/plugins/aves_magnifier/lib/src/magnifier.dart
index 88c3f7586..38f5e1d02 100644
--- a/plugins/aves_magnifier/lib/src/magnifier.dart
+++ b/plugins/aves_magnifier/lib/src/magnifier.dart
@@ -32,6 +32,7 @@ class AvesMagnifier extends StatelessWidget {
this.onScaleStart,
this.onScaleUpdate,
this.onScaleEnd,
+ this.onFling,
this.onTap,
this.onDoubleTap,
required this.child,
@@ -58,6 +59,7 @@ class AvesMagnifier extends StatelessWidget {
final MagnifierGestureScaleStartCallback? onScaleStart;
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
final MagnifierGestureScaleEndCallback? onScaleEnd;
+ final MagnifierGestureFlingCallback? onFling;
final MagnifierTapCallback? onTap;
final MagnifierDoubleTapCallback? onDoubleTap;
final Widget child;
@@ -82,6 +84,7 @@ class AvesMagnifier extends StatelessWidget {
onScaleStart: onScaleStart,
onScaleUpdate: onScaleUpdate,
onScaleEnd: onScaleEnd,
+ onFling: onFling,
onTap: onTap,
onDoubleTap: onDoubleTap,
child: child,
@@ -101,3 +104,4 @@ typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment);
typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details);
typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details);
+typedef MagnifierGestureFlingCallback = void Function(AxisDirection direction);
diff --git a/plugins/aves_magnifier/lib/src/pan/corner_hit_detector.dart b/plugins/aves_magnifier/lib/src/pan/corner_hit_detector.dart
deleted file mode 100644
index 8faa9e8a4..000000000
--- a/plugins/aves_magnifier/lib/src/pan/corner_hit_detector.dart
+++ /dev/null
@@ -1,69 +0,0 @@
-import 'package:aves_magnifier/src/controller/controller_delegate.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/widgets.dart';
-
-mixin CornerHitDetector on AvesMagnifierControllerDelegate {
- // the child width/height is not accurate for some image size & scale combos
- // e.g. 3580.0 * 0.1005586592178771 yields 360.0
- // but 4764.0 * 0.07556675062972293 yields 360.00000000000006
- // so be sure to compare with `precisionErrorTolerance`
-
- _CornerHit _hitCornersX() {
- final boundaries = scaleBoundaries;
- if (boundaries == null) return const _CornerHit(false, false);
-
- final childWidth = boundaries.childSize.width * scale!;
- final viewportWidth = boundaries.viewportSize.width;
- if (viewportWidth + precisionErrorTolerance >= childWidth) {
- return const _CornerHit(true, true);
- }
- final x = -position.dx;
- final cornersX = this.cornersX();
- return _CornerHit(x <= cornersX.min, x >= cornersX.max);
- }
-
- _CornerHit _hitCornersY() {
- final boundaries = scaleBoundaries;
- if (boundaries == null) return const _CornerHit(false, false);
-
- final childHeight = boundaries.childSize.height * scale!;
- final viewportHeight = boundaries.viewportSize.height;
- if (viewportHeight + precisionErrorTolerance >= childHeight) {
- return const _CornerHit(true, true);
- }
- final y = -position.dy;
- final cornersY = this.cornersY();
- return _CornerHit(y <= cornersY.min, y >= cornersY.max);
- }
-
- bool shouldMoveX(Offset move) {
- final hitCornersX = _hitCornersX();
- if (hitCornersX.hasHitAny && move != Offset.zero) {
- if (hitCornersX.hasHitBoth) return false;
- if (hitCornersX.hasHitMax) return move.dx < 0;
- return move.dx > 0;
- }
- return true;
- }
-
- bool shouldMoveY(Offset move) {
- final hitCornersY = _hitCornersY();
- if (hitCornersY.hasHitAny && move != Offset.zero) {
- if (hitCornersY.hasHitBoth) return false;
- if (hitCornersY.hasHitMax) return move.dy < 0;
- return move.dy > 0;
- }
- return true;
- }
-}
-
-class _CornerHit {
- const _CornerHit(this.hasHitMin, this.hasHitMax);
-
- final bool hasHitMin;
- final bool hasHitMax;
-
- bool get hasHitAny => hasHitMin || hasHitMax;
-
- bool get hasHitBoth => hasHitMin && hasHitMax;
-}
diff --git a/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart b/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart
new file mode 100644
index 000000000..551c7c0a9
--- /dev/null
+++ b/plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart
@@ -0,0 +1,68 @@
+import 'package:aves_magnifier/src/controller/controller_delegate.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/widgets.dart';
+
+mixin EdgeHitDetector on AvesMagnifierControllerDelegate {
+ // the child width/height is not accurate for some image size & scale combos
+ // e.g. 3580.0 * 0.1005586592178771 yields 360.0
+ // but 4764.0 * 0.07556675062972293 yields 360.00000000000006
+ // so be sure to compare with `precisionErrorTolerance`
+
+ EdgeHit getXEdgeHit() {
+ final boundaries = scaleBoundaries;
+ if (boundaries == null) return const EdgeHit(false, false);
+
+ final childWidth = boundaries.childSize.width * scale!;
+ final viewportWidth = boundaries.viewportSize.width;
+ if (viewportWidth + precisionErrorTolerance >= childWidth) {
+ return const EdgeHit(true, true);
+ }
+ final x = -position.dx;
+ final range = getXEdges();
+ return EdgeHit(x <= range.min, x >= range.max);
+ }
+
+ EdgeHit getYEdgeHit() {
+ final boundaries = scaleBoundaries;
+ if (boundaries == null) return const EdgeHit(false, false);
+
+ final childHeight = boundaries.childSize.height * scale!;
+ final viewportHeight = boundaries.viewportSize.height;
+ if (viewportHeight + precisionErrorTolerance >= childHeight) {
+ return const EdgeHit(true, true);
+ }
+ final y = -position.dy;
+ final range = getYEdges();
+ return EdgeHit(y <= range.min, y >= range.max);
+ }
+
+ bool shouldMoveX(Offset move, bool canFling) {
+ final hit = getXEdgeHit();
+ return _shouldMove(hit, move.dx) || (canFling && !hit.hasHitBoth);
+ }
+
+ bool shouldMoveY(Offset move, bool canFling) {
+ final hit = getYEdgeHit();
+ return _shouldMove(hit, move.dy) || (canFling && !hit.hasHitBoth);
+ }
+
+ bool _shouldMove(EdgeHit hit, double move) {
+ if (hit.hasHitAny && move != 0) {
+ if (hit.hasHitBoth) return false;
+ if (hit.hasHitMax) return move < 0;
+ return move > 0;
+ }
+ return true;
+ }
+}
+
+class EdgeHit {
+ const EdgeHit(this.hasHitMin, this.hasHitMax);
+
+ final bool hasHitMin;
+ final bool hasHitMax;
+
+ bool get hasHitAny => hasHitMin || hasHitMax;
+
+ bool get hasHitBoth => hasHitMin && hasHitMax;
+}
diff --git a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart
index 82da68aea..0ac3689a6 100644
--- a/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart
+++ b/plugins/aves_magnifier/lib/src/pan/gesture_detector_scope.dart
@@ -14,12 +14,18 @@ class MagnifierGestureDetectorScope extends InheritedWidget {
// <1: less reactive but gives the most leeway to other recognizers
// 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree
final double touchSlopFactor;
+
+ // when zoomed in and hitting an edge, allow using a fling gesture to go to the previous/next page,
+ // instead of yielding to the outer scrollable right away
+ final bool escapeByFling;
+
final bool? Function(Offset move)? acceptPointerEvent;
const MagnifierGestureDetectorScope({
super.key,
required this.axis,
this.touchSlopFactor = .8,
+ this.escapeByFling = true,
this.acceptPointerEvent,
required Widget child,
}) : super(child: child);
diff --git a/plugins/aves_magnifier/pubspec.lock b/plugins/aves_magnifier/pubspec.lock
index e07b21f96..bb225830d 100644
--- a/plugins/aves_magnifier/pubspec.lock
+++ b/plugins/aves_magnifier/pubspec.lock
@@ -91,6 +91,14 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
+ tuple:
+ dependency: "direct main"
+ description:
+ name: tuple
+ sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.1"
vector_math:
dependency: transitive
description:
diff --git a/plugins/aves_magnifier/pubspec.yaml b/plugins/aves_magnifier/pubspec.yaml
index bf57a7c34..3ef32e0bd 100644
--- a/plugins/aves_magnifier/pubspec.yaml
+++ b/plugins/aves_magnifier/pubspec.yaml
@@ -10,6 +10,7 @@ dependencies:
sdk: flutter
equatable:
provider:
+ tuple:
dev_dependencies:
flutter_lints:
diff --git a/pubspec.lock b/pubspec.lock
index 6c8596cb0..fd44d6786 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -917,14 +917,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.0"
- pinput:
+ pin_code_fields:
dependency: "direct main"
description:
- name: pinput
- sha256: e6aabd1571dde622f9b942f62ac2c80f84b0b50f95fa209a93e78f7d621e1f82
+ name: pin_code_fields
+ sha256: c8652519d14688f3fe2a8288d86910a46aa0b9046d728f292d3bf6067c31b4c7
url: "https://pub.dev"
source: hosted
- version: "2.2.23"
+ version: "7.4.0"
platform:
dependency: transitive
description:
@@ -1162,14 +1162,6 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
- smart_auth:
- dependency: transitive
- description:
- name: smart_auth
- sha256: "8cfaec55b77d5930ed1666bb7ae70db5bade099bb1422401386853b400962113"
- url: "https://pub.dev"
- source: hosted
- version: "1.0.8"
smooth_page_indicator:
dependency: "direct main"
description:
@@ -1339,14 +1331,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.3.1"
- universal_platform:
- dependency: transitive
- description:
- name: universal_platform
- sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc
- url: "https://pub.dev"
- source: hosted
- version: "1.0.0+1"
url_launcher:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 270faa71b..5b8a9a7f3 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
# - play changelog: /whatsnew/whatsnew-en-US
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt
-version: 1.8.0+91
+version: 1.8.1+92
publish_to: none
environment:
@@ -83,7 +83,7 @@ dependencies:
pdf:
percent_indicator:
permission_handler:
- pinput:
+ pin_code_fields:
printing:
proj4dart:
provider:
diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US
index d1e8ac1c4..aa868aeac 100644
--- a/whatsnew/whatsnew-en-US
+++ b/whatsnew/whatsnew-en-US
@@ -1,4 +1,4 @@
-In v1.8.0:
+In v1.8.1:
- Android TV support (cont'd)
- hide your secrets in vaults
- enjoy the app in Basque