Merge branch 'develop'
This commit is contained in:
commit
298150c162
29 changed files with 437 additions and 247 deletions
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone the repository.
|
- name: Clone the repository.
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Get packages for the Flutter project.
|
- name: Get packages for the Flutter project.
|
||||||
run: scripts/pub_get_all.sh
|
run: scripts/pub_get_all.sh
|
||||||
|
|
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
|
@ -10,12 +10,13 @@ jobs:
|
||||||
name: Build and release artifacts.
|
name: Build and release artifacts.
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-java@v1
|
- uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '11.x'
|
distribution: 'zulu'
|
||||||
|
java-version: '11'
|
||||||
|
|
||||||
- name: Clone the repository.
|
- name: Clone the repository.
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Get packages for the Flutter project.
|
- name: Get packages for the Flutter project.
|
||||||
run: scripts/pub_get_all.sh
|
run: scripts/pub_get_all.sh
|
||||||
|
@ -74,7 +75,7 @@ jobs:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload app bundle
|
- name: Upload app bundle
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: appbundle
|
name: appbundle
|
||||||
path: outputs/app-play-release.aab
|
path: outputs/app-play-release.aab
|
||||||
|
@ -84,15 +85,15 @@ jobs:
|
||||||
needs: [ build ]
|
needs: [ build ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Get appbundle from artifacts.
|
- name: Get appbundle from artifacts.
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: appbundle
|
name: appbundle
|
||||||
|
|
||||||
- name: Release app to beta channel.
|
- name: Release app to beta channel.
|
||||||
uses: r0adkll/upload-google-play@v1
|
uses: r0adkll/upload-google-play@v1.1.1
|
||||||
with:
|
with:
|
||||||
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
|
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
|
||||||
packageName: deckers.thibault.aves
|
packageName: deckers.thibault.aves
|
||||||
|
|
|
@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <a id="unreleased"></a>[Unreleased]
|
||||||
|
|
||||||
## <a id="v1.8.0"></a>[v1.8.0] - 2023-02-20
|
## <a id="v1.8.1"></a>[v1.8.1] - 2023-02-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
@ -28,6 +28,8 @@ All notable changes to this project will be documented in this file.
|
||||||
- copying to SD card in some cases
|
- copying to SD card in some cases
|
||||||
- sharing SD card files referred by `file` URI
|
- sharing SD card files referred by `file` URI
|
||||||
|
|
||||||
|
## <a id="v1.8.0"></a>[v1.8.0] - 2023-02-20 [YANKED]
|
||||||
|
|
||||||
## <a id="v1.7.10"></a>[v1.7.10] - 2023-01-18
|
## <a id="v1.7.10"></a>[v1.7.10] - 2023-01-18
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
|
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
|
||||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||||
|
@ -219,18 +220,20 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
||||||
entriesToNewName[AvesEntry(rawEntry)] = newName
|
entriesToNewName[AvesEntry(rawEntry)] = newName
|
||||||
}
|
}
|
||||||
|
|
||||||
// assume same provider for all entries
|
val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(kv.key.uri) }
|
||||||
val firstEntry = entriesToNewName.keys.first()
|
for ((provider, entryList) in byProvider) {
|
||||||
val provider = getProvider(firstEntry.uri)
|
|
||||||
if (provider == null) {
|
if (provider == null) {
|
||||||
error("rename-provider", "failed to find provider for entry=$firstEntry", null)
|
error("rename-provider", "failed to find provider for entry=${entryList.firstOrNull()}", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.renameMultiple(activity, entriesToNewName, ::isCancelledOp, object : ImageOpCallback {
|
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 onSuccess(fields: FieldMap) = success(fields)
|
||||||
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
endOfStream()
|
endOfStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package deckers.thibault.aves.model.provider
|
package deckers.thibault.aves.model.provider
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -53,6 +54,26 @@ internal class FileImageProvider : ImageProvider() {
|
||||||
throw Exception("failed to delete entry with uri=$uri path=$path")
|
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) {
|
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
|
||||||
try {
|
try {
|
||||||
val file = File(path)
|
val file = File(path)
|
||||||
|
|
|
@ -68,13 +68,75 @@ abstract class ImageProvider {
|
||||||
callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider"))
|
callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider"))
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun renameMultiple(
|
suspend fun renameMultiple(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
entriesToNewName: Map<AvesEntry, String>,
|
entriesToNewName: Map<AvesEntry, String>,
|
||||||
isCancelledOp: CancelCheck,
|
isCancelledOp: CancelCheck,
|
||||||
callback: ImageOpCallback,
|
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) {
|
open fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
|
||||||
|
|
|
@ -552,10 +552,10 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
)
|
)
|
||||||
} else if (toVault) {
|
} else if (toVault) {
|
||||||
hashMapOf(
|
hashMapOf(
|
||||||
|
"origin" to SourceEntry.ORIGIN_VAULT,
|
||||||
"uri" to File(targetPath).toUri().toString(),
|
"uri" to File(targetPath).toUri().toString(),
|
||||||
"contentId" to null,
|
"contentId" to null,
|
||||||
"path" to targetPath,
|
"path" to targetPath,
|
||||||
"origin" to SourceEntry.ORIGIN_VAULT,
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
scanNewPath(activity, targetPath, mimeType)
|
scanNewPath(activity, targetPath, mimeType)
|
||||||
|
@ -626,75 +626,17 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return targetDir + fileName
|
return targetDir + fileName
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun renameMultiple(
|
override suspend fun renameSingle(
|
||||||
activity: Activity,
|
|
||||||
entriesToNewName: Map<AvesEntry, String>,
|
|
||||||
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(
|
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
oldMediaUri: Uri,
|
oldMediaUri: Uri,
|
||||||
oldPath: String,
|
oldPath: String,
|
||||||
desiredName: String,
|
newFile: File,
|
||||||
): FieldMap {
|
): FieldMap = when {
|
||||||
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)
|
StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile)
|
||||||
isMediaUriPermissionGranted(activity, oldMediaUri, mimeType) -> renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
|
isMediaUriPermissionGranted(activity, oldMediaUri, mimeType) -> renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
|
||||||
else -> renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
|
else -> renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun renameSingleByMediaStore(
|
private suspend fun renameSingleByMediaStore(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
|
@ -851,10 +793,12 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
try {
|
try {
|
||||||
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
val cursor = context.contentResolver.query(uri, projection, null, null, null)
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
val newFields = HashMap<String, Any?>()
|
val newFields = hashMapOf<String, Any?>(
|
||||||
newFields["uri"] = uri.toString()
|
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
|
||||||
newFields["contentId"] = uri.tryParseId()
|
"uri" to uri.toString(),
|
||||||
newFields["path"] = path
|
"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_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.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
|
5
fastlane/metadata/android/en-US/changelogs/92.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/92.txt
Normal file
|
@ -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
|
5
fastlane/metadata/android/en-US/changelogs/9201.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/9201.txt
Normal file
|
@ -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
|
|
@ -266,9 +266,9 @@ class Dependencies {
|
||||||
sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator',
|
sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Pinput',
|
name: 'Pin Code Fields',
|
||||||
license: mit,
|
license: mit,
|
||||||
sourceUrl: 'https://github.com/Tkko/Flutter_PinPut',
|
sourceUrl: 'https://github.com/adar2378/pin_code_fields',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Provider',
|
name: 'Provider',
|
||||||
|
|
|
@ -44,7 +44,18 @@ class _PasswordDialogState extends State<PasswordDialog> {
|
||||||
onSubmitted: (password) {
|
onSubmitted: (password) {
|
||||||
if (widget.needConfirmation) {
|
if (widget.needConfirmation) {
|
||||||
if (_confirming) {
|
if (_confirming) {
|
||||||
Navigator.maybeOf(context)?.pop<String>(_firstPassword == password ? password : null);
|
final match = _firstPassword == password;
|
||||||
|
Navigator.maybeOf(context)?.pop<String>(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 {
|
} else {
|
||||||
_firstPassword = password;
|
_firstPassword = password;
|
||||||
_controller.clear();
|
_controller.clear();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:pinput/pinput.dart';
|
import 'package:pin_code_fields/pin_code_fields.dart';
|
||||||
|
|
||||||
class PinDialog extends StatefulWidget {
|
class PinDialog extends StatefulWidget {
|
||||||
static const routeName = '/dialog/pin';
|
static const routeName = '/dialog/pin';
|
||||||
|
@ -19,18 +19,12 @@ class PinDialog extends StatefulWidget {
|
||||||
|
|
||||||
class _PinDialogState extends State<PinDialog> {
|
class _PinDialogState extends State<PinDialog> {
|
||||||
final _controller = TextEditingController();
|
final _controller = TextEditingController();
|
||||||
final _focusNode = FocusNode();
|
|
||||||
bool _confirming = false;
|
bool _confirming = false;
|
||||||
String? _firstPin;
|
String? _firstPin;
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return AvesDialog(
|
return AvesDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
@ -38,24 +32,48 @@ class _PinDialogState extends State<PinDialog> {
|
||||||
Text(_confirming ? context.l10n.pinDialogConfirm : context.l10n.pinDialogEnter),
|
Text(_confirming ? context.l10n.pinDialogConfirm : context.l10n.pinDialogEnter),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
child: Pinput(
|
child: PinCodeTextField(
|
||||||
|
appContext: context,
|
||||||
|
length: 4,
|
||||||
|
controller: _controller,
|
||||||
|
obscureText: true,
|
||||||
|
onChanged: (v) {},
|
||||||
onCompleted: (pin) {
|
onCompleted: (pin) {
|
||||||
if (widget.needConfirmation) {
|
if (widget.needConfirmation) {
|
||||||
if (_confirming) {
|
if (_confirming) {
|
||||||
Navigator.maybeOf(context)?.pop<String>(_firstPin == pin ? pin : null);
|
final match = _firstPin == pin;
|
||||||
|
Navigator.maybeOf(context)?.pop<String>(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 {
|
} else {
|
||||||
_firstPin = pin;
|
_firstPin = pin;
|
||||||
_controller.clear();
|
_controller.clear();
|
||||||
setState(() => _confirming = true);
|
setState(() => _confirming = true);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _focusNode.requestFocus());
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Navigator.maybeOf(context)?.pop<String>(pin);
|
Navigator.maybeOf(context)?.pop<String>(pin);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller: _controller,
|
animationType: AnimationType.scale,
|
||||||
focusNode: _focusNode,
|
keyboardType: TextInputType.number,
|
||||||
obscureText: true,
|
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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -466,14 +466,16 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
if (!_overlayVisible.value) {
|
if (!_overlayVisible.value) {
|
||||||
_overlayVisible.value = true;
|
_overlayVisible.value = true;
|
||||||
}
|
}
|
||||||
|
} else if (notification is PopVisualNotification) {
|
||||||
|
_popVisual();
|
||||||
} else if (notification is ShowInfoPageNotification) {
|
} else if (notification is ShowInfoPageNotification) {
|
||||||
_goToVerticalPage(infoPage);
|
_goToVerticalPage(infoPage);
|
||||||
} else if (notification is JumpToPreviousEntryNotification) {
|
} else if (notification is ShowPreviousEntryNotification) {
|
||||||
_jumpToHorizontalPageByDelta(-1);
|
_goToHorizontalPageByDelta(delta: -1, animate: notification.animate);
|
||||||
} else if (notification is JumpToNextEntryNotification) {
|
} else if (notification is ShowNextEntryNotification) {
|
||||||
_jumpToHorizontalPageByDelta(1);
|
_goToHorizontalPageByDelta(delta: 1, animate: notification.animate);
|
||||||
} else if (notification is JumpToEntryNotification) {
|
} else if (notification is ShowEntryNotification) {
|
||||||
_jumpToHorizontalPageByIndex(notification.index);
|
_goToHorizontalPageByIndex(page: notification.index, animate: notification.animate);
|
||||||
} else if (notification is VideoActionNotification) {
|
} else if (notification is VideoActionNotification) {
|
||||||
final controller = notification.controller;
|
final controller = notification.controller;
|
||||||
final action = notification.action;
|
final action = notification.action;
|
||||||
|
@ -545,23 +547,33 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _jumpToHorizontalPageByDelta(int delta) {
|
void _goToHorizontalPageByDelta({required int delta, required bool animate}) {
|
||||||
if (_horizontalPager.positions.isEmpty) return;
|
if (_horizontalPager.positions.isEmpty) return;
|
||||||
|
|
||||||
final page = _horizontalPager.page?.round();
|
final page = _horizontalPager.page?.round();
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
_jumpToHorizontalPageByIndex(page + delta);
|
_goToHorizontalPageByIndex(page: page + delta, animate: animate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _jumpToHorizontalPageByIndex(int target) {
|
Future<void> _goToHorizontalPageByIndex({required int page, required bool animate}) async {
|
||||||
final _collection = collection;
|
final _collection = collection;
|
||||||
if (_collection != null) {
|
if (_collection != null) {
|
||||||
if (!widget.viewerController.repeat) {
|
if (!widget.viewerController.repeat) {
|
||||||
target = target.clamp(0, _collection.entryCount - 1);
|
page = page.clamp(0, _collection.entryCount - 1);
|
||||||
|
}
|
||||||
|
if (_currentEntryIndex != page) {
|
||||||
|
final animationDuration = animate ? context.read<DurationsData>().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);
|
||||||
}
|
}
|
||||||
if (_currentEntryIndex != target) {
|
|
||||||
_horizontalPager.jumpToPage(target);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import 'package:aves/widgets/viewer/video/controller.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
class PopVisualNotification extends Notification {}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class ShowImageNotification extends Notification {}
|
class ShowImageNotification extends Notification {}
|
||||||
|
|
||||||
|
@ -13,10 +16,29 @@ class ShowImageNotification extends Notification {}
|
||||||
class ShowInfoPageNotification extends Notification {}
|
class ShowInfoPageNotification extends Notification {}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class TvShowLessInfoNotification extends Notification {}
|
class ShowPreviousEntryNotification extends Notification {
|
||||||
|
final bool animate;
|
||||||
|
|
||||||
|
const ShowPreviousEntryNotification({required this.animate});
|
||||||
|
}
|
||||||
|
|
||||||
@immutable
|
@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
|
@immutable
|
||||||
class ToggleOverlayNotification extends Notification {
|
class ToggleOverlayNotification extends Notification {
|
||||||
|
@ -26,17 +48,10 @@ class ToggleOverlayNotification extends Notification {
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class JumpToPreviousEntryNotification extends Notification {}
|
class TvShowLessInfoNotification extends Notification {}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class JumpToNextEntryNotification extends Notification {}
|
class TvShowMoreInfoNotification extends Notification {}
|
||||||
|
|
||||||
@immutable
|
|
||||||
class JumpToEntryNotification extends Notification {
|
|
||||||
final int index;
|
|
||||||
|
|
||||||
const JumpToEntryNotification({required this.index});
|
|
||||||
}
|
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class VideoActionNotification extends Notification {
|
class VideoActionNotification extends Notification {
|
||||||
|
|
|
@ -60,13 +60,13 @@ class _ViewerThumbnailPreviewState extends State<ViewerThumbnailPreview> {
|
||||||
entryCount: entryCount,
|
entryCount: entryCount,
|
||||||
entryBuilder: (index) => 0 <= index && index < entryCount ? entries[index] : null,
|
entryBuilder: (index) => 0 <= index && index < entryCount ? entries[index] : null,
|
||||||
indexNotifier: _entryIndexNotifier,
|
indexNotifier: _entryIndexNotifier,
|
||||||
onTap: (index) => JumpToEntryNotification(index: index).dispatch(context),
|
onTap: (index) => ShowEntryNotification(animate: false, index: index).dispatch(context),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScrollerIndexChanged() => _debouncer(() {
|
void _onScrollerIndexChanged() => _debouncer(() {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
JumpToEntryNotification(index: _entryIndexNotifier.value).dispatch(context);
|
ShowEntryNotification(animate: false, index: _entryIndexNotifier.value).dispatch(context);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -401,20 +401,38 @@ class _EntryPageViewState extends State<EntryPageView> with SingleTickerProvider
|
||||||
onScaleStart: onScaleStart,
|
onScaleStart: onScaleStart,
|
||||||
onScaleUpdate: onScaleUpdate,
|
onScaleUpdate: onScaleUpdate,
|
||||||
onScaleEnd: onScaleEnd,
|
onScaleEnd: onScaleEnd,
|
||||||
|
onFling: _onFling,
|
||||||
onTap: (c, s, a, p) => _onTap(alignment: a),
|
onTap: (c, s, a, p) => _onTap(alignment: a),
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
child: child,
|
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}) {
|
void _onTap({Alignment? alignment}) {
|
||||||
if (settings.viewerGestureSideTapNext && alignment != null) {
|
if (settings.viewerGestureSideTapNext && alignment != null) {
|
||||||
final x = alignment.x;
|
final x = alignment.x;
|
||||||
if (x < sideRatio) {
|
if (x < sideRatio) {
|
||||||
JumpToPreviousEntryNotification().dispatch(context);
|
const ShowPreviousEntryNotification(animate: false).dispatch(context);
|
||||||
return;
|
return;
|
||||||
} else if (x > 1 - sideRatio) {
|
} else if (x > 1 - sideRatio) {
|
||||||
JumpToNextEntryNotification().dispatch(context);
|
const ShowNextEntryNotification(animate: false).dispatch(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,9 +138,9 @@ mixin AvesMagnifierControllerDelegate on State<MagnifierCore> {
|
||||||
controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint);
|
controller.setScaleState(nextScaleState, source, childFocalPoint: childFocalPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
CornersRange cornersX({double? scale}) {
|
EdgeRange getXEdges({double? scale}) {
|
||||||
final boundaries = scaleBoundaries;
|
final boundaries = scaleBoundaries;
|
||||||
if (boundaries == null) return const CornersRange(0, 0);
|
if (boundaries == null) return const EdgeRange(0, 0);
|
||||||
|
|
||||||
final _scale = scale ?? this.scale!;
|
final _scale = scale ?? this.scale!;
|
||||||
|
|
||||||
|
@ -152,12 +152,12 @@ mixin AvesMagnifierControllerDelegate on State<MagnifierCore> {
|
||||||
|
|
||||||
final minX = ((positionX - 1).abs() / 2) * widthDiff * -1;
|
final minX = ((positionX - 1).abs() / 2) * widthDiff * -1;
|
||||||
final maxX = ((positionX + 1).abs() / 2) * widthDiff;
|
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;
|
final boundaries = scaleBoundaries;
|
||||||
if (boundaries == null) return const CornersRange(0, 0);
|
if (boundaries == null) return const EdgeRange(0, 0);
|
||||||
|
|
||||||
final _scale = scale ?? this.scale!;
|
final _scale = scale ?? this.scale!;
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ mixin AvesMagnifierControllerDelegate on State<MagnifierCore> {
|
||||||
|
|
||||||
final minY = ((positionY - 1).abs() / 2) * heightDiff * -1;
|
final minY = ((positionY - 1).abs() / 2) * heightDiff * -1;
|
||||||
final maxY = ((positionY + 1).abs() / 2) * heightDiff;
|
final maxY = ((positionY + 1).abs() / 2) * heightDiff;
|
||||||
return CornersRange(minY, maxY);
|
return EdgeRange(minY, maxY);
|
||||||
}
|
}
|
||||||
|
|
||||||
Offset clampPosition({Offset? position, double? scale}) {
|
Offset clampPosition({Offset? position, double? scale}) {
|
||||||
|
@ -187,14 +187,14 @@ mixin AvesMagnifierControllerDelegate on State<MagnifierCore> {
|
||||||
|
|
||||||
var finalX = 0.0;
|
var finalX = 0.0;
|
||||||
if (screenWidth < computedWidth) {
|
if (screenWidth < computedWidth) {
|
||||||
final cornersX = this.cornersX(scale: _scale);
|
final range = getXEdges(scale: _scale);
|
||||||
finalX = _position.dx.clamp(cornersX.min, cornersX.max);
|
finalX = _position.dx.clamp(range.min, range.max);
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalY = 0.0;
|
var finalY = 0.0;
|
||||||
if (screenHeight < computedHeight) {
|
if (screenHeight < computedHeight) {
|
||||||
final cornersY = this.cornersY(scale: _scale);
|
final range = getYEdges(scale: _scale);
|
||||||
finalY = _position.dy.clamp(cornersY.min, cornersY.max);
|
finalY = _position.dy.clamp(range.min, range.max);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Offset(finalX, finalY);
|
return Offset(finalX, finalY);
|
||||||
|
@ -202,8 +202,8 @@ mixin AvesMagnifierControllerDelegate on State<MagnifierCore> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple class to store a min and a max value
|
/// Simple class to store a min and a max value
|
||||||
class CornersRange {
|
class EdgeRange {
|
||||||
const CornersRange(this.min, this.max);
|
const EdgeRange(this.min, this.max);
|
||||||
|
|
||||||
final double min;
|
final double min;
|
||||||
final double max;
|
final double max;
|
||||||
|
|
|
@ -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/controller/state.dart';
|
||||||
import 'package:aves_magnifier/src/core/gesture_detector.dart';
|
import 'package:aves_magnifier/src/core/gesture_detector.dart';
|
||||||
import 'package:aves_magnifier/src/magnifier.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/scale_boundaries.dart';
|
||||||
import 'package:aves_magnifier/src/scale/state.dart';
|
import 'package:aves_magnifier/src/scale/state.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/widgets.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
|
/// Internal widget in which controls all animations lifecycle, core responses
|
||||||
/// to user gestures, updates to the controller state and mounts the entire Layout
|
/// 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 MagnifierGestureScaleStartCallback? onScaleStart;
|
||||||
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
|
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
|
||||||
final MagnifierGestureScaleEndCallback? onScaleEnd;
|
final MagnifierGestureScaleEndCallback? onScaleEnd;
|
||||||
|
final MagnifierGestureFlingCallback? onFling;
|
||||||
final MagnifierTapCallback? onTap;
|
final MagnifierTapCallback? onTap;
|
||||||
final MagnifierDoubleTapCallback? onDoubleTap;
|
final MagnifierDoubleTapCallback? onDoubleTap;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -34,6 +38,7 @@ class MagnifierCore extends StatefulWidget {
|
||||||
this.onScaleStart,
|
this.onScaleStart,
|
||||||
this.onScaleUpdate,
|
this.onScaleUpdate,
|
||||||
this.onScaleEnd,
|
this.onScaleEnd,
|
||||||
|
this.onFling,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
@ -43,7 +48,7 @@ class MagnifierCore extends StatefulWidget {
|
||||||
State<StatefulWidget> createState() => _MagnifierCoreState();
|
State<StatefulWidget> createState() => _MagnifierCoreState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, CornerHitDetector {
|
class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateMixin, AvesMagnifierControllerDelegate, EdgeHitDetector {
|
||||||
Offset? _startFocalPoint, _lastViewportFocalPosition;
|
Offset? _startFocalPoint, _lastViewportFocalPosition;
|
||||||
double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
|
double? _startScale, _quickScaleLastY, _quickScaleLastDistance;
|
||||||
late bool _dropped, _doubleTap, _quickScaleMoved;
|
late bool _dropped, _doubleTap, _quickScaleMoved;
|
||||||
|
@ -57,6 +62,8 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
|
|
||||||
ScaleBoundaries? cachedScaleBoundaries;
|
ScaleBoundaries? cachedScaleBoundaries;
|
||||||
|
|
||||||
|
static const _flingPointerKind = PointerDeviceKind.unknown;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -104,12 +111,21 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
controller.update(position: _positionAnimation.value, source: ChangeSource.animation);
|
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) {
|
void onScaleStart(ScaleStartDetails details, bool doubleTap) {
|
||||||
final boundaries = scaleBoundaries;
|
final boundaries = scaleBoundaries;
|
||||||
if (boundaries == null) return;
|
if (boundaries == null) return;
|
||||||
|
|
||||||
widget.onScaleStart?.call(details, doubleTap, boundaries);
|
widget.onScaleStart?.call(details, doubleTap, boundaries);
|
||||||
|
|
||||||
|
_scaleStopwatch = Stopwatch()..start();
|
||||||
|
_velocityTracker = VelocityTracker.withKind(_flingPointerKind);
|
||||||
|
_mayFlingLTRB = const Tuple4(true, true, true, true);
|
||||||
|
_updateMayFling();
|
||||||
|
|
||||||
_startScale = scale;
|
_startScale = scale;
|
||||||
_startFocalPoint = details.localFocalPoint;
|
_startFocalPoint = details.localFocalPoint;
|
||||||
_lastViewportFocalPosition = _startFocalPoint;
|
_lastViewportFocalPosition = _startFocalPoint;
|
||||||
|
@ -130,6 +146,12 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
_dropped |= widget.onScaleUpdate?.call(details) ?? false;
|
_dropped |= widget.onScaleUpdate?.call(details) ?? false;
|
||||||
if (_dropped) return;
|
if (_dropped) return;
|
||||||
|
|
||||||
|
final elapsed = _scaleStopwatch?.elapsed;
|
||||||
|
if (elapsed != null) {
|
||||||
|
_velocityTracker?.addPosition(elapsed, details.focalPoint);
|
||||||
|
}
|
||||||
|
_updateMayFling();
|
||||||
|
|
||||||
double newScale;
|
double newScale;
|
||||||
if (_doubleTap) {
|
if (_doubleTap) {
|
||||||
// quick scale, aka one finger zoom
|
// quick scale, aka one finger zoom
|
||||||
|
@ -168,6 +190,29 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
||||||
|
|
||||||
widget.onScaleEnd?.call(details);
|
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 _position = controller.position;
|
||||||
final _scale = controller.scale!;
|
final _scale = controller.scale!;
|
||||||
final maxScale = boundaries.maxScale;
|
final maxScale = boundaries.maxScale;
|
||||||
|
@ -208,6 +253,31 @@ class _MagnifierCoreState extends State<MagnifierCore> 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<MediaQueryData>().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({
|
Duration _getAnimationDurationForVelocity({
|
||||||
required Cubic curve,
|
required Cubic curve,
|
||||||
required Tween<Offset> tween,
|
required Tween<Offset> tween,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves_magnifier/src/core/scale_gesture_recognizer.dart';
|
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:aves_magnifier/src/pan/gesture_detector_scope.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
@ -19,7 +19,7 @@ class MagnifierGestureDetector extends StatefulWidget {
|
||||||
this.child,
|
this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
final CornerHitDetector hitDetector;
|
final EdgeHitDetector hitDetector;
|
||||||
final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart;
|
final void Function(ScaleStartDetails details, bool doubleTap)? onScaleStart;
|
||||||
final GestureScaleUpdateCallback? onScaleUpdate;
|
final GestureScaleUpdateCallback? onScaleUpdate;
|
||||||
final GestureScaleEndCallback? onScaleEnd;
|
final GestureScaleEndCallback? onScaleEnd;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves_magnifier/aves_magnifier.dart';
|
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/gestures.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
final CornerHitDetector hitDetector;
|
final EdgeHitDetector hitDetector;
|
||||||
final MagnifierGestureDetectorScope scope;
|
final MagnifierGestureDetectorScope scope;
|
||||||
final ValueNotifier<TapDownDetails?> doubleTapDetails;
|
final ValueNotifier<TapDownDetails?> doubleTapDetails;
|
||||||
|
|
||||||
|
@ -104,14 +104,15 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
}
|
}
|
||||||
|
|
||||||
final validateAxis = scope.axis;
|
final validateAxis = scope.axis;
|
||||||
|
final canFling = scope.escapeByFling;
|
||||||
final move = _initialFocalPoint! - _currentFocalPoint!;
|
final move = _initialFocalPoint! - _currentFocalPoint!;
|
||||||
bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false;
|
bool shouldMove = scope.acceptPointerEvent?.call(move) ?? false;
|
||||||
|
|
||||||
if (!shouldMove) {
|
if (!shouldMove) {
|
||||||
if (validateAxis.length == 2) {
|
if (validateAxis.length == 2) {
|
||||||
// the image is the descendant of gesture detector(s) handling drag in both directions
|
// the image is the descendant of gesture detector(s) handling drag in both directions
|
||||||
final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move);
|
final shouldMoveX = validateAxis.contains(Axis.horizontal) && hitDetector.shouldMoveX(move, canFling);
|
||||||
final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move);
|
final shouldMoveY = validateAxis.contains(Axis.vertical) && hitDetector.shouldMoveY(move, canFling);
|
||||||
if (shouldMoveX == shouldMoveY) {
|
if (shouldMoveX == shouldMoveY) {
|
||||||
// consistently can/cannot pan the image in both direction the same way
|
// consistently can/cannot pan the image in both direction the same way
|
||||||
shouldMove = shouldMoveX;
|
shouldMove = shouldMoveX;
|
||||||
|
@ -122,7 +123,7 @@ class MagnifierGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// the image is the descendant of a gesture detector handling drag in one direction
|
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ class AvesMagnifier extends StatelessWidget {
|
||||||
this.onScaleStart,
|
this.onScaleStart,
|
||||||
this.onScaleUpdate,
|
this.onScaleUpdate,
|
||||||
this.onScaleEnd,
|
this.onScaleEnd,
|
||||||
|
this.onFling,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
@ -58,6 +59,7 @@ class AvesMagnifier extends StatelessWidget {
|
||||||
final MagnifierGestureScaleStartCallback? onScaleStart;
|
final MagnifierGestureScaleStartCallback? onScaleStart;
|
||||||
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
|
final MagnifierGestureScaleUpdateCallback? onScaleUpdate;
|
||||||
final MagnifierGestureScaleEndCallback? onScaleEnd;
|
final MagnifierGestureScaleEndCallback? onScaleEnd;
|
||||||
|
final MagnifierGestureFlingCallback? onFling;
|
||||||
final MagnifierTapCallback? onTap;
|
final MagnifierTapCallback? onTap;
|
||||||
final MagnifierDoubleTapCallback? onDoubleTap;
|
final MagnifierDoubleTapCallback? onDoubleTap;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -82,6 +84,7 @@ class AvesMagnifier extends StatelessWidget {
|
||||||
onScaleStart: onScaleStart,
|
onScaleStart: onScaleStart,
|
||||||
onScaleUpdate: onScaleUpdate,
|
onScaleUpdate: onScaleUpdate,
|
||||||
onScaleEnd: onScaleEnd,
|
onScaleEnd: onScaleEnd,
|
||||||
|
onFling: onFling,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onDoubleTap: onDoubleTap,
|
onDoubleTap: onDoubleTap,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -101,3 +104,4 @@ typedef MagnifierDoubleTapCallback = bool Function(Alignment alignment);
|
||||||
typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
|
typedef MagnifierGestureScaleStartCallback = void Function(ScaleStartDetails details, bool doubleTap, ScaleBoundaries boundaries);
|
||||||
typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details);
|
typedef MagnifierGestureScaleUpdateCallback = bool Function(ScaleUpdateDetails details);
|
||||||
typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details);
|
typedef MagnifierGestureScaleEndCallback = void Function(ScaleEndDetails details);
|
||||||
|
typedef MagnifierGestureFlingCallback = void Function(AxisDirection direction);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
68
plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart
Normal file
68
plugins/aves_magnifier/lib/src/pan/edge_hit_detector.dart
Normal file
|
@ -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;
|
||||||
|
}
|
|
@ -14,12 +14,18 @@ class MagnifierGestureDetectorScope extends InheritedWidget {
|
||||||
// <1: less reactive but gives the most leeway to other recognizers
|
// <1: less reactive but gives the most leeway to other recognizers
|
||||||
// 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree
|
// 1: will not be able to compete with a `HorizontalDragGestureRecognizer` up the widget tree
|
||||||
final double touchSlopFactor;
|
final double touchSlopFactor;
|
||||||
|
|
||||||
|
// 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;
|
final bool? Function(Offset move)? acceptPointerEvent;
|
||||||
|
|
||||||
const MagnifierGestureDetectorScope({
|
const MagnifierGestureDetectorScope({
|
||||||
super.key,
|
super.key,
|
||||||
required this.axis,
|
required this.axis,
|
||||||
this.touchSlopFactor = .8,
|
this.touchSlopFactor = .8,
|
||||||
|
this.escapeByFling = true,
|
||||||
this.acceptPointerEvent,
|
this.acceptPointerEvent,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) : super(child: child);
|
}) : super(child: child);
|
||||||
|
|
|
@ -91,6 +91,14 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -10,6 +10,7 @@ dependencies:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
equatable:
|
equatable:
|
||||||
provider:
|
provider:
|
||||||
|
tuple:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_lints:
|
flutter_lints:
|
||||||
|
|
24
pubspec.lock
24
pubspec.lock
|
@ -917,14 +917,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.1.0"
|
||||||
pinput:
|
pin_code_fields:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: pinput
|
name: pin_code_fields
|
||||||
sha256: e6aabd1571dde622f9b942f62ac2c80f84b0b50f95fa209a93e78f7d621e1f82
|
sha256: c8652519d14688f3fe2a8288d86910a46aa0b9046d728f292d3bf6067c31b4c7
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.23"
|
version: "7.4.0"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1162,14 +1162,6 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
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:
|
smooth_page_indicator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1339,14 +1331,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.1"
|
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:
|
url_launcher:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves
|
||||||
# - play changelog: /whatsnew/whatsnew-en-US
|
# - play changelog: /whatsnew/whatsnew-en-US
|
||||||
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt
|
# - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt
|
||||||
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt
|
# - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt
|
||||||
version: 1.8.0+91
|
version: 1.8.1+92
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
@ -83,7 +83,7 @@ dependencies:
|
||||||
pdf:
|
pdf:
|
||||||
percent_indicator:
|
percent_indicator:
|
||||||
permission_handler:
|
permission_handler:
|
||||||
pinput:
|
pin_code_fields:
|
||||||
printing:
|
printing:
|
||||||
proj4dart:
|
proj4dart:
|
||||||
provider:
|
provider:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
In v1.8.0:
|
In v1.8.1:
|
||||||
- Android TV support (cont'd)
|
- Android TV support (cont'd)
|
||||||
- hide your secrets in vaults
|
- hide your secrets in vaults
|
||||||
- enjoy the app in Basque
|
- enjoy the app in Basque
|
||||||
|
|
Loading…
Reference in a new issue