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