Merge branch 'develop'
This commit is contained in:
commit
fd92d1b27f
98 changed files with 2657 additions and 842 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit 9944297138845a94256f1cf37beb88ff9a8e811a
|
||||
Subproject commit c07f7888888435fd9df505aa2efc38d3cf65681b
|
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
## <a id="v1.8.2"></a>[v1.8.2] - 2023-02-28
|
||||
|
||||
### Added
|
||||
|
||||
- Export: bulk converting
|
||||
- Export: write metadata when converting
|
||||
- Places: page & navigation entry
|
||||
|
||||
### Changed
|
||||
|
||||
- rating/tagging action icons
|
||||
- upgraded Flutter to stable v3.7.5
|
||||
|
||||
### Fixed
|
||||
|
||||
- viewer pan/scale gestures interpreted as fling gestures
|
||||
- replacing when moving item to vault
|
||||
- exporting item to vault
|
||||
|
||||
## <a id="v1.8.1"></a>[v1.8.1] - 2023-02-21
|
||||
|
||||
### Added
|
||||
|
|
|
@ -188,12 +188,12 @@ dependencies {
|
|||
implementation 'androidx.lifecycle:lifecycle-process:2.5.1'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha04'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha05'
|
||||
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.commonsware.cwac:document:0.5.0'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.14.2'
|
||||
implementation 'com.github.bumptech.glide:glide:4.15.0'
|
||||
// SLF4J implementation for `mp4parser`
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.6'
|
||||
|
||||
|
@ -207,10 +207,10 @@ dependencies {
|
|||
implementation 'com.github.deckerst:pixymeta-android:706bd73d6e'
|
||||
|
||||
// huawei flavor only
|
||||
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.7.2.300'
|
||||
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.8.0.300'
|
||||
|
||||
kapt 'androidx.annotation:annotation:1.5.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.14.2'
|
||||
kapt 'androidx.annotation:annotation:1.6.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.15.0'
|
||||
|
||||
compileOnly rootProject.findProject(':streams_channel')
|
||||
}
|
||||
|
|
|
@ -224,7 +224,7 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
|
||||
<service
|
||||
android:name=".MediaPlaybackService"
|
||||
android:exported="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
|
|
|
@ -50,7 +50,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
when (op) {
|
||||
"delete" -> ioScope.launch { delete() }
|
||||
"export" -> ioScope.launch { export() }
|
||||
"convert" -> ioScope.launch { convert() }
|
||||
"move" -> ioScope.launch { move() }
|
||||
"rename" -> ioScope.launch { rename() }
|
||||
else -> endOfStream()
|
||||
|
@ -92,19 +92,6 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
}
|
||||
|
||||
private suspend fun delete() {
|
||||
if (entryMapList.isEmpty()) {
|
||||
endOfStream()
|
||||
return
|
||||
}
|
||||
|
||||
// assume same provider for all entries
|
||||
val firstEntry = entryMapList.first()
|
||||
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) }
|
||||
if (provider == null) {
|
||||
error("delete-provider", "failed to find provider for entry=$firstEntry", null)
|
||||
return
|
||||
}
|
||||
|
||||
val entries = entryMapList.map(::AvesEntry)
|
||||
for (entry in entries) {
|
||||
val mimeType = entry.mimeType
|
||||
|
@ -119,12 +106,14 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
if (isCancelledOp()) {
|
||||
result["skipped"] = true
|
||||
} else {
|
||||
try {
|
||||
provider.delete(activity, uri, path, mimeType)
|
||||
result["success"] = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
|
||||
result["success"] = false
|
||||
result["success"] = false
|
||||
getProvider(uri)?.let { provider ->
|
||||
try {
|
||||
provider.delete(activity, uri, path, mimeType)
|
||||
result["success"] = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
success(result)
|
||||
|
@ -132,7 +121,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
endOfStream()
|
||||
}
|
||||
|
||||
private suspend fun export() {
|
||||
private suspend fun convert() {
|
||||
if (arguments !is Map<*, *> || entryMapList.isEmpty()) {
|
||||
endOfStream()
|
||||
return
|
||||
|
@ -140,11 +129,13 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
var destinationDir = arguments["destinationPath"] as String?
|
||||
val mimeType = arguments["mimeType"] as String?
|
||||
val lengthUnit = arguments["lengthUnit"] as String?
|
||||
val width = (arguments["width"] as Number?)?.toInt()
|
||||
val height = (arguments["height"] as Number?)?.toInt()
|
||||
val writeMetadata = arguments["writeMetadata"] as Boolean?
|
||||
val nameConflictStrategy = NameConflictStrategy.get(arguments["nameConflictStrategy"] as String?)
|
||||
if (destinationDir == null || mimeType == null || width == null || height == null || nameConflictStrategy == null) {
|
||||
error("export-args", "missing arguments", null)
|
||||
if (destinationDir == null || mimeType == null || lengthUnit == null || width == null || height == null || writeMetadata == null || nameConflictStrategy == null) {
|
||||
error("convert-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -152,16 +143,27 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
val firstEntry = entryMapList.first()
|
||||
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) }
|
||||
if (provider == null) {
|
||||
error("export-provider", "failed to find provider for entry=$firstEntry", null)
|
||||
error("convert-provider", "failed to find provider for entry=$firstEntry", null)
|
||||
return
|
||||
}
|
||||
|
||||
destinationDir = ensureTrailingSeparator(destinationDir)
|
||||
val entries = entryMapList.map(::AvesEntry)
|
||||
provider.exportMultiple(activity, mimeType, destinationDir, entries, width, height, nameConflictStrategy, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("export-failure", "failed to export entries", throwable)
|
||||
})
|
||||
provider.convertMultiple(
|
||||
activity = activity,
|
||||
imageExportMimeType = mimeType,
|
||||
targetDir = destinationDir,
|
||||
entries = entries,
|
||||
lengthUnit = lengthUnit,
|
||||
width = width,
|
||||
height = height,
|
||||
writeMetadata = writeMetadata,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
callback = object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("convert-failure", "failed to convert entries", throwable)
|
||||
},
|
||||
)
|
||||
endOfStream()
|
||||
}
|
||||
|
||||
|
@ -193,10 +195,17 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
// always use Media Store (as we move from or to it)
|
||||
val provider = MediaStoreImageProvider()
|
||||
|
||||
provider.moveMultiple(activity, copy, nameConflictStrategy, entriesByTargetDir, ::isCancelledOp, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable)
|
||||
})
|
||||
provider.moveMultiple(
|
||||
activity = activity,
|
||||
copy = copy,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
entriesByTargetDir = entriesByTargetDir,
|
||||
isCancelledOp = ::isCancelledOp,
|
||||
callback = object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable)
|
||||
},
|
||||
)
|
||||
endOfStream()
|
||||
}
|
||||
|
||||
|
@ -228,10 +237,15 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
}
|
||||
|
||||
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 = activity,
|
||||
entriesToNewName = entryMap,
|
||||
isCancelledOp = ::isCancelledOp,
|
||||
callback = object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
endOfStream()
|
||||
|
|
|
@ -15,6 +15,9 @@ class AvesEntry(map: FieldMap) {
|
|||
val trashed = map["trashed"] as Boolean
|
||||
val trashPath = map["trashPath"] as String?
|
||||
|
||||
val isRotated: Boolean
|
||||
get() = rotationDegrees % 180 == 90
|
||||
|
||||
companion object {
|
||||
// convenience method
|
||||
private fun toLong(o: Any?): Long? = when (o) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context
|
|||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.SourceEntry
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -12,12 +13,19 @@ import java.io.File
|
|||
|
||||
internal class FileImageProvider : ImageProvider() {
|
||||
override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) {
|
||||
if (sourceMimeType == null) {
|
||||
callback.onFailure(Exception("MIME type is null for uri=$uri"))
|
||||
return
|
||||
val mimeType = if (sourceMimeType != null) {
|
||||
sourceMimeType
|
||||
} else {
|
||||
val fromExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
||||
if (fromExtension != null) {
|
||||
fromExtension
|
||||
} else {
|
||||
callback.onFailure(Exception("MIME type was not provided and cannot be guessed from extension of uri=$uri"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, sourceMimeType)
|
||||
val entry = SourceEntry(SourceEntry.ORIGIN_FILE, uri, mimeType)
|
||||
|
||||
val path = uri.path
|
||||
if (path != null) {
|
||||
|
@ -45,13 +53,15 @@ internal class FileImageProvider : ImageProvider() {
|
|||
}
|
||||
|
||||
override suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
|
||||
val file = File(File(uri.path!!).path)
|
||||
if (!file.exists()) return
|
||||
path ?: throw Exception("failed to delete file because path is null")
|
||||
|
||||
Log.d(LOG_TAG, "delete file at uri=$uri")
|
||||
if (file.delete()) return
|
||||
|
||||
throw Exception("failed to delete entry with uri=$uri path=$path")
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
Log.d(LOG_TAG, "delete file at path=$path")
|
||||
if (!file.delete()) {
|
||||
throw Exception("failed to delete entry with uri=$uri path=$path")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun renameSingle(
|
||||
|
|
|
@ -31,19 +31,20 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper.updateRotation
|
|||
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateXmp
|
||||
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
|
||||
import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
|
||||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.ExifOrientationOp
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.NameConflictStrategy
|
||||
import deckers.thibault.aves.model.*
|
||||
import deckers.thibault.aves.utils.*
|
||||
import deckers.thibault.aves.utils.FileUtils.transferFrom
|
||||
import deckers.thibault.aves.utils.FileUtils.transferTo
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditExif
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditIptc
|
||||
import deckers.thibault.aves.utils.MimeTypes.canEditXmp
|
||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
|
||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta
|
||||
import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata
|
||||
import deckers.thibault.aves.utils.MimeTypes.extensionFor
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import pixy.meta.meta.Metadata
|
||||
import pixy.meta.meta.MetadataType
|
||||
import java.io.*
|
||||
import java.nio.channels.Channels
|
||||
import java.util.*
|
||||
|
@ -53,6 +54,35 @@ abstract class ImageProvider {
|
|||
callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider"))
|
||||
}
|
||||
|
||||
suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap {
|
||||
return if (StorageUtils.isInVault(context, path)) {
|
||||
val uri = Uri.fromFile(File(path))
|
||||
hashMapOf(
|
||||
"origin" to SourceEntry.ORIGIN_VAULT,
|
||||
"uri" to uri.toString(),
|
||||
"contentId" to null,
|
||||
"path" to path,
|
||||
)
|
||||
} else {
|
||||
MediaStoreImageProvider().scanNewPathByMediaStore(context, path, mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deletePath(contextWrapper: ContextWrapper, path: String, mimeType: String) {
|
||||
if (StorageUtils.isInVault(contextWrapper, path)) {
|
||||
FileImageProvider().apply {
|
||||
val uri = Uri.fromFile(File(path))
|
||||
delete(contextWrapper, uri, path, mimeType)
|
||||
}
|
||||
} else {
|
||||
MediaStoreImageProvider().apply {
|
||||
val uri = getContentUriForPath(contextWrapper, path)
|
||||
uri ?: throw Exception("failed to find content URI for path=$path")
|
||||
delete(contextWrapper, uri, path, mimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun delete(contextWrapper: ContextWrapper, uri: Uri, path: String?, mimeType: String) {
|
||||
throw UnsupportedOperationException("`delete` is not supported by this image provider")
|
||||
}
|
||||
|
@ -143,13 +173,15 @@ abstract class ImageProvider {
|
|||
throw UnsupportedOperationException("`scanPostMetadataEdit` is not supported by this image provider")
|
||||
}
|
||||
|
||||
suspend fun exportMultiple(
|
||||
suspend fun convertMultiple(
|
||||
activity: Activity,
|
||||
imageExportMimeType: String,
|
||||
targetDir: String,
|
||||
entries: List<AvesEntry>,
|
||||
lengthUnit: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
writeMetadata: Boolean,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
|
@ -177,32 +209,36 @@ abstract class ImageProvider {
|
|||
val sourceMimeType = entry.mimeType
|
||||
val exportMimeType = if (isVideo(sourceMimeType)) sourceMimeType else imageExportMimeType
|
||||
try {
|
||||
val newFields = exportSingle(
|
||||
val newFields = convertSingle(
|
||||
activity = activity,
|
||||
sourceEntry = entry,
|
||||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
lengthUnit = lengthUnit,
|
||||
width = width,
|
||||
height = height,
|
||||
writeMetadata = writeMetadata,
|
||||
nameConflictStrategy = nameConflictStrategy,
|
||||
exportMimeType = exportMimeType,
|
||||
)
|
||||
result["newFields"] = newFields
|
||||
result["success"] = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to export to targetDir=$targetDir entry with sourcePath=$sourcePath pageId=$pageId", e)
|
||||
Log.w(LOG_TAG, "failed to convert to targetDir=$targetDir entry with sourcePath=$sourcePath pageId=$pageId", e)
|
||||
}
|
||||
callback.onSuccess(result)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun exportSingle(
|
||||
private suspend fun convertSingle(
|
||||
activity: Activity,
|
||||
sourceEntry: AvesEntry,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
lengthUnit: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
writeMetadata: Boolean,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
exportMimeType: String,
|
||||
): FieldMap {
|
||||
|
@ -240,6 +276,13 @@ abstract class ImageProvider {
|
|||
sourceDocFile.copyTo(output)
|
||||
}
|
||||
} else {
|
||||
var targetWidthPx: Int = if (sourceEntry.isRotated) height else width
|
||||
var targetHeightPx: Int = if (sourceEntry.isRotated) width else height
|
||||
if (lengthUnit == LENGTH_UNIT_PERCENT) {
|
||||
targetWidthPx = sourceEntry.width * targetWidthPx / 100
|
||||
targetHeightPx = sourceEntry.height * targetHeightPx / 100
|
||||
}
|
||||
|
||||
val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) {
|
||||
MultiTrackImage(activity, sourceUri, pageId)
|
||||
} else if (sourceMimeType == MimeTypes.TIFF) {
|
||||
|
@ -260,7 +303,7 @@ abstract class ImageProvider {
|
|||
.asBitmap()
|
||||
.apply(glideOptions)
|
||||
.load(model)
|
||||
.submit(width, height)
|
||||
.submit(targetWidthPx, targetHeightPx)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
var bitmap = target.get()
|
||||
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) {
|
||||
|
@ -294,8 +337,7 @@ abstract class ImageProvider {
|
|||
}
|
||||
}
|
||||
|
||||
val mediaStoreImageProvider = MediaStoreImageProvider()
|
||||
val targetPath = mediaStoreImageProvider.createSingle(
|
||||
val targetPath = MediaStoreImageProvider().createSingle(
|
||||
activity = activity,
|
||||
mimeType = targetMimeType,
|
||||
targetDir = targetDir,
|
||||
|
@ -303,13 +345,114 @@ abstract class ImageProvider {
|
|||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
write = write,
|
||||
)
|
||||
return mediaStoreImageProvider.scanNewPath(activity, targetPath, exportMimeType)
|
||||
|
||||
val newFields = scanNewPath(activity, targetPath, exportMimeType)
|
||||
val targetUri = Uri.parse(newFields["uri"] as String)
|
||||
if (writeMetadata) {
|
||||
copyMetadata(
|
||||
context = activity,
|
||||
sourceMimeType = sourceMimeType,
|
||||
sourceUri = sourceUri,
|
||||
targetMimeType = targetMimeType,
|
||||
targetUri = targetUri,
|
||||
targetPath = targetPath,
|
||||
)
|
||||
}
|
||||
|
||||
return newFields
|
||||
} finally {
|
||||
// clearing Glide target should happen after effectively writing the bitmap
|
||||
Glide.with(activity).clear(target)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyMetadata(
|
||||
context: Context,
|
||||
sourceMimeType: String,
|
||||
sourceUri: Uri,
|
||||
targetMimeType: String,
|
||||
targetUri: Uri,
|
||||
targetPath: String,
|
||||
) {
|
||||
val editableFile = File.createTempFile("aves", null).apply {
|
||||
deleteOnExit()
|
||||
// copy original file to a temporary file for editing
|
||||
val inputStream = StorageUtils.openInputStream(context, targetUri)
|
||||
transferFrom(inputStream, File(targetPath).length())
|
||||
}
|
||||
|
||||
// copy IPTC / XMP via PixyMeta
|
||||
|
||||
var pixyIptc: pixy.meta.meta.iptc.IPTC? = null
|
||||
var pixyXmp: pixy.meta.meta.xmp.XMP? = null
|
||||
if (canReadWithPixyMeta(sourceMimeType)) {
|
||||
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
|
||||
val metadata = Metadata.readMetadata(input)
|
||||
if (canEditIptc(targetMimeType)) {
|
||||
pixyIptc = metadata[MetadataType.IPTC] as pixy.meta.meta.iptc.IPTC?
|
||||
}
|
||||
if (canEditXmp(targetMimeType)) {
|
||||
pixyXmp = metadata[MetadataType.XMP] as pixy.meta.meta.xmp.XMP?
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pixyIptc != null || pixyXmp != null) {
|
||||
editableFile.outputStream().use { output ->
|
||||
if (pixyIptc != null) {
|
||||
// reopen input to read from start
|
||||
StorageUtils.openInputStream(context, targetUri)?.use { input ->
|
||||
val iptcs = pixyIptc!!.dataSets.flatMap { it.value }
|
||||
Metadata.insertIPTC(input, output, iptcs)
|
||||
}
|
||||
}
|
||||
if (pixyXmp != null) {
|
||||
// reopen input to read from start
|
||||
StorageUtils.openInputStream(context, targetUri)?.use { input ->
|
||||
val xmpString = pixyXmp!!.xmpDocString()
|
||||
val extendedXmp = if (pixyXmp!!.hasExtendedXmp()) pixyXmp!!.extendedXmpDocString() else null
|
||||
PixyMetaHelper.setXmp(input, output, xmpString, if (targetMimeType == MimeTypes.JPEG) extendedXmp else null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// copy Exif via ExifInterface
|
||||
|
||||
val exif = HashMap<String, String?>()
|
||||
val skippedTags = listOf(
|
||||
ExifInterface.TAG_IMAGE_LENGTH,
|
||||
ExifInterface.TAG_IMAGE_WIDTH,
|
||||
ExifInterface.TAG_ORIENTATION,
|
||||
// Thumbnail Offset / Length
|
||||
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
|
||||
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
|
||||
// Exif Image Width / Height
|
||||
ExifInterface.TAG_PIXEL_X_DIMENSION,
|
||||
ExifInterface.TAG_PIXEL_Y_DIMENSION,
|
||||
)
|
||||
if (canReadWithExifInterface(sourceMimeType) && canEditExif(targetMimeType)) {
|
||||
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
|
||||
ExifInterface(input).apply {
|
||||
ExifInterfaceHelper.allTags.keys.filterNot { skippedTags.contains(it) }.filter { hasAttribute(it) }.forEach { tag ->
|
||||
exif[tag] = getAttribute(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (exif.isNotEmpty()) {
|
||||
ExifInterface(editableFile).apply {
|
||||
exif.entries.forEach { (tag, value) ->
|
||||
setAttribute(tag, value)
|
||||
}
|
||||
saveAttributes()
|
||||
}
|
||||
}
|
||||
|
||||
// copy the edited temporary file back to the original
|
||||
editableFile.transferTo(outputStream(context, targetMimeType, targetUri, targetPath))
|
||||
editableFile.delete()
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun captureFrame(
|
||||
contextWrapper: ContextWrapper,
|
||||
|
@ -422,7 +565,7 @@ abstract class ImageProvider {
|
|||
|
||||
val fileName = targetDocFile.name
|
||||
val targetFullPath = targetDir + fileName
|
||||
val newFields = MediaStoreImageProvider().scanNewPath(contextWrapper, targetFullPath, captureMimeType)
|
||||
val newFields = scanNewPath(contextWrapper, targetFullPath, captureMimeType)
|
||||
callback.onSuccess(newFields)
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(e)
|
||||
|
@ -451,12 +594,7 @@ abstract class ImageProvider {
|
|||
}
|
||||
NameConflictStrategy.REPLACE -> {
|
||||
if (targetFile.exists()) {
|
||||
val path = targetFile.path
|
||||
MediaStoreImageProvider().apply {
|
||||
val uri = getContentUriForPath(contextWrapper, path)
|
||||
uri ?: throw Exception("failed to find content URI for path=$path")
|
||||
delete(contextWrapper, uri, path, mimeType)
|
||||
}
|
||||
deletePath(contextWrapper, targetFile.path, mimeType)
|
||||
}
|
||||
desiredNameWithoutExtension
|
||||
}
|
||||
|
@ -1189,6 +1327,8 @@ abstract class ImageProvider {
|
|||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<ImageProvider>()
|
||||
|
||||
private const val LENGTH_UNIT_PERCENT = "percent"
|
||||
|
||||
val supportedExportMimeTypes = listOf(MimeTypes.BMP, MimeTypes.JPEG, MimeTypes.PNG, MimeTypes.WEBP)
|
||||
|
||||
// used when skipping a move/creation op because the target file already exists
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.net.toUri
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
|
||||
|
@ -30,6 +30,7 @@ import deckers.thibault.aves.utils.UriUtils.tryParseId
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.SyncFailedException
|
||||
import java.util.*
|
||||
|
@ -474,7 +475,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
mimeType = mimeType,
|
||||
copy = copy,
|
||||
toBin = toBin,
|
||||
toVault = toVault,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -501,7 +501,6 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
mimeType: String,
|
||||
copy: Boolean,
|
||||
toBin: Boolean,
|
||||
toVault: Boolean,
|
||||
): FieldMap {
|
||||
val sourcePath = sourceFile.path
|
||||
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) }
|
||||
|
@ -550,21 +549,11 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
"trashed" to true,
|
||||
"trashPath" to targetPath,
|
||||
)
|
||||
} else if (toVault) {
|
||||
hashMapOf(
|
||||
"origin" to SourceEntry.ORIGIN_VAULT,
|
||||
"uri" to File(targetPath).toUri().toString(),
|
||||
"contentId" to null,
|
||||
"path" to targetPath,
|
||||
)
|
||||
} else {
|
||||
scanNewPath(activity, targetPath, mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
|
||||
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
|
||||
// when used with entry URI as `sourceDocumentUri`, and targetDirDocFile URI as `targetParentDocumentUri`
|
||||
fun createSingle(
|
||||
activity: Activity,
|
||||
mimeType: String,
|
||||
|
@ -573,33 +562,86 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetNameWithoutExtension: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
if (StorageUtils.isInVault(activity, targetDir)) {
|
||||
return insertByFile(
|
||||
targetDir = targetDir,
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir)
|
||||
val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath)
|
||||
if (isDownloadSubdir) {
|
||||
val volumePath = StorageUtils.getVolumePath(activity, targetDir)
|
||||
val relativePath = targetDir.substring(volumePath?.length ?: 0)
|
||||
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
val resolver = activity.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
uri?.let {
|
||||
resolver.openOutputStream(uri)?.use(write)
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
} ?: throw Exception("MediaStore failed for some reason")
|
||||
|
||||
return File(targetDir, targetFileName).path
|
||||
return insertByMediaStore(
|
||||
activity = activity,
|
||||
targetDir = targetDir,
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return insertByTreeDoc(
|
||||
activity = activity,
|
||||
mimeType = mimeType,
|
||||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertByFile(
|
||||
targetDir: String,
|
||||
targetFileName: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
val file = File(targetDir, targetFileName)
|
||||
FileOutputStream(file).use(write)
|
||||
return file.path
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun insertByMediaStore(
|
||||
activity: Activity,
|
||||
targetDir: String,
|
||||
targetFileName: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
val volumePath = StorageUtils.getVolumePath(activity, targetDir)
|
||||
val relativePath = targetDir.substring(volumePath?.length ?: 0)
|
||||
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
val resolver = activity.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
uri?.let {
|
||||
resolver.openOutputStream(uri)?.use(write)
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
} ?: throw Exception("MediaStore failed for some reason")
|
||||
|
||||
return File(targetDir, targetFileName).path
|
||||
}
|
||||
|
||||
// `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry
|
||||
// `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument"
|
||||
// when used with entry URI as `sourceDocumentUri`, and targetDirDocFile URI as `targetParentDocumentUri`
|
||||
private fun insertByTreeDoc(
|
||||
activity: Activity,
|
||||
mimeType: String,
|
||||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
targetNameWithoutExtension: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
|
||||
|
||||
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
|
||||
|
@ -670,7 +712,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
|
||||
// URI should not change
|
||||
return scanNewPath(activity, newFile.path, mimeType)
|
||||
return scanNewPathByMediaStore(activity, newFile.path, mimeType)
|
||||
}
|
||||
|
||||
private suspend fun renameSingleByTreeDoc(
|
||||
|
@ -690,7 +732,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
throw Exception("failed to rename document at path=$oldPath")
|
||||
}
|
||||
scanObsoletePath(activity, oldMediaUri, oldPath, mimeType)
|
||||
return scanNewPath(activity, newFile.path, mimeType)
|
||||
return scanNewPathByMediaStore(activity, newFile.path, mimeType)
|
||||
}
|
||||
|
||||
private suspend fun renameSingleByFile(
|
||||
|
@ -706,7 +748,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
throw Exception("failed to rename file at path=$oldPath")
|
||||
}
|
||||
scanObsoletePath(activity, oldMediaUri, oldPath, mimeType)
|
||||
return scanNewPath(activity, newFile.path, mimeType)
|
||||
return scanNewPathByMediaStore(activity, newFile.path, mimeType)
|
||||
}
|
||||
|
||||
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
|
||||
|
@ -757,10 +799,23 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
|
||||
suspendCoroutine { cont -> tryScanNewPath(context, path = path, mimeType = mimeType, cont) }
|
||||
suspend fun scanNewPathByMediaStore(context: Context, path: String, mimeType: String): FieldMap =
|
||||
suspendCoroutine { cont ->
|
||||
tryScanNewPathByMediaStore(
|
||||
context = context,
|
||||
path = path,
|
||||
mimeType = mimeType,
|
||||
cont = cont,
|
||||
)
|
||||
}
|
||||
|
||||
private fun tryScanNewPath(context: Context, path: String, mimeType: String, cont: Continuation<FieldMap>, iteration: Int = 0) {
|
||||
private fun tryScanNewPathByMediaStore(
|
||||
context: Context,
|
||||
path: String,
|
||||
mimeType: String,
|
||||
cont: Continuation<FieldMap>,
|
||||
iteration: Int = 0,
|
||||
) {
|
||||
// `scanFile` may (e.g. when copying to SD card on Android 10 (API 29)):
|
||||
// 1) yield no URI,
|
||||
// 2) yield a temporary URI that fails when queried,
|
||||
|
@ -832,7 +887,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
tryScanNewPath(context, path = path, mimeType = mimeType, cont, iteration + 1)
|
||||
tryScanNewPathByMediaStore(context, path = path, mimeType = mimeType, cont, iteration + 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ object MimeTypes {
|
|||
|
||||
fun isRaw(mimeType: String): Boolean {
|
||||
return when (mimeType) {
|
||||
ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW -> true
|
||||
ARW, CR2, CRW, DCR, DNG, ERF, K25, KDC, MRW, NEF, NRW, ORF, PEF, RAF, RAW, RW2, SR2, SRF, SRW, X3F -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -250,8 +250,8 @@ object PermissionManager {
|
|||
for (uriPermission in context.contentResolver.persistedUriPermissions) {
|
||||
val uri = uriPermission.uri
|
||||
val path = StorageUtils.convertTreeDocumentUriToDirPath(context, uri)
|
||||
if (path != null && !File(path).exists()) {
|
||||
Log.d(LOG_TAG, "revoke URI permission for obsolete path=$path")
|
||||
if (path == null || !File(path).exists()) {
|
||||
Log.d(LOG_TAG, "revoke URI permission for obsolete uri=$uri path=$path")
|
||||
releaseUriPermission(context, uri)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -693,7 +693,7 @@ object StorageUtils {
|
|||
class PathSegments(context: Context, fullPath: String) {
|
||||
var volumePath: String? = null // `volumePath` with trailing "/"
|
||||
var relativeDir: String? = null // `relativeDir` with trailing "/"
|
||||
var fileName: String? = null // null for directories
|
||||
private var fileName: String? = null // null for directories
|
||||
|
||||
init {
|
||||
volumePath = getVolumePath(context, fullPath)
|
||||
|
|
12
android/app/src/main/res/values-ckb/strings.xml
Normal file
12
android/app/src/main/res/values-ckb/strings.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">ئاڤیس</string>
|
||||
<string name="app_widget_label">قەراغی وێنە</string>
|
||||
<string name="wallpaper">ڕوونما</string>
|
||||
<string name="search_shortcut_short_label">گەڕان</string>
|
||||
<string name="videos_shortcut_short_label">ڤیدیۆ</string>
|
||||
<string name="analysis_channel_name">گەڕان بۆ فایل</string>
|
||||
<string name="analysis_service_description">گەڕان بۆ وێنە و ڤیدیۆ</string>
|
||||
<string name="analysis_notification_default_title">گەڕان بۆ فایلەکان</string>
|
||||
<string name="analysis_notification_action_stop">وەستاندن</string>
|
||||
</resources>
|
|
@ -24,13 +24,13 @@ buildscript {
|
|||
|
||||
if (useCrashlytics) {
|
||||
// GMS & Firebase Crashlytics (used by some flavors only)
|
||||
classpath 'com.google.gms:google-services:4.3.14'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2'
|
||||
classpath 'com.google.gms:google-services:4.3.15'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.4'
|
||||
}
|
||||
|
||||
if (useHms) {
|
||||
// HMS (used by some flavors only)
|
||||
classpath 'com.huawei.agconnect:agcp:1.7.2.300'
|
||||
classpath 'com.huawei.agconnect:agcp:1.8.0.300'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
fastlane/metadata/android/ckb/full_description.txt
Normal file
5
fastlane/metadata/android/ckb/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
1
fastlane/metadata/android/ckb/short_description.txt
Normal file
1
fastlane/metadata/android/ckb/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Gallery and metadata explorer
|
5
fastlane/metadata/android/en-US/changelogs/93.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/93.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
In v1.8.2:
|
||||
- write metadata when converting
|
||||
- convert many items at once
|
||||
- list places in their own page
|
||||
Full changelog available on GitHub
|
5
fastlane/metadata/android/en-US/changelogs/9301.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/9301.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
In v1.8.2:
|
||||
- write metadata when converting
|
||||
- convert many items at once
|
||||
- list places in their own page
|
||||
Full changelog available on GitHub
|
166
lib/l10n/app_ckb.arb
Normal file
166
lib/l10n/app_ckb.arb
Normal file
|
@ -0,0 +1,166 @@
|
|||
{
|
||||
"@@locale" : "ckb",
|
||||
"welcomeOptional": "ئارەزومەندانە",
|
||||
"@welcomeOptional": {},
|
||||
"welcomeTermsToggle": "ڕازیم بە مەرج و یاساکانی بەکارهێنان",
|
||||
"@welcomeTermsToggle": {},
|
||||
"columnCount": "{count, plural, =1{١ ڕیز} other{ڕیز {count}}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"timeSeconds": "{seconds, plural, =1{١ چرکە} other{چرکە {seconds}}}",
|
||||
"@timeSeconds": {
|
||||
"placeholders": {
|
||||
"seconds": {}
|
||||
}
|
||||
},
|
||||
"applyButtonLabel": "جێبەجێکردن",
|
||||
"@applyButtonLabel": {},
|
||||
"deleteButtonLabel": "سڕینەوە",
|
||||
"@deleteButtonLabel": {},
|
||||
"nextButtonLabel": "دواتر",
|
||||
"@nextButtonLabel": {},
|
||||
"hideButtonLabel": "شاردنەوە",
|
||||
"@hideButtonLabel": {},
|
||||
"continueButtonLabel": "بەردەوامبون",
|
||||
"@continueButtonLabel": {},
|
||||
"cancelTooltip": "پاشگەزبونەوە",
|
||||
"@cancelTooltip": {},
|
||||
"changeTooltip": "گۆڕین",
|
||||
"@changeTooltip": {},
|
||||
"clearTooltip": "لابردن",
|
||||
"@clearTooltip": {},
|
||||
"previousTooltip": "پێشوتر",
|
||||
"@previousTooltip": {},
|
||||
"showTooltip": "پیشاندان",
|
||||
"@showTooltip": {},
|
||||
"hideTooltip": "شاردنەوە",
|
||||
"@hideTooltip": {},
|
||||
"actionRemove": "لابردن",
|
||||
"@actionRemove": {},
|
||||
"resetTooltip": "سفرکردنەوە",
|
||||
"@resetTooltip": {},
|
||||
"saveTooltip": "پاراستن",
|
||||
"@saveTooltip": {},
|
||||
"pickTooltip": "هەڵبژاردن",
|
||||
"@pickTooltip": {},
|
||||
"doubleBackExitMessage": "دووبارە پەنجەبنێ بە گەڕانەوە ”back“ دا بۆ دەرچوون",
|
||||
"@doubleBackExitMessage": {},
|
||||
"sourceStateLoading": "چاوەڕوانبە",
|
||||
"@sourceStateLoading": {},
|
||||
"sourceStateCataloguing": "تۆماری زانیاری",
|
||||
"@sourceStateCataloguing": {},
|
||||
"sourceStateLocatingCountries": "دۆزینەوەی ووڵات",
|
||||
"@sourceStateLocatingCountries": {},
|
||||
"sourceStateLocatingPlaces": "دۆزینەوەی شوێن",
|
||||
"@sourceStateLocatingPlaces": {},
|
||||
"chipActionDelete": "سڕینەوە",
|
||||
"@chipActionDelete": {},
|
||||
"chipActionGoToAlbumPage": "بینین لە ئەلبومدا",
|
||||
"@chipActionGoToAlbumPage": {},
|
||||
"chipActionGoToTagPage": "پیشاندانی نیشانەکراوەکان",
|
||||
"@chipActionGoToTagPage": {},
|
||||
"chipActionFilterOut": "کاریگەرییەکان",
|
||||
"@chipActionFilterOut": {},
|
||||
"chipActionFilterIn": "کاریگەرییەکان",
|
||||
"@chipActionFilterIn": {},
|
||||
"chipActionHide": "شاردنەوە",
|
||||
"@chipActionHide": {},
|
||||
"chipActionPin": "جێگیرکردن لە سەرەوە",
|
||||
"@chipActionPin": {},
|
||||
"chipActionUnpin": "لابردنی جێگیری",
|
||||
"@chipActionUnpin": {},
|
||||
"chipActionRename": "ناوگۆڕین",
|
||||
"@chipActionRename": {},
|
||||
"chipActionSetCover": "وێنەی بەرگ",
|
||||
"@chipActionSetCover": {},
|
||||
"chipActionCreateVault": "دروستکردنی تایبەت",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionConfigureVault": "ڕێکخستنی قفڵکراو",
|
||||
"@chipActionConfigureVault": {},
|
||||
"entryActionCopyToClipboard": "لەبەرگرتنەوە",
|
||||
"@entryActionCopyToClipboard": {},
|
||||
"entryActionDelete": "سڕینەوە",
|
||||
"@entryActionDelete": {},
|
||||
"entryActionConvert": "گۆڕینی جۆر",
|
||||
"@entryActionConvert": {},
|
||||
"entryActionExport": "هەڵگرتن",
|
||||
"@entryActionExport": {},
|
||||
"entryActionInfo": "زانیاری",
|
||||
"@entryActionInfo": {},
|
||||
"entryActionRestore": "گێڕانەوە",
|
||||
"@entryActionRestore": {},
|
||||
"timeDays": "{days, plural, =1{١ ڕۆژ} other{ڕۆژ {days}}}",
|
||||
"@timeDays": {
|
||||
"placeholders": {
|
||||
"days": {}
|
||||
}
|
||||
},
|
||||
"focalLength": "{length} ملیمەتر",
|
||||
"@focalLength": {
|
||||
"placeholders": {
|
||||
"length": {
|
||||
"type": "String",
|
||||
"example": "5.4"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entryActionPrint": "چاپکردن",
|
||||
"@entryActionPrint": {},
|
||||
"entryActionShare": "بڵاوکردنەوە",
|
||||
"@entryActionShare": {},
|
||||
"entryActionViewSource": "بینینی سەرچاوە",
|
||||
"@entryActionViewSource": {},
|
||||
"entryActionEdit": "دەستکاریکردن",
|
||||
"@entryActionEdit": {},
|
||||
"entryActionOpen": "کردنەوە لەڕێی",
|
||||
"@entryActionOpen": {},
|
||||
"entryActionRotateScreen": "سوڕانەوەی خۆکار",
|
||||
"@entryActionRotateScreen": {},
|
||||
"entryActionAddFavourite": "زیادکردن بۆ دڵخواز",
|
||||
"@entryActionAddFavourite": {},
|
||||
"entryActionRemoveFavourite": "لابردن لە دڵخواز",
|
||||
"@entryActionRemoveFavourite": {},
|
||||
"videoActionMute": "بێدەنگکردن",
|
||||
"@videoActionMute": {},
|
||||
"videoActionUnmute": "کاراکردنی دەنگ",
|
||||
"@videoActionUnmute": {},
|
||||
"videoActionPause": "ڕاگرتن",
|
||||
"@videoActionPause": {},
|
||||
"videoActionPlay": "لێدان",
|
||||
"@videoActionPlay": {},
|
||||
"itemCount": "{count, plural, =1{١ دانە} other{دانە {count}}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"appName": "ئاڤیس",
|
||||
"@appName": {},
|
||||
"welcomeMessage": "بەخێربێی بۆ ئاڤیس",
|
||||
"@welcomeMessage": {},
|
||||
"nextTooltip": "دواتر",
|
||||
"@nextTooltip": {},
|
||||
"showButtonLabel": "پیشاندان",
|
||||
"@showButtonLabel": {},
|
||||
"doNotAskAgain": "دووبارە مەپرسەوە",
|
||||
"@doNotAskAgain": {},
|
||||
"chipActionGoToCountryPage": "پیشاندان لە ووڵاتەکاندا",
|
||||
"@chipActionGoToCountryPage": {},
|
||||
"chipActionLock": "قفڵکردن",
|
||||
"@chipActionLock": {},
|
||||
"chipActionCreateAlbum": "دروستکردنی ئەلبوم",
|
||||
"@chipActionCreateAlbum": {},
|
||||
"entryActionRename": "ناوگۆڕین",
|
||||
"@entryActionRename": {},
|
||||
"timeMinutes": "{minutes, plural, =1{١ خولەک} other{خولەک {minutes}}}",
|
||||
"@timeMinutes": {
|
||||
"placeholders": {
|
||||
"minutes": {}
|
||||
}
|
||||
},
|
||||
"entryActionSetAs": "دانان وەک",
|
||||
"@entryActionSetAs": {}
|
||||
}
|
|
@ -1248,5 +1248,9 @@
|
|||
"tooManyItemsErrorDialogMessage": "Δοκιμάστε ξανά με λιγότερα αρχεία.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Σύρετε προς τα πάνω ή προς τα κάτω για να ρυθμίσετε τη φωτεινότητα/την ένταση του ήχου",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@
|
|||
"chipActionDelete": "Delete",
|
||||
"chipActionGoToAlbumPage": "Show in Albums",
|
||||
"chipActionGoToCountryPage": "Show in Countries",
|
||||
"chipActionGoToPlacePage": "Show in Places",
|
||||
"chipActionGoToTagPage": "Show in Tags",
|
||||
"chipActionFilterOut": "Filter out",
|
||||
"chipActionFilterIn": "Filter in",
|
||||
|
@ -199,6 +200,9 @@
|
|||
"keepScreenOnViewerOnly": "Viewer page only",
|
||||
"keepScreenOnAlways": "Always",
|
||||
|
||||
"lengthUnitPixel": "px",
|
||||
"lengthUnitPercent": "%",
|
||||
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
|
||||
|
@ -418,6 +422,7 @@
|
|||
"exportEntryDialogFormat": "Format:",
|
||||
"exportEntryDialogWidth": "Width",
|
||||
"exportEntryDialogHeight": "Height",
|
||||
"exportEntryDialogWriteMetadata": "Write metadata",
|
||||
|
||||
"renameEntryDialogLabel": "New name",
|
||||
|
||||
|
@ -621,6 +626,7 @@
|
|||
"drawerCollectionSphericalVideos": "360° Videos",
|
||||
"drawerAlbumPage": "Albums",
|
||||
"drawerCountryPage": "Countries",
|
||||
"drawerPlacePage": "Places",
|
||||
"drawerTagPage": "Tags",
|
||||
|
||||
"sortByDate": "By date",
|
||||
|
@ -665,6 +671,9 @@
|
|||
"countryPageTitle": "Countries",
|
||||
"countryEmpty": "No countries",
|
||||
|
||||
"placePageTitle": "Places",
|
||||
"placeEmpty": "No places",
|
||||
|
||||
"tagPageTitle": "Tags",
|
||||
"tagEmpty": "No tags",
|
||||
|
||||
|
|
|
@ -1248,5 +1248,19 @@
|
|||
"authenticateToUnlockVault": "Autentificarse para desbloquear la caja fuerte",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"settingsConfirmationVaultDataLoss": "Mostrar un aviso de pérdida de datos de la caja fuerte",
|
||||
"@settingsConfirmationVaultDataLoss": {}
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"drawerPlacePage": "Lugares",
|
||||
"@drawerPlacePage": {},
|
||||
"placePageTitle": "Lugares",
|
||||
"@placePageTitle": {},
|
||||
"placeEmpty": "Ningún lugar",
|
||||
"@placeEmpty": {},
|
||||
"chipActionGoToPlacePage": "Mostrar en lugares",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"exportEntryDialogWriteMetadata": "Escribir metadatos",
|
||||
"@exportEntryDialogWriteMetadata": {}
|
||||
}
|
||||
|
|
|
@ -1366,5 +1366,59 @@
|
|||
"filePickerUseThisFolder": "Erabili karpeta hau",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"settingsWidgetOpenPage": "Widgetan sakatzean",
|
||||
"@settingsWidgetOpenPage": {}
|
||||
"@settingsWidgetOpenPage": {},
|
||||
"exportEntryDialogWriteMetadata": "Idatzi metadatuak",
|
||||
"@exportEntryDialogWriteMetadata": {},
|
||||
"configureVaultDialogTitle": "Konfiguratu kutxa gotorra",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"newVaultDialogTitle": "Kutxa gotor berria",
|
||||
"@newVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Blokeatu pantaila itzaltzean",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Blokeatze modua",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogEnter": "Sartu PINa",
|
||||
"@pinDialogEnter": {},
|
||||
"chipActionGoToPlacePage": "Erakutsi lekuetan",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"chipActionLock": "Blokeatu",
|
||||
"@chipActionLock": {},
|
||||
"chipActionCreateVault": "Sortu kutxa gotorra",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionConfigureVault": "Konfiguratu kutxa gotorra",
|
||||
"@chipActionConfigureVault": {},
|
||||
"albumTierVaults": "Kutxa gotorrak",
|
||||
"@albumTierVaults": {},
|
||||
"placePageTitle": "Lekuak",
|
||||
"@placePageTitle": {},
|
||||
"placeEmpty": "Lekurik ez",
|
||||
"@placeEmpty": {},
|
||||
"settingsConfirmationVaultDataLoss": "Erakutsi kutxa gotorreko datuen galeraren inguruko abisua",
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"newVaultWarningDialogMessage": "Kutxa gotorreko elementuak aplikazio honetarako soilik daude eskuragarri eta ez beste edozeinetarako.\n\nAplikazio hau desinstalatzen baduzu, edo aplikazio honen datuak garbitu, elementu guzti hauek galduko dituzu.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Zakarrontziko elementuak betirako ezabatuko dira.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"pinDialogConfirm": "Konfirmatu PINa",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Sartu pasahitza",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Berretsi pasahitza",
|
||||
"@passwordDialogConfirm": {},
|
||||
"authenticateToConfigureVault": "Autentifikatu kutxa gotorra konfiguratzeko",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"authenticateToUnlockVault": "Autentifikatu kutxa gotorra desblokeatzeko",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"vaultBinUsageDialogMessage": "Kutxa gotor batzuk zakarrontzia erabiltzen ari dira.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"vaultLockTypePin": "PIN kodea",
|
||||
"@vaultLockTypePin": {},
|
||||
"vaultLockTypePassword": "Pasahitza",
|
||||
"@vaultLockTypePassword": {},
|
||||
"drawerPlacePage": "Lekuak",
|
||||
"@drawerPlacePage": {}
|
||||
}
|
||||
|
|
|
@ -1248,5 +1248,19 @@
|
|||
"authenticateToUnlockVault": "Authentification pour déverrouiller le coffre-fort",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Les éléments dans la corbeille seront supprimés définitivement.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {}
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"drawerPlacePage": "Lieux",
|
||||
"@drawerPlacePage": {},
|
||||
"placePageTitle": "Lieux",
|
||||
"@placePageTitle": {},
|
||||
"chipActionGoToPlacePage": "Afficher dans Lieux",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"placeEmpty": "Aucun lieu",
|
||||
"@placeEmpty": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"exportEntryDialogWriteMetadata": "Écrire les métadonnées",
|
||||
"@exportEntryDialogWriteMetadata": {}
|
||||
}
|
||||
|
|
|
@ -1248,5 +1248,19 @@
|
|||
"newVaultWarningDialogMessage": "Item dalam brankas hanya tersedia untuk aplikasi ini dan bukan yang lain.\n\nJika Anda menghapus aplikasi ini, atau menghapus data aplikasi ini, Anda akan kehilangan semua item tersebut.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"settingsConfirmationVaultDataLoss": "Tampilkan peringatan kehilangan data brankas",
|
||||
"@settingsConfirmationVaultDataLoss": {}
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"chipActionGoToPlacePage": "Tampilkan di Tempat",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"placePageTitle": "Tempat",
|
||||
"@placePageTitle": {},
|
||||
"placeEmpty": "Tidak ada tempat",
|
||||
"@placeEmpty": {},
|
||||
"drawerPlacePage": "Tempat",
|
||||
"@drawerPlacePage": {},
|
||||
"exportEntryDialogWriteMetadata": "Tulis metadata",
|
||||
"@exportEntryDialogWriteMetadata": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {}
|
||||
}
|
||||
|
|
|
@ -1208,5 +1208,45 @@
|
|||
"tooManyItemsErrorDialogMessage": "Riprova con meno elementi.",
|
||||
"@tooManyItemsErrorDialogMessage": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "Trascina su o giù per aggiustare luminosità/volume",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {}
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Blocca allo spegnimento dello schermo",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"chipActionCreateVault": "Crea cassaforte",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionConfigureVault": "Configura cassaforte",
|
||||
"@chipActionConfigureVault": {},
|
||||
"chipActionLock": "Proteggi",
|
||||
"@chipActionLock": {},
|
||||
"albumTierVaults": "Casseforti",
|
||||
"@albumTierVaults": {},
|
||||
"vaultLockTypePin": "PIN",
|
||||
"@vaultLockTypePin": {},
|
||||
"vaultLockTypePassword": "Password",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultWarningDialogMessage": "Gli elementi nelle casseforti sono disponibili solo per questa app e non per altre.\n\nSe disinstalli l'app o ne cancelli i dati, perderai tutti questi elementi.",
|
||||
"@newVaultWarningDialogMessage": {},
|
||||
"newVaultDialogTitle": "Nuova Cassaforte",
|
||||
"@newVaultDialogTitle": {},
|
||||
"vaultDialogLockTypeLabel": "Tipo di protezione",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"authenticateToConfigureVault": "Accedi per configurare la cassaforte",
|
||||
"@authenticateToConfigureVault": {},
|
||||
"authenticateToUnlockVault": "Accedi per sbloccare la cassaforte",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"vaultBinUsageDialogMessage": "Alcune casseforti stanno usando il cestino.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"pinDialogEnter": "Inserisci PIN",
|
||||
"@pinDialogEnter": {},
|
||||
"pinDialogConfirm": "Conferma PIN",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Inserisci password",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Conferma password",
|
||||
"@passwordDialogConfirm": {},
|
||||
"settingsConfirmationVaultDataLoss": "Mostra avviso di perdita dati delle casseforti",
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"settingsDisablingBinWarningDialogMessage": "Gli elementi nel cestino verranno eliminati permanentemente.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"configureVaultDialogTitle": "Configura Cassaforte",
|
||||
"@configureVaultDialogTitle": {}
|
||||
}
|
||||
|
|
|
@ -1248,5 +1248,19 @@
|
|||
"vaultBinUsageDialogMessage": "휴지통을 사용하는 금고가 있습니다.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"settingsConfirmationVaultDataLoss": "금고에 관한 데이터 손실 경고",
|
||||
"@settingsConfirmationVaultDataLoss": {}
|
||||
"@settingsConfirmationVaultDataLoss": {},
|
||||
"placePageTitle": "장소",
|
||||
"@placePageTitle": {},
|
||||
"drawerPlacePage": "장소",
|
||||
"@drawerPlacePage": {},
|
||||
"chipActionGoToPlacePage": "장소 페이지에서 보기",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"placeEmpty": "장소가 없습니다",
|
||||
"@placeEmpty": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"exportEntryDialogWriteMetadata": "메타데이터 저장",
|
||||
"@exportEntryDialogWriteMetadata": {}
|
||||
}
|
||||
|
|
|
@ -133,7 +133,7 @@
|
|||
"@entryActionOpenMap": {},
|
||||
"sourceStateCataloguing": "Katalogowanie",
|
||||
"@sourceStateCataloguing": {},
|
||||
"sourceStateLocatingPlaces": "Lokowanie miejsc",
|
||||
"sourceStateLocatingPlaces": "Lokalizowanie miejsc",
|
||||
"@sourceStateLocatingPlaces": {},
|
||||
"chipActionFilterOut": "Odfiltruj",
|
||||
"@chipActionFilterOut": {},
|
||||
|
@ -1406,5 +1406,19 @@
|
|||
"vaultBinUsageDialogMessage": "Niektóre skarbce korzystają z kosza.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"vaultLockTypePin": "PIN",
|
||||
"@vaultLockTypePin": {}
|
||||
"@vaultLockTypePin": {},
|
||||
"chipActionGoToPlacePage": "Pokaż w Miejscach",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"drawerPlacePage": "Miejsca",
|
||||
"@drawerPlacePage": {},
|
||||
"placeEmpty": "Brak miejsc",
|
||||
"@placeEmpty": {},
|
||||
"exportEntryDialogWriteMetadata": "Zapisz metadane",
|
||||
"@exportEntryDialogWriteMetadata": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"placePageTitle": "Miejsca",
|
||||
"@placePageTitle": {}
|
||||
}
|
||||
|
|
|
@ -1198,5 +1198,23 @@
|
|||
"settingsDisplayUseTvInterface": "Interface de TV Android",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"settingsViewerShowRatingTags": "Mostrar avaliações e tags",
|
||||
"@settingsViewerShowRatingTags": {}
|
||||
"@settingsViewerShowRatingTags": {},
|
||||
"chipActionLock": "Bloquear",
|
||||
"@chipActionLock": {},
|
||||
"vaultLockTypePassword": "Senha",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultDialogTitle": "Novo Cofre",
|
||||
"@newVaultDialogTitle": {},
|
||||
"configureVaultDialogTitle": "Configurar Cofre",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Bloquear quando a tela desligar",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"vaultDialogLockTypeLabel": "Tipo de bloqueio",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogEnter": "Digite o PIN",
|
||||
"@pinDialogEnter": {},
|
||||
"pinDialogConfirm": "Confirme o PIN",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Digite a senha",
|
||||
"@passwordDialogEnter": {}
|
||||
}
|
||||
|
|
|
@ -1196,5 +1196,39 @@
|
|||
"entryActionShareVideoOnly": "Поделиться только видео",
|
||||
"@entryActionShareVideoOnly": {},
|
||||
"settingsViewerShowDescription": "Показать описание",
|
||||
"@settingsViewerShowDescription": {}
|
||||
"@settingsViewerShowDescription": {},
|
||||
"chipActionConfigureVault": "Настроить хранилище",
|
||||
"@chipActionConfigureVault": {},
|
||||
"vaultLockTypePin": "Пин-код",
|
||||
"@vaultLockTypePin": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {},
|
||||
"vaultLockTypePassword": "Пароль",
|
||||
"@vaultLockTypePassword": {},
|
||||
"newVaultDialogTitle": "Новое хранилище",
|
||||
"@newVaultDialogTitle": {},
|
||||
"vaultDialogLockModeWhenScreenOff": "Блокировать при выключении экрана",
|
||||
"@vaultDialogLockModeWhenScreenOff": {},
|
||||
"pinDialogEnter": "Введите пин-код",
|
||||
"@pinDialogEnter": {},
|
||||
"pinDialogConfirm": "Подтвердите пин-код",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Введите пароль",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Подтвердите пароль",
|
||||
"@passwordDialogConfirm": {},
|
||||
"vaultDialogLockTypeLabel": "Тип защиты",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"vaultBinUsageDialogMessage": "Некоторые из хранилищ используют корзину.",
|
||||
"@vaultBinUsageDialogMessage": {},
|
||||
"chipActionCreateVault": "Создать хранилище",
|
||||
"@chipActionCreateVault": {},
|
||||
"settingsDisplayUseTvInterface": "Интерфейс Android TV",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"configureVaultDialogTitle": "Настроить хранилище",
|
||||
"@configureVaultDialogTitle": {},
|
||||
"albumTierVaults": "Хранилища",
|
||||
"@albumTierVaults": {},
|
||||
"newVaultWarningDialogMessage": "Элементы внутри хранилищ доступны только для этого приложения, и никакого другого.\n\nЕсли вы удалите приложение или очистите его данные, то вы потеряете все содержимое внутри хранилищ.",
|
||||
"@newVaultWarningDialogMessage": {}
|
||||
}
|
||||
|
|
|
@ -901,7 +901,7 @@
|
|||
"@searchDateSectionTitle": {},
|
||||
"searchCountriesSectionTitle": "Країни",
|
||||
"@searchCountriesSectionTitle": {},
|
||||
"searchPlacesSectionTitle": "Місця",
|
||||
"searchPlacesSectionTitle": "Локації",
|
||||
"@searchPlacesSectionTitle": {},
|
||||
"searchTagsSectionTitle": "Теги",
|
||||
"@searchTagsSectionTitle": {},
|
||||
|
@ -1115,7 +1115,7 @@
|
|||
"@settingsCollectionTile": {},
|
||||
"statsTopCountriesSectionTitle": "Топ Країн",
|
||||
"@statsTopCountriesSectionTitle": {},
|
||||
"statsTopPlacesSectionTitle": "Топ Локацій",
|
||||
"statsTopPlacesSectionTitle": "Топ локацій",
|
||||
"@statsTopPlacesSectionTitle": {},
|
||||
"statsTopTagsSectionTitle": "Топ Тегів",
|
||||
"@statsTopTagsSectionTitle": {},
|
||||
|
@ -1207,7 +1207,7 @@
|
|||
"@tagEditorSectionPlaceholders": {},
|
||||
"tagPlaceholderCountry": "Країна",
|
||||
"@tagPlaceholderCountry": {},
|
||||
"tagPlaceholderPlace": "Місце",
|
||||
"tagPlaceholderPlace": "Локація",
|
||||
"@tagPlaceholderPlace": {},
|
||||
"panoramaEnableSensorControl": "Увімкнути сенсорне керування",
|
||||
"@panoramaEnableSensorControl": {},
|
||||
|
@ -1406,5 +1406,19 @@
|
|||
"settingsDisablingBinWarningDialogMessage": "Елементи в кошику буде видалено назавжди.",
|
||||
"@settingsDisablingBinWarningDialogMessage": {},
|
||||
"pinDialogEnter": "Введіть пін-код",
|
||||
"@pinDialogEnter": {}
|
||||
"@pinDialogEnter": {},
|
||||
"drawerPlacePage": "Локації",
|
||||
"@drawerPlacePage": {},
|
||||
"placeEmpty": "Немає локацій",
|
||||
"@placeEmpty": {},
|
||||
"chipActionGoToPlacePage": "Показати в Локаціях",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"placePageTitle": "Локації",
|
||||
"@placePageTitle": {},
|
||||
"exportEntryDialogWriteMetadata": "Напишіть метадані",
|
||||
"@exportEntryDialogWriteMetadata": {},
|
||||
"lengthUnitPixel": "px",
|
||||
"@lengthUnitPixel": {},
|
||||
"lengthUnitPercent": "%",
|
||||
"@lengthUnitPercent": {}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart';
|
|||
enum ChipAction {
|
||||
goToAlbumPage,
|
||||
goToCountryPage,
|
||||
goToPlacePage,
|
||||
goToTagPage,
|
||||
reverse,
|
||||
hide,
|
||||
|
@ -18,6 +19,8 @@ extension ExtraChipAction on ChipAction {
|
|||
return context.l10n.chipActionGoToAlbumPage;
|
||||
case ChipAction.goToCountryPage:
|
||||
return context.l10n.chipActionGoToCountryPage;
|
||||
case ChipAction.goToPlacePage:
|
||||
return context.l10n.chipActionGoToPlacePage;
|
||||
case ChipAction.goToTagPage:
|
||||
return context.l10n.chipActionGoToTagPage;
|
||||
case ChipAction.reverse:
|
||||
|
@ -37,7 +40,9 @@ extension ExtraChipAction on ChipAction {
|
|||
case ChipAction.goToAlbumPage:
|
||||
return AIcons.album;
|
||||
case ChipAction.goToCountryPage:
|
||||
return AIcons.location;
|
||||
return AIcons.country;
|
||||
case ChipAction.goToPlacePage:
|
||||
return AIcons.place;
|
||||
case ChipAction.goToTagPage:
|
||||
return AIcons.tag;
|
||||
case ChipAction.reverse:
|
||||
|
|
|
@ -329,9 +329,9 @@ extension ExtraEntryAction on EntryAction {
|
|||
case EntryAction.editTitleDescription:
|
||||
return AIcons.description;
|
||||
case EntryAction.editRating:
|
||||
return AIcons.editRating;
|
||||
return AIcons.rating;
|
||||
case EntryAction.editTags:
|
||||
return AIcons.editTags;
|
||||
return AIcons.tag;
|
||||
case EntryAction.removeMetadata:
|
||||
return AIcons.clear;
|
||||
case EntryAction.exportMetadata:
|
||||
|
|
|
@ -25,6 +25,7 @@ enum EntrySetAction {
|
|||
copy,
|
||||
move,
|
||||
rename,
|
||||
convert,
|
||||
toggleFavourite,
|
||||
rotateCCW,
|
||||
rotateCW,
|
||||
|
@ -45,13 +46,16 @@ class EntrySetActions {
|
|||
EntrySetAction.selectNone,
|
||||
];
|
||||
|
||||
// `null` items are converted to dividers
|
||||
static const pageBrowsing = [
|
||||
EntrySetAction.searchCollection,
|
||||
EntrySetAction.toggleTitleSearch,
|
||||
EntrySetAction.addShortcut,
|
||||
null,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.slideshow,
|
||||
EntrySetAction.stats,
|
||||
null,
|
||||
EntrySetAction.rescan,
|
||||
EntrySetAction.emptyBin,
|
||||
];
|
||||
|
@ -67,6 +71,7 @@ class EntrySetActions {
|
|||
EntrySetAction.rescan,
|
||||
];
|
||||
|
||||
// `null` items are converted to dividers
|
||||
static const pageSelection = [
|
||||
EntrySetAction.share,
|
||||
EntrySetAction.delete,
|
||||
|
@ -74,10 +79,13 @@ class EntrySetActions {
|
|||
EntrySetAction.copy,
|
||||
EntrySetAction.move,
|
||||
EntrySetAction.rename,
|
||||
EntrySetAction.convert,
|
||||
EntrySetAction.toggleFavourite,
|
||||
null,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.slideshow,
|
||||
EntrySetAction.stats,
|
||||
null,
|
||||
EntrySetAction.rescan,
|
||||
// editing actions are in their subsection
|
||||
];
|
||||
|
@ -89,6 +97,7 @@ class EntrySetActions {
|
|||
EntrySetAction.copy,
|
||||
EntrySetAction.move,
|
||||
EntrySetAction.rename,
|
||||
EntrySetAction.convert,
|
||||
EntrySetAction.toggleFavourite,
|
||||
EntrySetAction.map,
|
||||
EntrySetAction.slideshow,
|
||||
|
@ -163,6 +172,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return context.l10n.collectionActionMove;
|
||||
case EntrySetAction.rename:
|
||||
return context.l10n.entryActionRename;
|
||||
case EntrySetAction.convert:
|
||||
return context.l10n.entryActionConvert;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return context.l10n.entryActionAddFavourite;
|
||||
|
@ -232,6 +243,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
return AIcons.move;
|
||||
case EntrySetAction.rename:
|
||||
return AIcons.name;
|
||||
case EntrySetAction.convert:
|
||||
return AIcons.convert;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
// different data depending on toggle state
|
||||
return AIcons.favourite;
|
||||
|
@ -248,9 +261,9 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
case EntrySetAction.editTitleDescription:
|
||||
return AIcons.description;
|
||||
case EntrySetAction.editRating:
|
||||
return AIcons.editRating;
|
||||
return AIcons.rating;
|
||||
case EntrySetAction.editTags:
|
||||
return AIcons.editTags;
|
||||
return AIcons.tag;
|
||||
case EntrySetAction.removeMetadata:
|
||||
return AIcons.clear;
|
||||
}
|
||||
|
|
|
@ -62,8 +62,12 @@ class EntryDir {
|
|||
final dir = Directory(resolved);
|
||||
if (dir.existsSync()) {
|
||||
final partLower = part.toLowerCase();
|
||||
final childrenDirs = dir.listSync().where((v) => v.absolute is Directory).toSet();
|
||||
found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower);
|
||||
try {
|
||||
final childrenDirs = dir.listSync().where((v) => v.absolute is Directory).toSet();
|
||||
found = childrenDirs.firstWhereOrNull((v) => pContext.basename(v.path).toLowerCase() == partLower);
|
||||
} catch (error) {
|
||||
// ignore, could be IO issue when listing directory
|
||||
}
|
||||
}
|
||||
resolved = found?.path ?? '$resolved${pContext.separator}$part';
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
|
||||
final appliedModifier = await _applyDateModifierToEntry(userModifier);
|
||||
if (appliedModifier == null) {
|
||||
if (!isMissingAtPath) {
|
||||
if (!isMissingAtPath && userModifier.action != DateEditAction.copyField) {
|
||||
await reportService.recordError('failed to get date for modifier=$userModifier, entry=$this', null);
|
||||
}
|
||||
return {};
|
||||
|
|
|
@ -51,6 +51,8 @@ class LocationFilter extends CoveredCollectionFilter {
|
|||
|
||||
String? get countryCode => _countryCode;
|
||||
|
||||
String get place => _location;
|
||||
|
||||
@override
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
|
@ -65,17 +67,25 @@ class LocationFilter extends CoveredCollectionFilter {
|
|||
|
||||
@override
|
||||
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
|
||||
if (_countryCode != null && device.canRenderFlagEmojis) {
|
||||
final flag = countryCodeToFlag(_countryCode);
|
||||
if (flag != null) {
|
||||
return Text(
|
||||
flag,
|
||||
style: TextStyle(fontSize: size),
|
||||
textScaleFactor: 1.0,
|
||||
);
|
||||
}
|
||||
if (_location.isEmpty) {
|
||||
return Icon(AIcons.locationUnlocated, size: size);
|
||||
}
|
||||
switch (level) {
|
||||
case LocationLevel.place:
|
||||
return Icon(AIcons.place, size: size);
|
||||
case LocationLevel.country:
|
||||
if (_countryCode != null && device.canRenderFlagEmojis) {
|
||||
final flag = countryCodeToFlag(_countryCode);
|
||||
if (flag != null) {
|
||||
return Text(
|
||||
flag,
|
||||
style: TextStyle(fontSize: size),
|
||||
textScaleFactor: 1.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
return Icon(AIcons.country, size: size);
|
||||
}
|
||||
return Icon(_location.isEmpty ? AIcons.locationUnlocated : AIcons.location, size: size);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -23,8 +23,10 @@ class PlaceholderFilter extends CollectionFilter {
|
|||
PlaceholderFilter._private(this.placeholder) : super(reversed: false) {
|
||||
switch (placeholder) {
|
||||
case _country:
|
||||
_icon = AIcons.country;
|
||||
break;
|
||||
case _place:
|
||||
_icon = AIcons.location;
|
||||
_icon = AIcons.place;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ enum DateFieldSource {
|
|||
exifGpsDate,
|
||||
}
|
||||
|
||||
enum LengthUnit { px, percent }
|
||||
|
||||
enum LocationEditAction {
|
||||
chooseOnMap,
|
||||
copyItem,
|
||||
|
|
14
lib/model/metadata/enums/length_unit.dart
Normal file
14
lib/model/metadata/enums/length_unit.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:aves/model/metadata/enums/enums.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
extension ExtraLengthUnit on LengthUnit {
|
||||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
case LengthUnit.px:
|
||||
return context.l10n.lengthUnitPixel;
|
||||
case LengthUnit.percent:
|
||||
return context.l10n.lengthUnitPercent;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'package:aves/model/filters/recent.dart';
|
|||
import 'package:aves/model/naming_pattern.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
|
@ -65,6 +66,7 @@ class SettingsDefaults {
|
|||
static const albumGroupFactor = AlbumChipGroupFactor.importance;
|
||||
static const albumSortFactor = ChipSortFactor.name;
|
||||
static const countrySortFactor = ChipSortFactor.name;
|
||||
static const placeSortFactor = ChipSortFactor.name;
|
||||
static const tagSortFactor = ChipSortFactor.name;
|
||||
|
||||
// viewer
|
||||
|
@ -113,6 +115,11 @@ class SettingsDefaults {
|
|||
|
||||
static const tagEditorCurrentFilterSectionExpanded = true;
|
||||
|
||||
// converter
|
||||
|
||||
static const convertMimeType = MimeTypes.jpeg;
|
||||
static const convertWriteMetadata = true;
|
||||
|
||||
// rendering
|
||||
static const imageBackground = EntryBackground.white;
|
||||
|
||||
|
|
|
@ -18,10 +18,9 @@ extension ExtraThumbnailOverlayLocationIcon on ThumbnailOverlayLocationIcon {
|
|||
|
||||
IconData getIcon(BuildContext context) {
|
||||
switch (this) {
|
||||
case ThumbnailOverlayLocationIcon.located:
|
||||
return AIcons.location;
|
||||
case ThumbnailOverlayLocationIcon.unlocated:
|
||||
return AIcons.locationUnlocated;
|
||||
case ThumbnailOverlayLocationIcon.located:
|
||||
case ThumbnailOverlayLocationIcon.none:
|
||||
return AIcons.location;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'package:aves/widgets/aves_app.dart';
|
|||
import 'package:aves/widgets/common/search/page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves_map/aves_map.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -106,9 +107,11 @@ class Settings extends ChangeNotifier {
|
|||
static const albumGroupFactorKey = 'album_group_factor';
|
||||
static const albumSortFactorKey = 'album_sort_factor';
|
||||
static const countrySortFactorKey = 'country_sort_factor';
|
||||
static const placeSortFactorKey = 'place_sort_factor';
|
||||
static const tagSortFactorKey = 'tag_sort_factor';
|
||||
static const albumSortReverseKey = 'album_sort_reverse';
|
||||
static const countrySortReverseKey = 'country_sort_reverse';
|
||||
static const placeSortReverseKey = 'place_sort_reverse';
|
||||
static const tagSortReverseKey = 'tag_sort_reverse';
|
||||
static const pinnedFiltersKey = 'pinned_filters';
|
||||
static const hiddenFiltersKey = 'hidden_filters';
|
||||
|
@ -155,6 +158,11 @@ class Settings extends ChangeNotifier {
|
|||
static const tagEditorCurrentFilterSectionExpandedKey = 'tag_editor_current_filter_section_expanded';
|
||||
static const tagEditorExpandedSectionKey = 'tag_editor_expanded_section';
|
||||
|
||||
// converter
|
||||
|
||||
static const convertMimeTypeKey = 'convert_mime_type';
|
||||
static const convertWriteMetadataKey = 'convert_write_metadata';
|
||||
|
||||
// map
|
||||
static const mapStyleKey = 'info_map_style';
|
||||
static const mapDefaultCenterKey = 'map_default_center';
|
||||
|
@ -247,39 +255,40 @@ class Settings extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
applyTvSettings();
|
||||
if (settings.useTvLayout) {
|
||||
applyTvSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void applyTvSettings() {
|
||||
if (settings.useTvLayout) {
|
||||
themeBrightness = AvesThemeBrightness.dark;
|
||||
mustBackTwiceToExit = false;
|
||||
// address `TV-BU` / `TV-BY` requirements from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||
keepScreenOn = KeepScreenOn.videoPlayback;
|
||||
enableBottomNavigationBar = false;
|
||||
drawerTypeBookmarks = [
|
||||
null,
|
||||
MimeFilter.video,
|
||||
FavouriteFilter.instance,
|
||||
];
|
||||
drawerPageBookmarks = [
|
||||
AlbumListPage.routeName,
|
||||
CountryListPage.routeName,
|
||||
TagListPage.routeName,
|
||||
SearchPage.routeName,
|
||||
];
|
||||
showOverlayOnOpening = false;
|
||||
showOverlayMinimap = false;
|
||||
showOverlayThumbnailPreview = false;
|
||||
viewerGestureSideTapNext = false;
|
||||
viewerUseCutout = true;
|
||||
viewerMaxBrightness = false;
|
||||
videoControls = VideoControls.none;
|
||||
videoGestureDoubleTapTogglePlay = false;
|
||||
videoGestureSideDoubleTapSeek = false;
|
||||
enableBin = false;
|
||||
showPinchGestureAlternatives = true;
|
||||
}
|
||||
themeBrightness = AvesThemeBrightness.dark;
|
||||
mustBackTwiceToExit = false;
|
||||
// address `TV-BU` / `TV-BY` requirements from https://developer.android.com/docs/quality-guidelines/tv-app-quality
|
||||
keepScreenOn = KeepScreenOn.videoPlayback;
|
||||
enableBottomNavigationBar = false;
|
||||
drawerTypeBookmarks = [
|
||||
null,
|
||||
MimeFilter.video,
|
||||
FavouriteFilter.instance,
|
||||
];
|
||||
drawerPageBookmarks = [
|
||||
AlbumListPage.routeName,
|
||||
CountryListPage.routeName,
|
||||
PlaceListPage.routeName,
|
||||
TagListPage.routeName,
|
||||
SearchPage.routeName,
|
||||
];
|
||||
showOverlayOnOpening = false;
|
||||
showOverlayMinimap = false;
|
||||
showOverlayThumbnailPreview = false;
|
||||
viewerGestureSideTapNext = false;
|
||||
viewerUseCutout = true;
|
||||
viewerMaxBrightness = false;
|
||||
videoControls = VideoControls.none;
|
||||
videoGestureDoubleTapTogglePlay = false;
|
||||
videoGestureSideDoubleTapSeek = false;
|
||||
enableBin = false;
|
||||
showPinchGestureAlternatives = true;
|
||||
}
|
||||
|
||||
Future<void> sanitize() async {
|
||||
|
@ -543,6 +552,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set countrySortFactor(ChipSortFactor newValue) => _set(countrySortFactorKey, newValue.toString());
|
||||
|
||||
ChipSortFactor get placeSortFactor => getEnumOrDefault(placeSortFactorKey, SettingsDefaults.placeSortFactor, ChipSortFactor.values);
|
||||
|
||||
set placeSortFactor(ChipSortFactor newValue) => _set(placeSortFactorKey, newValue.toString());
|
||||
|
||||
ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, SettingsDefaults.tagSortFactor, ChipSortFactor.values);
|
||||
|
||||
set tagSortFactor(ChipSortFactor newValue) => _set(tagSortFactorKey, newValue.toString());
|
||||
|
@ -555,6 +568,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set countrySortReverse(bool newValue) => _set(countrySortReverseKey, newValue);
|
||||
|
||||
bool get placeSortReverse => getBool(placeSortReverseKey) ?? false;
|
||||
|
||||
set placeSortReverse(bool newValue) => _set(placeSortReverseKey, newValue);
|
||||
|
||||
bool get tagSortReverse => getBool(tagSortReverseKey) ?? false;
|
||||
|
||||
set tagSortReverse(bool newValue) => _set(tagSortReverseKey, newValue);
|
||||
|
@ -712,6 +729,16 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set tagEditorExpandedSection(String? newValue) => _set(tagEditorExpandedSectionKey, newValue);
|
||||
|
||||
// converter
|
||||
|
||||
String get convertMimeType => getString(convertMimeTypeKey) ?? SettingsDefaults.convertMimeType;
|
||||
|
||||
set convertMimeType(String newValue) => _set(convertMimeTypeKey, newValue);
|
||||
|
||||
bool get convertWriteMetadata => getBool(convertWriteMetadataKey) ?? SettingsDefaults.convertWriteMetadata;
|
||||
|
||||
set convertWriteMetadata(bool newValue) => _set(convertWriteMetadataKey, newValue);
|
||||
|
||||
// map
|
||||
|
||||
EntryMapStyle? get mapStyle {
|
||||
|
@ -857,7 +884,7 @@ class Settings extends ChangeNotifier {
|
|||
bool? getBool(String key) {
|
||||
try {
|
||||
return settingsStore.getBool(key);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// ignore, could be obsolete value of different type
|
||||
return null;
|
||||
}
|
||||
|
@ -866,7 +893,7 @@ class Settings extends ChangeNotifier {
|
|||
int? getInt(String key) {
|
||||
try {
|
||||
return settingsStore.getInt(key);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// ignore, could be obsolete value of different type
|
||||
return null;
|
||||
}
|
||||
|
@ -875,7 +902,7 @@ class Settings extends ChangeNotifier {
|
|||
double? getDouble(String key) {
|
||||
try {
|
||||
return settingsStore.getDouble(key);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// ignore, could be obsolete value of different type
|
||||
return null;
|
||||
}
|
||||
|
@ -884,7 +911,7 @@ class Settings extends ChangeNotifier {
|
|||
String? getString(String key) {
|
||||
try {
|
||||
return settingsStore.getString(key);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// ignore, could be obsolete value of different type
|
||||
return null;
|
||||
}
|
||||
|
@ -893,7 +920,7 @@ class Settings extends ChangeNotifier {
|
|||
List<String>? getStringList(String key) {
|
||||
try {
|
||||
return settingsStore.getStringList(key);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// ignore, could be obsolete value of different type
|
||||
return null;
|
||||
}
|
||||
|
@ -907,7 +934,7 @@ class Settings extends ChangeNotifier {
|
|||
return v;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// ignore, could be obsolete value of different type
|
||||
}
|
||||
return defaultValue;
|
||||
|
@ -1038,6 +1065,7 @@ class Settings extends ChangeNotifier {
|
|||
case showThumbnailVideoDurationKey:
|
||||
case albumSortReverseKey:
|
||||
case countrySortReverseKey:
|
||||
case placeSortReverseKey:
|
||||
case tagSortReverseKey:
|
||||
case showOverlayOnOpeningKey:
|
||||
case showOverlayMinimapKey:
|
||||
|
@ -1056,6 +1084,7 @@ class Settings extends ChangeNotifier {
|
|||
case videoGestureVerticalDragBrightnessVolumeKey:
|
||||
case subtitleShowOutlineKey:
|
||||
case tagEditorCurrentFilterSectionExpandedKey:
|
||||
case convertWriteMetadataKey:
|
||||
case saveSearchHistoryKey:
|
||||
case showPinchGestureAlternativesKey:
|
||||
case filePickerShowHiddenFilesKey:
|
||||
|
@ -1084,6 +1113,7 @@ class Settings extends ChangeNotifier {
|
|||
case albumGroupFactorKey:
|
||||
case albumSortFactorKey:
|
||||
case countrySortFactorKey:
|
||||
case placeSortFactorKey:
|
||||
case tagSortFactorKey:
|
||||
case imageBackgroundKey:
|
||||
case videoAutoPlayModeKey:
|
||||
|
@ -1092,6 +1122,7 @@ class Settings extends ChangeNotifier {
|
|||
case subtitleTextAlignmentKey:
|
||||
case subtitleTextPositionKey:
|
||||
case tagEditorExpandedSectionKey:
|
||||
case convertMimeTypeKey:
|
||||
case mapStyleKey:
|
||||
case mapDefaultCenterKey:
|
||||
case coordinateFormatKey:
|
||||
|
|
|
@ -14,7 +14,7 @@ import 'package:aves/model/filters/trash.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/events.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/location/location.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
|
|
|
@ -15,7 +15,9 @@ import 'package:aves/model/source/album.dart';
|
|||
import 'package:aves/model/source/analysis_controller.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/model/source/events.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
import 'package:aves/model/source/location/location.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/model/source/trash.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
|
@ -54,7 +56,7 @@ mixin SourceBase {
|
|||
void invalidateEntries();
|
||||
}
|
||||
|
||||
abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin, TrashMixin {
|
||||
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, LocationMixin, TagMixin, TrashMixin {
|
||||
CollectionSource() {
|
||||
settings.updateStream.where((event) => event.key == Settings.localeKey).listen((_) => invalidateAlbumDisplayNames());
|
||||
settings.updateStream.where((event) => event.key == Settings.hiddenFiltersKey).listen((event) {
|
||||
|
@ -136,6 +138,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
invalidateEntries();
|
||||
invalidateAlbumFilterSummary(entries: entries, notify: notify);
|
||||
invalidateCountryFilterSummary(entries: entries, notify: notify);
|
||||
invalidatePlaceFilterSummary(entries: entries, notify: notify);
|
||||
invalidateTagFilterSummary(entries: entries, notify: notify);
|
||||
}
|
||||
|
||||
|
@ -501,21 +504,42 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
|
||||
int count(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter) return albumEntryCount(filter);
|
||||
if (filter is LocationFilter) return countryEntryCount(filter);
|
||||
if (filter is LocationFilter) {
|
||||
switch (filter.level) {
|
||||
case LocationLevel.country:
|
||||
return countryEntryCount(filter);
|
||||
case LocationLevel.place:
|
||||
return placeEntryCount(filter);
|
||||
}
|
||||
}
|
||||
if (filter is TagFilter) return tagEntryCount(filter);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int size(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter) return albumSize(filter);
|
||||
if (filter is LocationFilter) return countrySize(filter);
|
||||
if (filter is LocationFilter) {
|
||||
switch (filter.level) {
|
||||
case LocationLevel.country:
|
||||
return countrySize(filter);
|
||||
case LocationLevel.place:
|
||||
return placeSize(filter);
|
||||
}
|
||||
}
|
||||
if (filter is TagFilter) return tagSize(filter);
|
||||
return 0;
|
||||
}
|
||||
|
||||
AvesEntry? recentEntry(CollectionFilter filter) {
|
||||
if (filter is AlbumFilter) return albumRecentEntry(filter);
|
||||
if (filter is LocationFilter) return countryRecentEntry(filter);
|
||||
if (filter is LocationFilter) {
|
||||
switch (filter.level) {
|
||||
case LocationLevel.country:
|
||||
return countryRecentEntry(filter);
|
||||
case LocationLevel.place:
|
||||
return placeRecentEntry(filter);
|
||||
}
|
||||
}
|
||||
if (filter is TagFilter) return tagRecentEntry(filter);
|
||||
return null;
|
||||
}
|
||||
|
|
66
lib/model/source/location/country.dart
Normal file
66
lib/model/source/location/country.dart
Normal file
|
@ -0,0 +1,66 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
mixin CountryMixin on SourceBase {
|
||||
// filter summary
|
||||
|
||||
// by country code
|
||||
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
|
||||
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
|
||||
|
||||
void invalidateCountryFilterSummary({
|
||||
Set<AvesEntry>? entries,
|
||||
Set<String>? countryCodes,
|
||||
bool notify = true,
|
||||
}) {
|
||||
if (_filterEntryCountMap.isEmpty && _filterSizeMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
|
||||
|
||||
if (entries == null && countryCodes == null) {
|
||||
_filterEntryCountMap.clear();
|
||||
_filterSizeMap.clear();
|
||||
_filterRecentEntryMap.clear();
|
||||
} else {
|
||||
countryCodes ??= {};
|
||||
if (entries != null) {
|
||||
countryCodes.addAll(entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull());
|
||||
}
|
||||
countryCodes.forEach((countryCode) {
|
||||
_filterEntryCountMap.remove(countryCode);
|
||||
_filterSizeMap.remove(countryCode);
|
||||
_filterRecentEntryMap.remove(countryCode);
|
||||
});
|
||||
}
|
||||
if (notify) {
|
||||
eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes));
|
||||
}
|
||||
}
|
||||
|
||||
int countryEntryCount(LocationFilter filter) {
|
||||
final countryCode = filter.countryCode;
|
||||
if (countryCode == null) return 0;
|
||||
return _filterEntryCountMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).length);
|
||||
}
|
||||
|
||||
int countrySize(LocationFilter filter) {
|
||||
final countryCode = filter.countryCode;
|
||||
if (countryCode == null) return 0;
|
||||
return _filterSizeMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
|
||||
}
|
||||
|
||||
AvesEntry? countryRecentEntry(LocationFilter filter) {
|
||||
final countryCode = filter.countryCode;
|
||||
if (countryCode == null) return null;
|
||||
return _filterRecentEntryMap.putIfAbsent(countryCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
|
||||
}
|
||||
}
|
||||
|
||||
class CountriesChangedEvent {}
|
||||
|
||||
class CountrySummaryInvalidatedEvent {
|
||||
final Set<String>? countryCodes;
|
||||
|
||||
const CountrySummaryInvalidatedEvent(this.countryCodes);
|
||||
}
|
|
@ -6,15 +6,15 @@ import 'package:aves/model/filters/location.dart';
|
|||
import 'package:aves/model/metadata/address.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/analysis_controller.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
mixin LocationMixin on SourceBase {
|
||||
mixin LocationMixin on CountryMixin, PlaceMixin {
|
||||
static const commitCountThreshold = 200;
|
||||
static const _stopCheckCountThreshold = 50;
|
||||
|
||||
|
@ -150,7 +150,7 @@ mixin LocationMixin on SourceBase {
|
|||
}
|
||||
|
||||
void updateLocations() {
|
||||
final locations = visibleEntries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails).whereNotNull().toList();
|
||||
final locations = visibleEntries.map((entry) => entry.addressDetails).whereNotNull().toList();
|
||||
final updatedPlaces = locations.map((address) => address.place).whereNotNull().where((v) => v.isNotEmpty).toSet().toList()..sort(compareAsciiUpperCase);
|
||||
if (!listEquals(updatedPlaces, sortedPlaces)) {
|
||||
sortedPlaces = List.unmodifiable(updatedPlaces);
|
||||
|
@ -177,67 +177,6 @@ mixin LocationMixin on SourceBase {
|
|||
eventBus.fire(CountriesChangedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
// filter summary
|
||||
|
||||
// by country code
|
||||
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
|
||||
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
|
||||
|
||||
void invalidateCountryFilterSummary({
|
||||
Set<AvesEntry>? entries,
|
||||
Set<String>? countryCodes,
|
||||
bool notify = true,
|
||||
}) {
|
||||
if (_filterEntryCountMap.isEmpty && _filterSizeMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
|
||||
|
||||
if (entries == null && countryCodes == null) {
|
||||
_filterEntryCountMap.clear();
|
||||
_filterSizeMap.clear();
|
||||
_filterRecentEntryMap.clear();
|
||||
} else {
|
||||
countryCodes ??= {};
|
||||
if (entries != null) {
|
||||
countryCodes.addAll(entries.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull());
|
||||
}
|
||||
countryCodes.forEach((countryCode) {
|
||||
_filterEntryCountMap.remove(countryCode);
|
||||
_filterSizeMap.remove(countryCode);
|
||||
_filterRecentEntryMap.remove(countryCode);
|
||||
});
|
||||
}
|
||||
if (notify) {
|
||||
eventBus.fire(CountrySummaryInvalidatedEvent(countryCodes));
|
||||
}
|
||||
}
|
||||
|
||||
int countryEntryCount(LocationFilter filter) {
|
||||
final countryCode = filter.countryCode;
|
||||
if (countryCode == null) return 0;
|
||||
return _filterEntryCountMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).length);
|
||||
}
|
||||
|
||||
int countrySize(LocationFilter filter) {
|
||||
final countryCode = filter.countryCode;
|
||||
if (countryCode == null) return 0;
|
||||
return _filterSizeMap.putIfAbsent(countryCode, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
|
||||
}
|
||||
|
||||
AvesEntry? countryRecentEntry(LocationFilter filter) {
|
||||
final countryCode = filter.countryCode;
|
||||
if (countryCode == null) return null;
|
||||
return _filterRecentEntryMap.putIfAbsent(countryCode, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
|
||||
}
|
||||
}
|
||||
|
||||
class AddressMetadataChangedEvent {}
|
||||
|
||||
class PlacesChangedEvent {}
|
||||
|
||||
class CountriesChangedEvent {}
|
||||
|
||||
class CountrySummaryInvalidatedEvent {
|
||||
final Set<String>? countryCodes;
|
||||
|
||||
const CountrySummaryInvalidatedEvent(this.countryCodes);
|
||||
}
|
60
lib/model/source/location/place.dart
Normal file
60
lib/model/source/location/place.dart
Normal file
|
@ -0,0 +1,60 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
mixin PlaceMixin on SourceBase {
|
||||
// filter summary
|
||||
|
||||
// by place
|
||||
final Map<String, int> _filterEntryCountMap = {}, _filterSizeMap = {};
|
||||
final Map<String, AvesEntry?> _filterRecentEntryMap = {};
|
||||
|
||||
void invalidatePlaceFilterSummary({
|
||||
Set<AvesEntry>? entries,
|
||||
Set<String>? places,
|
||||
bool notify = true,
|
||||
}) {
|
||||
if (_filterEntryCountMap.isEmpty && _filterSizeMap.isEmpty && _filterRecentEntryMap.isEmpty) return;
|
||||
|
||||
if (entries == null && places == null) {
|
||||
_filterEntryCountMap.clear();
|
||||
_filterSizeMap.clear();
|
||||
_filterRecentEntryMap.clear();
|
||||
} else {
|
||||
places ??= {};
|
||||
if (entries != null) {
|
||||
places.addAll(entries.map((entry) => entry.addressDetails?.place).whereNotNull());
|
||||
}
|
||||
places.forEach((place) {
|
||||
_filterEntryCountMap.remove(place);
|
||||
_filterSizeMap.remove(place);
|
||||
_filterRecentEntryMap.remove(place);
|
||||
});
|
||||
}
|
||||
if (notify) {
|
||||
eventBus.fire(PlaceSummaryInvalidatedEvent(places));
|
||||
}
|
||||
}
|
||||
|
||||
int placeEntryCount(LocationFilter filter) {
|
||||
return _filterEntryCountMap.putIfAbsent(filter.place, () => visibleEntries.where(filter.test).length);
|
||||
}
|
||||
|
||||
int placeSize(LocationFilter filter) {
|
||||
return _filterSizeMap.putIfAbsent(filter.place, () => visibleEntries.where(filter.test).map((v) => v.sizeBytes).sum);
|
||||
}
|
||||
|
||||
AvesEntry? placeRecentEntry(LocationFilter filter) {
|
||||
return _filterRecentEntryMap.putIfAbsent(filter.place, () => sortedEntriesByDate.firstWhereOrNull(filter.test));
|
||||
}
|
||||
}
|
||||
|
||||
class PlacesChangedEvent {}
|
||||
|
||||
class PlaceSummaryInvalidatedEvent {
|
||||
final Set<String>? places;
|
||||
|
||||
const PlaceSummaryInvalidatedEvent(this.places);
|
||||
}
|
|
@ -105,7 +105,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
// with items that may be hidden right away because of their metadata
|
||||
addEntries(knownEntries, notify: false);
|
||||
|
||||
await _addVaultEntries(directory);
|
||||
await _loadVaultEntries(directory);
|
||||
|
||||
debugPrint('$runtimeType refresh ${stopwatch.elapsed} load metadata');
|
||||
if (directory != null) {
|
||||
|
@ -266,6 +266,13 @@ class MediaStoreSource extends CollectionSource {
|
|||
}
|
||||
}
|
||||
|
||||
await _refreshVaultEntries(
|
||||
changedUris: changedUris.where(vaults.isVaultEntryUri).toSet(),
|
||||
newEntries: newEntries,
|
||||
entriesToRefresh: entriesToRefresh,
|
||||
existingDirectories: existingDirectories,
|
||||
);
|
||||
|
||||
invalidateAlbumFilterSummary(directories: existingDirectories);
|
||||
|
||||
if (newEntries.isNotEmpty) {
|
||||
|
@ -278,21 +285,21 @@ class MediaStoreSource extends CollectionSource {
|
|||
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
|
||||
}
|
||||
|
||||
await _refreshVaultEntries(changedUris.where(vaults.isVaultEntryUri).toSet());
|
||||
|
||||
return tempUris;
|
||||
}
|
||||
|
||||
// vault
|
||||
|
||||
Future<void> _addVaultEntries(String? directory) async {
|
||||
Future<void> _loadVaultEntries(String? directory) async {
|
||||
addEntries(await metadataDb.loadEntries(origin: EntryOrigins.vault, directory: directory));
|
||||
}
|
||||
|
||||
Future<void> _refreshVaultEntries(Set<String> changedUris) async {
|
||||
final entriesToRefresh = <AvesEntry>{};
|
||||
final existingDirectories = <String>{};
|
||||
|
||||
Future<void> _refreshVaultEntries({
|
||||
required Set<String> changedUris,
|
||||
required Set<AvesEntry> newEntries,
|
||||
required Set<AvesEntry> entriesToRefresh,
|
||||
required Set<String> existingDirectories,
|
||||
}) async {
|
||||
for (final uri in changedUris) {
|
||||
final existingEntry = allEntries.firstWhereOrNull((entry) => entry.uri == uri);
|
||||
if (existingEntry != null) {
|
||||
|
@ -301,13 +308,15 @@ class MediaStoreSource extends CollectionSource {
|
|||
if (existingDirectory != null) {
|
||||
existingDirectories.add(existingDirectory);
|
||||
}
|
||||
} else {
|
||||
final sourceEntry = await mediaFetchService.getEntry(uri, null);
|
||||
if (sourceEntry != null) {
|
||||
newEntries.add(sourceEntry.copyWith(
|
||||
id: metadataDb.nextId,
|
||||
origin: EntryOrigins.vault,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalidateAlbumFilterSummary(directories: existingDirectories);
|
||||
|
||||
if (entriesToRefresh.isNotEmpty) {
|
||||
await refreshEntries(entriesToRefresh, EntryDataType.values.toSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,7 +154,10 @@ class Vaults extends ChangeNotifier {
|
|||
localizedReason: context.l10n.authenticateToUnlockVault,
|
||||
);
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
if (e.code != 'auth_in_progress') {
|
||||
// `auth_in_progress`: `Authentication in progress`
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case VaultLockType.pin:
|
||||
|
|
|
@ -27,9 +27,9 @@ class OptionalEventChannel extends EventChannel {
|
|||
});
|
||||
try {
|
||||
await methodChannel.invokeMethod<void>('listen', arguments);
|
||||
} catch (exception, stack) {
|
||||
} catch (error, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
exception: error,
|
||||
stack: stack,
|
||||
library: 'services library',
|
||||
context: ErrorDescription('while activating platform stream on channel $name'),
|
||||
|
@ -39,9 +39,14 @@ class OptionalEventChannel extends EventChannel {
|
|||
binaryMessenger.setMessageHandler(name, null);
|
||||
try {
|
||||
await methodChannel.invokeMethod<void>('cancel', arguments);
|
||||
} catch (exception, stack) {
|
||||
} catch (error, stack) {
|
||||
if (error is PlatformException && error.message == 'No active stream to cancel') {
|
||||
// ignore
|
||||
return;
|
||||
}
|
||||
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
exception: error,
|
||||
stack: stack,
|
||||
library: 'services library',
|
||||
context: ErrorDescription('while de-activating platform stream on channel $name'),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/metadata/enums/enums.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
|
@ -28,7 +29,7 @@ abstract class MediaEditService {
|
|||
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required EntryConvertOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
});
|
||||
|
@ -113,18 +114,20 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
@override
|
||||
Stream<ExportOpEvent> export(
|
||||
Iterable<AvesEntry> entries, {
|
||||
required EntryExportOptions options,
|
||||
required EntryConvertOptions options,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
}) {
|
||||
try {
|
||||
return _opStream
|
||||
.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'export',
|
||||
'op': 'convert',
|
||||
'entries': entries.map((entry) => entry.toPlatformEntryMap()).toList(),
|
||||
'mimeType': options.mimeType,
|
||||
'lengthUnit': options.lengthUnit.name,
|
||||
'width': options.width,
|
||||
'height': options.height,
|
||||
'writeMetadata': options.writeMetadata,
|
||||
'destinationPath': destinationAlbum,
|
||||
'nameConflictStrategy': nameConflictStrategy.toPlatform(),
|
||||
})
|
||||
|
@ -183,15 +186,19 @@ class PlatformMediaEditService implements MediaEditService {
|
|||
}
|
||||
|
||||
@immutable
|
||||
class EntryExportOptions extends Equatable {
|
||||
class EntryConvertOptions extends Equatable {
|
||||
final String mimeType;
|
||||
final bool writeMetadata;
|
||||
final LengthUnit lengthUnit;
|
||||
final int width, height;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mimeType, width, height];
|
||||
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height];
|
||||
|
||||
const EntryExportOptions({
|
||||
const EntryConvertOptions({
|
||||
required this.mimeType,
|
||||
required this.writeMetadata,
|
||||
required this.lengthUnit,
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
|
|
@ -36,6 +36,8 @@ class AIcons {
|
|||
static const IconData language = Icons.translate_outlined;
|
||||
static const IconData location = Icons.place_outlined;
|
||||
static const IconData locationUnlocated = Icons.location_off_outlined;
|
||||
static const IconData country = Icons.flag_outlined;
|
||||
static const IconData place = Icons.place_outlined;
|
||||
static const IconData mainStorage = Icons.smartphone_outlined;
|
||||
static const IconData mimeType = Icons.code_outlined;
|
||||
static const IconData opacity = Icons.opacity;
|
||||
|
@ -81,8 +83,6 @@ class AIcons {
|
|||
static const IconData debug = Icons.whatshot_outlined;
|
||||
static const IconData delete = Icons.delete_outlined;
|
||||
static const IconData edit = Icons.edit_outlined;
|
||||
static const IconData editRating = MdiIcons.starPlusOutline;
|
||||
static const IconData editTags = MdiIcons.tagPlusOutline;
|
||||
static const IconData emptyBin = Icons.delete_sweep_outlined;
|
||||
static const IconData export = Icons.open_with_outlined;
|
||||
static const IconData fileExport = MdiIcons.fileExportOutline;
|
||||
|
|
|
@ -36,7 +36,7 @@ DateTime? dateTimeFromMillis(int? millis, {bool isUtc = false}) {
|
|||
if (millis == null || millis == 0) return null;
|
||||
try {
|
||||
return DateTime.fromMillisecondsSinceEpoch(millis, isUtc: isUtc);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// `DateTime`s can represent time values that are at a distance of at most 100,000,000
|
||||
// days from epoch (1970-01-01 UTC): -271821-04-20 to 275760-09-13.
|
||||
debugPrint('failed to build DateTime from timestamp in millis=$millis');
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/flutter_version.dart';
|
||||
|
@ -141,6 +142,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
|||
}
|
||||
|
||||
Future<String> _getInfo(BuildContext context) async {
|
||||
final accessibility = window.accessibilityFeatures;
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
final flavor = context.read<AppFlavor>().toString().split('.')[1];
|
||||
|
@ -159,6 +161,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
|||
'System locales: ${WidgetsBinding.instance.window.locales.join(', ')}',
|
||||
'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}',
|
||||
'Installer: ${packageInfo.installerStore}',
|
||||
'Accessibility: accessibleNavigation=${accessibility.accessibleNavigation}, disableAnimations=${accessibility.disableAnimations}',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ class AboutTranslators extends StatelessWidget {
|
|||
Contributor('rehork', 'cooky@e.email'),
|
||||
Contributor('Eric', 'hamburger2048@users.noreply.hosted.weblate.org'),
|
||||
Contributor('Aitor Salaberria', 'trslbrr@gmail.com'),
|
||||
Contributor('Felipe Nogueira', 'contato.fnog@gmail.com'),
|
||||
Contributor('kaajjo', 'claymanoff@gmail.com'),
|
||||
// Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic
|
||||
// Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic
|
||||
// Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian
|
||||
|
@ -55,6 +57,7 @@ class AboutTranslators extends StatelessWidget {
|
|||
// Contributor('Nattapong K', 'mixer5056@gmail.com'), // Thai
|
||||
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
||||
// Contributor('Martin Frandel', 'martinko.fr@gmail.com'), // Slovak
|
||||
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
|
||||
};
|
||||
|
||||
@override
|
||||
|
|
|
@ -55,7 +55,8 @@ class AvesApp extends StatefulWidget {
|
|||
final AppFlavor flavor;
|
||||
|
||||
// temporary exclude locales not ready yet for prime time
|
||||
static final _unsupportedLocales = {'ar', 'fa', 'gl', 'he', 'nn', 'sk', 'th'}.map(Locale.new).toSet();
|
||||
// `ckb`: add `flutter_ckb_localization` and necessary app localization delegates when ready
|
||||
static final _unsupportedLocales = {'ar', 'ckb', 'fa', 'gl', 'he', 'nn', 'sk', 'th'}.map(Locale.new).toSet();
|
||||
static final List<Locale> supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList();
|
||||
static final ValueNotifier<EdgeInsets> cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero);
|
||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||
|
|
|
@ -32,6 +32,7 @@ import 'package:aves/widgets/common/tile_extent_controller.dart';
|
|||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -342,7 +343,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
return [
|
||||
...EntrySetActions.general,
|
||||
...isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing,
|
||||
].where(isVisible).map((action) {
|
||||
].whereNotNull().where(isVisible).map((action) {
|
||||
final enabled = canApply(action);
|
||||
return CaptionedButton(
|
||||
iconButtonBuilder: (context, focusNode) => _buildButtonIcon(
|
||||
|
@ -388,10 +389,21 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
|
||||
final browsingMenuActions = EntrySetActions.pageBrowsing.where((v) => !browsingQuickActions.contains(v));
|
||||
final selectionMenuActions = EntrySetActions.pageSelection.where((v) => !selectionQuickActions.contains(v));
|
||||
final contextualMenuItems = [
|
||||
...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
|
||||
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
|
||||
),
|
||||
final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<EntrySetAction?>[], (prev, v) {
|
||||
if (v == null && (prev.isEmpty || prev.last == null)) return prev;
|
||||
return [...prev, v];
|
||||
});
|
||||
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) {
|
||||
contextualMenuActions.removeLast();
|
||||
}
|
||||
|
||||
final contextualMenuItems = <PopupMenuEntry<EntrySetAction>>[
|
||||
...contextualMenuActions.map(
|
||||
(action) {
|
||||
if (action == null) return const PopupMenuDivider();
|
||||
return _toMenuItem(action, enabled: canApply(action), selection: selection);
|
||||
},
|
||||
),
|
||||
if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
|
||||
PopupMenuItem<EntrySetAction>(
|
||||
enabled: hasSelection,
|
||||
|
@ -630,6 +642,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
case EntrySetAction.copy:
|
||||
case EntrySetAction.move:
|
||||
case EntrySetAction.rename:
|
||||
case EntrySetAction.convert:
|
||||
case EntrySetAction.toggleFavourite:
|
||||
case EntrySetAction.rotateCCW:
|
||||
case EntrySetAction.rotateCW:
|
||||
|
|
|
@ -94,6 +94,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.copy:
|
||||
case EntrySetAction.move:
|
||||
case EntrySetAction.rename:
|
||||
case EntrySetAction.convert:
|
||||
case EntrySetAction.rotateCCW:
|
||||
case EntrySetAction.rotateCW:
|
||||
case EntrySetAction.flip:
|
||||
|
@ -145,6 +146,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.copy:
|
||||
case EntrySetAction.move:
|
||||
case EntrySetAction.rename:
|
||||
case EntrySetAction.convert:
|
||||
case EntrySetAction.toggleFavourite:
|
||||
case EntrySetAction.rotateCCW:
|
||||
case EntrySetAction.rotateCW:
|
||||
|
@ -211,6 +213,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
case EntrySetAction.rename:
|
||||
_rename(context);
|
||||
break;
|
||||
case EntrySetAction.convert:
|
||||
_convert(context);
|
||||
break;
|
||||
case EntrySetAction.toggleFavourite:
|
||||
_toggleFavourite(context);
|
||||
break;
|
||||
|
@ -379,6 +384,13 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
_browse(context);
|
||||
}
|
||||
|
||||
void _convert(BuildContext context) {
|
||||
final entries = _getTargetItems(context);
|
||||
convert(context, entries);
|
||||
|
||||
_browse(context);
|
||||
}
|
||||
|
||||
Future<void> _toggleFavourite(BuildContext context) async {
|
||||
final entries = _getTargetItems(context);
|
||||
if (entries.every((entry) => entry.isFavourite)) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
|
@ -27,6 +28,7 @@ import 'package:aves/widgets/common/action_mixins/size_aware.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
|
||||
import 'package:aves/widgets/viewer/notifications.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -34,6 +36,100 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
Future<void> convert(BuildContext context, Set<AvesEntry> targetEntries) async {
|
||||
final options = await showDialog<EntryConvertOptions>(
|
||||
context: context,
|
||||
builder: (context) => ConvertEntryDialog(entries: targetEntries),
|
||||
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
|
||||
);
|
||||
if (options == null) return;
|
||||
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
|
||||
if (destinationAlbum == null) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
||||
if (!await checkFreeSpaceForMove(context, targetEntries, destinationAlbum, MoveType.export)) return;
|
||||
|
||||
final selection = <AvesEntry>{};
|
||||
await Future.forEach(targetEntries, (targetEntry) async {
|
||||
if (targetEntry.isMultiPage) {
|
||||
final multiPageInfo = await targetEntry.getMultiPageInfo();
|
||||
if (multiPageInfo != null) {
|
||||
if (targetEntry.isMotionPhoto) {
|
||||
await multiPageInfo.extractMotionPhotoVideo();
|
||||
}
|
||||
if (multiPageInfo.pageCount > 1) {
|
||||
selection.addAll(multiPageInfo.exportEntries);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selection.add(targetEntry);
|
||||
}
|
||||
});
|
||||
|
||||
final selectionCount = selection.length;
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
await showOpReport<ExportOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaEditService.export(
|
||||
selection,
|
||||
options: options,
|
||||
destinationAlbum: destinationAlbum,
|
||||
nameConflictStrategy: NameConflictStrategy.rename,
|
||||
),
|
||||
itemCount: selectionCount,
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final exportedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
final newUris = exportedOps.map((v) => v.newFields['uri'] as String?).whereNotNull().toSet();
|
||||
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
|
||||
|
||||
source.resumeMonitoring();
|
||||
unawaited(source.refreshUris(newUris));
|
||||
|
||||
final l10n = context.l10n;
|
||||
final showAction = isMainMode && newUris.isNotEmpty
|
||||
? SnackBarAction(
|
||||
label: l10n.showButtonLabel,
|
||||
onPressed: () {
|
||||
// local context may be deactivated when action is triggered after navigation
|
||||
final context = AvesApp.navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: source,
|
||||
filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))},
|
||||
highlightTest: (entry) => newUris.contains(entry.uri),
|
||||
),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: null;
|
||||
final successCount = successOps.length;
|
||||
if (successCount < selectionCount) {
|
||||
final count = selectionCount - successCount;
|
||||
showFeedback(
|
||||
context,
|
||||
l10n.collectionExportFailureFeedback(count),
|
||||
showAction,
|
||||
);
|
||||
} else {
|
||||
showFeedback(
|
||||
context,
|
||||
l10n.genericSuccessFeedback,
|
||||
showAction,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> doQuickMove(
|
||||
BuildContext context, {
|
||||
required MoveType moveType,
|
||||
|
|
|
@ -26,7 +26,7 @@ mixin FeedbackMixin {
|
|||
ScaffoldMessengerState? scaffoldMessenger;
|
||||
try {
|
||||
scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// minor issue: the page triggering this feedback likely
|
||||
// allows the user to navigate away and they did so
|
||||
debugPrint('failed to find ScaffoldMessenger in context');
|
||||
|
|
14
lib/widgets/common/fx/transitions.dart
Normal file
14
lib/widgets/common/fx/transitions.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class AvesTransitions {
|
||||
static Widget formTransitionBuilder(Widget child, Animation<double> animation) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -96,6 +96,7 @@ class AvesFilterChip extends StatefulWidget {
|
|||
final actions = [
|
||||
if (filter is AlbumFilter) ChipAction.goToAlbumPage,
|
||||
if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage,
|
||||
if ((filter is LocationFilter && filter.level == LocationLevel.place)) ChipAction.goToPlacePage,
|
||||
if (filter is TagFilter) ChipAction.goToTagPage,
|
||||
ChipAction.reverse,
|
||||
ChipAction.hide,
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:aves/widgets/collection/collection_page.dart';
|
|||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/viewer/info/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -51,6 +52,7 @@ class DebugSettingsSection extends StatelessWidget {
|
|||
'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}',
|
||||
'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}',
|
||||
'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}',
|
||||
'tileExtent - Places': '${settings.getTileExtent(PlaceListPage.routeName)}',
|
||||
'tileExtent - Tags': '${settings.getTileExtent(TagListPage.routeName)}',
|
||||
'infoMapZoom': '${settings.infoMapZoom}',
|
||||
'collectionSelectionQuickActions': '${settings.collectionSelectionQuickActions}',
|
||||
|
|
271
lib/widgets/dialogs/convert_entry_dialog.dart
Normal file
271
lib/widgets/dialogs/convert_entry_dialog.dart
Normal file
|
@ -0,0 +1,271 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/metadata/enums/enums.dart';
|
||||
import 'package:aves/model/metadata/enums/length_unit.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/utils/mime_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'aves_dialog.dart';
|
||||
|
||||
class ConvertEntryDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/convert_entry';
|
||||
|
||||
final Set<AvesEntry> entries;
|
||||
|
||||
const ConvertEntryDialog({
|
||||
super.key,
|
||||
required this.entries,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ConvertEntryDialog> createState() => _ConvertEntryDialogState();
|
||||
}
|
||||
|
||||
class _ConvertEntryDialogState extends State<ConvertEntryDialog> {
|
||||
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
late ValueNotifier<String> _mimeTypeNotifier;
|
||||
late bool _writeMetadata, _sameSized;
|
||||
late List<LengthUnit> _lengthUnitOptions;
|
||||
late LengthUnit _lengthUnit;
|
||||
|
||||
Set<AvesEntry> get entries => widget.entries;
|
||||
|
||||
static const imageExportFormats = [
|
||||
MimeTypes.bmp,
|
||||
MimeTypes.jpeg,
|
||||
MimeTypes.png,
|
||||
MimeTypes.webp,
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_mimeTypeNotifier = ValueNotifier(settings.convertMimeType);
|
||||
_writeMetadata = settings.convertWriteMetadata;
|
||||
_sameSized = entries.map((entry) => entry.displaySize).toSet().length == 1;
|
||||
_lengthUnitOptions = [
|
||||
if (_sameSized) LengthUnit.px,
|
||||
LengthUnit.percent,
|
||||
];
|
||||
_lengthUnit = _lengthUnitOptions.first;
|
||||
_initDimensions();
|
||||
_validate();
|
||||
}
|
||||
|
||||
void _initDimensions() {
|
||||
switch (_lengthUnit) {
|
||||
case LengthUnit.px:
|
||||
final displaySize = entries.first.displaySize;
|
||||
_widthController.text = '${displaySize.width.round()}';
|
||||
_heightController.text = '${displaySize.height.round()}';
|
||||
break;
|
||||
case LengthUnit.percent:
|
||||
_widthController.text = '100';
|
||||
_heightController.text = '100';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_widthController.dispose();
|
||||
_heightController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding);
|
||||
|
||||
// used by the drop down to match input decoration
|
||||
final textFieldDecorationBorder = Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.38), //Color(0xFFBDBDBD),
|
||||
width: 1.0,
|
||||
),
|
||||
);
|
||||
|
||||
return AvesDialog(
|
||||
scrollableContent: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: contentHorizontalPadding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(l10n.exportEntryDialogFormat),
|
||||
const SizedBox(width: AvesDialog.controlCaptionPadding),
|
||||
TextDropdownButton<String>(
|
||||
values: imageExportFormats,
|
||||
valueText: MimeUtils.displayType,
|
||||
value: _mimeTypeNotifier.value,
|
||||
onChanged: (selected) {
|
||||
if (selected != null) {
|
||||
setState(() => _mimeTypeNotifier.value = selected);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: contentHorizontalPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _widthController,
|
||||
decoration: InputDecoration(labelText: l10n.exportEntryDialogWidth),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final width = int.tryParse(value);
|
||||
if (width != null) {
|
||||
switch (_lengthUnit) {
|
||||
case LengthUnit.px:
|
||||
_heightController.text = '${(width / entries.first.displayAspectRatio).round()}';
|
||||
break;
|
||||
case LengthUnit.percent:
|
||||
_heightController.text = '$width';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
_heightController.text = '';
|
||||
}
|
||||
_validate();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(AvesEntry.resolutionSeparator),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _heightController,
|
||||
decoration: InputDecoration(labelText: l10n.exportEntryDialogHeight),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final height = int.tryParse(value);
|
||||
if (height != null) {
|
||||
switch (_lengthUnit) {
|
||||
case LengthUnit.px:
|
||||
_widthController.text = '${(height * entries.first.displayAspectRatio).round()}';
|
||||
break;
|
||||
case LengthUnit.percent:
|
||||
_widthController.text = '$height';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
_widthController.text = '';
|
||||
}
|
||||
_validate();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
TextDropdownButton<LengthUnit>(
|
||||
values: _lengthUnitOptions,
|
||||
valueText: (v) => v.getText(context),
|
||||
value: _lengthUnit,
|
||||
onChanged: _lengthUnitOptions.length > 1
|
||||
? (v) {
|
||||
if (v != null && _lengthUnit != v) {
|
||||
_lengthUnit = v;
|
||||
_initDimensions();
|
||||
_validate();
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
underline: Container(
|
||||
height: 1.0,
|
||||
decoration: BoxDecoration(
|
||||
border: textFieldDecorationBorder,
|
||||
),
|
||||
),
|
||||
itemHeight: 60,
|
||||
dropdownColor: Themes.thirdLayerColor(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _mimeTypeNotifier,
|
||||
builder: (context, mimeType, child) {
|
||||
Widget child;
|
||||
if (MimeTypes.canEditExif(mimeType) || MimeTypes.canEditIptc(mimeType) || MimeTypes.canEditXmp(mimeType)) {
|
||||
child = SwitchListTile(
|
||||
value: _writeMetadata,
|
||||
onChanged: (v) => setState(() => _writeMetadata = v),
|
||||
title: Text(context.l10n.exportEntryDialogWriteMetadata),
|
||||
contentPadding: const EdgeInsetsDirectional.only(
|
||||
start: AvesDialog.defaultHorizontalContentPadding,
|
||||
end: AvesDialog.defaultHorizontalContentPadding - 8,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
child = const SizedBox(height: 16);
|
||||
}
|
||||
return AnimatedSwitcher(
|
||||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _isValidNotifier,
|
||||
builder: (context, isValid, child) {
|
||||
return TextButton(
|
||||
onPressed: isValid
|
||||
? () {
|
||||
final width = int.tryParse(_widthController.text);
|
||||
final height = int.tryParse(_heightController.text);
|
||||
final options = (width != null && height != null)
|
||||
? EntryConvertOptions(
|
||||
mimeType: _mimeTypeNotifier.value,
|
||||
writeMetadata: _writeMetadata,
|
||||
lengthUnit: _lengthUnit,
|
||||
width: width,
|
||||
height: height,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (options != null) {
|
||||
settings.convertMimeType = options.mimeType;
|
||||
settings.convertWriteMetadata = options.writeMetadata;
|
||||
}
|
||||
|
||||
Navigator.maybeOf(context)?.pop(options);
|
||||
}
|
||||
: null,
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _validate() async {
|
||||
final width = int.tryParse(_widthController.text);
|
||||
final height = int.tryParse(_heightController.text);
|
||||
_isValidNotifier.value = (width ?? 0) > 0 && (height ?? 0) > 0;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import 'package:aves/utils/time_utils.dart';
|
|||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/basic/wheel.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
|
@ -111,7 +112,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: _formTransitionBuilder,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: Column(
|
||||
key: ValueKey(_action),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -143,15 +144,6 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _formTransitionBuilder(Widget child, Animation<double> animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildSetCustomContent(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final locale = l10n.localeName;
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/theme/themes.dart';
|
|||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/item_picker.dart';
|
||||
|
@ -114,7 +115,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: _formTransitionBuilder,
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: Column(
|
||||
key: ValueKey(_action),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -145,15 +146,6 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _formTransitionBuilder(Widget child, Animation<double> animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildChooseOnMapContent(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
|
@ -297,7 +289,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
|||
double? tryParse(String text) {
|
||||
try {
|
||||
return double.tryParse(text) ?? (coordinateFormatter.parse(text).toDouble());
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// ignore
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/utils/mime_utils.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'aves_dialog.dart';
|
||||
|
||||
class ExportEntryDialog extends StatefulWidget {
|
||||
static const routeName = '/dialog/export_entry';
|
||||
|
||||
final AvesEntry entry;
|
||||
|
||||
const ExportEntryDialog({
|
||||
super.key,
|
||||
required this.entry,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ExportEntryDialog> createState() => _ExportEntryDialogState();
|
||||
}
|
||||
|
||||
class _ExportEntryDialogState extends State<ExportEntryDialog> {
|
||||
final TextEditingController _widthController = TextEditingController(), _heightController = TextEditingController();
|
||||
final ValueNotifier<bool> _isValidNotifier = ValueNotifier(false);
|
||||
String _mimeType = MimeTypes.jpeg;
|
||||
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
||||
static const imageExportFormats = [
|
||||
MimeTypes.bmp,
|
||||
MimeTypes.jpeg,
|
||||
MimeTypes.png,
|
||||
MimeTypes.webp,
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_widthController.text = '${entry.isRotated ? entry.height : entry.width}';
|
||||
_heightController.text = '${entry.isRotated ? entry.width : entry.height}';
|
||||
_validate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_widthController.dispose();
|
||||
_heightController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: AvesDialog.defaultHorizontalContentPadding);
|
||||
|
||||
return AvesDialog(
|
||||
scrollableContent: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: contentHorizontalPadding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(l10n.exportEntryDialogFormat),
|
||||
const SizedBox(width: AvesDialog.controlCaptionPadding),
|
||||
TextDropdownButton<String>(
|
||||
values: imageExportFormats,
|
||||
valueText: MimeUtils.displayType,
|
||||
value: _mimeType,
|
||||
onChanged: (selected) {
|
||||
if (selected != null) {
|
||||
setState(() => _mimeType = selected);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: contentHorizontalPadding,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||
textBaseline: TextBaseline.alphabetic,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _widthController,
|
||||
decoration: InputDecoration(labelText: l10n.exportEntryDialogWidth),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final width = int.tryParse(value);
|
||||
_heightController.text = width != null ? '${(width / entry.displayAspectRatio).round()}' : '';
|
||||
_validate();
|
||||
},
|
||||
),
|
||||
),
|
||||
const Text(AvesEntry.resolutionSeparator),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _heightController,
|
||||
decoration: InputDecoration(labelText: l10n.exportEntryDialogHeight),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final height = int.tryParse(value);
|
||||
_widthController.text = height != null ? '${(height * entry.displayAspectRatio).round()}' : '';
|
||||
_validate();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _isValidNotifier,
|
||||
builder: (context, isValid, child) {
|
||||
return TextButton(
|
||||
onPressed: isValid
|
||||
? () {
|
||||
final width = int.tryParse(_widthController.text);
|
||||
final height = int.tryParse(_heightController.text);
|
||||
final options = (width != null && height != null)
|
||||
? EntryExportOptions(
|
||||
mimeType: _mimeType,
|
||||
width: width,
|
||||
height: height,
|
||||
)
|
||||
: null;
|
||||
Navigator.maybeOf(context)?.pop(options);
|
||||
}
|
||||
: null,
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _validate() async {
|
||||
final width = int.tryParse(_widthController.text);
|
||||
final height = int.tryParse(_heightController.text);
|
||||
_isValidNotifier.value = (width ?? 0) > 0 && (height ?? 0) > 0;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart';
|
|||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_caption.dart';
|
||||
import 'package:aves/widgets/common/identity/highlight_title.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
|
@ -98,14 +99,7 @@ class _TileViewDialogState<S, G, L> extends State<TileViewDialog<S, G, L>> with
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: _buildSection(
|
||||
show: canGroup,
|
||||
icon: AIcons.group,
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -23,6 +24,7 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
|
|||
switch (action) {
|
||||
case ChipAction.goToAlbumPage:
|
||||
case ChipAction.goToCountryPage:
|
||||
case ChipAction.goToPlacePage:
|
||||
case ChipAction.goToTagPage:
|
||||
case ChipAction.reverse:
|
||||
return true;
|
||||
|
@ -42,6 +44,9 @@ class ChipActionDelegate with FeedbackMixin, VaultAwareMixin {
|
|||
case ChipAction.goToCountryPage:
|
||||
_goTo(context, filter, CountryListPage.routeName, (context) => const CountryListPage());
|
||||
break;
|
||||
case ChipAction.goToPlacePage:
|
||||
_goTo(context, filter, PlaceListPage.routeName, (context) => const PlaceListPage());
|
||||
break;
|
||||
case ChipAction.goToTagPage:
|
||||
_goTo(context, filter, TagListPage.routeName, (context) => const TagListPage());
|
||||
break;
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
|
||||
class PlaceChipSetActionDelegate extends ChipSetActionDelegate<LocationFilter> {
|
||||
final Iterable<FilterGridItem<LocationFilter>> _items;
|
||||
|
||||
PlaceChipSetActionDelegate(Iterable<FilterGridItem<LocationFilter>> items) : _items = items;
|
||||
|
||||
@override
|
||||
Iterable<FilterGridItem<LocationFilter>> get allItems => _items;
|
||||
|
||||
@override
|
||||
ChipSortFactor get sortFactor => settings.placeSortFactor;
|
||||
|
||||
@override
|
||||
set sortFactor(ChipSortFactor factor) => settings.placeSortFactor = factor;
|
||||
|
||||
@override
|
||||
bool get sortReverse => settings.placeSortReverse;
|
||||
|
||||
@override
|
||||
set sortReverse(bool value) => settings.placeSortReverse = value;
|
||||
|
||||
@override
|
||||
TileLayout get tileLayout => settings.getTileLayout(PlaceListPage.routeName);
|
||||
|
||||
@override
|
||||
set tileLayout(TileLayout tileLayout) => settings.setTileLayout(PlaceListPage.routeName, tileLayout);
|
||||
}
|
|
@ -327,9 +327,13 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
|
|||
|
||||
final browsingMenuActions = ChipSetActions.browsing.where((v) => !browsingQuickActions.contains(v));
|
||||
final selectionMenuActions = ChipSetActions.selection.where((v) => !selectionQuickActions.contains(v));
|
||||
final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).toList();
|
||||
if (contextualMenuActions.isNotEmpty && contextualMenuActions.first == null) contextualMenuActions.removeAt(0);
|
||||
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) contextualMenuActions.removeLast();
|
||||
final contextualMenuActions = (isSelecting ? selectionMenuActions : browsingMenuActions).where((v) => v == null || isVisible(v)).fold(<ChipSetAction?>[], (prev, v) {
|
||||
if (v == null && (prev.isEmpty || prev.last == null)) return prev;
|
||||
return [...prev, v];
|
||||
});
|
||||
if (contextualMenuActions.isNotEmpty && contextualMenuActions.last == null) {
|
||||
contextualMenuActions.removeLast();
|
||||
}
|
||||
|
||||
return [
|
||||
...generalMenuItems,
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:aves/model/filters/location.dart';
|
|||
import 'package:aves/model/filters/tag.dart';
|
||||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
|
|
|
@ -3,7 +3,7 @@ import 'package:aves/model/filters/location.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
|
@ -43,7 +43,7 @@ class CountryListPage extends StatelessWidget {
|
|||
filterSections: _groupToSections(gridItems),
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.location,
|
||||
icon: AIcons.country,
|
||||
text: context.l10n.countryEmpty,
|
||||
),
|
||||
);
|
||||
|
|
80
lib/widgets/filter_grids/places_page.dart
Normal file
80
lib/widgets/filter_grids/places_page.dart
Normal file
|
@ -0,0 +1,80 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/filters/location.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/place_set.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/section_keys.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class PlaceListPage extends StatelessWidget {
|
||||
static const routeName = '/places';
|
||||
|
||||
const PlaceListPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
return Selector<Settings, Tuple3<ChipSortFactor, bool, Set<CollectionFilter>>>(
|
||||
selector: (context, s) => Tuple3(s.placeSortFactor, s.placeSortReverse, s.pinnedFilters),
|
||||
shouldRebuild: (t1, t2) {
|
||||
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within `TupleN`
|
||||
const eq = DeepCollectionEquality();
|
||||
return !(eq.equals(t1.item1, t2.item1) && eq.equals(t1.item2, t2.item2) && eq.equals(t1.item3, t2.item3));
|
||||
},
|
||||
builder: (context, s, child) {
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<PlacesChangedEvent>(),
|
||||
builder: (context, snapshot) {
|
||||
final gridItems = _getGridItems(source);
|
||||
return FilterNavigationPage<LocationFilter, PlaceChipSetActionDelegate>(
|
||||
source: source,
|
||||
title: context.l10n.placePageTitle,
|
||||
sortFactor: settings.placeSortFactor,
|
||||
actionDelegate: PlaceChipSetActionDelegate(gridItems),
|
||||
filterSections: _groupToSections(gridItems),
|
||||
applyQuery: applyQuery,
|
||||
emptyBuilder: () => EmptyContent(
|
||||
icon: AIcons.place,
|
||||
text: context.l10n.placeEmpty,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<FilterGridItem<LocationFilter>> applyQuery(BuildContext context, List<FilterGridItem<LocationFilter>> filters, String query) {
|
||||
return filters.where((item) => item.filter.getLabel(context).toUpperCase().contains(query)).toList();
|
||||
}
|
||||
|
||||
List<FilterGridItem<LocationFilter>> _getGridItems(CollectionSource source) {
|
||||
final filters = source.sortedPlaces.map((location) => LocationFilter(LocationLevel.place, location)).toSet();
|
||||
|
||||
return FilterNavigationPage.sort(settings.placeSortFactor, settings.placeSortReverse, source, filters);
|
||||
}
|
||||
|
||||
static Map<ChipSectionKey, List<FilterGridItem<LocationFilter>>> _groupToSections(Iterable<FilterGridItem<LocationFilter>> sortedMapEntries) {
|
||||
final pinned = settings.pinnedFilters.whereType<LocationFilter>();
|
||||
final byPin = groupBy<FilterGridItem<LocationFilter>, bool>(sortedMapEntries, (e) => pinned.contains(e.filter));
|
||||
final pinnedMapEntries = (byPin[true] ?? []);
|
||||
final unpinnedMapEntries = (byPin[false] ?? []);
|
||||
|
||||
return {
|
||||
if (pinnedMapEntries.isNotEmpty || unpinnedMapEntries.isNotEmpty)
|
||||
const ChipSectionKey(): [
|
||||
...pinnedMapEntries,
|
||||
...unpinnedMapEntries,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
|
@ -6,7 +6,8 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -19,6 +20,7 @@ import 'package:aves/widgets/common/identity/aves_logo.dart';
|
|||
import 'package:aves/widgets/debug/app_debug_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/collection_nav_tile.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/page_nav_tile.dart';
|
||||
|
@ -242,6 +244,12 @@ class _AppDrawerState extends State<AppDrawer> {
|
|||
builder: (context, _) => Text('${source.sortedCountries.length}'),
|
||||
);
|
||||
break;
|
||||
case PlaceListPage.routeName:
|
||||
trailing = StreamBuilder(
|
||||
stream: source.eventBus.on<PlacesChangedEvent>(),
|
||||
builder: (context, _) => Text('${source.sortedPlaces.length}'),
|
||||
);
|
||||
break;
|
||||
case TagListPage.routeName:
|
||||
trailing = StreamBuilder(
|
||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/widgets/common/search/route.dart';
|
|||
import 'package:aves/widgets/debug/app_debug_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/tile.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
|
@ -88,6 +89,8 @@ class PageNavTile extends StatelessWidget {
|
|||
return (_) => const AlbumListPage();
|
||||
case CountryListPage.routeName:
|
||||
return (_) => const CountryListPage();
|
||||
case PlaceListPage.routeName:
|
||||
return (_) => const PlaceListPage();
|
||||
case TagListPage.routeName:
|
||||
return (_) => const TagListPage();
|
||||
case SettingsPage.routeName:
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/widgets/common/search/page.dart';
|
|||
import 'package:aves/widgets/debug/app_debug_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/settings/settings_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -35,6 +36,8 @@ class NavigationDisplay {
|
|||
return l10n.drawerAlbumPage;
|
||||
case CountryListPage.routeName:
|
||||
return l10n.drawerCountryPage;
|
||||
case PlaceListPage.routeName:
|
||||
return l10n.drawerPlacePage;
|
||||
case TagListPage.routeName:
|
||||
return l10n.drawerTagPage;
|
||||
case SettingsPage.routeName:
|
||||
|
@ -55,7 +58,9 @@ class NavigationDisplay {
|
|||
case AlbumListPage.routeName:
|
||||
return AIcons.album;
|
||||
case CountryListPage.routeName:
|
||||
return AIcons.location;
|
||||
return AIcons.country;
|
||||
case PlaceListPage.routeName:
|
||||
return AIcons.place;
|
||||
case TagListPage.routeName:
|
||||
return AIcons.tag;
|
||||
case SettingsPage.routeName:
|
||||
|
|
|
@ -15,7 +15,8 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/album.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/location.dart';
|
||||
import 'package:aves/model/source/location/country.dart';
|
||||
import 'package:aves/model/source/location/place.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/settings/common/tile_leading.dart';
|
||||
import 'package:aves/widgets/settings/common/tiles.dart';
|
||||
import 'package:aves/widgets/settings/privacy/privacy.dart';
|
||||
import 'package:aves/widgets/settings/settings_definition.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -134,6 +135,8 @@ class SettingsTileDisplayForceTvLayout extends SettingsTile {
|
|||
if (confirmed == null || !confirmed) return;
|
||||
}
|
||||
|
||||
if (v && !(await SettingsTilePrivacyEnableBin.setBinUsage(context, false))) return;
|
||||
|
||||
settings.forceTvLayout = v;
|
||||
},
|
||||
title: title(context),
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/search/page.dart';
|
||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/places_page.dart';
|
||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
|
||||
import 'package:aves/widgets/navigation/drawer/tile.dart';
|
||||
|
@ -40,6 +41,7 @@ class _NavigationDrawerEditorPageState extends State<NavigationDrawerEditorPage>
|
|||
static const Set<String> _pageOptions = {
|
||||
AlbumListPage.routeName,
|
||||
CountryListPage.routeName,
|
||||
PlaceListPage.routeName,
|
||||
TagListPage.routeName,
|
||||
SearchPage.routeName,
|
||||
};
|
||||
|
|
|
@ -97,49 +97,51 @@ class SettingsTilePrivacyEnableBin extends SettingsTile {
|
|||
@override
|
||||
Widget build(BuildContext context) => SettingsSwitchListTile(
|
||||
selector: (context, s) => s.enableBin,
|
||||
onChanged: (v) async {
|
||||
final l10n = context.l10n;
|
||||
if (!v) {
|
||||
if (vaults.all.any((v) => v.useBin)) {
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AvesDialog(
|
||||
content: Text(l10n.vaultBinUsageDialogMessage),
|
||||
actions: const [OkButton()],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
final trashedEntries = source.trashedEntries;
|
||||
if (trashedEntries.isNotEmpty) {
|
||||
if (!await showConfirmationDialog(
|
||||
context: context,
|
||||
message: l10n.settingsDisablingBinWarningDialogMessage,
|
||||
confirmationButtonLabel: l10n.applyButtonLabel,
|
||||
)) return;
|
||||
|
||||
// delete forever trashed items
|
||||
await EntrySetActionDelegate().doDelete(
|
||||
context: context,
|
||||
entries: trashedEntries,
|
||||
enableBin: false,
|
||||
);
|
||||
|
||||
// in case of failure or cancellation
|
||||
if (source.trashedEntries.isNotEmpty) return;
|
||||
}
|
||||
}
|
||||
|
||||
settings.enableBin = v;
|
||||
if (!v) {
|
||||
settings.searchHistory = [];
|
||||
}
|
||||
},
|
||||
onChanged: (v) => setBinUsage(context, v),
|
||||
title: title(context),
|
||||
subtitle: context.l10n.settingsEnableBinSubtitle,
|
||||
);
|
||||
|
||||
static Future<bool> setBinUsage(BuildContext context, bool enabled) async {
|
||||
final l10n = context.l10n;
|
||||
if (!enabled) {
|
||||
if (vaults.all.any((v) => v.useBin)) {
|
||||
await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AvesDialog(
|
||||
content: Text(l10n.vaultBinUsageDialogMessage),
|
||||
actions: const [OkButton()],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
final trashedEntries = source.trashedEntries;
|
||||
if (trashedEntries.isNotEmpty) {
|
||||
if (!await showConfirmationDialog(
|
||||
context: context,
|
||||
message: l10n.settingsDisablingBinWarningDialogMessage,
|
||||
confirmationButtonLabel: l10n.applyButtonLabel,
|
||||
)) return false;
|
||||
|
||||
// delete forever trashed items
|
||||
await EntrySetActionDelegate().doDelete(
|
||||
context: context,
|
||||
entries: trashedEntries,
|
||||
enableBin: false,
|
||||
);
|
||||
|
||||
// in case of failure or cancellation
|
||||
if (source.trashedEntries.isNotEmpty) return false;
|
||||
}
|
||||
|
||||
settings.searchHistory = [];
|
||||
}
|
||||
|
||||
settings.enableBin = enabled;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsTilePrivacyHiddenItems extends SettingsTile {
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/entry.dart';
|
|||
import 'package:aves/model/filters/date.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/stats/date/axis.dart';
|
||||
import 'package:charts_flutter/flutter.dart' as charts;
|
||||
|
@ -343,14 +344,7 @@ class _HistogramState extends State<Histogram> with AutomaticKeepAliveClientMixi
|
|||
duration: context.read<DurationsData>().formTransition,
|
||||
switchInCurve: Curves.easeInOutCubic,
|
||||
switchOutCurve: Curves.easeInOutCubic,
|
||||
transitionBuilder: (child, animation) => FadeTransition(
|
||||
opacity: animation,
|
||||
child: SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
transitionBuilder: AvesTransitions.formTransitionBuilder,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -8,20 +8,14 @@ import 'package:aves/model/actions/share_actions.dart';
|
|||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/entry_metadata_edition.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/services/common/image_op_events.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/services/media/enums.dart';
|
||||
import 'package:aves/services/media/media_edit_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/entry_storage.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||
|
@ -32,8 +26,6 @@ import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
|
|||
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/export_entry_dialog.dart';
|
||||
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
|
||||
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
||||
import 'package:aves/widgets/viewer/action/printer.dart';
|
||||
import 'package:aves/widgets/viewer/action/single_entry_editor.dart';
|
||||
|
@ -42,7 +34,6 @@ import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
|||
import 'package:aves/widgets/viewer/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/source_viewer_page.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -197,7 +188,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
_move(context, targetEntry, moveType: MoveType.fromBin);
|
||||
break;
|
||||
case EntryAction.convert:
|
||||
_convert(context, targetEntry);
|
||||
convert(context, {targetEntry});
|
||||
break;
|
||||
case EntryAction.print:
|
||||
EntryPrinter(targetEntry).print(context);
|
||||
|
@ -412,98 +403,6 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _convert(BuildContext context, AvesEntry targetEntry) async {
|
||||
final options = await showDialog<EntryExportOptions>(
|
||||
context: context,
|
||||
builder: (context) => ExportEntryDialog(entry: targetEntry),
|
||||
routeSettings: const RouteSettings(name: ExportEntryDialog.routeName),
|
||||
);
|
||||
if (options == null) return;
|
||||
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
|
||||
if (destinationAlbum == null) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
||||
if (!await checkFreeSpaceForMove(context, {targetEntry}, destinationAlbum, MoveType.export)) return;
|
||||
|
||||
final selection = <AvesEntry>{};
|
||||
if (targetEntry.isMultiPage) {
|
||||
final multiPageInfo = await targetEntry.getMultiPageInfo();
|
||||
if (multiPageInfo != null) {
|
||||
if (targetEntry.isMotionPhoto) {
|
||||
await multiPageInfo.extractMotionPhotoVideo();
|
||||
}
|
||||
if (multiPageInfo.pageCount > 1) {
|
||||
selection.addAll(multiPageInfo.exportEntries);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selection.add(targetEntry);
|
||||
}
|
||||
|
||||
final selectionCount = selection.length;
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
await showOpReport<ExportOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaEditService.export(
|
||||
selection,
|
||||
options: options,
|
||||
destinationAlbum: destinationAlbum,
|
||||
nameConflictStrategy: NameConflictStrategy.rename,
|
||||
),
|
||||
itemCount: selectionCount,
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final exportedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
final newUris = exportedOps.map((v) => v.newFields['uri'] as String?).whereNotNull().toSet();
|
||||
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
|
||||
|
||||
source.resumeMonitoring();
|
||||
unawaited(source.refreshUris(newUris));
|
||||
|
||||
final l10n = context.l10n;
|
||||
final showAction = isMainMode && newUris.isNotEmpty
|
||||
? SnackBarAction(
|
||||
label: l10n.showButtonLabel,
|
||||
onPressed: () {
|
||||
// local context may be deactivated when action is triggered after navigation
|
||||
final context = AvesApp.navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||
builder: (context) => CollectionPage(
|
||||
source: source,
|
||||
filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))},
|
||||
highlightTest: (entry) => newUris.contains(entry.uri),
|
||||
),
|
||||
),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: null;
|
||||
final successCount = successOps.length;
|
||||
if (successCount < selectionCount) {
|
||||
final count = selectionCount - successCount;
|
||||
showFeedback(
|
||||
context,
|
||||
l10n.collectionExportFailureFeedback(count),
|
||||
showAction,
|
||||
);
|
||||
} else {
|
||||
showFeedback(
|
||||
context,
|
||||
l10n.genericSuccessFeedback,
|
||||
showAction,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => doMove(
|
||||
context,
|
||||
moveType: moveType,
|
||||
|
|
|
@ -55,8 +55,8 @@ mixin SingleEntryEditorMixin on FeedbackMixin, PermissionAwareMixin {
|
|||
} else {
|
||||
showFeedback(context, l10n.genericFailureFeedback);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
} catch (error, stack) {
|
||||
await reportService.recordError(error, stack);
|
||||
}
|
||||
source?.resumeMonitoring();
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class ViewerDebugPage extends StatelessWidget {
|
|||
if (time != null && time > 0) {
|
||||
try {
|
||||
value += ' (${DateTime.fromMillisecondsSinceEpoch(time * factor)})';
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
value += ' (invalid DateTime})';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class _MetadataTabState extends State<MetadataTab> {
|
|||
}
|
||||
try {
|
||||
value += ' (${DateTime.fromMillisecondsSinceEpoch(v)})';
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
value += ' (invalid DateTime})';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,11 +39,11 @@ class _WelcomePageState extends State<WelcomePage> {
|
|||
WidgetsBinding.instance.addPostFrameCallback((_) => _initWelcomeSettings());
|
||||
}
|
||||
|
||||
// explicitly set consent values to current defaults
|
||||
// so they are not subject to future default changes
|
||||
void _initWelcomeSettings() {
|
||||
// this should be done outside of `initState`/`build`
|
||||
settings.setContextualDefaults(context.read<AppFlavor>());
|
||||
// explicitly set consent values to current defaults
|
||||
// so they are not subject to future default changes
|
||||
settings.isInstalledAppAccessAllowed = SettingsDefaults.isInstalledAppAccessAllowed;
|
||||
settings.isErrorReportingAllowed = SettingsDefaults.isErrorReportingAllowed;
|
||||
}
|
||||
|
|
|
@ -197,18 +197,22 @@ class _MagnifierCoreState extends State<MagnifierCore> with TickerProviderStateM
|
|||
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);
|
||||
if (left ^ right) {
|
||||
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);
|
||||
if (up ^ down) {
|
||||
if (up) {
|
||||
onFling(AxisDirection.up);
|
||||
} else if (down) {
|
||||
onFling(AxisDirection.down);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,10 +66,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
|
@ -5,10 +5,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "6215ac7d00ed98300b72f45ed2b38c2ca841f9f4e6965fab33cbd591e45e4473"
|
||||
sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.13"
|
||||
version: "1.0.16"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -68,10 +68,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: be13e431c0c950f0fc66bdb67b41b8059121d7e7d8bbbc21fb59164892d561f8
|
||||
sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.7.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -84,26 +84,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "4b3a41410f3313bb95fd560aa5eb761b6ad65c185de772c72231e8b4aeed6d18"
|
||||
sha256: "291fbcace608aca6c860652e1358ef89752be8cc3ef227f8bbcd1e62775b833a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.2.1"
|
||||
firebase_crashlytics:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: "6f1dc5321aa7d8bb84e8eec2ed517b0a7e7bc99a2708549c55c78359a323ecd0"
|
||||
sha256: "816bbb920316c8fe257b460b8856b01e274e867a729961bf7a3be6322cdf13e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.12"
|
||||
version: "3.0.15"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: "3728ad632a5209499cdfed6a1f7154982a524a076566f5a1d02af623bf46f947"
|
||||
sha256: "120e47b9bac3654848d1bdc60b8027f3574b53ee0b81b1a2e5e76ddaa58f6645"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.12"
|
||||
version: "3.3.15"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -179,10 +179,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
|
|
@ -135,10 +135,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b"
|
||||
sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -156,34 +156,34 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: google_maps_flutter
|
||||
sha256: "0c6b72b4b1e0f6204973e2b40868a75fe6380725d498f215cd7e35ed920d1c57"
|
||||
sha256: e4f117747e210e68fe34196c93040e0712ba8aa91f8813fe8cb3f52ad667e70b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.2.4"
|
||||
google_maps_flutter_android:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_maps_flutter_android
|
||||
sha256: f238a04c378df6fbe7d84a3ea19362e9db19ea1717d06deffa9edaa17973e916
|
||||
sha256: "69bac3fd18db5638763b06e04c2f2ff2e65e0a76fe93e0e211d8a77afaca033f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.4.6"
|
||||
google_maps_flutter_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_maps_flutter_ios
|
||||
sha256: "33bbca8d4148ed373251ea2ec2344fdc63009926b6d6be71a0854fd42409b1ba"
|
||||
sha256: "7ed57e6f1351aebf1442dea0028f2c880989ead866eaf25e9121237c622d4af0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.13"
|
||||
version: "2.1.14"
|
||||
google_maps_flutter_platform_interface:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_maps_flutter_platform_interface
|
||||
sha256: "0967430c25240836b794d42336bd4c61f0e78e9fd33d1365fa9316bb36b6b410"
|
||||
sha256: a07811d2b82055815ede75e1fe4b7b76f71a0b4820b26f71bdaddd157d6a3e20
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.5"
|
||||
version: "2.2.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -284,10 +284,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
polylabel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -222,10 +222,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
polylabel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
178
pubspec.lock
178
pubspec.lock
|
@ -5,26 +5,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201"
|
||||
sha256: e440ac42679dfc04bbbefb58ed225c994bc7e07fccc8a68ec7d3631a127e5da9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "52.0.0"
|
||||
version: "54.0.0"
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: "6215ac7d00ed98300b72f45ed2b38c2ca841f9f4e6965fab33cbd591e45e4473"
|
||||
sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.13"
|
||||
version: "1.0.16"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4
|
||||
sha256: "2c2e3721ee9fb36de92faa060f3480c81b23e904352b087e5c64224b1a044427"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
version: "5.6.0"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -37,10 +37,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: args
|
||||
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
|
||||
sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.4.0"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -321,10 +321,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: be13e431c0c950f0fc66bdb67b41b8059121d7e7d8bbbc21fb59164892d561f8
|
||||
sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.7.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -337,26 +337,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "4b3a41410f3313bb95fd560aa5eb761b6ad65c185de772c72231e8b4aeed6d18"
|
||||
sha256: "291fbcace608aca6c860652e1358ef89752be8cc3ef227f8bbcd1e62775b833a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.2.1"
|
||||
firebase_crashlytics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics
|
||||
sha256: "6f1dc5321aa7d8bb84e8eec2ed517b0a7e7bc99a2708549c55c78359a323ecd0"
|
||||
sha256: "816bbb920316c8fe257b460b8856b01e274e867a729961bf7a3be6322cdf13e1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.12"
|
||||
version: "3.0.15"
|
||||
firebase_crashlytics_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_crashlytics_platform_interface
|
||||
sha256: "3728ad632a5209499cdfed6a1f7154982a524a076566f5a1d02af623bf46f947"
|
||||
sha256: "120e47b9bac3654848d1bdc60b8027f3574b53ee0b81b1a2e5e76ddaa58f6645"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.12"
|
||||
version: "3.3.15"
|
||||
flex_color_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -440,18 +440,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "818cf6c28377ba2c91ed283c96fd712e9c175dd2d2488eb7fc93b6afb9ad2e08"
|
||||
sha256: "7b25c10de1fea883f3c4f9b8389506b54053cd00807beab69fd65c8653a2711f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.13+1"
|
||||
version: "0.6.14"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b"
|
||||
sha256: "4bef634684b2c7f3468c77c766c831229af829a0cd2d4ee6c1b99558bd14e5d2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
flutter_staggered_animations:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -511,34 +511,34 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: google_maps_flutter
|
||||
sha256: "0c6b72b4b1e0f6204973e2b40868a75fe6380725d498f215cd7e35ed920d1c57"
|
||||
sha256: e4f117747e210e68fe34196c93040e0712ba8aa91f8813fe8cb3f52ad667e70b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.2.4"
|
||||
google_maps_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_maps_flutter_android
|
||||
sha256: f238a04c378df6fbe7d84a3ea19362e9db19ea1717d06deffa9edaa17973e916
|
||||
sha256: "69bac3fd18db5638763b06e04c2f2ff2e65e0a76fe93e0e211d8a77afaca033f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.4.6"
|
||||
google_maps_flutter_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_maps_flutter_ios
|
||||
sha256: "33bbca8d4148ed373251ea2ec2344fdc63009926b6d6be71a0854fd42409b1ba"
|
||||
sha256: "7ed57e6f1351aebf1442dea0028f2c880989ead866eaf25e9121237c622d4af0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.13"
|
||||
version: "2.1.14"
|
||||
google_maps_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_maps_flutter_platform_interface
|
||||
sha256: "0967430c25240836b794d42336bd4c61f0e78e9fd33d1365fa9316bb36b6b410"
|
||||
sha256: a07811d2b82055815ede75e1fe4b7b76f71a0b4820b26f71bdaddd157d6a3e20
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.5"
|
||||
version: "2.2.6"
|
||||
highlight:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -575,10 +575,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "3686865febd85c57a632d87a0fb1f0a8a9ef602fdb68d909c3e9aa29ec70fd24"
|
||||
sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.13"
|
||||
version: "4.0.15"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -631,42 +631,42 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth
|
||||
sha256: "8cea55dca20d1e0efa5480df2d47ae30851e7a24cb8e7d225be7e67ae8485aa4"
|
||||
sha256: "1625a217c599db02044df992509eb52584cfdaa8219958d516b74f644ef2c626"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.5"
|
||||
local_auth_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: cfcbc4936e288d61ef85a04feef6b95f49ba496d4fd98364e6abafb462b06a1f
|
||||
sha256: "9106024549f411fea2543630582ae7937fcb97ce197755a6d985b23b980410c7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.18"
|
||||
version: "1.0.19"
|
||||
local_auth_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_ios
|
||||
sha256: aa32478d7513066564139af57e11e2cad1bbd535c1efd224a88a8764c5665e3b
|
||||
sha256: "14e7822264fbb6842240e09ff32a60e2ca2b1c3207de8adf2f56c149cccd49fd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.12"
|
||||
version: "1.0.13"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_platform_interface
|
||||
sha256: fbb6973f2fd088e2677f39a5ab550aa1cfbc00997859d5e865569872499d6d61
|
||||
sha256: "9e160d59ef0743e35f1b50f4fb84dc64f55676b1b8071e319ef35e7f3bc13367"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
version: "1.0.7"
|
||||
local_auth_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_windows
|
||||
sha256: "888482e4f9ca3560e00bc227ce2badeb4857aad450c42a31c6cfc9dc21e0ccbc"
|
||||
sha256: "69c4a6b1201e7b5467e7180c7dd84cf96c308982680cc1778984552bea84b0bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.6"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -679,10 +679,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: markdown
|
||||
sha256: c2b81e184067b41d0264d514f7cdaa2c02d38511e39d6521a1ccc238f6d7b3f2
|
||||
sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "7.0.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -833,26 +833,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
|
||||
sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.1.9"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76
|
||||
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
version: "2.0.6"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
|
||||
sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
pdf:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -937,10 +937,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
polylabel:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1073,58 +1073,58 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9"
|
||||
sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.17"
|
||||
version: "2.0.18"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7"
|
||||
sha256: a51a4f9375097f94df1c6e0a49c0374440d31ab026b59d58a7e7660675879db4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.0.16"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0"
|
||||
sha256: "6b84fdf06b32bb336f972d373cd38b63734f3461ba56ac2ba01b56d052796259"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874
|
||||
sha256: d7fb71e6e20cd3dfffcc823a28da3539b392e53ed5fc5c2b90b55fdaa8a7e8fa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3
|
||||
sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958
|
||||
sha256: "6737b757e49ba93de2a233df229d0b6a87728cea1684da828cbc718b65dcf9d7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
version: "2.0.5"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9"
|
||||
sha256: bd014168e8484837c39ef21065b78f305810ceabc1d4f90be6e3b392ce81b46d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1166,10 +1166,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: smooth_page_indicator
|
||||
sha256: "49e9b6a265790454c39bd4a447a02f398c02b44b2602e7c5e3a381dc2e3b4102"
|
||||
sha256: "8c301bc686892306cd41672c1880167f140c16be305d5ede8201fefd9fcda829"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0+2"
|
||||
version: "1.0.1"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1182,10 +1182,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427"
|
||||
sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.11"
|
||||
version: "0.10.12"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1303,10 +1303,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: transparent_image
|
||||
sha256: e566a616922a781489f4d91cc939b1b3203b6e4a093317805f2f82f0bb0f8dec
|
||||
sha256: e8991d955a2094e197ca24c645efec2faf4285772a4746126ca12875e54ca02f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
version: "2.0.1"
|
||||
tuple:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1335,66 +1335,66 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: e8f2efc804810c0f2f5b485f49e7942179f56eabcfe81dce3387fec4bb55876b
|
||||
sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.9"
|
||||
version: "6.1.10"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1"
|
||||
sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.23"
|
||||
version: "6.0.24"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "0a5af0aefdd8cf820dd739886efb1637f1f24489900204f50984634c07a54815"
|
||||
sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.0"
|
||||
version: "6.1.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc"
|
||||
sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094"
|
||||
sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6"
|
||||
sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0"
|
||||
sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.14"
|
||||
version: "2.0.15"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615
|
||||
sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "3.0.4"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1471,10 +1471,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
|
||||
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0+3"
|
||||
version: "1.0.0"
|
||||
xml:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1493,4 +1493,4 @@ packages:
|
|||
version: "3.1.1"
|
||||
sdks:
|
||||
dart: ">=2.19.0 <3.0.0"
|
||||
flutter: ">=3.7.3"
|
||||
flutter: ">=3.7.5"
|
||||
|
|
10
pubspec.yaml
10
pubspec.yaml
|
@ -7,13 +7,13 @@ 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.1+92
|
||||
version: 1.8.2+93
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
# this project bundles Flutter SDK via `flutter_wrapper`
|
||||
# cf https://github.com/passsy/flutter_wrapper
|
||||
flutter: 3.7.3
|
||||
flutter: 3.7.5
|
||||
sdk: ">=2.19.0 <3.0.0"
|
||||
|
||||
# following https://github.blog/2021-09-01-improving-git-protocol-security-github/
|
||||
|
@ -89,7 +89,11 @@ dependencies:
|
|||
provider:
|
||||
screen_brightness:
|
||||
screen_state:
|
||||
shared_preferences:
|
||||
# as of `shared_preferences` v2.0.18, upgrading packages downgrades `shared_preferences` to v0.5.4+6
|
||||
# because its dependency `shared_preferences_windows` v2.1.4 gets removed
|
||||
# because its dependency `path_provider_windows` v2.1.4 gets removed
|
||||
# so that the transitive `win32` gets upgraded to v4.x.x
|
||||
shared_preferences: ">=2.0.0"
|
||||
smooth_page_indicator:
|
||||
sqflite:
|
||||
streams_channel:
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
In v1.8.1:
|
||||
- Android TV support (cont'd)
|
||||
- hide your secrets in vaults
|
||||
- enjoy the app in Basque
|
||||
In v1.8.2:
|
||||
- write metadata when converting
|
||||
- convert many items at once
|
||||
- list places in their own page
|
||||
Full changelog available on GitHub
|
Loading…
Reference in a new issue