Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2021-12-22 09:20:19 +09:00
commit 369647555f
175 changed files with 3326 additions and 1668 deletions

View file

@ -15,7 +15,7 @@ jobs:
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
with: with:
channel: stable channel: stable
flutter-version: '2.5.3' flutter-version: '2.8.1'
- name: Clone the repository. - name: Clone the repository.
uses: actions/checkout@v2 uses: actions/checkout@v2

View file

@ -17,7 +17,7 @@ jobs:
- uses: subosito/flutter-action@v1 - uses: subosito/flutter-action@v1
with: with:
channel: stable channel: stable
flutter-version: '2.5.3' flutter-version: '2.8.1'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1): # Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441 # https://issuetracker.google.com/issues/144111441
@ -52,12 +52,12 @@ jobs:
rm release.keystore.asc rm release.keystore.asc
mkdir outputs mkdir outputs
(cd scripts/; ./apply_flavor_play.sh) (cd scripts/; ./apply_flavor_play.sh)
flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.5.3.sksl.json flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.8.1.sksl.json
cp build/app/outputs/bundle/playRelease/*.aab outputs cp build/app/outputs/bundle/playRelease/*.aab outputs
flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.5.3.sksl.json flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_2.8.1.sksl.json
cp build/app/outputs/apk/play/release/*.apk outputs cp build/app/outputs/apk/play/release/*.apk outputs
(cd scripts/; ./apply_flavor_izzy.sh) (cd scripts/; ./apply_flavor_izzy.sh)
flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_2.5.3.sksl.json flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_2.8.1.sksl.json
cp build/app/outputs/apk/izzy/release/*.apk outputs cp build/app/outputs/apk/izzy/release/*.apk outputs
rm $AVES_STORE_FILE rm $AVES_STORE_FILE
env: env:
@ -71,6 +71,7 @@ jobs:
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: "outputs/*" artifacts: "outputs/*"
body: "[Changelog](https://github.com/${{ github.repository }}/blob/develop/CHANGELOG.md#${{ github.ref_name }})"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload app bundle - name: Upload app bundle

View file

@ -2,9 +2,27 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [Unreleased] ## <a id="unreleased"></a>[Unreleased]
## [v1.5.7] - 2021-12-01 ## <a id="v1.5.8"></a>[v1.5.8] - 2021-12-22
### Added
- Collection / Albums / Countries / Tags: list view (scalable like the grid view)
- moving, editing or deleting multiple items can be cancelled
- Viewer: option to auto play motion photos (after a small delay to show first the high-res photo)
- German translation (thanks JanWaldhorn)
### Changed
- upgraded Flutter to stable v2.8.1
### Fixed
- Collection: more consistent scroll bar thumb position to match the viewport
- Settings: fixed file selection to import settings on older devices
## <a id="v1.5.7"></a>[v1.5.7] - 2021-12-01
### Added ### Added
@ -24,7 +42,7 @@ All notable changes to this project will be documented in this file.
- double-tap gesture in the viewer was ignored in some cases - double-tap gesture in the viewer was ignored in some cases
- copied items had the wrong date - copied items had the wrong date
## [v1.5.6] - 2021-11-12 ## <a id="v1.5.6"></a>[v1.5.6] - 2021-11-12
### Added ### Added
@ -502,4 +520,4 @@ upgraded libtiff to 4.2.0 for TIFF decoding
## [v1.2.3] - 2020-10-22 ## [v1.2.3] - 2020-10-22
... ...

View file

@ -35,6 +35,10 @@ Aves integrates with Android (from **API 19 to 31**, i.e. from KitKat to S) with
<img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/2-S10-viewer.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/5-S10-stats.png" alt='Stats screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/3-S10-info__basic_.png" alt='Info (basic) screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/4-S10-info__metadata_.png" alt='Info (metadata) screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/6-S10-countries.png" alt='Countries screenshot' height="400" /> <img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/1-S10-collection.png" alt='Collection screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/2-S10-viewer.png" alt='Image screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/5-S10-stats.png" alt='Stats screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/3-S10-info__basic_.png" alt='Info (basic) screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/4-S10-info__metadata_.png" alt='Info (metadata) screenshot' height="400" /><img src="https://raw.githubusercontent.com/deckerst/aves_extra/main/screenshots/S10/6-S10-countries.png" alt='Countries screenshot' height="400" />
## Changelog
The list of changes for past and future releases is available [here](https://github.com/deckerst/aves/blob/develop/CHANGELOG.md).
## Permissions ## Permissions
Aves requires a few permissions to do its job: Aves requires a few permissions to do its job:

View file

@ -56,6 +56,9 @@ android {
// minSdkVersion constraints: // minSdkVersion constraints:
// - Flutter & other plugins: 16 // - Flutter & other plugins: 16
// - google_maps_flutter v2.1.1: 20 // - google_maps_flutter v2.1.1: 20
// - to build XML documents from XMP data, `metadata-extractor` and `PixyMeta` rely on `DocumentBuilder`,
// which implementation `DocumentBuilderImpl` is provided by the OS and is not customizable on Android,
// but the implementation on API <19 is not robust enough and fails to build XMP documents
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 31 targetSdkVersion 31
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()

View file

@ -155,7 +155,7 @@ class MainActivity : FlutterActivity() {
} }
} }
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant", "ObsoleteSdkInt")
private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) { private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
val treeUri = data?.data val treeUri = data?.data
if (resultCode != RESULT_OK || treeUri == null) { if (resultCode != RESULT_OK || treeUri == null) {

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
@ -24,6 +25,7 @@ class AccessibilityHandler(private val activity: Activity) : MethodCallHandler {
private fun areAnimationsRemoved(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun areAnimationsRemoved(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var removed = false var removed = false
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
try { try {
removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint
import android.content.* import android.content.*
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.res.Configuration import android.content.res.Configuration
@ -63,6 +64,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// apps tend to use their name in English when creating directories // apps tend to use their name in English when creating directories
// so we get their names in English as well as the current locale // so we get their names in English as well as the current locale
val englishConfig = Configuration().apply { val englishConfig = Configuration().apply {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
setLocale(Locale.ENGLISH) setLocale(Locale.ENGLISH)
} else { } else {
@ -272,13 +274,13 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} else { } else {
var mimeType = "*/*" var mimeType = "*/*"
if (mimeTypes.size == 1) { if (mimeTypes.size == 1) {
// items have the same mime type & subtype // items have the same MIME type & subtype
mimeType = mimeTypes.first() mimeType = mimeTypes.first()
} else { } else {
// items have different subtypes // items have different subtypes
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct() val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
if (mimeTypeTypes.size == 1) { if (mimeTypeTypes.size == 1) {
// items have the same mime type // items have the same MIME type
mimeType = "${mimeTypeTypes.first()}/*" mimeType = "${mimeTypeTypes.first()}/*"
} }
} }

View file

@ -1,9 +1,11 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import android.content.res.Resources
import android.os.Build import android.os.Build
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -14,6 +16,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
when (call.method) { when (call.method) {
"getCapabilities" -> safe(call, result, ::getCapabilities) "getCapabilities" -> safe(call, result, ::getCapabilities)
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone) "getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
"getLocales" -> safe(call, result, ::getLocales)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
else -> result.notImplemented() else -> result.notImplemented()
} }
@ -41,6 +44,32 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(TimeZone.getDefault().id) result.success(TimeZone.getDefault().id)
} }
private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
fun toMap(locale: Locale): FieldMap {
val fields: HashMap<String, Any?> = hashMapOf(
"language" to locale.language,
"country" to locale.country,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
fields["script"] = locale.script
}
return fields
}
val locales = ArrayList<FieldMap>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// when called from a window-less service, locales from `context.resources`
// do not reflect the current system settings, so we use `Resources.getSystem()` instead
val list = Resources.getSystem().configuration.locales
for (i in 0 until list.size()) {
locales.add(toMap(list.get(i)))
}
} else {
locales.add(toMap(Locale.getDefault()))
}
result.success(locales)
}
private fun getPerformanceClass(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getPerformanceClass(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val performanceClass = Build.VERSION.MEDIA_PERFORMANCE_CLASS val performanceClass = Build.VERSION.MEDIA_PERFORMANCE_CLASS

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls
import android.app.Activity import android.app.Activity
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.util.Log
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@ -14,6 +15,7 @@ import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictStrategy import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -34,6 +36,7 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
"getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) } "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) }
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) } "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) }
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) } "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) }
"cancelFileOp" -> safe(call, result, ::cancelFileOp)
"captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) } "captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) }
"clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) } "clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) }
else -> result.notImplemented() else -> result.notImplemented()
@ -138,6 +141,19 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
} }
} }
private fun cancelFileOp(call: MethodCall, result: MethodChannel.Result) {
val opId = call.argument<String>("opId")
if (opId == null) {
result.error("cancelFileOp-args", "failed because of missing arguments", null)
return
}
Log.i(LOG_TAG, "cancelling file op $opId")
cancelledOps.add(opId)
result.success(null)
}
private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) { private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val desiredName = call.argument<String>("desiredName") val desiredName = call.argument<String>("desiredName")
@ -169,6 +185,9 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
} }
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<MediaFileHandler>()
const val CHANNEL = "deckers.thibault/aves/media_file" const val CHANNEL = "deckers.thibault/aves/media_file"
val cancelledOps = HashSet<String>()
} }
} }

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@ -191,6 +192,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val key = kv.key val key = kv.key
// `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1 // `PNG-iTXt` uses UTF-8, contrary to `PNG-tEXt` and `PNG-zTXt` using Latin-1 / ISO-8859-1
val charset = if (baseDirName == PNG_ITXT_DIR_NAME) { val charset = if (baseDirName == PNG_ITXT_DIR_NAME) {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
StandardCharsets.UTF_8 StandardCharsets.UTF_8
} else { } else {
@ -409,19 +411,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// File type // File type
for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) {
// * `metadata-extractor` sometimes detects the wrong mime type (e.g. `pef` file as `tiff`) // * `metadata-extractor` sometimes detects the wrong MIME type (e.g. `pef` file as `tiff`, `mpeg` as `dvd`)
// * the content resolver / media store sometimes reports the wrong mime type (e.g. `png` file as `jpeg`, `tiff` as `srw`) // * the content resolver / media store sometimes reports the wrong MIME type (e.g. `png` file as `jpeg`, `tiff` as `srw`)
// * `context.getContentResolver().getType()` sometimes returns an incorrect value // * `context.getContentResolver().getType()` sometimes returns an incorrect value
// * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000` // * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000`
// * file extension is unreliable // * file extension is unreliable
// In the end, `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives), // In the end, `metadata-extractor` is the most reliable, except for `tiff`/`dvd` (false positives, false negatives),
// in which case we trust the file extension // in which case we trust the file extension
// cf https://github.com/drewnoakes/metadata-extractor/issues/296 // cf https://github.com/drewnoakes/metadata-extractor/issues/296
if (path?.matches(TIFF_EXTENSION_PATTERN) == true) { if (path?.matches(TIFF_EXTENSION_PATTERN) == true) {
metadataMap[KEY_MIME_TYPE] = MimeTypes.TIFF metadataMap[KEY_MIME_TYPE] = MimeTypes.TIFF
} else { } else {
dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) {
if (it != MimeTypes.TIFF) { if (it != MimeTypes.TIFF && it != MimeTypes.DVD) {
metadataMap[KEY_MIME_TYPE] = it metadataMap[KEY_MIME_TYPE] = it
} }
} }
@ -584,6 +586,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
try { try {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it } retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
} }

View file

@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import deckers.thibault.aves.channel.calls.MediaFileHandler.Companion.cancelledOps
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictStrategy import deckers.thibault.aves.model.NameConflictStrategy
@ -24,11 +25,13 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
private lateinit var handler: Handler private lateinit var handler: Handler
private var op: String? = null private var op: String? = null
private var opId: String? = null
private val entryMapList = ArrayList<FieldMap>() private val entryMapList = ArrayList<FieldMap>()
init { init {
if (arguments is Map<*, *>) { if (arguments is Map<*, *>) {
op = arguments["op"] as String? op = arguments["op"] as String?
opId = arguments["id"] as String?
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
val rawEntries = arguments["entries"] as List<FieldMap>? val rawEntries = arguments["entries"] as List<FieldMap>?
if (rawEntries != null) { if (rawEntries != null) {
@ -74,6 +77,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
} }
private fun endOfStream() { private fun endOfStream() {
cancelledOps.remove(opId)
handler.post { handler.post {
try { try {
eventSink.endOfStream() eventSink.endOfStream()
@ -97,14 +101,18 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
return return
} }
for (entryMap in entryMapList) { val entries = entryMapList.map(::AvesEntry)
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } for (entry in entries) {
val path = entryMap["path"] as String? val uri = entry.uri
val mimeType = entryMap["mimeType"] as String? val path = entry.path
if (uri != null && mimeType != null) { val mimeType = entry.mimeType
val result: FieldMap = hashMapOf(
"uri" to uri.toString(), val result: FieldMap = hashMapOf(
) "uri" to uri.toString(),
)
if (isCancelledOp()) {
result["skipped"] = true
} else {
try { try {
provider.delete(activity, uri, path, mimeType) provider.delete(activity, uri, path, mimeType)
result["success"] = true result["success"] = true
@ -112,8 +120,8 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
Log.w(LOG_TAG, "failed to delete entry with path=$path", e) Log.w(LOG_TAG, "failed to delete entry with path=$path", e)
result["success"] = false result["success"] = false
} }
success(result)
} }
success(result)
} }
endOfStream() endOfStream()
} }
@ -173,7 +181,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir) destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
val entries = entryMapList.map(::AvesEntry) val entries = entryMapList.map(::AvesEntry)
provider.moveMultiple(activity, copy, destinationDir, nameConflictStrategy, entries, object : ImageOpCallback { provider.moveMultiple(activity, copy, destinationDir, nameConflictStrategy, entries, ::isCancelledOp, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = success(fields) override fun onSuccess(fields: FieldMap) = success(fields)
override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable) override fun onFailure(throwable: Throwable) = error("move-failure", "failed to move entries", throwable)
}) })
@ -201,13 +209,15 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
} }
val entries = entryMapList.map(::AvesEntry) val entries = entryMapList.map(::AvesEntry)
provider.renameMultiple(activity, newName, entries, object : ImageOpCallback { provider.renameMultiple(activity, newName, entries, ::isCancelledOp, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = success(fields) override fun onSuccess(fields: FieldMap) = success(fields)
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message) override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
}) })
endOfStream() endOfStream()
} }
private fun isCancelledOp() = cancelledOps.contains(opId)
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>() private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/media_op_stream" const val CHANNEL = "deckers.thibault/aves/media_op_stream"

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.streams package deckers.thibault.aves.channel.streams
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.database.ContentObserver import android.database.ContentObserver
import android.net.Uri import android.net.Uri
@ -36,6 +37,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
val settings: FieldMap = hashMapOf( val settings: FieldMap = hashMapOf(
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation, Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
) )
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
settings[Settings.Global.TRANSITION_ANIMATION_SCALE] = transitionAnimationScale settings[Settings.Global.TRANSITION_ANIMATION_SCALE] = transitionAnimationScale
} }
@ -51,6 +53,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
accelerometerRotation = newAccelerometerRotation accelerometerRotation = newAccelerometerRotation
changed = true changed = true
} }
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
if (transitionAnimationScale != newTransitionAnimationScale) { if (transitionAnimationScale != newTransitionAnimationScale) {

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.channel.streams package deckers.thibault.aves.channel.streams
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -10,6 +11,7 @@ import android.util.Log
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.PendingStorageAccessResultHandler import deckers.thibault.aves.PendingStorageAccessResultHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.PermissionManager
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink import io.flutter.plugin.common.EventChannel.EventSink
@ -91,8 +93,8 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
} }
private fun createFile() { private fun createFile() {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
// TODO TLAD [<=API18] create file
error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return return
} }
@ -133,24 +135,16 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
} }
private fun openFile() { private suspend fun openFile() {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
// TODO TLAD [<=API18] open file
error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return return
} }
val mimeType = args["mimeType"] as String? val mimeType = args["mimeType"] as String? // optional
if (mimeType == null) {
error("openFile-args", "failed because of missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { fun onGranted(uri: Uri) {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
}
MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri ->
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
activity.contentResolver.openInputStream(uri)?.use { input -> activity.contentResolver.openInputStream(uri)?.use { input ->
val buffer = ByteArray(BUFFER_SIZE) val buffer = ByteArray(BUFFER_SIZE)
@ -161,11 +155,24 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
endOfStream() endOfStream()
} }
} }
}, { }
fun onDenied() {
success(ByteArray(0)) success(ByteArray(0))
endOfStream() endOfStream()
}) }
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
}
if (intent.resolveActivity(activity.packageManager) != null) {
MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied)
activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST)
} else {
MainActivity.notifyError("failed to resolve activity for intent=$intent")
onDenied()
}
} }
override fun onCancel(arguments: Any?) {} override fun onCancel(arguments: Any?) {}

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.annotation.SuppressLint
import android.media.MediaFormat import android.media.MediaFormat
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.os.Build import android.os.Build
@ -31,6 +32,7 @@ object MediaMetadataRetrieverHelper {
MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer", MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer",
MediaMetadataRetriever.METADATA_KEY_YEAR to "Year", MediaMetadataRetriever.METADATA_KEY_YEAR to "Year",
).apply { ).apply {
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
put(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION, "Video Rotation") put(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION, "Video Rotation")
} }

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.media.MediaExtractor import android.media.MediaExtractor
import android.media.MediaFormat import android.media.MediaFormat
@ -56,6 +57,7 @@ object MultiPage {
format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it } format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it } format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it }
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 } format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 }
} }

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.model package deckers.thibault.aves.model
import android.annotation.SuppressLint
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
@ -139,6 +140,7 @@ class SourceEntry {
retriever.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it } retriever.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it }
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it } retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it }
retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = it } retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = it }
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it } retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it }
} }
@ -161,7 +163,7 @@ class SourceEntry {
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input) val metadata = ImageMetadataReader.readMetadata(input)
// do not switch on specific mime types, as the reported mime type could be wrong // do not switch on specific MIME types, as the reported MIME type could be wrong
// (e.g. PNG registered as JPG) // (e.g. PNG registered as JPG)
if (isVideo) { if (isVideo) {
for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) {

View file

@ -47,11 +47,25 @@ abstract class ImageProvider {
throw UnsupportedOperationException("`delete` is not supported by this image provider") throw UnsupportedOperationException("`delete` is not supported by this image provider")
} }
open suspend fun moveMultiple(activity: Activity, copy: Boolean, targetDir: String, nameConflictStrategy: NameConflictStrategy, entries: List<AvesEntry>, callback: ImageOpCallback) { open suspend fun moveMultiple(
activity: Activity,
copy: Boolean,
targetDir: String,
nameConflictStrategy: NameConflictStrategy,
entries: List<AvesEntry>,
isCancelledOp: CancelCheck,
callback: ImageOpCallback,
) {
callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider")) callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider"))
} }
open suspend fun renameMultiple(activity: Activity, newFileName: String, entries: List<AvesEntry>, callback: ImageOpCallback) { open suspend fun renameMultiple(
activity: Activity,
newFileName: String,
entries: List<AvesEntry>,
isCancelledOp: CancelCheck,
callback: ImageOpCallback,
) {
callback.onFailure(UnsupportedOperationException("`renameMultiple` is not supported by this image provider")) callback.onFailure(UnsupportedOperationException("`renameMultiple` is not supported by this image provider"))
} }
@ -937,3 +951,5 @@ abstract class ImageProvider {
} }
} }
} }
typealias CancelCheck = () -> Boolean

View file

@ -175,7 +175,7 @@ class MediaStoreImageProvider : ImageProvider() {
// but for single items, `contentUri` already contains the ID // but for single items, `contentUri` already contains the ID
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong()) val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong())
// `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices) // `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices)
// in that case we try to use the mime type provided along the URI // in that case we try to use the MIME type provided along the URI
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
val width = cursor.getInt(widthColumn) val width = cursor.getInt(widthColumn)
val height = cursor.getInt(heightColumn) val height = cursor.getInt(heightColumn)
@ -331,6 +331,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String, targetDir: String,
nameConflictStrategy: NameConflictStrategy, nameConflictStrategy: NameConflictStrategy,
entries: List<AvesEntry>, entries: List<AvesEntry>,
isCancelledOp: CancelCheck,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
@ -366,7 +367,7 @@ class MediaStoreImageProvider : ImageProvider() {
// - there is no documentation regarding support for usage with removable storage // - there is no documentation regarding support for usage with removable storage
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
try { try {
val newFields = moveSingle( val newFields = if (isCancelledOp()) skippedFieldMap else moveSingle(
activity = activity, activity = activity,
sourcePath = sourcePath, sourcePath = sourcePath,
sourceUri = sourceUri, sourceUri = sourceUri,
@ -505,6 +506,7 @@ class MediaStoreImageProvider : ImageProvider() {
activity: Activity, activity: Activity,
newFileName: String, newFileName: String,
entries: List<AvesEntry>, entries: List<AvesEntry>,
isCancelledOp: CancelCheck,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
for (entry in entries) { for (entry in entries) {
@ -519,7 +521,7 @@ class MediaStoreImageProvider : ImageProvider() {
if (sourcePath != null) { if (sourcePath != null) {
try { try {
val newFields = renameSingle( val newFields = if (isCancelledOp()) skippedFieldMap else renameSingle(
activity = activity, activity = activity,
mimeType = mimeType, mimeType = mimeType,
oldMediaUri = sourceUri, oldMediaUri = sourceUri,
@ -563,6 +565,7 @@ class MediaStoreImageProvider : ImageProvider() {
throw Exception("unsupported Android version") throw Exception("unsupported Android version")
} }
Log.d(LOG_TAG, "rename content at uri=$mediaUri")
val uri = StorageUtils.getMediaStoreScopedStorageSafeUri(mediaUri, mimeType) val uri = StorageUtils.getMediaStoreScopedStorageSafeUri(mediaUri, mimeType)
// `IS_PENDING` is necessary for `TITLE`, not for `DISPLAY_NAME` // `IS_PENDING` is necessary for `TITLE`, not for `DISPLAY_NAME`

View file

@ -3,7 +3,7 @@ package deckers.thibault.aves.utils
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
object MimeTypes { object MimeTypes {
private const val IMAGE = "image" const val ANY = "*/*"
// generic raster // generic raster
const val BMP = "image/bmp" const val BMP = "image/bmp"
@ -45,10 +45,9 @@ object MimeTypes {
// vector // vector
const val SVG = "image/svg+xml" const val SVG = "image/svg+xml"
private const val VIDEO = "video"
private const val AVI = "video/avi" private const val AVI = "video/avi"
private const val AVI_VND = "video/vnd.avi" private const val AVI_VND = "video/vnd.avi"
const val DVD = "video/dvd"
private const val MKV = "video/x-matroska" private const val MKV = "video/x-matroska"
private const val MOV = "video/quicktime" private const val MOV = "video/quicktime"
private const val MP2T = "video/mp2t" private const val MP2T = "video/mp2t"
@ -57,9 +56,9 @@ object MimeTypes {
private const val OGV = "video/ogg" private const val OGV = "video/ogg"
private const val WEBM = "video/webm" private const val WEBM = "video/webm"
fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE) fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith("image")
fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO) fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith("video")
fun isHeic(mimeType: String?) = mimeType != null && (mimeType == HEIC || mimeType == HEIF) fun isHeic(mimeType: String?) = mimeType != null && (mimeType == HEIC || mimeType == HEIF)

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.utils package deckers.thibault.aves.utils
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -182,6 +183,7 @@ object PermissionManager {
// from API 19 / Android 4.4 / KitKat, removable storage requires access permission, at the file level // from API 19 / Android 4.4 / KitKat, removable storage requires access permission, at the file level
// from API 21 / Android 5.0 / Lollipop, removable storage requires access permission, but directory access grant is possible // from API 21 / Android 5.0 / Lollipop, removable storage requires access permission, but directory access grant is possible
// from API 30 / Android 11 / R, any storage requires access permission // from API 30 / Android 11 / R, any storage requires access permission
@SuppressLint("ObsoleteSdkInt")
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
accessibleDirs.addAll(StorageUtils.getVolumePaths(context)) accessibleDirs.addAll(StorageUtils.getVolumePaths(context))
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { } else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {

View file

@ -93,7 +93,6 @@ object StorageUtils {
} }
} }
@SuppressLint("ObsoleteSdkInt")
private fun findVolumePaths(context: Context): Array<String> { private fun findVolumePaths(context: Context): Array<String> {
// Final set of paths // Final set of paths
val paths = HashSet<String>() val paths = HashSet<String>()

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="search_shortcut_short_label">Suche</string>
<string name="videos_shortcut_short_label">Videos</string>
<string name="analysis_channel_name">Analyse von Medien</string>
<string name="analysis_service_description">Bilder &amp; Videos scannen</string>
<string name="analysis_notification_default_title">Medien scannen</string>
<string name="analysis_notification_action_stop">Abbrechen</string>
</resources>

View file

@ -1,16 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.6.0' ext.kotlin_version = '1.6.10'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.3' classpath 'com.android.tools.build:gradle:7.0.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// GMS & Firebase Crashlytics are not actually used by all flavors // GMS & Firebase Crashlytics are not actually used by all flavors
classpath 'com.google.gms:google-services:4.3.10' classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.0' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1'
} }
} }

526
lib/l10n/app_de.arb Normal file
View file

@ -0,0 +1,526 @@
{
"appName": "Aves",
"welcomeMessage": "Willkommen bei Aves",
"welcomeOptional": "Optional",
"welcomeTermsToggle": "Ich stimme den Bedingungen und Konditionen zu",
"itemCount": " {count, plural, =1{1 Element} other{{count} Elemente}}",
"timeSeconds": " {seconds, plural, =1{1 Sekunde} other{{seconds} Sekunde}}",
"timeMinutes": " {minutes, plural, =1{1 Minute} other{{minutes} Minuten}}",
"applyButtonLabel": "ANWENDEN",
"deleteButtonLabel": "LÖSCHEN",
"nextButtonLabel": "NÄCHSTE",
"showButtonLabel": "ANZEIGEN",
"hideButtonLabel": "VERBERGEN",
"continueButtonLabel": "WEITER",
"cancelTooltip": "Abbrechen",
"changeTooltip": "Ändern",
"clearTooltip": "Aufräumen",
"previousTooltip": "Vorherige",
"nextTooltip": "Nächste",
"showTooltip": "Anzeigen",
"hideTooltip": "Ausblenden",
"removeTooltip": "Entfernen",
"resetButtonTooltip": "Zurücksetzen",
"doubleBackExitMessage": "Tippen Sie zum Verlassen erneut auf „Zurück“.",
"sourceStateLoading": "Laden",
"sourceStateCataloguing": "Katalogisierung",
"sourceStateLocatingCountries": "Länder lokalisieren",
"sourceStateLocatingPlaces": "Lokalisierung von Orten",
"chipActionDelete": "Löschen",
"chipActionGoToAlbumPage": "Anzeigen in Alben",
"chipActionGoToCountryPage": "Anzeigen in Ländern",
"chipActionGoToTagPage": "Zeige in Tags",
"chipActionHide": "Ausblenden",
"chipActionPin": "Oben Anpinnen",
"chipActionUnpin": "Nicht mehr Anpinen",
"chipActionRename": "Umbenennen",
"chipActionSetCover": "Titelbild bestimmen",
"chipActionCreateAlbum": "Album erstellen",
"entryActionCopyToClipboard": "In die Zwischenablage kopieren",
"entryActionDelete": "Löschen",
"entryActionExport": "Exportieren",
"entryActionInfo": "Info",
"entryActionRename": "Umbenennen",
"entryActionRotateCCW": "Drehen gegen den Uhrzeigersinn",
"entryActionRotateCW": "Drehen im Uhrzeigersinn",
"entryActionFlip": "Horizontal spiegeln",
"entryActionPrint": "Drucken",
"entryActionShare": "Teilen",
"entryActionViewSource": "Quelle anzeigen",
"entryActionViewMotionPhotoVideo": "Bewegtes Foto öffnen",
"entryActionEdit": "Bearbeiten mit...",
"entryActionOpen": "Öffnen Sie mit...",
"entryActionSetAs": "Einstellen als...",
"entryActionOpenMap": "In der Karten-App anzeigen...",
"entryActionRotateScreen": "Bildschirm rotieren",
"entryActionAddFavourite": "Zu Favoriten hinzufügen ",
"entryActionRemoveFavourite": "Aus Favoriten entfernen",
"videoActionCaptureFrame": "Frame aufnehmen",
"videoActionPause": "Pause",
"videoActionPlay": "Spielen",
"videoActionReplay10": "10 Sekunden rückwärts springen",
"videoActionSkip10": "10 Sekunden vorwärts springen",
"videoActionSelectStreams": "Titel auswählen",
"videoActionSetSpeed": "Wiedergabegeschwindigkeit",
"videoActionSettings": "Einstellungen",
"entryInfoActionEditDate": "Datum & Uhrzeit bearbeiten",
"entryInfoActionEditTags": "Tags bearbeiten",
"entryInfoActionRemoveMetadata": "Metadaten entfernen",
"filterFavouriteLabel": "Favorit",
"filterLocationEmptyLabel": "Ungeortet",
"filterTagEmptyLabel": "Unmarkiert",
"filterTypeAnimatedLabel": "Animationen",
"filterTypeMotionPhotoLabel": "Bewegtes Foto",
"filterTypePanoramaLabel": "Panorama",
"filterTypeRawLabel": "Rohdaten",
"filterTypeSphericalVideoLabel": "360° Video",
"filterTypeGeotiffLabel": "GeoTIFF",
"filterMimeImageLabel": "Bild",
"filterMimeVideoLabel": "Video",
"coordinateFormatDms": "GMS",
"coordinateFormatDecimal": "Dezimalgrad",
"coordinateDms": " {coordinate} {direction}",
"coordinateDmsNorth": "N",
"coordinateDmsSouth": "s",
"coordinateDmsEast": "O",
"coordinateDmsWest": "W",
"unitSystemMetric": "Metrisch",
"unitSystemImperial": "Imperiale",
"videoLoopModeNever": "Niemals",
"videoLoopModeShortOnly": "Nur kurze Videos",
"videoLoopModeAlways": "Immer",
"mapStyleGoogleNormal": "Google Maps",
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"mapStyleGoogleTerrain": "Google Maps (Gelände)",
"mapStyleOsmHot": "Humanitäres OSM",
"mapStyleStamenToner": "Stamen Toner (SchwarzWeiß)",
"mapStyleStamenWatercolor": "Stamen Aquarell",
"nameConflictStrategyRename": "Umbenennen",
"nameConflictStrategyReplace": "Ersetzen Sie",
"nameConflictStrategySkip": "Überspringen",
"keepScreenOnNever": "Niemals",
"keepScreenOnViewerOnly": "Nur bei Bildbetrachtung",
"keepScreenOnAlways": "Immer",
"accessibilityAnimationsRemove": "Verhinderung von Bildschirmeffekten",
"accessibilityAnimationsKeep": "Bildschirmeffekte beibehalten",
"albumTierNew": "Neu",
"albumTierPinned": "Angeheftet",
"albumTierSpecial": "Häufig verwendet",
"albumTierApps": "Apps",
"albumTierRegular": "Andere",
"storageVolumeDescriptionFallbackPrimary": "Interner Speicher",
"storageVolumeDescriptionFallbackNonPrimary": "SD-Karte",
"rootDirectoryDescription": "Hauptverzeichnis",
"otherDirectoryDescription": "„{name}“ Verzeichnis",
"storageAccessDialogTitle": "Speicherzugriff",
"storageAccessDialogMessage": "Bitte wählen Sie den {directory} von „{volume}“ auf dem nächsten Bildschirm, um dieser App Zugriff darauf zu geben.",
"restrictedAccessDialogTitle": "Eingeschränkter Zugang",
"restrictedAccessDialogMessage": "Diese Anwendung darf keine Dateien im {directory} von „{volume}“ verändern.\n\nBitte verwenden Sie einen vorinstallierten Dateimanager oder eine Galerie-App, um die Objekte in ein anderes Verzeichnis zu verschieben.",
"notEnoughSpaceDialogTitle": "Nicht genug Platz",
"notEnoughSpaceDialogMessage": "Diese Operation benötigt {neededSize} freien Platz auf „{volume}“, um abgeschlossen zu werden, aber es ist nur noch {freeSize} übrig.",
"unsupportedTypeDialogTitle": "Nicht unterstützte Typen",
"unsupportedTypeDialogMessage": " {count, plural, =1{Dieser Vorgang wird für Elemente des folgenden Typs nicht unterstützt: {types}.} other{Dieser Vorgang wird für Elemente der folgenden Typen nicht unterstützt: {types}.}}",
"nameConflictDialogSingleSourceMessage": "Einige Dateien im Zielordner haben den gleichen Namen.",
"nameConflictDialogMultipleSourceMessage": "Einige Dateien haben denselben Namen.",
"addShortcutDialogLabel": "Shortcut-Etikett",
"addShortcutButtonLabel": "Hinzufügen",
"noMatchingAppDialogTitle": "Keine passende App",
"noMatchingAppDialogMessage": "Es gibt keine Anwendungen, die dies bewältigen können.",
"deleteEntriesConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie dieses Element löschen möchten?} other{Sind Sie sicher, dass Sie diese {count} Elemente löschen möchten?}}",
"videoResumeDialogMessage": "Möchten Sie bei {time} weiter abspielen?",
"videoStartOverButtonLabel": "NEU BEGINNEN",
"videoResumeButtonLabel": "FORTSETZTEN",
"setCoverDialogTitle": "Titelbild bestimmen",
"setCoverDialogLatest": "Letzter Artikel",
"setCoverDialogCustom": "Benutzerdefiniert",
"hideFilterConfirmationDialogMessage": "Passende Fotos und Videos werden aus Ihrer Sammlung ausgeblendet. Sie können sie in den „Datenschutz“-Einstellungen wieder einblenden.\n\nSind Sie sicher, dass Sie sie ausblenden möchten?",
"newAlbumDialogTitle": "Neues Album",
"newAlbumDialogNameLabel": "Album Name",
"newAlbumDialogNameLabelAlreadyExistsHelper": "Verzeichnis existiert bereits",
"newAlbumDialogStorageLabel": "Speicher:",
"renameAlbumDialogLabel": "Neuer Name",
"renameAlbumDialogLabelAlreadyExistsHelper": "Verzeichnis existiert bereits",
"deleteSingleAlbumConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie dieses Album und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie dieses Album und deren {count} Elemente löschen möchten?}}",
"deleteMultiAlbumConfirmationDialogMessage": " {count, plural, =1{Sind Sie sicher, dass Sie diese Alben und ihren Inhalt löschen möchten?} other{Sind Sie sicher, dass Sie diese Alben und deren {count} Elemente löschen möchten?}}",
"exportEntryDialogFormat": "Format:",
"renameEntryDialogLabel": "Neuer Name",
"editEntryDateDialogTitle": "Datum & Uhrzeit",
"editEntryDateDialogSet": "Festlegen",
"editEntryDateDialogShift": "Verschieben",
"editEntryDateDialogExtractFromTitle": "Auszug aus dem Titel",
"editEntryDateDialogClear": "Aufräumen",
"editEntryDateDialogFieldSelection": "Feldauswahl",
"editEntryDateDialogHours": "Stunden",
"editEntryDateDialogMinutes": "Minuten",
"removeEntryMetadataDialogTitle": "Entfernung von Metadaten",
"removeEntryMetadataDialogMore": "Mehr",
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP ist erforderlich, um das Video innerhalb eines bewegten Bildes abzuspielen.\n\nSind Sie sicher, dass Sie es entfernen möchten?",
"videoSpeedDialogLabel": "Wiedergabegeschwindigkeit",
"videoStreamSelectionDialogVideo": "Video",
"videoStreamSelectionDialogAudio": "Audio",
"videoStreamSelectionDialogText": "Untertitel",
"videoStreamSelectionDialogOff": "Aus",
"videoStreamSelectionDialogTrack": "Spur",
"videoStreamSelectionDialogNoSelection": "Es gibt keine anderen Spuren.",
"genericSuccessFeedback": "Erledigt!",
"genericFailureFeedback": "Gescheitert",
"menuActionConfigureView": "Sortierung",
"menuActionSelect": "Auswahl",
"menuActionSelectAll": "Alle auswählen",
"menuActionSelectNone": "Keine auswählen",
"menuActionMap": "Karte",
"menuActionStats": "Statistiken",
"viewDialogTabSort": "Sortieren",
"viewDialogTabGroup": "Gruppe",
"viewDialogTabLayout": "Layout",
"tileLayoutGrid": "Kacheln",
"tileLayoutList": "Liste",
"aboutPageTitle": "Über",
"aboutLinkSources": "Quellen",
"aboutLinkLicense": "Lizenz",
"aboutLinkPolicy": "Datenschutzrichtlinie",
"aboutUpdate": "Neue Version verfügbar",
"aboutUpdateLinks1": "Eine neue Version von Aves ist verfügbar unter",
"aboutUpdateLinks2": "und",
"aboutUpdateLinks3": ".",
"aboutUpdateGitHub": "github",
"aboutUpdateGooglePlay": "Google Play",
"aboutBug": "Fehlerbericht",
"aboutBugSaveLogInstruction": "Anwendungsprotokolle in einer Datei speichern",
"aboutBugSaveLogButton": "Speichern",
"aboutBugCopyInfoInstruction": "Systeminformationen kopieren",
"aboutBugCopyInfoButton": "Kopieren",
"aboutBugReportInstruction": "Bericht auf GitHub mit den Protokollen und Systeminformationen",
"aboutBugReportButton": "Bericht",
"aboutCredits": "Credits",
"aboutCreditsWorldAtlas1": "Diese Anwendung verwendet eine TopoJSON-Datei von",
"aboutCreditsWorldAtlas2": "unter ISC-Lizenz.",
"aboutCreditsTranslators": "Übersetzer:",
"aboutCreditsTranslatorLine": "{language}: {names}",
"aboutLicenses": "Open-Source-Lizenzen",
"aboutLicensesBanner": "Diese Anwendung verwendet die folgenden Open-Source-Pakete und -Bibliotheken.",
"aboutLicensesAndroidLibraries": "Android-Bibliotheken",
"aboutLicensesFlutterPlugins": "Flutter-Plugins",
"aboutLicensesFlutterPackages": "Flatter-Pakete",
"aboutLicensesDartPackages": "Dart-Pakete",
"aboutLicensesShowAllButtonLabel": "Alle Lizenzen anzeigen",
"policyPageTitle": "Datenschutzrichtlinie",
"collectionPageTitle": "Sammlung",
"collectionPickPageTitle": "Wähle",
"collectionSelectionPageTitle": " {count, plural, =0{Elemente auswählen} =1{1 Element} other{{count} Elemente}}",
"collectionActionShowTitleSearch": "Titelfilter anzeigen",
"collectionActionHideTitleSearch": "Titelfilter ausblenden",
"collectionActionAddShortcut": "Verknüpfung hinzufügen",
"collectionActionCopy": "In Album kopieren",
"collectionActionMove": "Zum Album verschieben",
"collectionActionRescan": "Neu scannen",
"collectionActionEdit": "Bearbeiten",
"collectionSearchTitlesHintText": "Titel suchen",
"collectionSortDate": "Nach Datum",
"collectionSortSize": "Nach Größe",
"collectionSortName": "Nach Album & Dateiname",
"collectionGroupAlbum": "Nach Album",
"collectionGroupMonth": "Nach Monat",
"collectionGroupDay": "Nach Tag",
"collectionGroupNone": "Nicht gruppieren",
"sectionUnknown": "Unbekannt",
"dateToday": "Heute",
"dateYesterday": "Gestern",
"dateThisMonth": "Diesen Monat",
"collectionDeleteFailureFeedback": " {count, plural, =1{1 Element konnte nicht gelöscht werden} other{{count} Elemente konnten nicht gelöscht werden}}",
"collectionCopyFailureFeedback": " {count, plural, =1{1 Element konnte nicht kopiert werden} other{{count} Element konnten nicht kopiert werden}}",
"collectionMoveFailureFeedback": " {count, plural, =1{1 Element konnte nicht verschoben werden} other{{count} Elemente konnten nicht verschoben werden}}",
"collectionEditFailureFeedback": " {count, plural, =1{1 Element konnte nicht bearbeitet werden} other{{count} 1 Elemente konnten nicht bearbeitet werden}}",
"collectionExportFailureFeedback": " {count, plural, =1{1 Seite konnte nicht exportiert werden} other{{count} Seiten konnten nicht exportiert werden}}",
"collectionCopySuccessFeedback": " {count, plural, =1{1 Element kopier} other{ {count} Elemente kopiert}}",
"collectionMoveSuccessFeedback": " {count, plural, =1{1 Element verschoben} other{{count} Elemente verschoben}}",
"collectionEditSuccessFeedback": " {count, plural, =1{1 Element bearbeitet} other{ {count} Elemente bearbeitet}}",
"collectionEmptyFavourites": "Keine Favoriten",
"collectionEmptyVideos": "Keine Videos",
"collectionEmptyImages": "Keine Bilder",
"collectionSelectSectionTooltip": "Bereich auswählen",
"collectionDeselectSectionTooltip": "Bereich abwählen",
"drawerCollectionAll": "Alle Sammlung",
"drawerCollectionFavourites": "Favoriten",
"drawerCollectionImages": "Bilder",
"drawerCollectionVideos": "Videos",
"drawerCollectionAnimated": "Animationen",
"drawerCollectionMotionPhotos": "Bewegte Fotos",
"drawerCollectionPanoramas": "Panoramen",
"drawerCollectionRaws": "Rohdaten Fotos",
"drawerCollectionSphericalVideos": "360°-Videos",
"chipSortDate": "Nach Datum",
"chipSortName": "Nach Name",
"chipSortCount": "Nach Anzahl",
"albumGroupTier": "Nach Ebene",
"albumGroupVolume": "Nach Speichervolumen",
"albumGroupNone": "Nicht gruppieren",
"albumPickPageTitleCopy": "In Album kopieren",
"albumPickPageTitleExport": "In Album exportieren",
"albumPickPageTitleMove": "Zum Album verschieben",
"albumPickPageTitlePick": "Album auswählen",
"albumCamera": "Kamera",
"albumDownload": "Herunterladen",
"albumScreenshots": "Bildschirmfotos",
"albumScreenRecordings": "Bildschirmaufnahmen",
"albumVideoCaptures": "Video-Aufnahmen",
"albumPageTitle": "Alben",
"albumEmpty": "Keine Alben",
"createAlbumTooltip": "Album erstellen",
"createAlbumButtonLabel": "ERSTELLE",
"newFilterBanner": "Neu",
"countryPageTitle": "Länder",
"countryEmpty": "Keine Länder",
"tagPageTitle": "Tags",
"tagEmpty": "Keine Tags",
"searchCollectionFieldHint": "Sammlung durchsuchen",
"searchSectionRecent": "Neueste",
"searchSectionAlbums": "Alben",
"searchSectionCountries": "Länder",
"searchSectionPlaces": "Orte",
"searchSectionTags": "Tags",
"settingsPageTitle": "Einstellungen",
"settingsSystemDefault": "System",
"settingsDefault": "Standard",
"settingsActionExport": "Exportieren",
"settingsActionImport": "Importieren",
"settingsSectionNavigation": "Navigation",
"settingsHome": "Startseite",
"settingsKeepScreenOnTile": "Bildschirm eingeschaltet lassen",
"settingsKeepScreenOnTitle": "Bildschirm eingeschaltet lassen",
"settingsDoubleBackExit": "Zum Verlassen zweimal „zurück“ tippen",
"settingsNavigationDrawerTile": "Menü Navigation",
"settingsNavigationDrawerEditorTitle": "Menü Navigation",
"settingsNavigationDrawerBanner": "Berühren und halten Sie die Taste, um Menüpunkte zu verschieben und neu anzuordnen.",
"settingsNavigationDrawerTabTypes": "Typen",
"settingsNavigationDrawerTabAlbums": "Alben",
"settingsNavigationDrawerTabPages": "Seiten",
"settingsNavigationDrawerAddAlbum": "Album hinzufügen",
"settingsSectionThumbnails": "Vorschaubilder",
"settingsThumbnailShowLocationIcon": "Standort-Symbol anzeigen",
"settingsThumbnailShowMotionPhotoIcon": "Bewegungsfoto-Symbol anzeigen",
"settingsThumbnailShowRawIcon": "Rohdaten-Symbol anzeigen",
"settingsThumbnailShowVideoDuration": "Videodauer anzeigen",
"settingsCollectionQuickActionsTile": "Schnelle Aktionen",
"settingsCollectionQuickActionEditorTitle": "Schnelle Aktionen",
"settingsCollectionQuickActionTabBrowsing": "Durchsuchen",
"settingsCollectionQuickActionTabSelecting": "Auswahl",
"settingsCollectionBrowsingQuickActionEditorBanner": "Halten Sie die Taste gedrückt, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen beim Durchsuchen von Elementen angezeigt werden.",
"settingsCollectionSelectionQuickActionEditorBanner": "Halten Sie die Taste gedrückt, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen bei der Auswahl von Elementen angezeigt werden.",
"settingsSectionViewer": "Anzeige",
"settingsViewerUseCutout": "Ausgeschnittenen Bereich verwenden",
"settingsViewerMaximumBrightness": "Maximale Helligkeit",
"settingsMotionPhotoAutoPlay": "Automatische Wiedergabe bewegter Fotos",
"settingsImageBackground": "Bild-Hintergrund",
"settingsViewerQuickActionsTile": "Schnelle Aktionen",
"settingsViewerQuickActionEditorTitle": "Schnelle Aktionen",
"settingsViewerQuickActionEditorBanner": "Halten Sie die Taste gedrückt, um die Schaltflächen zu bewegen und auszuwählen, welche Aktionen im Viewer angezeigt werden sollen.",
"settingsViewerQuickActionEditorDisplayedButtons": "Angezeigte Schaltflächen",
"settingsViewerQuickActionEditorAvailableButtons": "Verfügbare Schaltflächen",
"settingsViewerQuickActionEmpty": "Keine Tasten",
"settingsViewerOverlayTile": "Überlagerung",
"settingsViewerOverlayTitle": "Überlagerung",
"settingsViewerShowOverlayOnOpening": "Bei Eröffnung anzeigen",
"settingsViewerShowMinimap": "Minimap anzeigen",
"settingsViewerShowInformation": "Informationen anzeigen",
"settingsViewerShowInformationSubtitle": "Titel, Datum, Ort, etc. anzeigen.",
"settingsViewerShowShootingDetails": "Aufnahmedetails anzeigen",
"settingsViewerEnableOverlayBlurEffect": "Unschärfe-Effekt",
"settingsVideoPageTitle": "Video-Einstellungen",
"settingsSectionVideo": "Video",
"settingsVideoShowVideos": "Videos anzeigen",
"settingsVideoEnableHardwareAcceleration": "Hardware-Beschleunigung",
"settingsVideoEnableAutoPlay": "Automatische Wiedergabe",
"settingsVideoLoopModeTile": "Schleifen-Modus",
"settingsVideoLoopModeTitle": "Schleifen-Modus",
"settingsVideoQuickActionsTile": "Schnelle Aktionen für Videos",
"settingsVideoQuickActionEditorTitle": "Schnelle Aktionen",
"settingsSubtitleThemeTile": "Untertitel",
"settingsSubtitleThemeTitle": "Untertitel",
"settingsSubtitleThemeSample": "Dies ist ein Beispiel.",
"settingsSubtitleThemeTextAlignmentTile": "Textausrichtung",
"settingsSubtitleThemeTextAlignmentTitle": "Textausrichtung",
"settingsSubtitleThemeTextSize": "Textgröße",
"settingsSubtitleThemeShowOutline": "Umriss und Schatten anzeigen",
"settingsSubtitleThemeTextColor": "Textfarbe",
"settingsSubtitleThemeTextOpacity": "Opazität des Textes",
"settingsSubtitleThemeBackgroundColor": "Hintergrundfarbe",
"settingsSubtitleThemeBackgroundOpacity": "Hintergrund-Opazität",
"settingsSubtitleThemeTextAlignmentLeft": "Links",
"settingsSubtitleThemeTextAlignmentCenter": "Zentrum",
"settingsSubtitleThemeTextAlignmentRight": "Rechts",
"settingsSectionPrivacy": "Datenschutz",
"settingsAllowInstalledAppAccess": "Zugriff auf die Liste der installierten Apps",
"settingsAllowInstalledAppAccessSubtitle": "zur Gruppierung von Bildern nach Apps",
"settingsAllowErrorReporting": "Anonyme Fehlermeldungen zulassen",
"settingsSaveSearchHistory": "Suchverlauf speichern",
"settingsHiddenItemsTile": "Versteckte Elemente",
"settingsHiddenItemsTitle": "Versteckte Gegenstände",
"settingsHiddenFiltersTitle": "Versteckte Filter",
"settingsHiddenFiltersBanner": "Fotos und Videos, die versteckten Filtern entsprechen, werden nicht in Ihrer Sammlung angezeigt.",
"settingsHiddenFiltersEmpty": "Keine versteckten Filter",
"settingsHiddenPathsTitle": "Verborgene Pfade",
"settingsHiddenPathsBanner": "Fotos und Videos, die sich in diesen Ordnern oder in einem ihrer Unterordner befinden, werden nicht in Ihrer Sammlung angezeigt.",
"addPathTooltip": "Pfad hinzufügen",
"settingsStorageAccessTile": "Speicherzugriff",
"settingsStorageAccessTitle": "Speicherzugriff",
"settingsStorageAccessBanner": "Einige Verzeichnisse erfordern eine explizite Zugriffsberechtigung, um Dateien darin zu ändern. Sie können hier Verzeichnisse überprüfen, auf die Sie zuvor Zugriff gewährt haben.",
"settingsStorageAccessEmpty": "Keine Zugangsberechtigungen",
"settingsStorageAccessRevokeTooltip": "Widerrufen",
"settingsSectionAccessibility": "Barrierefreiheit",
"settingsRemoveAnimationsTile": "Animationen entfernen",
"settingsRemoveAnimationsTitle": "Animationen entfernen",
"settingsTimeToTakeActionTile": "Zeit zum Reagieren",
"settingsTimeToTakeActionTitle": "Zeit zum Reagieren",
"settingsSectionLanguage": "Sprache & Formate",
"settingsLanguage": "Sprache",
"settingsCoordinateFormatTile": "Koordinatenformat",
"settingsCoordinateFormatTitle": "Koordinatenformat",
"settingsUnitSystemTile": "Einheiten",
"settingsUnitSystemTitle": "Einheiten",
"statsPageTitle": "Statistiken",
"statsWithGps": " {count, plural, =1{1 Element mit Standort} other{{count} Elemente mit Standort}}",
"statsTopCountries": "Top-Länder",
"statsTopPlaces": "Top-Plätze",
"statsTopTags": "Top-Tags",
"viewerOpenPanoramaButtonLabel": "ÖFFNE PANORAMA",
"viewerErrorUnknown": "Ups!",
"viewerErrorDoesNotExist": "Die Datei existiert nicht mehr.",
"viewerInfoPageTitle": "Info",
"viewerInfoBackToViewerTooltip": "Zurück zum Betrachter",
"viewerInfoUnknown": "Unbekannt",
"viewerInfoLabelTitle": "Titel",
"viewerInfoLabelDate": "Datum",
"viewerInfoLabelResolution": "Auflösung",
"viewerInfoLabelSize": "Größe",
"viewerInfoLabelUri": "URL",
"viewerInfoLabelPath": "Pfad",
"viewerInfoLabelDuration": "Dauer",
"viewerInfoLabelOwner": "Im Besitz von",
"viewerInfoLabelCoordinates": "Koordinaten",
"viewerInfoLabelAddress": "Adresse",
"mapStyleTitle": "Kartenstil",
"mapStyleTooltip": "Kartenstil auswählen",
"mapZoomInTooltip": "Vergrößern",
"mapZoomOutTooltip": "Verkleinern",
"mapPointNorthUpTooltip": "Richtung Norden aufwärts",
"mapAttributionOsmHot": "Kartendaten © [OpenStreetMap](https://www.openstreetmap.org/copyright) Mitwirkende - Kacheln von [HOT](https://www.hotosm.org/) - Gehostet von [OSM France](https://openstreetmap.fr/)",
"mapAttributionStamen": "Kartendaten © [OpenStreetMap](https://www.openstreetmap.org/copyright) Mitwirkende - Kacheln von [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)",
"openMapPageTooltip": "Auf der Karte anzeigen",
"mapEmptyRegion": "Keine Bilder in dieser Region",
"viewerInfoOpenEmbeddedFailureFeedback": "Eingebettete Daten konnten nicht extrahiert werden",
"viewerInfoOpenLinkText": "Öffnen Sie",
"viewerInfoViewXmlLinkText": "Ansicht XML",
"viewerInfoSearchFieldLabel": "Metadaten suchen",
"viewerInfoSearchEmpty": "Keine passenden Schlüssel",
"viewerInfoSearchSuggestionDate": "Datum & Uhrzeit",
"viewerInfoSearchSuggestionDescription": "Beschreibung",
"viewerInfoSearchSuggestionDimensions": "Abmessungen",
"viewerInfoSearchSuggestionResolution": "Auflösung",
"viewerInfoSearchSuggestionRights": "Rechte",
"tagEditorPageTitle": "Tags bearbeiten",
"tagEditorPageNewTagFieldLabel": "Neuer Tag",
"tagEditorPageAddTagTooltip": "Tag hinzufügen",
"tagEditorSectionRecent": "Neueste",
"panoramaEnableSensorControl": "Aktivieren der Sensorsteuerung",
"panoramaDisableSensorControl": "Sensorsteuerung deaktivieren",
"sourceViewerPageTitle": "Quelle",
"filePickerShowHiddenFiles": "Versteckte Dateien anzeigen",
"filePickerDoNotShowHiddenFiles": "Versteckte Dateien nicht anzeigen",
"filePickerOpenFrom": "Öffnen von",
"filePickerNoItems": "Keine Elemente",
"filePickerUseThisFolder": "Verwenden Sie diesen Ordner"
}

View file

@ -1,11 +1,8 @@
{ {
"appName": "Aves", "appName": "Aves",
"@appName": {},
"welcomeMessage": "Welcome to Aves", "welcomeMessage": "Welcome to Aves",
"@welcomeMessage": {},
"welcomeOptional": "Optional", "welcomeOptional": "Optional",
"welcomeTermsToggle": "I agree to the terms and conditions", "welcomeTermsToggle": "I agree to the terms and conditions",
"@welcomeTermsToggle": {},
"itemCount": "{count, plural, =1{1 item} other{{count} items}}", "itemCount": "{count, plural, =1{1 item} other{{count} items}}",
"@itemCount": { "@itemCount": {
"placeholders": { "placeholders": {
@ -27,158 +24,87 @@
}, },
"applyButtonLabel": "APPLY", "applyButtonLabel": "APPLY",
"@applyButtonLabel": {},
"deleteButtonLabel": "DELETE", "deleteButtonLabel": "DELETE",
"@deleteButtonLabel": {},
"nextButtonLabel": "NEXT", "nextButtonLabel": "NEXT",
"@nextButtonLabel": {},
"showButtonLabel": "SHOW", "showButtonLabel": "SHOW",
"@showButtonLabel": {},
"hideButtonLabel": "HIDE", "hideButtonLabel": "HIDE",
"@hideButtonLabel": {},
"continueButtonLabel": "CONTINUE", "continueButtonLabel": "CONTINUE",
"@continueButtonLabel": {},
"cancelTooltip": "Cancel",
"changeTooltip": "Change", "changeTooltip": "Change",
"@changeTooltip": {},
"clearTooltip": "Clear", "clearTooltip": "Clear",
"@clearTooltip": {},
"previousTooltip": "Previous", "previousTooltip": "Previous",
"@previousTooltip": {},
"nextTooltip": "Next", "nextTooltip": "Next",
"@nextTooltip": {},
"showTooltip": "Show", "showTooltip": "Show",
"@showTooltip": {},
"hideTooltip": "Hide", "hideTooltip": "Hide",
"@hideTooltip": {},
"removeTooltip": "Remove", "removeTooltip": "Remove",
"@removeTooltip": {},
"resetButtonTooltip": "Reset", "resetButtonTooltip": "Reset",
"@resetButtonTooltip": {},
"doubleBackExitMessage": "Tap “back” again to exit.", "doubleBackExitMessage": "Tap “back” again to exit.",
"@doubleBackExitMessage": {},
"sourceStateLoading": "Loading", "sourceStateLoading": "Loading",
"@sourceStateLoading": {},
"sourceStateCataloguing": "Cataloguing", "sourceStateCataloguing": "Cataloguing",
"@sourceStateCataloguing": {},
"sourceStateLocatingCountries": "Locating countries", "sourceStateLocatingCountries": "Locating countries",
"@sourceStateLocatingCountries": {},
"sourceStateLocatingPlaces": "Locating places", "sourceStateLocatingPlaces": "Locating places",
"@sourceStateLocatingPlaces": {},
"chipActionDelete": "Delete", "chipActionDelete": "Delete",
"@chipActionDelete": {},
"chipActionGoToAlbumPage": "Show in Albums", "chipActionGoToAlbumPage": "Show in Albums",
"@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "Show in Countries", "chipActionGoToCountryPage": "Show in Countries",
"@chipActionGoToCountryPage": {},
"chipActionGoToTagPage": "Show in Tags", "chipActionGoToTagPage": "Show in Tags",
"@chipActionGoToTagPage": {},
"chipActionHide": "Hide", "chipActionHide": "Hide",
"@chipActionHide": {},
"chipActionPin": "Pin to top", "chipActionPin": "Pin to top",
"@chipActionPin": {},
"chipActionUnpin": "Unpin from top", "chipActionUnpin": "Unpin from top",
"@chipActionUnpin": {},
"chipActionRename": "Rename", "chipActionRename": "Rename",
"@chipActionRename": {},
"chipActionSetCover": "Set cover", "chipActionSetCover": "Set cover",
"@chipActionSetCover": {},
"chipActionCreateAlbum": "Create album", "chipActionCreateAlbum": "Create album",
"@chipActionCreateAlbum": {},
"entryActionCopyToClipboard": "Copy to clipboard", "entryActionCopyToClipboard": "Copy to clipboard",
"@entryActionCopyToClipboard": {},
"entryActionDelete": "Delete", "entryActionDelete": "Delete",
"@entryActionDelete": {},
"entryActionExport": "Export", "entryActionExport": "Export",
"@entryActionExport": {},
"entryActionInfo": "Info", "entryActionInfo": "Info",
"@entryActionInfo": {},
"entryActionRename": "Rename", "entryActionRename": "Rename",
"@entryActionRename": {},
"entryActionRotateCCW": "Rotate counterclockwise", "entryActionRotateCCW": "Rotate counterclockwise",
"@entryActionRotateCCW": {},
"entryActionRotateCW": "Rotate clockwise", "entryActionRotateCW": "Rotate clockwise",
"@entryActionRotateCW": {},
"entryActionFlip": "Flip horizontally", "entryActionFlip": "Flip horizontally",
"@entryActionFlip": {},
"entryActionPrint": "Print", "entryActionPrint": "Print",
"@entryActionPrint": {},
"entryActionShare": "Share", "entryActionShare": "Share",
"@entryActionShare": {},
"entryActionViewSource": "View source", "entryActionViewSource": "View source",
"@entryActionViewSource": {},
"entryActionViewMotionPhotoVideo": "Open Motion Photo", "entryActionViewMotionPhotoVideo": "Open Motion Photo",
"@entryActionViewMotionPhotoVideo": {},
"entryActionEdit": "Edit with…", "entryActionEdit": "Edit with…",
"@entryActionEdit": {},
"entryActionOpen": "Open with…", "entryActionOpen": "Open with…",
"@entryActionOpen": {},
"entryActionSetAs": "Set as…", "entryActionSetAs": "Set as…",
"@entryActionSetAs": {},
"entryActionOpenMap": "Show in map app…", "entryActionOpenMap": "Show in map app…",
"@entryActionOpenMap": {},
"entryActionRotateScreen": "Rotate screen", "entryActionRotateScreen": "Rotate screen",
"@entryActionRotateScreen": {},
"entryActionAddFavourite": "Add to favourites", "entryActionAddFavourite": "Add to favourites",
"@entryActionAddFavourite": {},
"entryActionRemoveFavourite": "Remove from favourites", "entryActionRemoveFavourite": "Remove from favourites",
"@entryActionRemoveFavourite": {},
"videoActionCaptureFrame": "Capture frame", "videoActionCaptureFrame": "Capture frame",
"@videoActionCaptureFrame": {},
"videoActionPause": "Pause", "videoActionPause": "Pause",
"@videoActionPause": {},
"videoActionPlay": "Play", "videoActionPlay": "Play",
"@videoActionPlay": {},
"videoActionReplay10": "Seek backward 10 seconds", "videoActionReplay10": "Seek backward 10 seconds",
"@videoActionReplay10": {},
"videoActionSkip10": "Seek forward 10 seconds", "videoActionSkip10": "Seek forward 10 seconds",
"@videoActionSkip10": {},
"videoActionSelectStreams": "Select tracks", "videoActionSelectStreams": "Select tracks",
"@videoActionSelectStreams": {},
"videoActionSetSpeed": "Playback speed", "videoActionSetSpeed": "Playback speed",
"@videoActionSetSpeed": {},
"videoActionSettings": "Settings", "videoActionSettings": "Settings",
"@videoActionSettings": {},
"entryInfoActionEditDate": "Edit date & time", "entryInfoActionEditDate": "Edit date & time",
"@entryInfoActionEditDate": {},
"entryInfoActionEditTags": "Edit tags", "entryInfoActionEditTags": "Edit tags",
"@entryInfoActionEditTags": {},
"entryInfoActionRemoveMetadata": "Remove metadata", "entryInfoActionRemoveMetadata": "Remove metadata",
"@entryInfoActionRemoveMetadata": {},
"filterFavouriteLabel": "Favourite", "filterFavouriteLabel": "Favourite",
"@filterFavouriteLabel": {},
"filterLocationEmptyLabel": "Unlocated", "filterLocationEmptyLabel": "Unlocated",
"@filterLocationEmptyLabel": {},
"filterTagEmptyLabel": "Untagged", "filterTagEmptyLabel": "Untagged",
"@filterTagEmptyLabel": {},
"filterTypeAnimatedLabel": "Animated", "filterTypeAnimatedLabel": "Animated",
"@filterTypeAnimatedLabel": {},
"filterTypeMotionPhotoLabel": "Motion Photo", "filterTypeMotionPhotoLabel": "Motion Photo",
"@filterTypeMotionPhotoLabel": {},
"filterTypePanoramaLabel": "Panorama", "filterTypePanoramaLabel": "Panorama",
"@filterTypePanoramaLabel": {},
"filterTypeRawLabel": "Raw", "filterTypeRawLabel": "Raw",
"@filterTypeRawLabel": {},
"filterTypeSphericalVideoLabel": "360° Video", "filterTypeSphericalVideoLabel": "360° Video",
"@filterTypeSphericalVideoLabel": {},
"filterTypeGeotiffLabel": "GeoTIFF", "filterTypeGeotiffLabel": "GeoTIFF",
"@filterTypeGeotiffLabel": {},
"filterMimeImageLabel": "Image", "filterMimeImageLabel": "Image",
"@filterMimeImageLabel": {},
"filterMimeVideoLabel": "Video", "filterMimeVideoLabel": "Video",
"@filterMimeVideoLabel": {},
"coordinateFormatDms": "DMS", "coordinateFormatDms": "DMS",
"@coordinateFormatDms": {},
"coordinateFormatDecimal": "Decimal degrees", "coordinateFormatDecimal": "Decimal degrees",
"@coordinateFormatDecimal": {},
"coordinateDms": "{coordinate} {direction}", "coordinateDms": "{coordinate} {direction}",
"@coordinateDms": { "@coordinateDms": {
"placeholders": { "placeholders": {
@ -191,75 +117,44 @@
} }
}, },
"coordinateDmsNorth": "N", "coordinateDmsNorth": "N",
"@coordinateDmsNorth": {},
"coordinateDmsSouth": "S", "coordinateDmsSouth": "S",
"@coordinateDmsSouth": {},
"coordinateDmsEast": "E", "coordinateDmsEast": "E",
"@coordinateDmsEast": {},
"coordinateDmsWest": "W", "coordinateDmsWest": "W",
"@coordinateDmsWest": {},
"unitSystemMetric": "Metric", "unitSystemMetric": "Metric",
"@unitSystemMetric": {},
"unitSystemImperial": "Imperial", "unitSystemImperial": "Imperial",
"@unitSystemImperial": {},
"videoLoopModeNever": "Never", "videoLoopModeNever": "Never",
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "Short videos only", "videoLoopModeShortOnly": "Short videos only",
"@videoLoopModeShortOnly": {},
"videoLoopModeAlways": "Always", "videoLoopModeAlways": "Always",
"@videoLoopModeAlways": {},
"mapStyleGoogleNormal": "Google Maps", "mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleHybrid": "Google Maps (Hybrid)", "mapStyleGoogleHybrid": "Google Maps (Hybrid)",
"@mapStyleGoogleHybrid": {},
"mapStyleGoogleTerrain": "Google Maps (Terrain)", "mapStyleGoogleTerrain": "Google Maps (Terrain)",
"@mapStyleGoogleTerrain": {},
"mapStyleOsmHot": "Humanitarian OSM", "mapStyleOsmHot": "Humanitarian OSM",
"@mapStyleOsmHot": {},
"mapStyleStamenToner": "Stamen Toner", "mapStyleStamenToner": "Stamen Toner",
"@mapStyleStamenToner": {},
"mapStyleStamenWatercolor": "Stamen Watercolor", "mapStyleStamenWatercolor": "Stamen Watercolor",
"@mapStyleStamenWatercolor": {},
"nameConflictStrategyRename": "Rename", "nameConflictStrategyRename": "Rename",
"@nameConflictStrategyRename": {},
"nameConflictStrategyReplace": "Replace", "nameConflictStrategyReplace": "Replace",
"@nameConflictStrategyReplace": {},
"nameConflictStrategySkip": "Skip", "nameConflictStrategySkip": "Skip",
"@nameConflictStrategySkip": {},
"keepScreenOnNever": "Never", "keepScreenOnNever": "Never",
"@keepScreenOnNever": {},
"keepScreenOnViewerOnly": "Viewer page only", "keepScreenOnViewerOnly": "Viewer page only",
"@keepScreenOnViewerOnly": {},
"keepScreenOnAlways": "Always", "keepScreenOnAlways": "Always",
"@keepScreenOnAlways": {},
"accessibilityAnimationsRemove": "Prevent screen effects", "accessibilityAnimationsRemove": "Prevent screen effects",
"@accessibilityAnimationsRemove": {},
"accessibilityAnimationsKeep": "Keep screen effects", "accessibilityAnimationsKeep": "Keep screen effects",
"@accessibilityAnimationsKeep": {},
"albumTierNew": "New", "albumTierNew": "New",
"@albumTierNew": {},
"albumTierPinned": "Pinned", "albumTierPinned": "Pinned",
"@albumTierPinned": {},
"albumTierSpecial": "Common", "albumTierSpecial": "Common",
"@albumTierSpecial": {},
"albumTierApps": "Apps", "albumTierApps": "Apps",
"@albumTierApps": {},
"albumTierRegular": "Others", "albumTierRegular": "Others",
"@albumTierRegular": {},
"storageVolumeDescriptionFallbackPrimary": "Internal storage", "storageVolumeDescriptionFallbackPrimary": "Internal storage",
"@storageVolumeDescriptionFallbackPrimary": {},
"storageVolumeDescriptionFallbackNonPrimary": "SD card", "storageVolumeDescriptionFallbackNonPrimary": "SD card",
"@storageVolumeDescriptionFallbackNonPrimary": {},
"rootDirectoryDescription": "root directory", "rootDirectoryDescription": "root directory",
"@rootDirectoryDescription": {},
"otherDirectoryDescription": "“{name}” directory", "otherDirectoryDescription": "“{name}” directory",
"@otherDirectoryDescription": { "@otherDirectoryDescription": {
"placeholders": { "placeholders": {
@ -269,7 +164,6 @@
} }
}, },
"storageAccessDialogTitle": "Storage Access", "storageAccessDialogTitle": "Storage Access",
"@storageAccessDialogTitle": {},
"storageAccessDialogMessage": "Please select the {directory} of “{volume}” in the next screen to give this app access to it.", "storageAccessDialogMessage": "Please select the {directory} of “{volume}” in the next screen to give this app access to it.",
"@storageAccessDialogMessage": { "@storageAccessDialogMessage": {
"placeholders": { "placeholders": {
@ -282,7 +176,6 @@
} }
}, },
"restrictedAccessDialogTitle": "Restricted Access", "restrictedAccessDialogTitle": "Restricted Access",
"@restrictedAccessDialogTitle": {},
"restrictedAccessDialogMessage": "This app is not allowed to modify files in the {directory} of “{volume}”.\n\nPlease use a pre-installed file manager or gallery app to move the items to another directory.", "restrictedAccessDialogMessage": "This app is not allowed to modify files in the {directory} of “{volume}”.\n\nPlease use a pre-installed file manager or gallery app to move the items to another directory.",
"@restrictedAccessDialogMessage": { "@restrictedAccessDialogMessage": {
"placeholders": { "placeholders": {
@ -295,7 +188,6 @@
} }
}, },
"notEnoughSpaceDialogTitle": "Not Enough Space", "notEnoughSpaceDialogTitle": "Not Enough Space",
"@notEnoughSpaceDialogTitle": {},
"notEnoughSpaceDialogMessage": "This operation needs {neededSize} of free space on “{volume}” to complete, but there is only {freeSize} left.", "notEnoughSpaceDialogMessage": "This operation needs {neededSize} of free space on “{volume}” to complete, but there is only {freeSize} left.",
"@notEnoughSpaceDialogMessage": { "@notEnoughSpaceDialogMessage": {
"placeholders": { "placeholders": {
@ -312,7 +204,6 @@
}, },
"unsupportedTypeDialogTitle": "Unsupported Types", "unsupportedTypeDialogTitle": "Unsupported Types",
"@unsupportedTypeDialogTitle": {},
"unsupportedTypeDialogMessage": "{count, plural, =1{This operation is not supported for items of the following type: {types}.} other{This operation is not supported for items of the following types: {types}.}}", "unsupportedTypeDialogMessage": "{count, plural, =1{This operation is not supported for items of the following type: {types}.} other{This operation is not supported for items of the following types: {types}.}}",
"@unsupportedTypeDialogMessage": { "@unsupportedTypeDialogMessage": {
"placeholders": { "placeholders": {
@ -324,19 +215,13 @@
}, },
"nameConflictDialogSingleSourceMessage": "Some files in the destination folder have the same name.", "nameConflictDialogSingleSourceMessage": "Some files in the destination folder have the same name.",
"@nameConflictDialogSingleSourceMessage": {},
"nameConflictDialogMultipleSourceMessage": "Some files have the same name.", "nameConflictDialogMultipleSourceMessage": "Some files have the same name.",
"@nameConflictDialogMultipleSourceMessage": {},
"addShortcutDialogLabel": "Shortcut label", "addShortcutDialogLabel": "Shortcut label",
"@addShortcutDialogLabel": {},
"addShortcutButtonLabel": "ADD", "addShortcutButtonLabel": "ADD",
"@addShortcutButtonLabel": {},
"noMatchingAppDialogTitle": "No Matching App", "noMatchingAppDialogTitle": "No Matching App",
"@noMatchingAppDialogTitle": {},
"noMatchingAppDialogMessage": "There are no apps that can handle this.", "noMatchingAppDialogMessage": "There are no apps that can handle this.",
"@noMatchingAppDialogMessage": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this item?} other{Are you sure you want to delete these {count} items?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this item?} other{Are you sure you want to delete these {count} items?}}",
"@deleteEntriesConfirmationDialogMessage": { "@deleteEntriesConfirmationDialogMessage": {
@ -352,33 +237,21 @@
} }
}, },
"videoStartOverButtonLabel": "START OVER", "videoStartOverButtonLabel": "START OVER",
"@videoStartOverButtonLabel": {},
"videoResumeButtonLabel": "RESUME", "videoResumeButtonLabel": "RESUME",
"@videoResumeButtonLabel": {},
"setCoverDialogTitle": "Set Cover", "setCoverDialogTitle": "Set Cover",
"@setCoverDialogTitle": {},
"setCoverDialogLatest": "Latest item", "setCoverDialogLatest": "Latest item",
"@setCoverDialogLatest": {},
"setCoverDialogCustom": "Custom", "setCoverDialogCustom": "Custom",
"@setCoverDialogCustom": {},
"hideFilterConfirmationDialogMessage": "Matching photos and videos will be hidden from your collection. You can show them again from the “Privacy” settings.\n\nAre you sure you want to hide them?", "hideFilterConfirmationDialogMessage": "Matching photos and videos will be hidden from your collection. You can show them again from the “Privacy” settings.\n\nAre you sure you want to hide them?",
"@hideFilterConfirmationDialogMessage": {},
"newAlbumDialogTitle": "New Album", "newAlbumDialogTitle": "New Album",
"@newAlbumDialogTitle": {},
"newAlbumDialogNameLabel": "Album name", "newAlbumDialogNameLabel": "Album name",
"@newAlbumDialogNameLabel": {},
"newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists", "newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists",
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
"newAlbumDialogStorageLabel": "Storage:", "newAlbumDialogStorageLabel": "Storage:",
"@newAlbumDialogStorageLabel": {},
"renameAlbumDialogLabel": "New name", "renameAlbumDialogLabel": "New name",
"@renameAlbumDialogLabel": {},
"renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists", "renameAlbumDialogLabelAlreadyExistsHelper": "Directory already exists",
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this album and its item?} other{Are you sure you want to delete this album and its {count} items?}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Are you sure you want to delete this album and its item?} other{Are you sure you want to delete this album and its {count} items?}}",
"@deleteSingleAlbumConfirmationDialogMessage": { "@deleteSingleAlbumConfirmationDialogMessage": {
@ -394,117 +267,73 @@
}, },
"exportEntryDialogFormat": "Format:", "exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {},
"renameEntryDialogLabel": "New name", "renameEntryDialogLabel": "New name",
"@renameEntryDialogLabel": {},
"editEntryDateDialogTitle": "Date & Time", "editEntryDateDialogTitle": "Date & Time",
"@editEntryDateDialogTitle": {},
"editEntryDateDialogSet": "Set", "editEntryDateDialogSet": "Set",
"@editEntryDateDialogSet": {},
"editEntryDateDialogShift": "Shift", "editEntryDateDialogShift": "Shift",
"@editEntryDateDialogShift": {},
"editEntryDateDialogExtractFromTitle": "Extract from title", "editEntryDateDialogExtractFromTitle": "Extract from title",
"@editEntryDateDialogExtractFromTitle": {},
"editEntryDateDialogClear": "Clear", "editEntryDateDialogClear": "Clear",
"@editEntryDateDialogClear": {},
"editEntryDateDialogFieldSelection": "Field selection", "editEntryDateDialogFieldSelection": "Field selection",
"@editEntryDateDialogFieldSelection": {},
"editEntryDateDialogHours": "Hours", "editEntryDateDialogHours": "Hours",
"@editEntryDateDialogHours": {},
"editEntryDateDialogMinutes": "Minutes", "editEntryDateDialogMinutes": "Minutes",
"@editEntryDateDialogMinutes": {},
"removeEntryMetadataDialogTitle": "Metadata Removal", "removeEntryMetadataDialogTitle": "Metadata Removal",
"@removeEntryMetadataDialogTitle": {},
"removeEntryMetadataDialogMore": "More", "removeEntryMetadataDialogMore": "More",
"@removeEntryMetadataDialogMore": {},
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP is required to play the video inside a motion photo.\n\nAre you sure you want to remove it?", "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP is required to play the video inside a motion photo.\n\nAre you sure you want to remove it?",
"@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {},
"videoSpeedDialogLabel": "Playback speed", "videoSpeedDialogLabel": "Playback speed",
"@videoSpeedDialogLabel": {},
"videoStreamSelectionDialogVideo": "Video", "videoStreamSelectionDialogVideo": "Video",
"@videoStreamSelectionDialogVideo": {},
"videoStreamSelectionDialogAudio": "Audio", "videoStreamSelectionDialogAudio": "Audio",
"@videoStreamSelectionDialogAudio": {},
"videoStreamSelectionDialogText": "Subtitles", "videoStreamSelectionDialogText": "Subtitles",
"@videoStreamSelectionDialogText": {},
"videoStreamSelectionDialogOff": "Off", "videoStreamSelectionDialogOff": "Off",
"@videoStreamSelectionDialogOff": {},
"videoStreamSelectionDialogTrack": "Track", "videoStreamSelectionDialogTrack": "Track",
"@videoStreamSelectionDialogTrack": {},
"videoStreamSelectionDialogNoSelection": "There are no other tracks.", "videoStreamSelectionDialogNoSelection": "There are no other tracks.",
"@videoStreamSelectionDialogNoSelection": {},
"genericSuccessFeedback": "Done!", "genericSuccessFeedback": "Done!",
"@genericSuccessFeedback": {},
"genericFailureFeedback": "Failed", "genericFailureFeedback": "Failed",
"@genericFailureFeedback": {},
"menuActionSort": "Sort", "menuActionConfigureView": "View",
"@menuActionSort": {},
"menuActionGroup": "Group",
"@menuActionGroup": {},
"menuActionSelect": "Select", "menuActionSelect": "Select",
"@menuActionSelect": {},
"menuActionSelectAll": "Select all", "menuActionSelectAll": "Select all",
"@menuActionSelectAll": {},
"menuActionSelectNone": "Select none", "menuActionSelectNone": "Select none",
"@menuActionSelectNone": {},
"menuActionMap": "Map", "menuActionMap": "Map",
"@menuActionMap": {},
"menuActionStats": "Stats", "menuActionStats": "Stats",
"@menuActionStats": {},
"viewDialogTabSort": "Sort",
"viewDialogTabGroup": "Group",
"viewDialogTabLayout": "Layout",
"tileLayoutGrid": "Grid",
"tileLayoutList": "List",
"aboutPageTitle": "About", "aboutPageTitle": "About",
"@aboutPageTitle": {},
"aboutLinkSources": "Sources", "aboutLinkSources": "Sources",
"@aboutLinkSources": {},
"aboutLinkLicense": "License", "aboutLinkLicense": "License",
"@aboutLinkLicense": {},
"aboutLinkPolicy": "Privacy Policy", "aboutLinkPolicy": "Privacy Policy",
"@aboutLinkPolicy": {},
"aboutUpdate": "New Version Available", "aboutUpdate": "New Version Available",
"@aboutUpdate": {},
"aboutUpdateLinks1": "A new version of Aves is available on", "aboutUpdateLinks1": "A new version of Aves is available on",
"@aboutUpdateLinks1": {},
"aboutUpdateLinks2": "and", "aboutUpdateLinks2": "and",
"@aboutUpdateLinks2": {},
"aboutUpdateLinks3": ".", "aboutUpdateLinks3": ".",
"@aboutUpdateLinks3": {},
"aboutUpdateGitHub": "GitHub", "aboutUpdateGitHub": "GitHub",
"@aboutUpdateGitHub": {},
"aboutUpdateGooglePlay": "Google Play", "aboutUpdateGooglePlay": "Google Play",
"@aboutUpdateGooglePlay": {},
"aboutBug": "Bug Report", "aboutBug": "Bug Report",
"@aboutBug": {},
"aboutBugSaveLogInstruction": "Save app logs to a file", "aboutBugSaveLogInstruction": "Save app logs to a file",
"@aboutBugSaveLogInstruction": {},
"aboutBugSaveLogButton": "Save", "aboutBugSaveLogButton": "Save",
"@aboutBugSaveLogButton": {},
"aboutBugCopyInfoInstruction": "Copy system information", "aboutBugCopyInfoInstruction": "Copy system information",
"@aboutBugCopyInfoInstruction": {},
"aboutBugCopyInfoButton": "Copy", "aboutBugCopyInfoButton": "Copy",
"@aboutBugCopyInfoButton": {},
"aboutBugReportInstruction": "Report on GitHub with the logs and system information", "aboutBugReportInstruction": "Report on GitHub with the logs and system information",
"@aboutBugReportInstruction": {},
"aboutBugReportButton": "Report", "aboutBugReportButton": "Report",
"@aboutBugReportButton": {},
"aboutCredits": "Credits", "aboutCredits": "Credits",
"@aboutCredits": {},
"aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from", "aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from",
"@aboutCreditsWorldAtlas1": {},
"aboutCreditsWorldAtlas2": "under ISC License.", "aboutCreditsWorldAtlas2": "under ISC License.",
"@aboutCreditsWorldAtlas2": {},
"aboutCreditsTranslators": "Translators:", "aboutCreditsTranslators": "Translators:",
"@aboutCreditsTranslators": {},
"aboutCreditsTranslatorLine": "{language}: {names}", "aboutCreditsTranslatorLine": "{language}: {names}",
"@aboutCreditsTranslatorLine": { "@aboutCreditsTranslatorLine": {
"placeholders": { "placeholders": {
@ -518,27 +347,17 @@
}, },
"aboutLicenses": "Open-Source Licenses", "aboutLicenses": "Open-Source Licenses",
"@aboutLicenses": {},
"aboutLicensesBanner": "This app uses the following open-source packages and libraries.", "aboutLicensesBanner": "This app uses the following open-source packages and libraries.",
"@aboutLicensesBanner": {},
"aboutLicensesAndroidLibraries": "Android Libraries", "aboutLicensesAndroidLibraries": "Android Libraries",
"@aboutLicensesAndroidLibraries": {},
"aboutLicensesFlutterPlugins": "Flutter Plugins", "aboutLicensesFlutterPlugins": "Flutter Plugins",
"@aboutLicensesFlutterPlugins": {},
"aboutLicensesFlutterPackages": "Flutter Packages", "aboutLicensesFlutterPackages": "Flutter Packages",
"@aboutLicensesFlutterPackages": {},
"aboutLicensesDartPackages": "Dart Packages", "aboutLicensesDartPackages": "Dart Packages",
"@aboutLicensesDartPackages": {},
"aboutLicensesShowAllButtonLabel": "Show All Licenses", "aboutLicensesShowAllButtonLabel": "Show All Licenses",
"@aboutLicensesShowAllButtonLabel": {},
"policyPageTitle": "Privacy Policy", "policyPageTitle": "Privacy Policy",
"@policyPageTitle": {},
"collectionPageTitle": "Collection", "collectionPageTitle": "Collection",
"@collectionPageTitle": {},
"collectionPickPageTitle": "Pick", "collectionPickPageTitle": "Pick",
"@collectionPickPageTitle": {},
"collectionSelectionPageTitle": "{count, plural, =0{Select items} =1{1 item} other{{count} items}}", "collectionSelectionPageTitle": "{count, plural, =0{Select items} =1{1 item} other{{count} items}}",
"@collectionSelectionPageTitle": { "@collectionSelectionPageTitle": {
"placeholders": { "placeholders": {
@ -547,51 +366,28 @@
}, },
"collectionActionShowTitleSearch": "Show title filter", "collectionActionShowTitleSearch": "Show title filter",
"@collectionActionShowTitleSearch": {},
"collectionActionHideTitleSearch": "Hide title filter", "collectionActionHideTitleSearch": "Hide title filter",
"@collectionActionHideTitleSearch": {},
"collectionActionAddShortcut": "Add shortcut", "collectionActionAddShortcut": "Add shortcut",
"@collectionActionAddShortcut": {},
"collectionActionCopy": "Copy to album", "collectionActionCopy": "Copy to album",
"@collectionActionCopy": {},
"collectionActionMove": "Move to album", "collectionActionMove": "Move to album",
"@collectionActionMove": {},
"collectionActionRescan": "Rescan", "collectionActionRescan": "Rescan",
"@collectionActionRescan": {},
"collectionActionEdit": "Edit", "collectionActionEdit": "Edit",
"@collectionActionEdit": {},
"collectionSearchTitlesHintText": "Search titles", "collectionSearchTitlesHintText": "Search titles",
"@collectionSearchTitlesHintText": {},
"collectionSortTitle": "Sort",
"@collectionSortTitle": {},
"collectionSortDate": "By date", "collectionSortDate": "By date",
"@collectionSortDate": {},
"collectionSortSize": "By size", "collectionSortSize": "By size",
"@collectionSortSize": {},
"collectionSortName": "By album & file name", "collectionSortName": "By album & file name",
"@collectionSortName": {},
"collectionGroupTitle": "Group",
"@collectionGroupTitle": {},
"collectionGroupAlbum": "By album", "collectionGroupAlbum": "By album",
"@collectionGroupAlbum": {},
"collectionGroupMonth": "By month", "collectionGroupMonth": "By month",
"@collectionGroupMonth": {},
"collectionGroupDay": "By day", "collectionGroupDay": "By day",
"@collectionGroupDay": {},
"collectionGroupNone": "Do not group", "collectionGroupNone": "Do not group",
"@collectionGroupNone": {},
"sectionUnknown": "Unknown", "sectionUnknown": "Unknown",
"@sectionUnknown": {},
"dateToday": "Today", "dateToday": "Today",
"@dateToday": {},
"dateYesterday": "Yesterday", "dateYesterday": "Yesterday",
"@dateYesterday": {},
"dateThisMonth": "This month", "dateThisMonth": "This month",
"@dateThisMonth": {},
"collectionDeleteFailureFeedback": "{count, plural, =1{Failed to delete 1 item} other{Failed to delete {count} items}}", "collectionDeleteFailureFeedback": "{count, plural, =1{Failed to delete 1 item} other{Failed to delete {count} items}}",
"@collectionDeleteFailureFeedback": { "@collectionDeleteFailureFeedback": {
"placeholders": { "placeholders": {
@ -642,324 +438,178 @@
}, },
"collectionEmptyFavourites": "No favourites", "collectionEmptyFavourites": "No favourites",
"@collectionEmptyFavourites": {},
"collectionEmptyVideos": "No videos", "collectionEmptyVideos": "No videos",
"@collectionEmptyVideos": {},
"collectionEmptyImages": "No images", "collectionEmptyImages": "No images",
"@collectionEmptyImages": {},
"collectionSelectSectionTooltip": "Select section", "collectionSelectSectionTooltip": "Select section",
"@collectionSelectSectionTooltip": {},
"collectionDeselectSectionTooltip": "Deselect section", "collectionDeselectSectionTooltip": "Deselect section",
"@collectionDeselectSectionTooltip": {},
"drawerCollectionAll": "All collection", "drawerCollectionAll": "All collection",
"@drawerCollectionAll": {},
"drawerCollectionFavourites": "Favourites", "drawerCollectionFavourites": "Favourites",
"@drawerCollectionFavourites": {},
"drawerCollectionImages": "Images", "drawerCollectionImages": "Images",
"@drawerCollectionImages": {},
"drawerCollectionVideos": "Videos", "drawerCollectionVideos": "Videos",
"@drawerCollectionVideos": {},
"drawerCollectionAnimated": "Animated", "drawerCollectionAnimated": "Animated",
"@drawerCollectionAnimated": {},
"drawerCollectionMotionPhotos": "Motion photos", "drawerCollectionMotionPhotos": "Motion photos",
"@drawerCollectionMotionPhotos": {},
"drawerCollectionPanoramas": "Panoramas", "drawerCollectionPanoramas": "Panoramas",
"@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "Raw photos", "drawerCollectionRaws": "Raw photos",
"@drawerCollectionRaws": {},
"drawerCollectionSphericalVideos": "360° Videos", "drawerCollectionSphericalVideos": "360° Videos",
"@drawerCollectionSphericalVideos": {},
"chipSortTitle": "Sort",
"@chipSortTitle": {},
"chipSortDate": "By date", "chipSortDate": "By date",
"@chipSortDate": {},
"chipSortName": "By name", "chipSortName": "By name",
"@chipSortName": {},
"chipSortCount": "By item count", "chipSortCount": "By item count",
"@chipSortCount": {},
"albumGroupTitle": "Group",
"@albumGroupTitle": {},
"albumGroupTier": "By tier", "albumGroupTier": "By tier",
"@albumGroupTier": {},
"albumGroupVolume": "By storage volume", "albumGroupVolume": "By storage volume",
"@albumGroupVolume": {},
"albumGroupNone": "Do not group", "albumGroupNone": "Do not group",
"@albumGroupNone": {},
"albumPickPageTitleCopy": "Copy to Album", "albumPickPageTitleCopy": "Copy to Album",
"@albumPickPageTitleCopy": {},
"albumPickPageTitleExport": "Export to Album", "albumPickPageTitleExport": "Export to Album",
"@albumPickPageTitleExport": {},
"albumPickPageTitleMove": "Move to Album", "albumPickPageTitleMove": "Move to Album",
"@albumPickPageTitleMove": {},
"albumPickPageTitlePick": "Pick Album", "albumPickPageTitlePick": "Pick Album",
"@albumPickPageTitlePick": {},
"albumCamera": "Camera", "albumCamera": "Camera",
"@albumCamera": {},
"albumDownload": "Download", "albumDownload": "Download",
"@albumDownload": {},
"albumScreenshots": "Screenshots", "albumScreenshots": "Screenshots",
"@albumScreenshots": {},
"albumScreenRecordings": "Screen recordings", "albumScreenRecordings": "Screen recordings",
"@albumScreenRecordings": {},
"albumVideoCaptures": "Video Captures", "albumVideoCaptures": "Video Captures",
"@albumVideoCaptures": {},
"albumPageTitle": "Albums", "albumPageTitle": "Albums",
"@albumPageTitle": {},
"albumEmpty": "No albums", "albumEmpty": "No albums",
"@albumEmpty": {},
"createAlbumTooltip": "Create album", "createAlbumTooltip": "Create album",
"@createAlbumTooltip": {},
"createAlbumButtonLabel": "CREATE", "createAlbumButtonLabel": "CREATE",
"@createAlbumButtonLabel": {},
"newFilterBanner": "new", "newFilterBanner": "new",
"@newFilterBanner": {},
"countryPageTitle": "Countries", "countryPageTitle": "Countries",
"@countryPageTitle": {},
"countryEmpty": "No countries", "countryEmpty": "No countries",
"@countryEmpty": {},
"tagPageTitle": "Tags", "tagPageTitle": "Tags",
"@tagPageTitle": {},
"tagEmpty": "No tags", "tagEmpty": "No tags",
"@tagEmpty": {},
"searchCollectionFieldHint": "Search collection", "searchCollectionFieldHint": "Search collection",
"@searchCollectionFieldHint": {},
"searchSectionRecent": "Recent", "searchSectionRecent": "Recent",
"@searchSectionRecent": {},
"searchSectionAlbums": "Albums", "searchSectionAlbums": "Albums",
"@searchSectionAlbums": {},
"searchSectionCountries": "Countries", "searchSectionCountries": "Countries",
"@searchSectionCountries": {},
"searchSectionPlaces": "Places", "searchSectionPlaces": "Places",
"@searchSectionPlaces": {},
"searchSectionTags": "Tags", "searchSectionTags": "Tags",
"@searchSectionTags": {},
"settingsPageTitle": "Settings", "settingsPageTitle": "Settings",
"@settingsPageTitle": {},
"settingsSystemDefault": "System", "settingsSystemDefault": "System",
"@settingsSystemDefault": {},
"settingsDefault": "Default", "settingsDefault": "Default",
"@settingsDefault": {},
"settingsActionExport": "Export", "settingsActionExport": "Export",
"@settingsActionExport": {},
"settingsActionImport": "Import", "settingsActionImport": "Import",
"@settingsActionImport": {},
"settingsSectionNavigation": "Navigation", "settingsSectionNavigation": "Navigation",
"@settingsSectionNavigation": {},
"settingsHome": "Home", "settingsHome": "Home",
"@settingsHome": {},
"settingsKeepScreenOnTile": "Keep screen on", "settingsKeepScreenOnTile": "Keep screen on",
"@settingsKeepScreenOnTile": {},
"settingsKeepScreenOnTitle": "Keep Screen On", "settingsKeepScreenOnTitle": "Keep Screen On",
"@settingsKeepScreenOnTitle": {},
"settingsDoubleBackExit": "Tap “back” twice to exit", "settingsDoubleBackExit": "Tap “back” twice to exit",
"@settingsDoubleBackExit": {},
"settingsNavigationDrawerTile": "Navigation menu", "settingsNavigationDrawerTile": "Navigation menu",
"@settingsNavigationDrawerTile": {},
"settingsNavigationDrawerEditorTitle": "Navigation Menu", "settingsNavigationDrawerEditorTitle": "Navigation Menu",
"@settingsNavigationDrawerEditorTitle": {},
"settingsNavigationDrawerBanner": "Touch and hold to move and reorder menu items.", "settingsNavigationDrawerBanner": "Touch and hold to move and reorder menu items.",
"@settingsNavigationDrawerBanner": {},
"settingsNavigationDrawerTabTypes": "Types", "settingsNavigationDrawerTabTypes": "Types",
"@settingsNavigationDrawerTabTypes": {},
"settingsNavigationDrawerTabAlbums": "Albums", "settingsNavigationDrawerTabAlbums": "Albums",
"@settingsNavigationDrawerTabAlbums": {},
"settingsNavigationDrawerTabPages": "Pages", "settingsNavigationDrawerTabPages": "Pages",
"@settingsNavigationDrawerTabPages": {},
"settingsNavigationDrawerAddAlbum": "Add album", "settingsNavigationDrawerAddAlbum": "Add album",
"@settingsNavigationDrawerAddAlbum": {},
"settingsSectionThumbnails": "Thumbnails", "settingsSectionThumbnails": "Thumbnails",
"@settingsSectionThumbnails": {},
"settingsThumbnailShowLocationIcon": "Show location icon", "settingsThumbnailShowLocationIcon": "Show location icon",
"@settingsThumbnailShowLocationIcon": {},
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon", "settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
"@settingsThumbnailShowMotionPhotoIcon": {},
"settingsThumbnailShowRawIcon": "Show raw icon", "settingsThumbnailShowRawIcon": "Show raw icon",
"@settingsThumbnailShowRawIcon": {},
"settingsThumbnailShowVideoDuration": "Show video duration", "settingsThumbnailShowVideoDuration": "Show video duration",
"@settingsThumbnailShowVideoDuration": {},
"settingsCollectionQuickActionsTile": "Quick actions", "settingsCollectionQuickActionsTile": "Quick actions",
"@settingsCollectionQuickActionsTile": {},
"settingsCollectionQuickActionEditorTitle": "Quick Actions", "settingsCollectionQuickActionEditorTitle": "Quick Actions",
"@settingsCollectionQuickActionEditorTitle": {},
"settingsCollectionQuickActionTabBrowsing": "Browsing", "settingsCollectionQuickActionTabBrowsing": "Browsing",
"@settingsCollectionQuickActionTabBrowsing": {},
"settingsCollectionQuickActionTabSelecting": "Selecting", "settingsCollectionQuickActionTabSelecting": "Selecting",
"@settingsCollectionQuickActionTabSelecting": {},
"settingsCollectionBrowsingQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when browsing items.", "settingsCollectionBrowsingQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when browsing items.",
"@settingsCollectionBrowsingQuickActionEditorBanner": {},
"settingsCollectionSelectionQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when selecting items.", "settingsCollectionSelectionQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed when selecting items.",
"@settingsCollectionSelectionQuickActionEditorBanner": {},
"settingsSectionViewer": "Viewer", "settingsSectionViewer": "Viewer",
"@settingsSectionViewer": {},
"settingsViewerUseCutout": "Use cutout area", "settingsViewerUseCutout": "Use cutout area",
"@settingsViewerUseCutout": {},
"settingsViewerMaximumBrightness": "Maximum brightness", "settingsViewerMaximumBrightness": "Maximum brightness",
"@settingsViewerMaximumBrightness": {}, "settingsMotionPhotoAutoPlay": "Auto play motion photos",
"settingsImageBackground": "Image background", "settingsImageBackground": "Image background",
"@settingsImageBackground": {},
"settingsViewerQuickActionsTile": "Quick actions", "settingsViewerQuickActionsTile": "Quick actions",
"@settingsViewerQuickActionsTile": {},
"settingsViewerQuickActionEditorTitle": "Quick Actions", "settingsViewerQuickActionEditorTitle": "Quick Actions",
"@settingsViewerQuickActionEditorTitle": {},
"settingsViewerQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed in the viewer.", "settingsViewerQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed in the viewer.",
"@settingsViewerQuickActionEditorBanner": {},
"settingsViewerQuickActionEditorDisplayedButtons": "Displayed Buttons", "settingsViewerQuickActionEditorDisplayedButtons": "Displayed Buttons",
"@settingsViewerQuickActionEditorDisplayedButtons": {},
"settingsViewerQuickActionEditorAvailableButtons": "Available Buttons", "settingsViewerQuickActionEditorAvailableButtons": "Available Buttons",
"@settingsViewerQuickActionEditorAvailableButtons": {},
"settingsViewerQuickActionEmpty": "No buttons", "settingsViewerQuickActionEmpty": "No buttons",
"@settingsViewerQuickActionEmpty": {},
"settingsViewerOverlayTile": "Overlay", "settingsViewerOverlayTile": "Overlay",
"@settingsViewerOverlayTile": {},
"settingsViewerOverlayTitle": "Overlay", "settingsViewerOverlayTitle": "Overlay",
"@settingsViewerOverlayTitle": {},
"settingsViewerShowOverlayOnOpening": "Show on opening", "settingsViewerShowOverlayOnOpening": "Show on opening",
"@settingsViewerShowOverlayOnOpening": {},
"settingsViewerShowMinimap": "Show minimap", "settingsViewerShowMinimap": "Show minimap",
"@settingsViewerShowMinimap": {},
"settingsViewerShowInformation": "Show information", "settingsViewerShowInformation": "Show information",
"@settingsViewerShowInformation": {},
"settingsViewerShowInformationSubtitle": "Show title, date, location, etc.", "settingsViewerShowInformationSubtitle": "Show title, date, location, etc.",
"@settingsViewerShowInformationSubtitle": {},
"settingsViewerShowShootingDetails": "Show shooting details", "settingsViewerShowShootingDetails": "Show shooting details",
"@settingsViewerShowShootingDetails": {},
"settingsViewerEnableOverlayBlurEffect": "Blur effect", "settingsViewerEnableOverlayBlurEffect": "Blur effect",
"@settingsViewerEnableOverlayBlurEffect": {},
"settingsVideoPageTitle": "Video Settings", "settingsVideoPageTitle": "Video Settings",
"@settingsVideoPageTitle": {},
"settingsSectionVideo": "Video", "settingsSectionVideo": "Video",
"@settingsSectionVideo": {},
"settingsVideoShowVideos": "Show videos", "settingsVideoShowVideos": "Show videos",
"@settingsVideoShowVideos": {},
"settingsVideoEnableHardwareAcceleration": "Hardware acceleration", "settingsVideoEnableHardwareAcceleration": "Hardware acceleration",
"@settingsVideoEnableHardwareAcceleration": {},
"settingsVideoEnableAutoPlay": "Auto play", "settingsVideoEnableAutoPlay": "Auto play",
"@settingsVideoEnableAutoPlay": {},
"settingsVideoLoopModeTile": "Loop mode", "settingsVideoLoopModeTile": "Loop mode",
"@settingsVideoLoopModeTile": {},
"settingsVideoLoopModeTitle": "Loop Mode", "settingsVideoLoopModeTitle": "Loop Mode",
"@settingsVideoLoopModeTitle": {},
"settingsVideoQuickActionsTile": "Quick actions for videos", "settingsVideoQuickActionsTile": "Quick actions for videos",
"@settingsVideoQuickActionsTile": {},
"settingsVideoQuickActionEditorTitle": "Quick Actions", "settingsVideoQuickActionEditorTitle": "Quick Actions",
"@settingsVideoQuickActionEditorTitle": {},
"settingsSubtitleThemeTile": "Subtitles", "settingsSubtitleThemeTile": "Subtitles",
"@settingsSubtitleThemeTile": {},
"settingsSubtitleThemeTitle": "Subtitles", "settingsSubtitleThemeTitle": "Subtitles",
"@settingsSubtitleThemeTitle": {},
"settingsSubtitleThemeSample": "This is a sample.", "settingsSubtitleThemeSample": "This is a sample.",
"@settingsSubtitleThemeSample": {},
"settingsSubtitleThemeTextAlignmentTile": "Text alignment", "settingsSubtitleThemeTextAlignmentTile": "Text alignment",
"@settingsSubtitleThemeTextAlignmentTile": {},
"settingsSubtitleThemeTextAlignmentTitle": "Text Alignment", "settingsSubtitleThemeTextAlignmentTitle": "Text Alignment",
"@settingsSubtitleThemeTextAlignmentTitle": {},
"settingsSubtitleThemeTextSize": "Text size", "settingsSubtitleThemeTextSize": "Text size",
"@settingsSubtitleThemeTextSize": {},
"settingsSubtitleThemeShowOutline": "Show outline and shadow", "settingsSubtitleThemeShowOutline": "Show outline and shadow",
"@settingsSubtitleThemeShowOutline": {},
"settingsSubtitleThemeTextColor": "Text color", "settingsSubtitleThemeTextColor": "Text color",
"@settingsSubtitleThemeTextColor": {},
"settingsSubtitleThemeTextOpacity": "Text opacity", "settingsSubtitleThemeTextOpacity": "Text opacity",
"@settingsSubtitleThemeTextOpacity": {},
"settingsSubtitleThemeBackgroundColor": "Background color", "settingsSubtitleThemeBackgroundColor": "Background color",
"@settingsSubtitleThemeBackgroundColor": {},
"settingsSubtitleThemeBackgroundOpacity": "Background opacity", "settingsSubtitleThemeBackgroundOpacity": "Background opacity",
"@settingsSubtitleThemeBackgroundOpacity": {},
"settingsSubtitleThemeTextAlignmentLeft": "Left", "settingsSubtitleThemeTextAlignmentLeft": "Left",
"@settingsSubtitleThemeTextAlignmentLeft": {},
"settingsSubtitleThemeTextAlignmentCenter": "Center", "settingsSubtitleThemeTextAlignmentCenter": "Center",
"@settingsSubtitleThemeTextAlignmentCenter": {},
"settingsSubtitleThemeTextAlignmentRight": "Right", "settingsSubtitleThemeTextAlignmentRight": "Right",
"@settingsSubtitleThemeTextAlignmentRight": {},
"settingsSectionPrivacy": "Privacy", "settingsSectionPrivacy": "Privacy",
"@settingsSectionPrivacy": {},
"settingsAllowInstalledAppAccess": "Allow access to app inventory", "settingsAllowInstalledAppAccess": "Allow access to app inventory",
"@settingsAllowInstalledAppAccess": {},
"settingsAllowInstalledAppAccessSubtitle": "Used to improve album display", "settingsAllowInstalledAppAccessSubtitle": "Used to improve album display",
"@settingsAllowInstalledAppAccessSubtitle": {},
"settingsAllowErrorReporting": "Allow anonymous error reporting", "settingsAllowErrorReporting": "Allow anonymous error reporting",
"@settingsAllowErrorReporting": {},
"settingsSaveSearchHistory": "Save search history", "settingsSaveSearchHistory": "Save search history",
"@settingsSaveSearchHistory": {},
"settingsHiddenItemsTile": "Hidden items", "settingsHiddenItemsTile": "Hidden items",
"@settingsHiddenItemsTile": {},
"settingsHiddenItemsTitle": "Hidden Items", "settingsHiddenItemsTitle": "Hidden Items",
"@settingsHiddenItemsTitle": {},
"settingsHiddenFiltersTitle": "Hidden Filters", "settingsHiddenFiltersTitle": "Hidden Filters",
"@settingsHiddenFiltersTitle": {},
"settingsHiddenFiltersBanner": "Photos and videos matching hidden filters will not appear in your collection.", "settingsHiddenFiltersBanner": "Photos and videos matching hidden filters will not appear in your collection.",
"@settingsHiddenFiltersBanner": {},
"settingsHiddenFiltersEmpty": "No hidden filters", "settingsHiddenFiltersEmpty": "No hidden filters",
"@settingsHiddenFiltersEmpty": {},
"settingsHiddenPathsTitle": "Hidden Paths", "settingsHiddenPathsTitle": "Hidden Paths",
"@settingsHiddenPathsTitle": {},
"settingsHiddenPathsBanner": "Photos and videos in these folders, or any of their subfolders, will not appear in your collection.", "settingsHiddenPathsBanner": "Photos and videos in these folders, or any of their subfolders, will not appear in your collection.",
"@settingsHiddenPathsBanner": {},
"addPathTooltip": "Add path", "addPathTooltip": "Add path",
"@addPathTooltip": {},
"settingsStorageAccessTile": "Storage access", "settingsStorageAccessTile": "Storage access",
"@settingsStorageAccessTile": {},
"settingsStorageAccessTitle": "Storage Access", "settingsStorageAccessTitle": "Storage Access",
"@settingsStorageAccessTitle": {},
"settingsStorageAccessBanner": "Some directories require an explicit access grant to modify files in them. You can review here directories to which you previously gave access.", "settingsStorageAccessBanner": "Some directories require an explicit access grant to modify files in them. You can review here directories to which you previously gave access.",
"@settingsStorageAccessBanner": {},
"settingsStorageAccessEmpty": "No access grants", "settingsStorageAccessEmpty": "No access grants",
"@settingsStorageAccessEmpty": {},
"settingsStorageAccessRevokeTooltip": "Revoke", "settingsStorageAccessRevokeTooltip": "Revoke",
"@settingsStorageAccessRevokeTooltip": {},
"settingsSectionAccessibility": "Accessibility", "settingsSectionAccessibility": "Accessibility",
"@settingsSectionAccessibility": {},
"settingsRemoveAnimationsTile": "Remove animations", "settingsRemoveAnimationsTile": "Remove animations",
"@settingsRemoveAnimationsTile": {},
"settingsRemoveAnimationsTitle": "Remove Animations", "settingsRemoveAnimationsTitle": "Remove Animations",
"@settingsRemoveAnimationsTitle": {},
"settingsTimeToTakeActionTile": "Time to take action", "settingsTimeToTakeActionTile": "Time to take action",
"@settingsTimeToTakeActionTile": {},
"settingsTimeToTakeActionTitle": "Time to Take Action", "settingsTimeToTakeActionTitle": "Time to Take Action",
"@settingsTimeToTakeActionTitle": {},
"settingsSectionLanguage": "Language & Formats", "settingsSectionLanguage": "Language & Formats",
"@settingsSectionLanguage": {},
"settingsLanguage": "Language", "settingsLanguage": "Language",
"@settingsLanguage": {},
"settingsCoordinateFormatTile": "Coordinate format", "settingsCoordinateFormatTile": "Coordinate format",
"@settingsCoordinateFormatTile": {},
"settingsCoordinateFormatTitle": "Coordinate Format", "settingsCoordinateFormatTitle": "Coordinate Format",
"@settingsCoordinateFormatTitle": {},
"settingsUnitSystemTile": "Units", "settingsUnitSystemTile": "Units",
"@settingsUnitSystemTile": {},
"settingsUnitSystemTitle": "Units", "settingsUnitSystemTitle": "Units",
"@settingsUnitSystemTitle": {},
"statsPageTitle": "Stats", "statsPageTitle": "Stats",
"@statsPageTitle": {},
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}", "statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
"@statsWithGps": { "@statsWithGps": {
"placeholders": { "placeholders": {
@ -967,113 +617,64 @@
} }
}, },
"statsTopCountries": "Top Countries", "statsTopCountries": "Top Countries",
"@statsTopCountries": {},
"statsTopPlaces": "Top Places", "statsTopPlaces": "Top Places",
"@statsTopPlaces": {},
"statsTopTags": "Top Tags", "statsTopTags": "Top Tags",
"@statsTopTags": {},
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA", "viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
"@viewerOpenPanoramaButtonLabel": {},
"viewerErrorUnknown": "Oops!", "viewerErrorUnknown": "Oops!",
"@viewerErrorUnknown": {},
"viewerErrorDoesNotExist": "The file no longer exists.", "viewerErrorDoesNotExist": "The file no longer exists.",
"@viewerErrorDoesNotExist": {},
"viewerInfoPageTitle": "Info", "viewerInfoPageTitle": "Info",
"@viewerInfoPageTitle": {},
"viewerInfoBackToViewerTooltip": "Back to viewer", "viewerInfoBackToViewerTooltip": "Back to viewer",
"@viewerInfoBackToViewerTooltip": {},
"viewerInfoUnknown": "unknown", "viewerInfoUnknown": "unknown",
"@viewerInfoUnknown": {},
"viewerInfoLabelTitle": "Title", "viewerInfoLabelTitle": "Title",
"@viewerInfoLabelTitle": {},
"viewerInfoLabelDate": "Date", "viewerInfoLabelDate": "Date",
"@viewerInfoLabelDate": {},
"viewerInfoLabelResolution": "Resolution", "viewerInfoLabelResolution": "Resolution",
"@viewerInfoLabelResolution": {},
"viewerInfoLabelSize": "Size", "viewerInfoLabelSize": "Size",
"@viewerInfoLabelSize": {},
"viewerInfoLabelUri": "URI", "viewerInfoLabelUri": "URI",
"@viewerInfoLabelUri": {},
"viewerInfoLabelPath": "Path", "viewerInfoLabelPath": "Path",
"@viewerInfoLabelPath": {},
"viewerInfoLabelDuration": "Duration", "viewerInfoLabelDuration": "Duration",
"@viewerInfoLabelDuration": {},
"viewerInfoLabelOwner": "Owned by", "viewerInfoLabelOwner": "Owned by",
"@viewerInfoLabelOwner": {},
"viewerInfoLabelCoordinates": "Coordinates", "viewerInfoLabelCoordinates": "Coordinates",
"@viewerInfoLabelCoordinates": {},
"viewerInfoLabelAddress": "Address", "viewerInfoLabelAddress": "Address",
"@viewerInfoLabelAddress": {},
"mapStyleTitle": "Map Style", "mapStyleTitle": "Map Style",
"@mapStyleTitle": {},
"mapStyleTooltip": "Select map style", "mapStyleTooltip": "Select map style",
"@mapStyleTooltip": {},
"mapZoomInTooltip": "Zoom in", "mapZoomInTooltip": "Zoom in",
"@mapZoomInTooltip": {},
"mapZoomOutTooltip": "Zoom out", "mapZoomOutTooltip": "Zoom out",
"@mapZoomOutTooltip": {},
"mapPointNorthUpTooltip": "Point north up", "mapPointNorthUpTooltip": "Point north up",
"@mapPointNorthUpTooltip": {},
"mapAttributionOsmHot": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [HOT](https://www.hotosm.org/) • Hosted by [OSM France](https://openstreetmap.fr/)", "mapAttributionOsmHot": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [HOT](https://www.hotosm.org/) • Hosted by [OSM France](https://openstreetmap.fr/)",
"@mapAttributionOsmHot": {},
"mapAttributionStamen": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)", "mapAttributionStamen": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)",
"@mapAttributionStamen": {},
"openMapPageTooltip": "View on Map page", "openMapPageTooltip": "View on Map page",
"@openMapPageTooltip": {},
"mapEmptyRegion": "No images in this region", "mapEmptyRegion": "No images in this region",
"@mapEmpty": {},
"viewerInfoOpenEmbeddedFailureFeedback": "Failed to extract embedded data", "viewerInfoOpenEmbeddedFailureFeedback": "Failed to extract embedded data",
"@viewerInfoOpenEmbeddedFailureFeedback": {},
"viewerInfoOpenLinkText": "Open", "viewerInfoOpenLinkText": "Open",
"@viewerInfoOpenLinkText": {},
"viewerInfoViewXmlLinkText": "View XML", "viewerInfoViewXmlLinkText": "View XML",
"@viewerInfoViewXmlLinkText": {},
"viewerInfoSearchFieldLabel": "Search metadata", "viewerInfoSearchFieldLabel": "Search metadata",
"@viewerInfoSearchFieldLabel": {},
"viewerInfoSearchEmpty": "No matching keys", "viewerInfoSearchEmpty": "No matching keys",
"@viewerInfoSearchEmpty": {},
"viewerInfoSearchSuggestionDate": "Date & time", "viewerInfoSearchSuggestionDate": "Date & time",
"@viewerInfoSearchSuggestionDate": {},
"viewerInfoSearchSuggestionDescription": "Description", "viewerInfoSearchSuggestionDescription": "Description",
"@viewerInfoSearchSuggestionDescription": {},
"viewerInfoSearchSuggestionDimensions": "Dimensions", "viewerInfoSearchSuggestionDimensions": "Dimensions",
"@viewerInfoSearchSuggestionDimensions": {},
"viewerInfoSearchSuggestionResolution": "Resolution", "viewerInfoSearchSuggestionResolution": "Resolution",
"@viewerInfoSearchSuggestionResolution": {},
"viewerInfoSearchSuggestionRights": "Rights", "viewerInfoSearchSuggestionRights": "Rights",
"@viewerInfoSearchSuggestionRights": {},
"tagEditorPageTitle": "Edit Tags", "tagEditorPageTitle": "Edit Tags",
"@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "New tag", "tagEditorPageNewTagFieldLabel": "New tag",
"@tagEditorPageNewTagFieldLabel": {},
"tagEditorPageAddTagTooltip": "Add tag", "tagEditorPageAddTagTooltip": "Add tag",
"@tagEditorPageAddTagTooltip": {},
"tagEditorSectionRecent": "Recent", "tagEditorSectionRecent": "Recent",
"@tagEditorSectionRecent": {},
"panoramaEnableSensorControl": "Enable sensor control", "panoramaEnableSensorControl": "Enable sensor control",
"@panoramaEnableSensorControl": {},
"panoramaDisableSensorControl": "Disable sensor control", "panoramaDisableSensorControl": "Disable sensor control",
"@panoramaDisableSensorControl": {},
"sourceViewerPageTitle": "Source", "sourceViewerPageTitle": "Source",
"@sourceViewerPageTitle": {},
"filePickerShowHiddenFiles": "Show hidden files", "filePickerShowHiddenFiles": "Show hidden files",
"@filePickerShowHiddenFiles": {},
"filePickerDoNotShowHiddenFiles": "Dont show hidden files", "filePickerDoNotShowHiddenFiles": "Dont show hidden files",
"@filePickerDoNotShowHiddenFiles": {},
"filePickerOpenFrom": "Open from", "filePickerOpenFrom": "Open from",
"@filePickerOpenFrom": {},
"filePickerNoItems": "No items", "filePickerNoItems": "No items",
"@filePickerNoItems": {},
"filePickerUseThisFolder": "Use this folder", "filePickerUseThisFolder": "Use this folder",
"@filePickerUseThisFolder": {} "@filePickerUseThisFolder": {}
} }

View file

@ -15,6 +15,7 @@
"hideButtonLabel": "MASQUER", "hideButtonLabel": "MASQUER",
"continueButtonLabel": "CONTINUER", "continueButtonLabel": "CONTINUER",
"cancelTooltip": "Annuler",
"changeTooltip": "Modifier", "changeTooltip": "Modifier",
"clearTooltip": "Effacer", "clearTooltip": "Effacer",
"previousTooltip": "Précédent", "previousTooltip": "Précédent",
@ -202,14 +203,20 @@
"genericSuccessFeedback": "Succès !", "genericSuccessFeedback": "Succès !",
"genericFailureFeedback": "Échec", "genericFailureFeedback": "Échec",
"menuActionSort": "Trier", "menuActionConfigureView": "Vue",
"menuActionGroup": "Grouper",
"menuActionSelect": "Sélectionner", "menuActionSelect": "Sélectionner",
"menuActionSelectAll": "Tout sélectionner", "menuActionSelectAll": "Tout sélectionner",
"menuActionSelectNone": "Tout désélectionner", "menuActionSelectNone": "Tout désélectionner",
"menuActionMap": "Carte", "menuActionMap": "Carte",
"menuActionStats": "Statistiques", "menuActionStats": "Statistiques",
"viewDialogTabSort": "Tri",
"viewDialogTabGroup": "Groupes",
"viewDialogTabLayout": "Vue",
"tileLayoutGrid": "Grille",
"tileLayoutList": "Liste",
"aboutPageTitle": "À propos", "aboutPageTitle": "À propos",
"aboutLinkSources": "Sources", "aboutLinkSources": "Sources",
"aboutLinkLicense": "Licence", "aboutLinkLicense": "Licence",
@ -260,12 +267,10 @@
"collectionSearchTitlesHintText": "Recherche de titres", "collectionSearchTitlesHintText": "Recherche de titres",
"collectionSortTitle": "Trier",
"collectionSortDate": "par date", "collectionSortDate": "par date",
"collectionSortSize": "par taille", "collectionSortSize": "par taille",
"collectionSortName": "alphabétiquement", "collectionSortName": "alphabétique",
"collectionGroupTitle": "Grouper",
"collectionGroupAlbum": "par album", "collectionGroupAlbum": "par album",
"collectionGroupMonth": "par mois", "collectionGroupMonth": "par mois",
"collectionGroupDay": "par jour", "collectionGroupDay": "par jour",
@ -301,12 +306,10 @@
"drawerCollectionRaws": "Photos Raw", "drawerCollectionRaws": "Photos Raw",
"drawerCollectionSphericalVideos": "Vidéos à 360°", "drawerCollectionSphericalVideos": "Vidéos à 360°",
"chipSortTitle": "Trier",
"chipSortDate": "par date", "chipSortDate": "par date",
"chipSortName": "par nom", "chipSortName": "alphabétique",
"chipSortCount": "par nombre déléments", "chipSortCount": "par nombre déléments",
"albumGroupTitle": "Grouper",
"albumGroupTier": "par importance", "albumGroupTier": "par importance",
"albumGroupVolume": "par volume de stockage", "albumGroupVolume": "par volume de stockage",
"albumGroupNone": "ne pas grouper", "albumGroupNone": "ne pas grouper",
@ -378,6 +381,7 @@
"settingsSectionViewer": "Visionneuse", "settingsSectionViewer": "Visionneuse",
"settingsViewerUseCutout": "Utiliser la zone dencoche", "settingsViewerUseCutout": "Utiliser la zone dencoche",
"settingsViewerMaximumBrightness": "Luminosité maximale", "settingsViewerMaximumBrightness": "Luminosité maximale",
"settingsMotionPhotoAutoPlay": "Lecture automatique des photos animées",
"settingsImageBackground": "Arrière-plan de limage", "settingsImageBackground": "Arrière-plan de limage",
"settingsViewerQuickActionsTile": "Actions rapides", "settingsViewerQuickActionsTile": "Actions rapides",
@ -513,7 +517,6 @@
"panoramaDisableSensorControl": "Désactiver le contrôle par capteurs", "panoramaDisableSensorControl": "Désactiver le contrôle par capteurs",
"sourceViewerPageTitle": "Code source", "sourceViewerPageTitle": "Code source",
"@sourceViewerPageTitle": {},
"filePickerShowHiddenFiles": "Afficher les fichiers masqués", "filePickerShowHiddenFiles": "Afficher les fichiers masqués",
"filePickerDoNotShowHiddenFiles": "Ne pas afficher les fichiers masqués", "filePickerDoNotShowHiddenFiles": "Ne pas afficher les fichiers masqués",

View file

@ -15,6 +15,7 @@
"hideButtonLabel": "숨기기", "hideButtonLabel": "숨기기",
"continueButtonLabel": "다음", "continueButtonLabel": "다음",
"cancelTooltip": "취소",
"changeTooltip": "변경", "changeTooltip": "변경",
"clearTooltip": "초기화", "clearTooltip": "초기화",
"previousTooltip": "이전", "previousTooltip": "이전",
@ -202,14 +203,20 @@
"genericSuccessFeedback": "정상 처리됐습니다", "genericSuccessFeedback": "정상 처리됐습니다",
"genericFailureFeedback": "오류가 발생했습니다", "genericFailureFeedback": "오류가 발생했습니다",
"menuActionSort": "정렬", "menuActionConfigureView": "보기 설정",
"menuActionGroup": "묶음",
"menuActionSelect": "선택", "menuActionSelect": "선택",
"menuActionSelectAll": "모두 선택", "menuActionSelectAll": "모두 선택",
"menuActionSelectNone": "모두 해제", "menuActionSelectNone": "모두 해제",
"menuActionMap": "지도", "menuActionMap": "지도",
"menuActionStats": "통계", "menuActionStats": "통계",
"viewDialogTabSort": "정렬",
"viewDialogTabGroup": "묶음",
"viewDialogTabLayout": "배치",
"tileLayoutGrid": "바둑판",
"tileLayoutList": "목록",
"aboutPageTitle": "앱 정보", "aboutPageTitle": "앱 정보",
"aboutLinkSources": "소스 코드", "aboutLinkSources": "소스 코드",
"aboutLinkLicense": "라이선스", "aboutLinkLicense": "라이선스",
@ -260,12 +267,10 @@
"collectionSearchTitlesHintText": "제목 검색", "collectionSearchTitlesHintText": "제목 검색",
"collectionSortTitle": "정렬",
"collectionSortDate": "날짜", "collectionSortDate": "날짜",
"collectionSortSize": "크기", "collectionSortSize": "크기",
"collectionSortName": "이름", "collectionSortName": "이름",
"collectionGroupTitle": "묶음",
"collectionGroupAlbum": "앨범별로", "collectionGroupAlbum": "앨범별로",
"collectionGroupMonth": "월별로", "collectionGroupMonth": "월별로",
"collectionGroupDay": "날짜별로", "collectionGroupDay": "날짜별로",
@ -301,12 +306,10 @@
"drawerCollectionRaws": "Raw 이미지", "drawerCollectionRaws": "Raw 이미지",
"drawerCollectionSphericalVideos": "360° 동영상", "drawerCollectionSphericalVideos": "360° 동영상",
"chipSortTitle": "정렬",
"chipSortDate": "날짜", "chipSortDate": "날짜",
"chipSortName": "이름", "chipSortName": "이름",
"chipSortCount": "항목수", "chipSortCount": "항목수",
"albumGroupTitle": "묶음",
"albumGroupTier": "단계별로", "albumGroupTier": "단계별로",
"albumGroupVolume": "저장공간별로", "albumGroupVolume": "저장공간별로",
"albumGroupNone": "묶음 없음", "albumGroupNone": "묶음 없음",
@ -378,6 +381,7 @@
"settingsSectionViewer": "뷰어", "settingsSectionViewer": "뷰어",
"settingsViewerUseCutout": "컷아웃 영역 사용", "settingsViewerUseCutout": "컷아웃 영역 사용",
"settingsViewerMaximumBrightness": "최대 밝기", "settingsViewerMaximumBrightness": "최대 밝기",
"settingsMotionPhotoAutoPlay": "모션 포토 자동 재생",
"settingsImageBackground": "이미지 배경", "settingsImageBackground": "이미지 배경",
"settingsViewerQuickActionsTile": "빠른 작업", "settingsViewerQuickActionsTile": "빠른 작업",

View file

@ -15,6 +15,7 @@
"hideButtonLabel": "СКРЫТЬ", "hideButtonLabel": "СКРЫТЬ",
"continueButtonLabel": "ПРОДОЛЖИТЬ", "continueButtonLabel": "ПРОДОЛЖИТЬ",
"cancelTooltip": "Отмена",
"changeTooltip": "Изменить", "changeTooltip": "Изменить",
"clearTooltip": "Очистить", "clearTooltip": "Очистить",
"previousTooltip": "Предыдущий", "previousTooltip": "Предыдущий",
@ -22,6 +23,7 @@
"showTooltip": "Показать", "showTooltip": "Показать",
"hideTooltip": "Скрыть", "hideTooltip": "Скрыть",
"removeTooltip": "Удалить", "removeTooltip": "Удалить",
"resetButtonTooltip": "Сбросить",
"doubleBackExitMessage": "Нажмите «назад» еще раз, чтобы выйти.", "doubleBackExitMessage": "Нажмите «назад» еще раз, чтобы выйти.",
@ -71,6 +73,7 @@
"videoActionSettings": "Настройки", "videoActionSettings": "Настройки",
"entryInfoActionEditDate": "Изменить дату и время", "entryInfoActionEditDate": "Изменить дату и время",
"entryInfoActionEditTags": "Изменить теги",
"entryInfoActionRemoveMetadata": "Удалить метаданные", "entryInfoActionRemoveMetadata": "Удалить метаданные",
"filterFavouriteLabel": "Избранное", "filterFavouriteLabel": "Избранное",
@ -126,10 +129,10 @@
"storageVolumeDescriptionFallbackPrimary": "Внутренняя память", "storageVolumeDescriptionFallbackPrimary": "Внутренняя память",
"storageVolumeDescriptionFallbackNonPrimary": "SD-карта", "storageVolumeDescriptionFallbackNonPrimary": "SD-карта",
"rootDirectoryDescription": "корень", "rootDirectoryDescription": "корневой каталог",
"otherDirectoryDescription": "“{name}” каталог", "otherDirectoryDescription": "каталог «{name}»",
"storageAccessDialogTitle": "Доступ к хранилищу", "storageAccessDialogTitle": "Доступ к хранилищу",
"storageAccessDialogMessage": "Пожалуйста, выберите каталог {directory} на накопителе «{volume}» на следующем экране, чтобы предоставить этому приложению доступ к нему.", "storageAccessDialogMessage": "Пожалуйста, выберите {directory} на накопителе «{volume}» на следующем экране, чтобы предоставить этому приложению доступ к нему.",
"restrictedAccessDialogTitle": "Ограниченный доступ", "restrictedAccessDialogTitle": "Ограниченный доступ",
"restrictedAccessDialogMessage": "Этому приложению не разрешается изменять файлы в каталоге {directory} накопителя «{volume}».\n\nПожалуйста, используйте предустановленный файловый менеджер или галерею, чтобы переместить элементы в другой каталог.", "restrictedAccessDialogMessage": "Этому приложению не разрешается изменять файлы в каталоге {directory} накопителя «{volume}».\n\nПожалуйста, используйте предустановленный файловый менеджер или галерею, чтобы переместить элементы в другой каталог.",
"notEnoughSpaceDialogTitle": "Недостаточно свободного места.", "notEnoughSpaceDialogTitle": "Недостаточно свободного места.",
@ -200,14 +203,20 @@
"genericSuccessFeedback": "Выполнено!", "genericSuccessFeedback": "Выполнено!",
"genericFailureFeedback": "Не удалось", "genericFailureFeedback": "Не удалось",
"menuActionSort": "Сортировка", "menuActionConfigureView": "Вид",
"menuActionGroup": "Группировка",
"menuActionSelect": "Выбрать", "menuActionSelect": "Выбрать",
"menuActionSelectAll": "Выбрать все", "menuActionSelectAll": "Выбрать все",
"menuActionSelectNone": "Снять выделение", "menuActionSelectNone": "Снять выделение",
"menuActionMap": "Карта", "menuActionMap": "Карта",
"menuActionStats": "Статистика", "menuActionStats": "Статистика",
"viewDialogTabSort": "Сортировка",
"viewDialogTabGroup": "Группировка",
"viewDialogTabLayout": "Макет",
"tileLayoutGrid": "Сетка",
"tileLayoutList": "Список",
"aboutPageTitle": "О нас", "aboutPageTitle": "О нас",
"aboutLinkSources": "Исходники", "aboutLinkSources": "Исходники",
"aboutLinkLicense": "Лицензия", "aboutLinkLicense": "Лицензия",
@ -258,12 +267,10 @@
"collectionSearchTitlesHintText": "Поиск заголовков", "collectionSearchTitlesHintText": "Поиск заголовков",
"collectionSortTitle": "Сортировка",
"collectionSortDate": "По дате", "collectionSortDate": "По дате",
"collectionSortSize": "По размеру", "collectionSortSize": "По размеру",
"collectionSortName": "По имени альбома и файла", "collectionSortName": "По имени альбома и файла",
"collectionGroupTitle": "Группировка",
"collectionGroupAlbum": "По альбому", "collectionGroupAlbum": "По альбому",
"collectionGroupMonth": "По месяцу", "collectionGroupMonth": "По месяцу",
"collectionGroupDay": "По дню", "collectionGroupDay": "По дню",
@ -299,12 +306,10 @@
"drawerCollectionRaws": "RAW", "drawerCollectionRaws": "RAW",
"drawerCollectionSphericalVideos": "360° видео", "drawerCollectionSphericalVideos": "360° видео",
"chipSortTitle": "Сортировка",
"chipSortDate": "По дате", "chipSortDate": "По дате",
"chipSortName": "По названию", "chipSortName": "По названию",
"chipSortCount": "По количеству объектов", "chipSortCount": "По количеству объектов",
"albumGroupTitle": "Группировка",
"albumGroupTier": "По уровню", "albumGroupTier": "По уровню",
"albumGroupVolume": "По накопителю", "albumGroupVolume": "По накопителю",
"albumGroupNone": "Не группировать", "albumGroupNone": "Не группировать",
@ -375,6 +380,8 @@
"settingsSectionViewer": "Просмотрщик", "settingsSectionViewer": "Просмотрщик",
"settingsViewerUseCutout": "Использовать область выреза", "settingsViewerUseCutout": "Использовать область выреза",
"settingsViewerMaximumBrightness": "Максимальная яркость",
"settingsMotionPhotoAutoPlay": "Автовоспроизведение «Живых фото»",
"settingsImageBackground": "Фон изображения", "settingsImageBackground": "Фон изображения",
"settingsViewerQuickActionsTile": "Быстрые действия", "settingsViewerQuickActionsTile": "Быстрые действия",
@ -501,6 +508,9 @@
"viewerInfoSearchSuggestionResolution": "Разрешение", "viewerInfoSearchSuggestionResolution": "Разрешение",
"viewerInfoSearchSuggestionRights": "Права", "viewerInfoSearchSuggestionRights": "Права",
"tagEditorPageTitle": "Изменить теги",
"tagEditorPageNewTagFieldLabel": "Новый тег",
"tagEditorPageAddTagTooltip": "Добавить тег",
"tagEditorSectionRecent": "Недавние", "tagEditorSectionRecent": "Недавние",
"panoramaEnableSensorControl": "Включить сенсорное управление", "panoramaEnableSensorControl": "Включить сенсорное управление",

1
lib/l10n/l10n.dart Normal file
View file

@ -0,0 +1 @@
export 'package:flutter_gen/gen_l10n/app_localizations.dart';

View file

@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
enum ChipSetAction { enum ChipSetAction {
// general // general
sort, configureView,
group,
select, select,
selectAll, selectAll,
selectNone, selectNone,
@ -27,8 +26,7 @@ enum ChipSetAction {
class ChipSetActions { class ChipSetActions {
static const general = [ static const general = [
ChipSetAction.sort, ChipSetAction.configureView,
ChipSetAction.group,
ChipSetAction.select, ChipSetAction.select,
ChipSetAction.selectAll, ChipSetAction.selectAll,
ChipSetAction.selectNone, ChipSetAction.selectNone,
@ -57,10 +55,8 @@ extension ExtraChipSetAction on ChipSetAction {
String getText(BuildContext context) { String getText(BuildContext context) {
switch (this) { switch (this) {
// general // general
case ChipSetAction.sort: case ChipSetAction.configureView:
return context.l10n.menuActionSort; return context.l10n.menuActionConfigureView;
case ChipSetAction.group:
return context.l10n.menuActionGroup;
case ChipSetAction.select: case ChipSetAction.select:
return context.l10n.menuActionSelect; return context.l10n.menuActionSelect;
case ChipSetAction.selectAll: case ChipSetAction.selectAll:
@ -101,10 +97,8 @@ extension ExtraChipSetAction on ChipSetAction {
IconData _getIconData() { IconData _getIconData() {
switch (this) { switch (this) {
// general // general
case ChipSetAction.sort: case ChipSetAction.configureView:
return AIcons.sort; return AIcons.view;
case ChipSetAction.group:
return AIcons.group;
case ChipSetAction.select: case ChipSetAction.select:
return AIcons.select; return AIcons.select;
case ChipSetAction.selectAll: case ChipSetAction.selectAll:

View file

@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
enum EntrySetAction { enum EntrySetAction {
// general // general
sort, configureView,
group,
select, select,
selectAll, selectAll,
selectNone, selectNone,
@ -32,8 +31,7 @@ enum EntrySetAction {
class EntrySetActions { class EntrySetActions {
static const general = [ static const general = [
EntrySetAction.sort, EntrySetAction.configureView,
EntrySetAction.group,
EntrySetAction.select, EntrySetAction.select,
EntrySetAction.selectAll, EntrySetAction.selectAll,
EntrySetAction.selectNone, EntrySetAction.selectNone,
@ -63,10 +61,8 @@ extension ExtraEntrySetAction on EntrySetAction {
String getText(BuildContext context) { String getText(BuildContext context) {
switch (this) { switch (this) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
return context.l10n.menuActionSort; return context.l10n.menuActionConfigureView;
case EntrySetAction.group:
return context.l10n.menuActionGroup;
case EntrySetAction.select: case EntrySetAction.select:
return context.l10n.menuActionSelect; return context.l10n.menuActionSelect;
case EntrySetAction.selectAll: case EntrySetAction.selectAll:
@ -119,10 +115,8 @@ extension ExtraEntrySetAction on EntrySetAction {
IconData _getIconData() { IconData _getIconData() {
switch (this) { switch (this) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
return AIcons.sort; return AIcons.view;
case EntrySetAction.group:
return AIcons.group;
case EntrySetAction.select: case EntrySetAction.select:
return AIcons.select; return AIcons.select;
case EntrySetAction.selectAll: case EntrySetAction.selectAll:

View file

@ -3,7 +3,6 @@ import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:github/github.dart'; import 'package:github/github.dart';
import 'package:google_api_availability/google_api_availability.dart'; import 'package:google_api_availability/google_api_availability.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';

View file

@ -703,8 +703,8 @@ class AvesEntry {
Future<bool> delete() { Future<bool> delete() {
final completer = Completer<bool>(); final completer = Completer<bool>();
mediaFileService.delete([this]).listen( mediaFileService.delete(entries: {this}).listen(
(event) => completer.complete(event.success), (event) => completer.complete(event.success && !event.skipped),
onError: completer.completeError, onError: completer.completeError,
onDone: () { onDone: () {
if (!completer.isCompleted) { if (!completer.isCompleted) {

View file

@ -1,5 +1,4 @@
import 'dart:math'; import 'dart:math';
import 'dart:ui';
import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/image_providers/region_provider.dart';
import 'package:aves/image_providers/thumbnail_provider.dart'; import 'package:aves/image_providers/thumbnail_provider.dart';

View file

@ -44,12 +44,11 @@ class AlbumFilter extends CollectionFilter {
String getTooltip(BuildContext context) => album; String getTooltip(BuildContext context) => album;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return IconUtils.getAlbumIcon( return IconUtils.getAlbumIcon(
context: context, context: context,
albumPath: album, albumPath: album,
size: size, size: size,
embossed: embossed,
) ?? ) ??
(showGenericIcon ? Icon(AIcons.album, size: size) : null); (showGenericIcon ? Icon(AIcons.album, size: size) : null);
} }

View file

@ -1,3 +1,4 @@
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/coordinate_format.dart';
import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/enums.dart';
@ -7,7 +8,6 @@ import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -56,7 +56,7 @@ class CoordinateFilter extends CollectionFilter {
String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat); String getLabel(BuildContext context) => _formatBounds(context.l10n, context.read<Settings>().coordinateFormat);
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.geoBounds, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.geoBounds, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -3,7 +3,6 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class FavouriteFilter extends CollectionFilter { class FavouriteFilter extends CollectionFilter {
static const type = 'favourite'; static const type = 'favourite';
@ -30,7 +29,7 @@ class FavouriteFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel; String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.favourite, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size);
@override @override
Future<Color> color(BuildContext context) => SynchronousFuture(Colors.red); Future<Color> color(BuildContext context) => SynchronousFuture(Colors.red);

View file

@ -81,7 +81,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
String getTooltip(BuildContext context) => getLabel(context); String getTooltip(BuildContext context) => getLabel(context);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => null; Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null;
Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context))); Future<Color> color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context)));

View file

@ -58,15 +58,13 @@ class LocationFilter extends CollectionFilter {
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location; String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
if (_countryCode != null && device.canRenderFlagEmojis) { if (_countryCode != null && device.canRenderFlagEmojis) {
final flag = countryCodeToFlag(_countryCode); final flag = countryCodeToFlag(_countryCode);
// as of Flutter v1.22.3, emoji shadows are rendered as colorful duplicates,
// not filled with the shadow color as expected, so we remove them
if (flag != null) { if (flag != null) {
return Text( return Text(
flag, flag,
style: TextStyle(fontSize: size, shadows: const []), style: TextStyle(fontSize: size),
textScaleFactor: 1.0, textScaleFactor: 1.0,
); );
} }

View file

@ -68,7 +68,7 @@ class MimeFilter extends CollectionFilter {
} }
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -64,7 +64,7 @@ class QueryFilter extends CollectionFilter {
String get universalLabel => query; String get universalLabel => query;
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(AIcons.text, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size);
@override @override
Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor); Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor);

View file

@ -44,7 +44,7 @@ class TagFilter extends CollectionFilter {
String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag; String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null; Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagOff : AIcons.tag, size: size) : null;
@override @override
String get category => type; String get category => type;

View file

@ -94,7 +94,7 @@ class TypeFilter extends CollectionFilter {
} }
@override @override
Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true, bool embossed = false}) => Icon(_icon, size: size); Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -1,7 +1,7 @@
import 'package:aves/l10n/l10n.dart';
import 'package:aves/utils/geo_utils.dart'; import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';

View file

@ -19,6 +19,7 @@ class SettingsDefaults {
static const mustBackTwiceToExit = true; static const mustBackTwiceToExit = true;
static const keepScreenOn = KeepScreenOn.viewerOnly; static const keepScreenOn = KeepScreenOn.viewerOnly;
static const homePage = HomePageSetting.collection; static const homePage = HomePageSetting.collection;
static const tileLayout = TileLayout.grid;
// drawer // drawer
static final drawerTypeBookmarks = [ static final drawerTypeBookmarks = [
@ -66,6 +67,7 @@ class SettingsDefaults {
static const enableOverlayBlurEffect = true; // `enableOverlayBlurEffect` has a contextual default value static const enableOverlayBlurEffect = true; // `enableOverlayBlurEffect` has a contextual default value
static const viewerUseCutout = true; static const viewerUseCutout = true;
static const viewerMaxBrightness = false; static const viewerMaxBrightness = false;
static const enableMotionPhotoAutoPlay = false;
// video // video
static const videoQuickActions = [ static const videoQuickActions = [

View file

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/actions/video_actions.dart'; import 'package:aves/model/actions/video_actions.dart';
@ -15,7 +16,6 @@ import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
final Settings settings = Settings._private(); final Settings settings = Settings._private();
@ -49,6 +49,7 @@ class Settings extends ChangeNotifier {
static const homePageKey = 'home_page'; static const homePageKey = 'home_page';
static const catalogTimeZoneKey = 'catalog_time_zone'; static const catalogTimeZoneKey = 'catalog_time_zone';
static const tileExtentPrefixKey = 'tile_extent_'; static const tileExtentPrefixKey = 'tile_extent_';
static const tileLayoutPrefixKey = 'tile_layout_';
// drawer // drawer
static const drawerTypeBookmarksKey = 'drawer_type_bookmarks'; static const drawerTypeBookmarksKey = 'drawer_type_bookmarks';
@ -82,6 +83,8 @@ class Settings extends ChangeNotifier {
static const enableOverlayBlurEffectKey = 'enable_overlay_blur_effect'; static const enableOverlayBlurEffectKey = 'enable_overlay_blur_effect';
static const viewerUseCutoutKey = 'viewer_use_cutout'; static const viewerUseCutoutKey = 'viewer_use_cutout';
static const viewerMaxBrightnessKey = 'viewer_max_brightness'; static const viewerMaxBrightnessKey = 'viewer_max_brightness';
static const enableMotionPhotoAutoPlayKey = 'motion_photo_auto_play';
static const imageBackgroundKey = 'image_background';
// video // video
static const videoQuickActionsKey = 'video_quick_actions'; static const videoQuickActionsKey = 'video_quick_actions';
@ -103,9 +106,6 @@ class Settings extends ChangeNotifier {
static const coordinateFormatKey = 'coordinates_format'; static const coordinateFormatKey = 'coordinates_format';
static const unitSystemKey = 'unit_system'; static const unitSystemKey = 'unit_system';
// rendering
static const imageBackgroundKey = 'image_background';
// search // search
static const saveSearchHistoryKey = 'save_search_history'; static const saveSearchHistoryKey = 'save_search_history';
static const searchHistoryKey = 'search_history'; static const searchHistoryKey = 'search_history';
@ -217,12 +217,26 @@ class Settings extends ChangeNotifier {
_appliedLocale = null; _appliedLocale = null;
} }
List<Locale> _systemLocalesFallback = [];
set systemLocalesFallback(List<Locale> locales) => _systemLocalesFallback = locales;
Locale? _appliedLocale; Locale? _appliedLocale;
Locale get appliedLocale { Locale get appliedLocale {
if (_appliedLocale == null) { if (_appliedLocale == null) {
final preferredLocale = locale; final _locale = locale;
_appliedLocale = basicLocaleListResolution(preferredLocale != null ? [preferredLocale] : null, AppLocalizations.supportedLocales); final preferredLocales = <Locale>[];
if (_locale != null) {
preferredLocales.add(_locale);
} else {
preferredLocales.addAll(WidgetsBinding.instance!.window.locales);
if (preferredLocales.isEmpty) {
// the `window` locales may be empty in a window-less service context
preferredLocales.addAll(_systemLocalesFallback);
}
}
_appliedLocale = basicLocaleListResolution(preferredLocales, AppLocalizations.supportedLocales);
} }
return _appliedLocale!; return _appliedLocale!;
} }
@ -247,6 +261,10 @@ class Settings extends ChangeNotifier {
void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue); void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue);
TileLayout getTileLayout(String routeName) => getEnumOrDefault(tileLayoutPrefixKey + routeName, SettingsDefaults.tileLayout, TileLayout.values);
void setTileLayout(String routeName, TileLayout newValue) => setAndNotify(tileLayoutPrefixKey + routeName, newValue.toString());
// drawer // drawer
List<CollectionFilter?> get drawerTypeBookmarks => List<CollectionFilter?> get drawerTypeBookmarks =>
@ -360,6 +378,14 @@ class Settings extends ChangeNotifier {
set viewerMaxBrightness(bool newValue) => setAndNotify(viewerMaxBrightnessKey, newValue); set viewerMaxBrightness(bool newValue) => setAndNotify(viewerMaxBrightnessKey, newValue);
bool get enableMotionPhotoAutoPlay => getBoolOrDefault(enableMotionPhotoAutoPlayKey, SettingsDefaults.enableMotionPhotoAutoPlay);
set enableMotionPhotoAutoPlay(bool newValue) => setAndNotify(enableMotionPhotoAutoPlayKey, newValue);
EntryBackground get imageBackground => getEnumOrDefault(imageBackgroundKey, SettingsDefaults.imageBackground, EntryBackground.values);
set imageBackground(EntryBackground newValue) => setAndNotify(imageBackgroundKey, newValue.toString());
// video // video
List<VideoAction> get videoQuickActions => getEnumListOrDefault(videoQuickActionsKey, SettingsDefaults.videoQuickActions, VideoAction.values); List<VideoAction> get videoQuickActions => getEnumListOrDefault(videoQuickActionsKey, SettingsDefaults.videoQuickActions, VideoAction.values);
@ -422,12 +448,6 @@ class Settings extends ChangeNotifier {
set unitSystem(UnitSystem newValue) => setAndNotify(unitSystemKey, newValue.toString()); set unitSystem(UnitSystem newValue) => setAndNotify(unitSystemKey, newValue.toString());
// rendering
EntryBackground get imageBackground => getEnumOrDefault(imageBackgroundKey, SettingsDefaults.imageBackground, EntryBackground.values);
set imageBackground(EntryBackground newValue) => setAndNotify(imageBackgroundKey, newValue.toString());
// search // search
bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, SettingsDefaults.saveSearchHistory); bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, SettingsDefaults.saveSearchHistory);
@ -570,6 +590,12 @@ class Settings extends ChangeNotifier {
} else { } else {
debugPrint('failed to import key=$key, value=$value is not a double'); debugPrint('failed to import key=$key, value=$value is not a double');
} }
} else if (key.startsWith(tileLayoutPrefixKey)) {
if (value is String) {
_prefs!.setString(key, value);
} else {
debugPrint('failed to import key=$key, value=$value is not a string');
}
} else { } else {
switch (key) { switch (key) {
case subtitleTextColorKey: case subtitleTextColorKey:
@ -602,6 +628,7 @@ class Settings extends ChangeNotifier {
case enableOverlayBlurEffectKey: case enableOverlayBlurEffectKey:
case viewerUseCutoutKey: case viewerUseCutoutKey:
case viewerMaxBrightnessKey: case viewerMaxBrightnessKey:
case enableMotionPhotoAutoPlayKey:
case enableVideoHardwareAccelerationKey: case enableVideoHardwareAccelerationKey:
case enableVideoAutoPlayKey: case enableVideoAutoPlayKey:
case subtitleShowOutlineKey: case subtitleShowOutlineKey:
@ -622,12 +649,12 @@ class Settings extends ChangeNotifier {
case albumSortFactorKey: case albumSortFactorKey:
case countrySortFactorKey: case countrySortFactorKey:
case tagSortFactorKey: case tagSortFactorKey:
case imageBackgroundKey:
case videoLoopModeKey: case videoLoopModeKey:
case subtitleTextAlignmentKey: case subtitleTextAlignmentKey:
case infoMapStyleKey: case infoMapStyleKey:
case coordinateFormatKey: case coordinateFormatKey:
case unitSystemKey: case unitSystemKey:
case imageBackgroundKey:
case accessibilityAnimationsKey: case accessibilityAnimationsKey:
case timeToTakeActionKey: case timeToTakeActionKey:
if (value is String) { if (value is String) {

View file

@ -176,7 +176,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
processed.add, processed.add,
onError: (error) => reportService.recordError('renameEntry failed with error=$error', null), onError: (error) => reportService.recordError('renameEntry failed with error=$error', null),
onDone: () async { onDone: () async {
final successOps = processed.where((e) => e.success).toSet(); final successOps = processed.where((e) => e.success && !e.skipped).toSet();
if (successOps.isEmpty) { if (successOps.isEmpty) {
completer.complete(false); completer.complete(false);
return; return;

View file

@ -7,3 +7,5 @@ enum AlbumChipGroupFactor { none, importance, volume }
enum EntrySortFactor { date, size, name } enum EntrySortFactor { date, size, name }
enum EntryGroupFactor { none, album, month, day } enum EntryGroupFactor { none, album, month, day }
enum TileLayout { grid, list }

View file

@ -1,5 +1,5 @@
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension ExtraSourceState on SourceState { extension ExtraSourceState on SourceState {
String? getName(AppLocalizations l10n) { String? getName(AppLocalizations l10n) {

View file

@ -14,6 +14,7 @@ class MimeTypes {
static const art = 'image/x-jg'; static const art = 'image/x-jg';
static const djvu = 'image/vnd.djvu'; static const djvu = 'image/vnd.djvu';
static const jxl = 'image/jxl';
static const psdVnd = 'image/vnd.adobe.photoshop'; static const psdVnd = 'image/vnd.adobe.photoshop';
static const psdX = 'image/x-photoshop'; static const psdX = 'image/x-photoshop';
@ -47,6 +48,7 @@ class MimeTypes {
static const mp2t = 'video/mp2t'; // .m2ts, .ts static const mp2t = 'video/mp2t'; // .m2ts, .ts
static const mp2ts = 'video/mp2ts'; // .ts (prefer `mp2t` when possible) static const mp2ts = 'video/mp2ts'; // .ts (prefer `mp2t` when possible)
static const mp4 = 'video/mp4'; static const mp4 = 'video/mp4';
static const mpeg = 'video/mpeg';
static const ogv = 'video/ogg'; static const ogv = 'video/ogg';
static const webm = 'video/webm'; static const webm = 'video/webm';
@ -55,6 +57,7 @@ class MimeTypes {
// JB2, JPC, JPX? // JB2, JPC, JPX?
static const octetStream = 'application/octet-stream'; static const octetStream = 'application/octet-stream';
static const zip = 'application/zip';
// groups // groups
@ -64,11 +67,11 @@ class MimeTypes {
static const Set<String> rawImages = {arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f}; static const Set<String> rawImages = {arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f};
// TODO TLAD [codec] make it dynamic if it depends on OS/lib versions // TODO TLAD [codec] make it dynamic if it depends on OS/lib versions
static const Set<String> undecodableImages = {art, crw, djvu, psdVnd, psdX, octetStream}; static const Set<String> undecodableImages = {art, crw, djvu, jxl, psdVnd, psdX, octetStream, zip};
static const Set<String> _knownOpaqueImages = {heic, heif, jpeg}; static const Set<String> _knownOpaqueImages = {heic, heif, jpeg};
static const Set<String> _knownVideos = {avi, aviVnd, mkv, mov, mp2t, mp2ts, mp4, ogv, webm}; static const Set<String> _knownVideos = {avi, aviVnd, mkv, mov, mp2t, mp2ts, mp4, mpeg, ogv, webm};
static final Set<String> knownMediaTypes = {..._knownOpaqueImages, ...alphaImages, ...rawImages, ...undecodableImages, ..._knownVideos}; static final Set<String> knownMediaTypes = {..._knownOpaqueImages, ...alphaImages, ...rawImages, ...undecodableImages, ..._knownVideos};

View file

@ -9,10 +9,13 @@ class XMP {
'aux': 'Exif Aux', 'aux': 'Exif Aux',
'avm': 'Astronomy Visualization', 'avm': 'Astronomy Visualization',
'Camera': 'Camera', 'Camera': 'Camera',
'cc': 'Creative Commons',
'crd': 'Camera Raw Defaults',
'creatorAtom': 'After Effects', 'creatorAtom': 'After Effects',
'crs': 'Camera Raw Settings', 'crs': 'Camera Raw Settings',
'dc': 'Dublin Core', 'dc': 'Dublin Core',
'drone-dji': 'DJI Drone', 'drone-dji': 'DJI Drone',
'dwc': 'Darwin Core',
'exif': 'Exif', 'exif': 'Exif',
'exifEX': 'Exif Ex', 'exifEX': 'Exif Ex',
'GettyImagesGIFT': 'Getty Images', 'GettyImagesGIFT': 'Getty Images',

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/analysis_controller.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
@ -10,7 +11,6 @@ import 'package:aves/services/common/services.dart';
import 'package:fijkplayer/fijkplayer.dart'; import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AnalysisService { class AnalysisService {
static const platform = MethodChannel('deckers.thibault/aves/analysis'); static const platform = MethodChannel('deckers.thibault/aves/analysis');
@ -112,6 +112,7 @@ class Analyzer {
stopSignal: ValueNotifier(false), stopSignal: ValueNotifier(false),
); );
settings.systemLocalesFallback = await deviceService.getLocales();
_l10n = await AppLocalizations.delegate.load(settings.appliedLocale); _l10n = await AppLocalizations.delegate.load(settings.appliedLocale);
_serviceStateNotifier.value = AnalyzerState.running; _serviceStateNotifier.value = AnalyzerState.running;
await _source.init(); await _source.init();

View file

@ -44,6 +44,10 @@ class PlatformAndroidAppService implements AndroidAppService {
if (kakaoTalk != null) { if (kakaoTalk != null) {
kakaoTalk.ownedDirs.add('KakaoTalkDownload'); kakaoTalk.ownedDirs.add('KakaoTalkDownload');
} }
final imagingEdge = packages.firstWhereOrNull((package) => package.packageName == 'com.sony.playmemories.mobile');
if (imagingEdge != null) {
imagingEdge.ownedDirs.add('Imaging Edge Mobile');
}
return packages; return packages;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
@ -140,7 +144,7 @@ class PlatformAndroidAppService implements AndroidAppService {
@override @override
Future<bool> shareEntries(Iterable<AvesEntry> entries) async { Future<bool> shareEntries(Iterable<AvesEntry> entries) async {
// loosen mime type to a generic one, so we can share with badly defined apps // loosen MIME type to a generic one, so we can share with badly defined apps
// e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
try { try {

View file

@ -133,7 +133,7 @@ class AndroidDebugService {
static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async { static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async {
try { try {
// returns map with the mime type and tag count for each directory found by `metadata-extractor` // returns map with the MIME type and tag count for each directory found by `metadata-extractor`
final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{ final result = await platform.invokeMethod('getMetadataExtractorSummary', <String, dynamic>{
'mimeType': entry.mimeType, 'mimeType': entry.mimeType,
'uri': entry.uri, 'uri': entry.uri,

View file

@ -3,65 +3,87 @@ import 'package:flutter/foundation.dart';
@immutable @immutable
class ImageOpEvent extends Equatable { class ImageOpEvent extends Equatable {
final bool success; final bool success, skipped;
final String uri; final String uri;
@override @override
List<Object?> get props => [success, uri]; List<Object?> get props => [success, skipped, uri];
const ImageOpEvent({ const ImageOpEvent({
required this.success, required this.success,
required this.skipped,
required this.uri, required this.uri,
}); });
factory ImageOpEvent.fromMap(Map map) { factory ImageOpEvent.fromMap(Map map) {
final skipped = map['skipped'] ?? false;
return ImageOpEvent( return ImageOpEvent(
success: map['success'] ?? false, success: (map['success'] ?? false) || skipped,
skipped: skipped,
uri: map['uri'], uri: map['uri'],
); );
} }
} }
@immutable
class MoveOpEvent extends ImageOpEvent { class MoveOpEvent extends ImageOpEvent {
final Map newFields; final Map newFields;
const MoveOpEvent({required bool success, required String uri, required this.newFields}) @override
: super( List<Object?> get props => [success, skipped, uri, newFields];
const MoveOpEvent({
required bool success,
required bool skipped,
required String uri,
required this.newFields,
}) : super(
success: success, success: success,
skipped: skipped,
uri: uri, uri: uri,
); );
factory MoveOpEvent.fromMap(Map map) { factory MoveOpEvent.fromMap(Map map) {
final newFields = map['newFields'] ?? {};
final skipped = (map['skipped'] ?? false) || (newFields['skipped'] ?? false);
return MoveOpEvent( return MoveOpEvent(
success: map['success'] ?? false, success: (map['success'] ?? false) || skipped,
skipped: skipped,
uri: map['uri'], uri: map['uri'],
newFields: map['newFields'] ?? {}, newFields: newFields,
); );
} }
@override
String toString() => '$runtimeType#${shortHash(this)}{success=$success, uri=$uri, newFields=$newFields}';
} }
@immutable
class ExportOpEvent extends MoveOpEvent { class ExportOpEvent extends MoveOpEvent {
final int? pageId; final int? pageId;
@override @override
List<Object?> get props => [success, uri, pageId]; List<Object?> get props => [success, skipped, uri, pageId, newFields];
const ExportOpEvent({required bool success, required String uri, this.pageId, required Map newFields}) const ExportOpEvent({
: super( required bool success,
required bool skipped,
required String uri,
this.pageId,
required Map newFields,
}) : super(
success: success, success: success,
skipped: skipped,
uri: uri, uri: uri,
newFields: newFields, newFields: newFields,
); );
factory ExportOpEvent.fromMap(Map map) { factory ExportOpEvent.fromMap(Map map) {
final newFields = map['newFields'] ?? {};
final skipped = (map['skipped'] ?? false) || (newFields['skipped'] ?? false);
return ExportOpEvent( return ExportOpEvent(
success: map['success'] ?? false, success: (map['success'] ?? false) || skipped,
skipped: skipped,
uri: map['uri'], uri: map['uri'],
pageId: map['pageId'], pageId: map['pageId'],
newFields: map['newFields'] ?? {}, newFields: newFields,
); );
} }
} }

View file

@ -1,3 +1,5 @@
import 'dart:ui';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -6,6 +8,8 @@ abstract class DeviceService {
Future<String?> getDefaultTimeZone(); Future<String?> getDefaultTimeZone();
Future<List<Locale>> getLocales();
Future<int> getPerformanceClass(); Future<int> getPerformanceClass();
} }
@ -33,6 +37,26 @@ class PlatformDeviceService implements DeviceService {
return null; return null;
} }
@override
Future<List<Locale>> getLocales() async {
try {
final result = await platform.invokeMethod('getLocales');
if (result != null) {
return (result as List).cast<Map>().map((tags) {
final language = tags['language'] as String?;
final country = tags['country'] as String?;
return Locale(
language ?? 'und',
(country != null && country.isEmpty) ? null : country,
);
}).toList();
}
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return [];
}
@override @override
Future<int> getPerformanceClass() async { Future<int> getPerformanceClass() async {
try { try {

View file

@ -1,13 +1,11 @@
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
// names should match possible values on platform // names should match possible values on platform
enum NameConflictStrategy { rename, replace, skip } enum NameConflictStrategy { rename, replace, skip }
extension ExtraNameConflictStrategy on NameConflictStrategy { extension ExtraNameConflictStrategy on NameConflictStrategy {
// TODO TLAD [dart 2.15] replace `describeEnum()` by `enum.name` String toPlatform() => name;
String toPlatform() => describeEnum(this);
String getName(BuildContext context) { String getName(BuildContext context) {
switch (this) { switch (this) {

View file

@ -15,6 +15,8 @@ import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';
abstract class MediaFileService { abstract class MediaFileService {
String get newOpId;
Future<AvesEntry?> getEntry(String uri, String? mimeType); Future<AvesEntry?> getEntry(String uri, String? mimeType);
Future<Uint8List> getSvg( Future<Uint8List> getSvg(
@ -68,10 +70,16 @@ abstract class MediaFileService {
Future<T>? resumeLoading<T>(Object taskKey); Future<T>? resumeLoading<T>(Object taskKey);
Stream<ImageOpEvent> delete(Iterable<AvesEntry> entries); Future<void> cancelFileOp(String opId);
Stream<MoveOpEvent> move( Stream<ImageOpEvent> delete({
Iterable<AvesEntry> entries, { String? opId,
required Iterable<AvesEntry> entries,
});
Stream<MoveOpEvent> move({
String? opId,
required Iterable<AvesEntry> entries,
required bool copy, required bool copy,
required String destinationAlbum, required String destinationAlbum,
required NameConflictStrategy nameConflictStrategy, required NameConflictStrategy nameConflictStrategy,
@ -120,6 +128,9 @@ class PlatformMediaFileService implements MediaFileService {
}; };
} }
@override
String get newOpId => DateTime.now().millisecondsSinceEpoch.toString();
@override @override
Future<AvesEntry?> getEntry(String uri, String? mimeType) async { Future<AvesEntry?> getEntry(String uri, String? mimeType) async {
try { try {
@ -194,7 +205,9 @@ class PlatformMediaFileService implements MediaFileService {
// `await` here, so that `completeError` will be caught below // `await` here, so that `completeError` will be caught below
return await completer.future; return await completer.future;
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); if (!MimeTypes.knownMediaTypes.contains(mimeType)) {
await reportService.recordError(e, stack);
}
} }
return Uint8List(0); return Uint8List(0);
} }
@ -296,10 +309,25 @@ class PlatformMediaFileService implements MediaFileService {
Future<T>? resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey); Future<T>? resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
@override @override
Stream<ImageOpEvent> delete(Iterable<AvesEntry> entries) { Future<void> cancelFileOp(String opId) async {
try {
await platform.invokeMethod('cancelFileOp', <String, dynamic>{
'opId': opId,
});
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
@override
Stream<ImageOpEvent> delete({
String? opId,
required Iterable<AvesEntry> entries,
}) {
try { try {
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{ return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{
'op': 'delete', 'op': 'delete',
'id': opId,
'entries': entries.map(_toPlatformEntryMap).toList(), 'entries': entries.map(_toPlatformEntryMap).toList(),
}).map((event) => ImageOpEvent.fromMap(event)); }).map((event) => ImageOpEvent.fromMap(event));
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
@ -309,8 +337,9 @@ class PlatformMediaFileService implements MediaFileService {
} }
@override @override
Stream<MoveOpEvent> move( Stream<MoveOpEvent> move({
Iterable<AvesEntry> entries, { String? opId,
required Iterable<AvesEntry> entries,
required bool copy, required bool copy,
required String destinationAlbum, required String destinationAlbum,
required NameConflictStrategy nameConflictStrategy, required NameConflictStrategy nameConflictStrategy,
@ -318,6 +347,7 @@ class PlatformMediaFileService implements MediaFileService {
try { try {
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{ return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{
'op': 'move', 'op': 'move',
'id': opId,
'entries': entries.map(_toPlatformEntryMap).toList(), 'entries': entries.map(_toPlatformEntryMap).toList(),
'copy': copy, 'copy': copy,
'destinationPath': destinationAlbum, 'destinationPath': destinationAlbum,

View file

@ -4,7 +4,6 @@ import 'package:aves/model/entry.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/string_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';

View file

@ -36,7 +36,7 @@ abstract class StorageService {
// return whether operation succeeded (`null` if user cancelled) // return whether operation succeeded (`null` if user cancelled)
Future<bool?> createFile(String name, String mimeType, Uint8List bytes); Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
Future<Uint8List> openFile(String mimeType); Future<Uint8List> openFile([String? mimeType]);
} }
class PlatformStorageService implements StorageService { class PlatformStorageService implements StorageService {
@ -231,7 +231,7 @@ class PlatformStorageService implements StorageService {
} }
@override @override
Future<Uint8List> openFile(String mimeType) async { Future<Uint8List> openFile([String? mimeType]) async {
try { try {
final completer = Completer<Uint8List>.sync(); final completer = Completer<Uint8List>.sync();
final sink = OutputBuffer(); final sink = OutputBuffer();

View file

@ -59,6 +59,7 @@ class Durations {
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100); static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);
static const highlightJumpDelay = Duration(milliseconds: 400); static const highlightJumpDelay = Duration(milliseconds: 400);
static const highlightScrollInitDelay = Duration(milliseconds: 800); static const highlightScrollInitDelay = Duration(milliseconds: 800);
static const motionPhotoAutoPlayDelay = Duration(milliseconds: 700);
static const videoOverlayHideDelay = Duration(milliseconds: 500); static const videoOverlayHideDelay = Duration(milliseconds: 500);
static const videoProgressTimerInterval = Duration(milliseconds: 300); static const videoProgressTimerInterval = Duration(milliseconds: 300);
static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const doubleBackTimerDelay = Duration(milliseconds: 1000);

View file

@ -32,10 +32,16 @@ class AIcons {
static const IconData tag = Icons.local_offer_outlined; static const IconData tag = Icons.local_offer_outlined;
static const IconData tagOff = MdiIcons.tagOffOutline; static const IconData tagOff = MdiIcons.tagOffOutline;
// view
static const IconData group = Icons.group_work_outlined;
static const IconData layout = Icons.grid_view_outlined;
static const IconData sort = Icons.sort_outlined;
// actions // actions
static const IconData add = Icons.add_circle_outline; static const IconData add = Icons.add_circle_outline;
static const IconData addShortcut = Icons.add_to_home_screen_outlined; static const IconData addShortcut = Icons.add_to_home_screen_outlined;
static const IconData addTag = MdiIcons.tagPlusOutline; static const IconData addTag = MdiIcons.tagPlusOutline;
static const IconData cancel = Icons.cancel_outlined;
static const IconData replay10 = Icons.replay_10_outlined; static const IconData replay10 = Icons.replay_10_outlined;
static const IconData skip10 = Icons.forward_10_outlined; static const IconData skip10 = Icons.forward_10_outlined;
static const IconData captureFrame = Icons.screenshot_outlined; static const IconData captureFrame = Icons.screenshot_outlined;
@ -53,7 +59,6 @@ class AIcons {
static const IconData filterOff = MdiIcons.filterOffOutline; static const IconData filterOff = MdiIcons.filterOffOutline;
static const IconData geoBounds = Icons.public_outlined; static const IconData geoBounds = Icons.public_outlined;
static const IconData goUp = Icons.arrow_upward_outlined; static const IconData goUp = Icons.arrow_upward_outlined;
static const IconData group = Icons.group_work_outlined;
static const IconData hide = Icons.visibility_off_outlined; static const IconData hide = Icons.visibility_off_outlined;
static const IconData import = MdiIcons.fileImportOutline; static const IconData import = MdiIcons.fileImportOutline;
static const IconData info = Icons.info_outlined; static const IconData info = Icons.info_outlined;
@ -79,7 +84,6 @@ class AIcons {
static const IconData setCover = MdiIcons.imageEditOutline; static const IconData setCover = MdiIcons.imageEditOutline;
static const IconData share = Icons.share_outlined; static const IconData share = Icons.share_outlined;
static const IconData show = Icons.visibility_outlined; static const IconData show = Icons.visibility_outlined;
static const IconData sort = Icons.sort_outlined;
static const IconData speed = Icons.speed_outlined; static const IconData speed = Icons.speed_outlined;
static const IconData stats = Icons.pie_chart_outlined; static const IconData stats = Icons.pie_chart_outlined;
static const IconData streams = Icons.translate_outlined; static const IconData streams = Icons.translate_outlined;
@ -87,6 +91,7 @@ class AIcons {
static const IconData streamAudio = Icons.audiotrack_outlined; static const IconData streamAudio = Icons.audiotrack_outlined;
static const IconData streamText = Icons.closed_caption_outlined; static const IconData streamText = Icons.closed_caption_outlined;
static const IconData videoSettings = Icons.video_settings_outlined; static const IconData videoSettings = Icons.video_settings_outlined;
static const IconData view = Icons.grid_view_outlined;
static const IconData zoomIn = Icons.add_outlined; static const IconData zoomIn = Icons.add_outlined;
static const IconData zoomOut = Icons.remove_outlined; static const IconData zoomOut = Icons.remove_outlined;
static const IconData collapse = Icons.expand_less_outlined; static const IconData collapse = Icons.expand_less_outlined;

View file

@ -1,7 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class Themes { class Themes {
static const _accentColor = Colors.indigoAccent; static const _accentColor = Colors.indigoAccent;

View file

@ -2,11 +2,10 @@ import 'dart:ui';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
class Constants { class Constants {
// as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped // as of Flutter v2.8.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
// so we give it a `strutStyle` with a slightly larger height // so we give it a `strutStyle` with a slightly larger height
static const overflowStrutStyle = StrutStyle(height: 1.3); static const overflowStrutStyle = StrutStyle(height: 1.3);

View file

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/basic/link_chip.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
@ -9,6 +7,7 @@ class AboutCredits extends StatelessWidget {
const AboutCredits({Key? key}) : super(key: key); const AboutCredits({Key? key}) : super(key: key);
static const translators = { static const translators = {
'Deutsch': 'JanWaldhorn',
'Русский': 'D3ZOXY', 'Русский': 'D3ZOXY',
}; };

View file

@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/model/settings/accessibility_animations.dart'; import 'package:aves/model/settings/accessibility_animations.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
@ -27,7 +27,6 @@ import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:overlay_support/overlay_support.dart'; import 'package:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -122,9 +121,7 @@ class _AvesAppState extends State<AvesApp> {
darkTheme: Themes.darkTheme, darkTheme: Themes.darkTheme,
themeMode: ThemeMode.dark, themeMode: ThemeMode.dark,
locale: settingsLocale, locale: settingsLocale,
localizationsDelegates: const [ localizationsDelegates: AppLocalizations.localizationsDelegates,
...AppLocalizations.localizationsDelegates,
],
supportedLocales: AppLocalizations.supportedLocales, supportedLocales: AppLocalizations.supportedLocales,
// checkerboardRasterCacheImages: true, // checkerboardRasterCacheImages: true,
// checkerboardOffscreenLayers: true, // checkerboardOffscreenLayers: true,
@ -200,7 +197,7 @@ class _AvesAppState extends State<AvesApp> {
? 'profile' ? 'profile'
: 'debug', : 'debug',
'has_play_services': hasPlayServices, 'has_play_services': hasPlayServices,
'locales': window.locales.join(', '), 'locales': WidgetsBinding.instance!.window.locales.join(', '),
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})', 'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
}); });
_navigatorObservers = [ _navigatorObservers = [

View file

@ -12,6 +12,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/collection/filter_bar.dart';
import 'package:aves/widgets/collection/query_bar.dart'; import 'package:aves/widgets/collection/query_bar.dart';
@ -19,9 +20,8 @@ import 'package:aves/widgets/common/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar_title.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -283,9 +283,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
]; ];
} }
// key is expected by test driver (e.g. 'menu-sort', 'menu-group', 'menu-map') // key is expected by test driver (e.g. 'menu-configureView', 'menu-map')
// TODO TLAD [dart 2.15] replace `describeEnum()` by `enum.name` Key _getActionKey(EntrySetAction action) => Key('menu-${action.name}');
Key _getActionKey(EntrySetAction action) => Key('menu-${describeEnum(action)}');
Widget _toActionButton(EntrySetAction action, {required bool enabled}) { Widget _toActionButton(EntrySetAction action, {required bool enabled}) {
final onPressed = enabled ? () => _onActionSelected(action) : null; final onPressed = enabled ? () => _onActionSelected(action) : null;
@ -397,11 +396,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Future<void> _onActionSelected(EntrySetAction action) async { Future<void> _onActionSelected(EntrySetAction action) async {
switch (action) { switch (action) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
await _sort(); await _configureView();
break;
case EntrySetAction.group:
await _group();
break; break;
case EntrySetAction.select: case EntrySetAction.select:
context.read<Selection<AvesEntry>>().select(); context.read<Selection<AvesEntry>>().select();
@ -436,47 +432,42 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
} }
Future<void> _sort() async { Future<void> _configureView() async {
final value = await showDialog<EntrySortFactor>( final initialValue = Tuple3(
context: context, settings.collectionSortFactor,
builder: (context) => AvesSelectionDialog<EntrySortFactor>( settings.collectionSectionFactor,
initialValue: settings.collectionSortFactor, settings.getTileLayout(CollectionPage.routeName),
options: {
EntrySortFactor.date: context.l10n.collectionSortDate,
EntrySortFactor.size: context.l10n.collectionSortSize,
EntrySortFactor.name: context.l10n.collectionSortName,
},
title: context.l10n.collectionSortTitle,
),
); );
// wait for the dialog to hide as applying the change may block the UI final value = await showDialog<Tuple3<EntrySortFactor?, EntryGroupFactor?, TileLayout?>>(
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
if (value != null) {
settings.collectionSortFactor = value;
}
}
Future<void> _group() async {
final value = await showDialog<EntryGroupFactor>(
context: context, context: context,
builder: (context) { builder: (context) {
final l10n = context.l10n; final l10n = context.l10n;
return AvesSelectionDialog<EntryGroupFactor>( return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
initialValue: settings.collectionSectionFactor, initialValue: initialValue,
options: { sortOptions: {
EntrySortFactor.date: l10n.collectionSortDate,
EntrySortFactor.size: l10n.collectionSortSize,
EntrySortFactor.name: l10n.collectionSortName,
},
groupOptions: {
EntryGroupFactor.album: l10n.collectionGroupAlbum, EntryGroupFactor.album: l10n.collectionGroupAlbum,
EntryGroupFactor.month: l10n.collectionGroupMonth, EntryGroupFactor.month: l10n.collectionGroupMonth,
EntryGroupFactor.day: l10n.collectionGroupDay, EntryGroupFactor.day: l10n.collectionGroupDay,
EntryGroupFactor.none: l10n.collectionGroupNone, EntryGroupFactor.none: l10n.collectionGroupNone,
}, },
title: l10n.collectionGroupTitle, layoutOptions: {
TileLayout.grid: l10n.tileLayoutGrid,
TileLayout.list: l10n.tileLayoutList,
},
); );
}, },
); );
// wait for the dialog to hide as applying the change may block the UI // wait for the dialog to hide as applying the change may block the UI
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation); await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
if (value != null) { if (value != null && initialValue != value) {
settings.collectionSectionFactor = value; settings.collectionSortFactor = value.item1!;
settings.collectionSectionFactor = value.item2!;
settings.setTileLayout(CollectionPage.routeName, value.item3!);
} }
} }

View file

@ -4,6 +4,7 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
@ -11,21 +12,22 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/app_bar.dart';
import 'package:aves/widgets/collection/draggable_thumb_label.dart'; import 'package:aves/widgets/collection/draggable_thumb_label.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/section_layout.dart';
import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/grid/tile.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/item_tracker.dart'; import 'package:aves/widgets/common/grid/item_tracker.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/grid/selector.dart'; import 'package:aves/widgets/common/grid/selector.dart';
import 'package:aves/widgets/common/grid/sliver.dart'; import 'package:aves/widgets/common/grid/sliver.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart'; import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -74,11 +76,13 @@ class _CollectionGridContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
return Consumer<CollectionLens>( return Consumer<CollectionLens>(
builder: (context, collection, child) { builder: (context, collection, child) {
final sectionedListLayoutProvider = ValueListenableBuilder<double>( final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier), valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, tileExtent, child) { builder: (context, thumbnailExtent, child) {
return Selector<TileExtentController, Tuple3<double, int, double>>( return Selector<TileExtentController, Tuple3<double, int, double>>(
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing), selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
builder: (context, c, child) { builder: (context, c, child) {
@ -89,22 +93,27 @@ class _CollectionGridContent extends StatelessWidget {
final target = context.read<DurationsData>().staggeredAnimationPageTarget; final target = context.read<DurationsData>().staggeredAnimationPageTarget;
final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target); final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
return GridTheme( return GridTheme(
extent: tileExtent, extent: thumbnailExtent,
child: SectionedEntryListLayoutProvider( child: EntryListDetailsTheme(
collection: collection, extent: thumbnailExtent,
scrollableWidth: scrollableWidth, child: SectionedEntryListLayoutProvider(
columnCount: columnCount,
spacing: tileSpacing,
tileExtent: tileExtent,
tileBuilder: (entry) => InteractiveThumbnail(
key: ValueKey(entry.contentId),
collection: collection, collection: collection,
entry: entry, scrollableWidth: scrollableWidth,
tileExtent: tileExtent, tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier, columnCount: columnCount,
spacing: tileSpacing,
tileExtent: thumbnailExtent,
tileBuilder: (entry) => InteractiveTile(
key: ValueKey(entry.contentId),
collection: collection,
entry: entry,
thumbnailExtent: thumbnailExtent,
tileLayout: tileLayout,
isScrollingNotifier: _isScrollingNotifier,
),
tileAnimationDelay: tileAnimationDelay,
child: child!,
), ),
tileAnimationDelay: tileAnimationDelay,
child: child!,
), ),
); );
}, },
@ -115,6 +124,7 @@ class _CollectionGridContent extends StatelessWidget {
collection: collection, collection: collection,
isScrollingNotifier: _isScrollingNotifier, isScrollingNotifier: _isScrollingNotifier,
scrollController: PrimaryScrollController.of(context)!, scrollController: PrimaryScrollController.of(context)!,
tileLayout: tileLayout,
), ),
); );
return sectionedListLayoutProvider; return sectionedListLayoutProvider;
@ -127,27 +137,28 @@ class _CollectionSectionedContent extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final ValueNotifier<bool> isScrollingNotifier; final ValueNotifier<bool> isScrollingNotifier;
final ScrollController scrollController; final ScrollController scrollController;
final TileLayout tileLayout;
const _CollectionSectionedContent({ const _CollectionSectionedContent({
required this.collection, required this.collection,
required this.isScrollingNotifier, required this.isScrollingNotifier,
required this.scrollController, required this.scrollController,
required this.tileLayout,
}); });
@override @override
_CollectionSectionedContentState createState() => _CollectionSectionedContentState(); _CollectionSectionedContentState createState() => _CollectionSectionedContentState();
} }
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> with WidgetsBindingObserver, GridItemTrackerMixin<AvesEntry, _CollectionSectionedContent> { class _CollectionSectionedContentState extends State<_CollectionSectionedContent> {
CollectionLens get collection => widget.collection; CollectionLens get collection => widget.collection;
@override TileLayout get tileLayout => widget.tileLayout;
ScrollController get scrollController => widget.scrollController; ScrollController get scrollController => widget.scrollController;
@override
final ValueNotifier<double> appBarHeightNotifier = ValueNotifier(0); final ValueNotifier<double> appBarHeightNotifier = ValueNotifier(0);
@override
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable'); final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
@override @override
@ -169,6 +180,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
final scaler = _CollectionScaler( final scaler = _CollectionScaler(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
appBarHeightNotifier: appBarHeightNotifier, appBarHeightNotifier: appBarHeightNotifier,
tileLayout: tileLayout,
child: scrollView, child: scrollView,
); );
@ -181,18 +193,26 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
child: scaler, child: scaler,
); );
return selector; return GridItemTracker<AvesEntry>(
scrollableKey: scrollableKey,
tileLayout: tileLayout,
appBarHeightNotifier: appBarHeightNotifier,
scrollController: scrollController,
child: selector,
);
} }
} }
class _CollectionScaler extends StatelessWidget { class _CollectionScaler extends StatelessWidget {
final GlobalKey scrollableKey; final GlobalKey scrollableKey;
final ValueNotifier<double> appBarHeightNotifier; final ValueNotifier<double> appBarHeightNotifier;
final TileLayout tileLayout;
final Widget child; final Widget child;
const _CollectionScaler({ const _CollectionScaler({
required this.scrollableKey, required this.scrollableKey,
required this.appBarHeightNotifier, required this.appBarHeightNotifier,
required this.tileLayout,
required this.child, required this.child,
}); });
@ -201,10 +221,12 @@ class _CollectionScaler extends StatelessWidget {
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing); final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
return GridScaleGestureDetector<AvesEntry>( return GridScaleGestureDetector<AvesEntry>(
scrollableKey: scrollableKey, scrollableKey: scrollableKey,
tileLayout: tileLayout,
heightForWidth: (width) => width, heightForWidth: (width) => width,
gridBuilder: (center, tileSize, child) => CustomPaint( gridBuilder: (center, tileSize, child) => CustomPaint(
painter: GridPainter( painter: GridPainter(
center: center, tileLayout: tileLayout,
tileCenter: center,
tileSize: tileSize, tileSize: tileSize,
spacing: tileSpacing, spacing: tileSpacing,
borderWidth: DecoratedThumbnail.borderWidth, borderWidth: DecoratedThumbnail.borderWidth,
@ -213,11 +235,13 @@ class _CollectionScaler extends StatelessWidget {
), ),
child: child, child: child,
), ),
scaledBuilder: (entry, tileSize) => DecoratedThumbnail( scaledBuilder: (entry, tileSize) => EntryListDetailsTheme(
entry: entry, extent: tileSize.height,
tileExtent: context.read<TileExtentController>().effectiveExtentMax, child: Tile(
selectable: false, entry: entry,
highlightable: false, thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
tileLayout: tileLayout,
),
), ),
child: child, child: child,
); );

View file

@ -36,7 +36,6 @@ import 'package:aves/widgets/search/search_delegate.dart';
import 'package:aves/widgets/stats/stats_page.dart'; import 'package:aves/widgets/stats/stats_page.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
@ -51,10 +50,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
}) { }) {
switch (action) { switch (action) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
return true; return true;
case EntrySetAction.group:
return sortFactor == EntrySortFactor.date;
case EntrySetAction.select: case EntrySetAction.select:
return appMode.canSelect && !isSelecting; return appMode.canSelect && !isSelecting;
case EntrySetAction.selectAll: case EntrySetAction.selectAll:
@ -98,8 +95,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
final hasSelection = selectedItemCount > 0; final hasSelection = selectedItemCount > 0;
switch (action) { switch (action) {
case EntrySetAction.sort: case EntrySetAction.configureView:
case EntrySetAction.group:
return true; return true;
case EntrySetAction.select: case EntrySetAction.select:
return hasItems; return hasItems;
@ -133,8 +129,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
void onActionSelected(BuildContext context, EntrySetAction action) { void onActionSelected(BuildContext context, EntrySetAction action) {
switch (action) { switch (action) {
// general // general
case EntrySetAction.sort: case EntrySetAction.configureView:
case EntrySetAction.group:
case EntrySetAction.select: case EntrySetAction.select:
case EntrySetAction.selectAll: case EntrySetAction.selectAll:
case EntrySetAction.selectNone: case EntrySetAction.selectNone:
@ -227,7 +222,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)), content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)),
actions: [ actions: [
TextButton( TextButton(
@ -247,19 +241,23 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return; if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
source.pauseMonitoring(); source.pauseMonitoring();
final opId = mediaFileService.newOpId;
showOpReport<ImageOpEvent>( showOpReport<ImageOpEvent>(
context: context, context: context,
opStream: mediaFileService.delete(selectedItems), opStream: mediaFileService.delete(opId: opId, entries: selectedItems),
itemCount: todoCount, itemCount: todoCount,
onCancel: () => mediaFileService.cancelFileOp(opId),
onDone: (processed) async { onDone: (processed) async {
final deletedUris = processed.where((event) => event.success).map((event) => event.uri).toSet(); final successOps = processed.where((e) => e.success).toSet();
final deletedOps = successOps.where((e) => !e.skipped).toSet();
final deletedUris = deletedOps.map((event) => event.uri).toSet();
await source.removeEntries(deletedUris); await source.removeEntries(deletedUris);
selection.browse(); selection.browse();
source.resumeMonitoring(); source.resumeMonitoring();
final deletedCount = deletedUris.length; final successCount = successOps.length;
if (deletedCount < todoCount) { if (successCount < todoCount) {
final count = todoCount - deletedCount; final count = todoCount - successCount;
showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count)); showFeedback(context, context.l10n.collectionDeleteFailureFeedback(count));
} }
@ -271,19 +269,12 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
Future<void> _move(BuildContext context, {required MoveType moveType}) async { Future<void> _move(BuildContext context, {required MoveType moveType}) async {
final l10n = context.l10n; final l10n = context.l10n;
final source = context.read<CollectionSource>();
final selection = context.read<Selection<AvesEntry>>(); final selection = context.read<Selection<AvesEntry>>();
final selectedItems = _getExpandedSelectedItems(selection); final selectedItems = _getExpandedSelectedItems(selection);
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet(); final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
final destinationAlbum = await Navigator.push( final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
context, if (destinationAlbum == null) return;
MaterialPageRoute<String>(
settings: const RouteSettings(name: AlbumPickPage.routeName),
builder: (context) => AlbumPickPage(source: source, moveType: moveType),
),
);
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return; if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
@ -323,19 +314,23 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
nameConflictStrategy = value; nameConflictStrategy = value;
} }
final source = context.read<CollectionSource>();
source.pauseMonitoring(); source.pauseMonitoring();
final opId = mediaFileService.newOpId;
showOpReport<MoveOpEvent>( showOpReport<MoveOpEvent>(
context: context, context: context,
opStream: mediaFileService.move( opStream: mediaFileService.move(
todoItems, opId: opId,
entries: todoItems,
copy: copy, copy: copy,
destinationAlbum: destinationAlbum, destinationAlbum: destinationAlbum,
nameConflictStrategy: nameConflictStrategy, nameConflictStrategy: nameConflictStrategy,
), ),
itemCount: todoCount, itemCount: todoCount,
onCancel: () => mediaFileService.cancelFileOp(opId),
onDone: (processed) async { onDone: (processed) async {
final successOps = processed.where((e) => e.success).toSet(); final successOps = processed.where((e) => e.success).toSet();
final movedOps = successOps.where((e) => !e.newFields.containsKey('skipped')).toSet(); final movedOps = successOps.where((e) => !e.skipped).toSet();
await source.updateAfterMove( await source.updateAfterMove(
todoEntries: todoItems, todoEntries: todoItems,
copy: copy, copy: copy,
@ -414,18 +409,25 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
source.pauseMonitoring(); source.pauseMonitoring();
var cancelled = false;
showOpReport<ImageOpEvent>( showOpReport<ImageOpEvent>(
context: context, context: context,
opStream: Stream.fromIterable(todoItems).asyncMap((entry) async { opStream: Stream.fromIterable(todoItems).asyncMap((entry) async {
final dataTypes = await op(entry); if (cancelled) {
return ImageOpEvent(success: dataTypes.isNotEmpty, uri: entry.uri); return ImageOpEvent(success: true, skipped: true, uri: entry.uri);
} else {
final dataTypes = await op(entry);
return ImageOpEvent(success: dataTypes.isNotEmpty, skipped: false, uri: entry.uri);
}
}).asBroadcastStream(), }).asBroadcastStream(),
itemCount: todoCount, itemCount: todoCount,
onCancel: () => cancelled = true,
onDone: (processed) async { onDone: (processed) async {
final successOps = processed.where((e) => e.success).toSet(); final successOps = processed.where((e) => e.success).toSet();
final editedOps = successOps.where((e) => !e.skipped).toSet();
selection.browse(); selection.browse();
source.resumeMonitoring(); source.resumeMonitoring();
unawaited(source.refreshUris(successOps.map((v) => v.uri).toSet())); unawaited(source.refreshUris(editedOps.map((v) => v.uri).toSet()));
final l10n = context.l10n; final l10n = context.l10n;
final successCount = successOps.length; final successCount = successOps.length;
@ -433,7 +435,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
final count = todoCount - successCount; final count = todoCount - successCount;
showFeedback(context, l10n.collectionEditFailureFeedback(count)); showFeedback(context, l10n.collectionEditFailureFeedback(count));
} else { } else {
final count = successCount; final count = editedOps.length;
showFeedback(context, l10n.collectionEditSuccessFeedback(count)); showFeedback(context, l10n.collectionEditSuccessFeedback(count));
} }
}, },
@ -457,7 +459,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
builder: (context) { builder: (context) {
final l10n = context.l10n; final l10n = context.l10n;
return AvesDialog( return AvesDialog(
context: context,
title: l10n.unsupportedTypeDialogTitle, title: l10n.unsupportedTypeDialogTitle,
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))), content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
actions: [ actions: [

View file

@ -0,0 +1,95 @@
import 'package:aves/model/entry.dart';
import 'package:aves/model/settings/coordinate_format.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class EntryListDetails extends StatelessWidget {
final AvesEntry entry;
const EntryListDetails({
Key? key,
required this.entry,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final detailsTheme = context.watch<EntryListDetailsThemeData>();
return Container(
padding: EntryListDetailsTheme.contentPadding,
foregroundDecoration: BoxDecoration(
border: Border(top: AvesBorder.side),
),
margin: EntryListDetailsTheme.contentMargin,
child: IconTheme.merge(
data: detailsTheme.iconTheme,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.bestTitle ?? context.l10n.viewerInfoUnknown,
style: detailsTheme.titleStyle,
softWrap: false,
overflow: detailsTheme.titleMaxLines == 1 ? TextOverflow.fade : TextOverflow.ellipsis,
maxLines: detailsTheme.titleMaxLines,
),
const SizedBox(height: EntryListDetailsTheme.titleDetailPadding),
if (detailsTheme.showDate) _buildDateRow(context, detailsTheme.captionStyle),
if (detailsTheme.showLocation && entry.hasGps) _buildLocationRow(context, detailsTheme.captionStyle),
],
),
),
);
}
Widget _buildDateRow(BuildContext context, TextStyle style) {
final locale = context.l10n.localeName;
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
final date = entry.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : Constants.overlayUnknown;
return Row(
children: [
const Icon(AIcons.date),
const SizedBox(width: 8),
Expanded(
child: Text(
dateText,
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
],
);
}
Widget _buildLocationRow(BuildContext context, TextStyle style) {
final location = entry.hasAddress ? entry.shortAddress : settings.coordinateFormat.format(context.l10n, entry.latLng!);
return Row(
children: [
const Icon(AIcons.location),
const SizedBox(width: 8),
Expanded(
child: Text(
location,
style: style,
strutStyle: Constants.overflowStrutStyle,
softWrap: false,
overflow: TextOverflow.fade,
),
),
],
);
}
}

View file

@ -0,0 +1,99 @@
import 'package:aves/theme/format.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
class EntryListDetailsTheme extends StatelessWidget {
final double extent;
final Widget child;
static const EdgeInsets contentMargin = EdgeInsets.symmetric(horizontal: 8);
static const EdgeInsets contentPadding = EdgeInsets.symmetric(vertical: 4);
static const double titleDetailPadding = 6;
const EntryListDetailsTheme({
Key? key,
required this.extent,
required this.child,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return ProxyProvider<MediaQueryData, EntryListDetailsThemeData>(
update: (context, mq, previous) {
final locale = context.l10n.localeName;
final use24hour = mq.alwaysUse24HourFormat;
final textScaleFactor = mq.textScaleFactor;
final textTheme = Theme.of(context).textTheme;
final titleStyle = textTheme.bodyText2!;
final captionStyle = textTheme.caption!;
final titleLineHeight = (RenderParagraph(
TextSpan(text: 'Fake Title', style: titleStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
final captionLineHeight = (RenderParagraph(
TextSpan(text: formatDateTime(DateTime.now(), locale, use24hour), style: captionStyle),
textDirection: TextDirection.ltr,
textScaleFactor: textScaleFactor,
strutStyle: Constants.overflowStrutStyle,
)..layout(const BoxConstraints(), parentUsesSize: true))
.getMaxIntrinsicHeight(double.infinity);
var titleMaxLines = 1;
var showDate = false;
var showLocation = false;
var availableHeight = extent - contentMargin.vertical - contentPadding.vertical;
if (availableHeight >= titleLineHeight + titleDetailPadding + captionLineHeight) {
showDate = true;
availableHeight -= titleLineHeight + titleDetailPadding + captionLineHeight;
if (availableHeight >= captionLineHeight) {
showLocation = true;
availableHeight -= captionLineHeight;
titleMaxLines += availableHeight ~/ titleLineHeight;
}
}
return EntryListDetailsThemeData(
extent: extent,
titleMaxLines: titleMaxLines,
showDate: showDate,
showLocation: showLocation,
titleStyle: titleStyle,
captionStyle: captionStyle,
iconTheme: IconThemeData(
color: captionStyle.color,
size: captionStyle.fontSize! * textScaleFactor,
),
);
},
child: child,
);
}
}
class EntryListDetailsThemeData {
final double extent;
final int titleMaxLines;
final bool showDate, showLocation;
final TextStyle titleStyle, captionStyle;
final IconThemeData iconTheme;
const EntryListDetailsThemeData({
required this.extent,
required this.titleMaxLines,
required this.showDate,
required this.showLocation,
required this.titleStyle,
required this.captionStyle,
required this.iconTheme,
});
}

View file

@ -1,5 +1,6 @@
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/widgets/collection/grid/headers/any.dart'; import 'package:aves/widgets/collection/grid/headers/any.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
@ -12,6 +13,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
Key? key, Key? key,
required this.collection, required this.collection,
required double scrollableWidth, required double scrollableWidth,
required TileLayout tileLayout,
required int columnCount, required int columnCount,
required double spacing, required double spacing,
required double tileExtent, required double tileExtent,
@ -21,6 +23,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
}) : super( }) : super(
key: key, key: key,
scrollableWidth: scrollableWidth, scrollableWidth: scrollableWidth,
tileLayout: tileLayout,
columnCount: columnCount, columnCount: columnCount,
spacing: spacing, spacing: spacing,
tileWidth: tileExtent, tileWidth: tileExtent,

View file

@ -2,32 +2,36 @@ import 'package:aves/app_mode.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/services/viewer_service.dart'; import 'package:aves/services/viewer_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class InteractiveThumbnail extends StatelessWidget { class InteractiveTile extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
final AvesEntry entry; final AvesEntry entry;
final double tileExtent; final double thumbnailExtent;
final TileLayout tileLayout;
final ValueNotifier<bool>? isScrollingNotifier; final ValueNotifier<bool>? isScrollingNotifier;
const InteractiveThumbnail({ const InteractiveTile({
Key? key, Key? key,
required this.collection, required this.collection,
required this.entry, required this.entry,
required this.tileExtent, required this.thumbnailExtent,
required this.tileLayout,
this.isScrollingNotifier, this.isScrollingNotifier,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return InkWell(
key: ValueKey(entry.uri),
onTap: () { onTap: () {
final appMode = context.read<ValueNotifier<AppMode>>().value; final appMode = context.read<ValueNotifier<AppMode>>().value;
switch (appMode) { switch (appMode) {
@ -51,13 +55,13 @@ class InteractiveThumbnail extends StatelessWidget {
}, },
child: MetaData( child: MetaData(
metaData: ScalerMetadata(entry), metaData: ScalerMetadata(entry),
child: DecoratedThumbnail( child: Tile(
entry: entry, entry: entry,
tileExtent: tileExtent, thumbnailExtent: thumbnailExtent,
// when the user is scrolling faster than we can retrieve the thumbnails, tileLayout: tileLayout,
// the retrieval task queue can pile up for thumbnails that got disposed selectable: true,
// in this case we pause the image retrieval task to get it out of the queue highlightable: true,
cancellableNotifier: isScrollingNotifier, isScrollingNotifier: isScrollingNotifier,
// hero tag should include a collection identifier, so that it animates // hero tag should include a collection identifier, so that it animates
// between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // between different views of the entry in the same collection (e.g. thumbnails <-> viewer)
// but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer)
@ -86,3 +90,58 @@ class InteractiveThumbnail extends StatelessWidget {
); );
} }
} }
class Tile extends StatelessWidget {
final AvesEntry entry;
final double thumbnailExtent;
final TileLayout tileLayout;
final bool selectable, highlightable;
final ValueNotifier<bool>? isScrollingNotifier;
final Object? Function()? heroTagger;
const Tile({
Key? key,
required this.entry,
required this.thumbnailExtent,
required this.tileLayout,
this.selectable = false,
this.highlightable = false,
this.isScrollingNotifier,
this.heroTagger,
}) : super(key: key);
@override
Widget build(BuildContext context) {
switch (tileLayout) {
case TileLayout.grid:
return _buildThumbnail();
case TileLayout.list:
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox.square(
dimension: context.select<EntryListDetailsThemeData, double>((v) => v.extent),
child: _buildThumbnail(),
),
Expanded(
child: EntryListDetails(
entry: entry,
),
),
],
);
}
}
Widget _buildThumbnail() => DecoratedThumbnail(
entry: entry,
tileExtent: thumbnailExtent,
// when the user is scrolling faster than we can retrieve the thumbnails,
// the retrieval task queue can pile up for thumbnails that got disposed
// in this case we pause the image retrieval task to get it out of the queue
cancellableNotifier: isScrollingNotifier,
selectable: selectable,
highlightable: highlightable,
heroTagger: heroTagger,
);
}

View file

@ -55,7 +55,6 @@ mixin EntryEditorMixin {
context: context, context: context,
builder: (context) { builder: (context) {
return AvesDialog( return AvesDialog(
context: context,
content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage), content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage),
actions: [ actions: [
TextButton( TextButton(

View file

@ -6,6 +6,9 @@ import 'package:aves/model/settings/enums.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/accessibility_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:percent_indicator/circular_percent_indicator.dart'; import 'package:percent_indicator/circular_percent_indicator.dart';
@ -15,7 +18,17 @@ mixin FeedbackMixin {
void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar(); void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar();
void showFeedback(BuildContext context, String message, [SnackBarAction? action]) { void showFeedback(BuildContext context, String message, [SnackBarAction? action]) {
showFeedbackWithMessenger(context, ScaffoldMessenger.of(context), message, action); ScaffoldMessengerState? scaffoldMessenger;
try {
scaffoldMessenger = ScaffoldMessenger.of(context);
} catch (e) {
// 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');
}
if (scaffoldMessenger != null) {
showFeedbackWithMessenger(context, scaffoldMessenger, message, action);
}
} }
// provide the messenger if feedback happens as the widget is disposed // provide the messenger if feedback happens as the widget is disposed
@ -60,32 +73,36 @@ mixin FeedbackMixin {
required BuildContext context, required BuildContext context,
required Stream<T> opStream, required Stream<T> opStream,
required int itemCount, required int itemCount,
VoidCallback? onCancel,
void Function(Set<T> processed)? onDone, void Function(Set<T> processed)? onDone,
}) { }) {
late OverlayEntry _opReportOverlayEntry; showDialog(
_opReportOverlayEntry = OverlayEntry( context: context,
barrierDismissible: false,
builder: (context) => ReportOverlay<T>( builder: (context) => ReportOverlay<T>(
opStream: opStream, opStream: opStream,
itemCount: itemCount, itemCount: itemCount,
onCancel: onCancel,
onDone: (processed) { onDone: (processed) {
_opReportOverlayEntry.remove(); Navigator.of(context).pop();
onDone?.call(processed); onDone?.call(processed);
}, },
), ),
); );
Overlay.of(context)!.insert(_opReportOverlayEntry);
} }
} }
class ReportOverlay<T> extends StatefulWidget { class ReportOverlay<T> extends StatefulWidget {
final Stream<T> opStream; final Stream<T> opStream;
final int itemCount; final int itemCount;
final VoidCallback? onCancel;
final void Function(Set<T> processed) onDone; final void Function(Set<T> processed) onDone;
const ReportOverlay({ const ReportOverlay({
Key? key, Key? key,
required this.opStream, required this.opStream,
required this.itemCount, required this.itemCount,
required this.onCancel,
required this.onDone, required this.onDone,
}) : super(key: key); }) : super(key: key);
@ -100,8 +117,9 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
Stream<T> get opStream => widget.opStream; Stream<T> get opStream => widget.opStream;
static const fontSize = 18.0;
static const radius = 160.0; static const radius = 160.0;
static const strokeWidth = 16.0; static const strokeWidth = 8.0;
@override @override
void initState() { void initState() {
@ -136,52 +154,70 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final progressColor = Theme.of(context).colorScheme.secondary; final progressColor = Theme.of(context).colorScheme.secondary;
return AbsorbPointer( final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
return WillPopScope(
onWillPop: () => SynchronousFuture(false),
child: StreamBuilder<T>( child: StreamBuilder<T>(
stream: opStream, stream: opStream,
builder: (context, snapshot) { builder: (context, snapshot) {
final processedCount = processed.length.toDouble(); final processedCount = processed.length.toDouble();
final total = widget.itemCount; final total = widget.itemCount;
assert(processedCount <= total);
final percent = min(1.0, processedCount / total); final percent = min(1.0, processedCount / total);
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
return FadeTransition( return FadeTransition(
opacity: _animation, opacity: _animation,
child: Container( child: Stack(
decoration: const BoxDecoration( alignment: Alignment.center,
gradient: RadialGradient( children: [
colors: [ Container(
Colors.black, width: radius + 2,
Colors.black54, height: radius + 2,
], decoration: const BoxDecoration(
color: Color(0xBB000000),
shape: BoxShape.circle,
),
), ),
), if (animate)
child: Center( Container(
child: Stack( width: radius,
children: [ height: radius,
if (animate) padding: const EdgeInsets.all(strokeWidth / 2),
Container( child: CircularProgressIndicator(
width: radius, color: progressColor.withOpacity(.1),
height: radius, strokeWidth: strokeWidth,
padding: const EdgeInsets.all(strokeWidth / 2), ),
child: CircularProgressIndicator( ),
color: progressColor.withOpacity(.1), CircularPercentIndicator(
strokeWidth: strokeWidth, percent: percent,
lineWidth: strokeWidth,
radius: radius,
backgroundColor: Colors.white24,
progressColor: progressColor,
animation: animate,
center: Text(
NumberFormat.percentPattern().format(percent),
style: const TextStyle(fontSize: fontSize),
),
animateFromLastPercent: true,
),
if (widget.onCancel != null)
Material(
color: Colors.transparent,
child: Container(
width: radius,
height: radius,
margin: const EdgeInsets.only(top: fontSize),
alignment: const FractionalOffset(0.5, 0.75),
child: Tooltip(
message: context.l10n.cancelTooltip,
preferBelow: false,
child: IconButton(
icon: const Icon(AIcons.cancel),
onPressed: widget.onCancel,
), ),
), ),
CircularPercentIndicator(
percent: percent,
lineWidth: strokeWidth,
radius: radius,
backgroundColor: Colors.white24,
progressColor: progressColor,
animation: animate,
center: Text(NumberFormat.percentPattern().format(percent)),
animateFromLastPercent: true,
), ),
], ),
), ],
),
), ),
); );
}, },

View file

@ -50,7 +50,6 @@ mixin PermissionAwareMixin {
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir); final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
final volume = dir.getVolumeDescription(context); final volume = dir.getVolumeDescription(context);
return AvesDialog( return AvesDialog(
context: context,
title: context.l10n.storageAccessDialogTitle, title: context.l10n.storageAccessDialogTitle,
content: Text(context.l10n.storageAccessDialogMessage(directory, volume)), content: Text(context.l10n.storageAccessDialogMessage(directory, volume)),
actions: [ actions: [
@ -84,7 +83,6 @@ mixin PermissionAwareMixin {
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir); final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
final volume = dir.getVolumeDescription(context); final volume = dir.getVolumeDescription(context);
return AvesDialog( return AvesDialog(
context: context,
title: context.l10n.restrictedAccessDialogTitle, title: context.l10n.restrictedAccessDialogTitle,
content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)), content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)),
actions: [ actions: [

View file

@ -11,7 +11,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
mixin SizeAwareMixin { mixin SizeAwareMixin {
Future<bool> checkFreeSpaceForMove( Future<bool> checkFreeSpaceForMove(
@ -81,7 +80,6 @@ mixin SizeAwareMixin {
final freeSize = formatFileSize(locale, free); final freeSize = formatFileSize(locale, free);
final volume = destinationVolume.getDescription(context); final volume = destinationVolume.getDescription(context);
return AvesDialog( return AvesDialog(
context: context,
title: l10n.notEnoughSpaceDialogTitle, title: l10n.notEnoughSpaceDialogTitle,
content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)),
actions: [ actions: [

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:highlight/highlight.dart' show highlight, Node; import 'package:highlight/highlight.dart' show highlight, Node;
// adapted from package `flutter_highlight` v0.7.0 `HighlightView` // adapted from package `flutter_highlight` v0.7.0 `HighlightView`

View file

@ -71,7 +71,6 @@ class _ColorPickerDialogState extends State<ColorPickerDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesDialog( return AvesDialog(
context: context,
scrollableContent: [ scrollableContent: [
ColorPicker( ColorPicker(
color: color, color: color,

View file

@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/* /*

View file

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';

View file

@ -2,9 +2,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class QueryBar extends StatefulWidget { class QueryBar extends StatefulWidget {
final ValueNotifier<String> queryNotifier; final ValueNotifier<String> queryNotifier;

View file

@ -132,18 +132,20 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
Matrix4? _lastTransform; Matrix4? _lastTransform;
late Offset _initialFocalPoint; late Offset _initialFocalPoint;
late Offset _currentFocalPoint; Offset? _currentFocalPoint;
late double _initialSpan; late double _initialSpan;
late double _currentSpan; late double _currentSpan;
late double _initialHorizontalSpan; late double _initialHorizontalSpan;
late double _currentHorizontalSpan; late double _currentHorizontalSpan;
late double _initialVerticalSpan; late double _initialVerticalSpan;
late double _currentVerticalSpan; late double _currentVerticalSpan;
late Offset _localFocalPoint;
_LineBetweenPointers? _initialLine; _LineBetweenPointers? _initialLine;
_LineBetweenPointers? _currentLine; _LineBetweenPointers? _currentLine;
late Map<int, Offset> _pointerLocations; late Map<int, Offset> _pointerLocations;
late List<int> _pointerQueue; // A queue to sort pointers in order of entrance late List<int> _pointerQueue; // A queue to sort pointers in order of entrance
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{}; final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
late Offset _delta;
double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0; double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
@ -222,11 +224,28 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
void _update() { void _update() {
final int count = _pointerLocations.keys.length; final int count = _pointerLocations.keys.length;
final Offset? previousFocalPoint = _currentFocalPoint;
// Compute the focal point // Compute the focal point
Offset focalPoint = Offset.zero; Offset focalPoint = Offset.zero;
for (final int pointer in _pointerLocations.keys) focalPoint += _pointerLocations[pointer]!; for (final int pointer in _pointerLocations.keys) focalPoint += _pointerLocations[pointer]!;
_currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero; _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
if (previousFocalPoint == null) {
_localFocalPoint = PointerEvent.transformPosition(
_lastTransform,
_currentFocalPoint!,
);
_delta = Offset.zero;
} else {
final Offset localPreviousFocalPoint = _localFocalPoint;
_localFocalPoint = PointerEvent.transformPosition(
_lastTransform,
_currentFocalPoint!,
);
_delta = _localFocalPoint - localPreviousFocalPoint;
}
// Span is the average deviation from focal point. Horizontal and vertical // Span is the average deviation from focal point. Horizontal and vertical
// spans are the average deviations from the focal point's horizontal and // spans are the average deviations from the focal point's horizontal and
// vertical coordinates, respectively. // vertical coordinates, respectively.
@ -234,9 +253,9 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
double totalHorizontalDeviation = 0.0; double totalHorizontalDeviation = 0.0;
double totalVerticalDeviation = 0.0; double totalVerticalDeviation = 0.0;
for (final int pointer in _pointerLocations.keys) { for (final int pointer in _pointerLocations.keys) {
totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]!).distance; totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance;
totalHorizontalDeviation += (_currentFocalPoint.dx - _pointerLocations[pointer]!.dx).abs(); totalHorizontalDeviation += (_currentFocalPoint!.dx - _pointerLocations[pointer]!.dx).abs();
totalVerticalDeviation += (_currentFocalPoint.dy - _pointerLocations[pointer]!.dy).abs(); totalVerticalDeviation += (_currentFocalPoint!.dy - _pointerLocations[pointer]!.dy).abs();
} }
_currentSpan = count > 0 ? totalDeviation / count : 0.0; _currentSpan = count > 0 ? totalDeviation / count : 0.0;
_currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0; _currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
@ -273,7 +292,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
} }
bool _reconfigure(int pointer) { bool _reconfigure(int pointer) {
_initialFocalPoint = _currentFocalPoint; _initialFocalPoint = _currentFocalPoint!;
_initialSpan = _currentSpan; _initialSpan = _currentSpan;
_initialLine = _currentLine; _initialLine = _currentLine;
_initialHorizontalSpan = _currentHorizontalSpan; _initialHorizontalSpan = _currentHorizontalSpan;
@ -288,7 +307,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity); if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length))); invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length)));
} else { } else {
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: Velocity.zero, pointerCount: _pointerQueue.length))); invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerQueue.length)));
} }
} }
_state = _ScaleState.accepted; _state = _ScaleState.accepted;
@ -308,8 +327,8 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (_state == _ScaleState.possible) { if (_state == _ScaleState.possible) {
final double spanDelta = (_currentSpan - _initialSpan).abs(); final double spanDelta = (_currentSpan - _initialSpan).abs();
final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance; final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind)) resolve(GestureDisposition.accepted); if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings)) resolve(GestureDisposition.accepted);
} else if (_state.index >= _ScaleState.accepted.index) { } else if (_state.index >= _ScaleState.accepted.index) {
resolve(GestureDisposition.accepted); resolve(GestureDisposition.accepted);
} }
@ -325,11 +344,11 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
scale: _scaleFactor, scale: _scaleFactor,
horizontalScale: _horizontalScaleFactor, horizontalScale: _horizontalScaleFactor,
verticalScale: _verticalScaleFactor, verticalScale: _verticalScaleFactor,
focalPoint: _currentFocalPoint, focalPoint: _currentFocalPoint!,
localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint), localFocalPoint: _localFocalPoint,
rotation: _computeRotationFactor(), rotation: _computeRotationFactor(),
pointerCount: _pointerQueue.length, pointerCount: _pointerQueue.length,
delta: _currentFocalPoint - _initialFocalPoint, focalPointDelta: _delta,
)); ));
}); });
} }
@ -339,8 +358,8 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
if (onStart != null) if (onStart != null)
invokeCallback<void>('onStart', () { invokeCallback<void>('onStart', () {
onStart!(ScaleStartDetails( onStart!(ScaleStartDetails(
focalPoint: _currentFocalPoint, focalPoint: _currentFocalPoint!,
localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint), localFocalPoint: _localFocalPoint,
pointerCount: _pointerQueue.length, pointerCount: _pointerQueue.length,
)); ));
}); });
@ -352,7 +371,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
_state = _ScaleState.started; _state = _ScaleState.started;
_dispatchOnStartCallbackIfNeeded(); _dispatchOnStartCallbackIfNeeded();
if (dragStartBehavior == DragStartBehavior.start) { if (dragStartBehavior == DragStartBehavior.start) {
_initialFocalPoint = _currentFocalPoint; _initialFocalPoint = _currentFocalPoint!;
_initialSpan = _currentSpan; _initialSpan = _currentSpan;
_initialLine = _currentLine; _initialLine = _currentLine;
_initialHorizontalSpan = _currentHorizontalSpan; _initialHorizontalSpan = _currentHorizontalSpan;

View file

@ -1,5 +1,5 @@
import 'package:aves/l10n/l10n.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
extension ExtraContext on BuildContext { extension ExtraContext on BuildContext {
String? get currentRouteName => ModalRoute.of(this)?.settings.name; String? get currentRouteName => ModalRoute.of(this)?.settings.name;

View file

@ -2,22 +2,45 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBindingObserver { class GridItemTracker<T> extends StatefulWidget {
ValueNotifier<double> get appBarHeightNotifier; final GlobalKey scrollableKey;
final ValueNotifier<double> appBarHeightNotifier;
final TileLayout tileLayout;
final ScrollController scrollController;
final Widget child;
ScrollController get scrollController; const GridItemTracker({
Key? key,
required this.scrollableKey,
required this.appBarHeightNotifier,
required this.tileLayout,
required this.scrollController,
required this.child,
}) : super(key: key);
GlobalKey get scrollableKey; @override
_GridItemTrackerState createState() => _GridItemTrackerState<T>();
}
class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBindingObserver {
ValueNotifier<double> get appBarHeightNotifier => widget.appBarHeightNotifier;
ScrollController get scrollController => widget.scrollController;
@override
Widget build(BuildContext context) {
return widget.child;
}
Size get scrollableSize { Size get scrollableSize {
final scrollableContext = scrollableKey.currentContext!; final scrollableContext = widget.scrollableKey.currentContext!;
return (scrollableContext.findRenderObject() as RenderBox).size; return (scrollableContext.findRenderObject() as RenderBox).size;
} }
@ -44,11 +67,27 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
} }
@override @override
void didUpdateWidget(covariant oldWidget) { void didUpdateWidget(covariant GridItemTracker<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.tileLayout != widget.tileLayout) {
_onLayoutChange();
}
_saveLayoutMetrics(); _saveLayoutMetrics();
} }
@override
void didChangeMetrics() {
// the order of `WidgetsBindingObserver` metrics change notification is unreliable
// w.r.t. the `MediaQuery` update, and consequentially to this widget update:
// `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update
final orientation = _windowOrientation;
if (_lastOrientation != orientation) {
_lastOrientation = orientation;
_onLayoutChange();
_saveLayoutMetrics();
}
}
@override @override
void dispose() { void dispose() {
WidgetsBinding.instance!.removeObserver(this); WidgetsBinding.instance!.removeObserver(this);
@ -96,18 +135,9 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
} }
} }
@override
void didChangeMetrics() {
final orientation = _windowOrientation;
if (_lastOrientation != orientation) {
_lastOrientation = orientation;
_onWindowOrientationChange();
}
}
Future<void> _saveLayoutMetrics() async { Future<void> _saveLayoutMetrics() async {
// use a delay to obtain current layout metrics // use a delay to obtain current layout metrics
// so that we can handle window orientation change beforehand with the previous metrics, // so that we can handle window orientation change with the previous metrics,
// regardless of the `MediaQuery`/`WidgetsBindingObserver` order uncertainty // regardless of the `MediaQuery`/`WidgetsBindingObserver` order uncertainty
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
@ -117,10 +147,10 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
} }
} }
// the order of `WidgetsBindingObserver` metrics change notification is unreliable void _onLayoutChange() {
// w.r.t. the `MediaQuery` update, and consequentially to this widget update // do not track when view shows top edge
// `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update if (scrollController.offset == 0) return;
void _onWindowOrientationChange() {
final layout = _lastSectionedListLayout; final layout = _lastSectionedListLayout;
final halfSize = _lastScrollableSize / 2; final halfSize = _lastScrollableSize / 2;
final center = Offset( final center = Offset(

View file

@ -1,6 +1,7 @@
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart'; import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
@ -21,6 +22,7 @@ class ScalerMetadata<T> {
class GridScaleGestureDetector<T> extends StatefulWidget { class GridScaleGestureDetector<T> extends StatefulWidget {
final GlobalKey scrollableKey; final GlobalKey scrollableKey;
final TileLayout tileLayout;
final double Function(double width) heightForWidth; final double Function(double width) heightForWidth;
final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder; final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
final Widget Function(T item, Size tileSize) scaledBuilder; final Widget Function(T item, Size tileSize) scaledBuilder;
@ -30,6 +32,7 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
const GridScaleGestureDetector({ const GridScaleGestureDetector({
Key? key, Key? key,
required this.scrollableKey, required this.scrollableKey,
required this.tileLayout,
required this.heightForWidth, required this.heightForWidth,
required this.gridBuilder, required this.gridBuilder,
required this.scaledBuilder, required this.scaledBuilder,
@ -111,17 +114,29 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
_extentMax = tileExtentController.effectiveExtentMax; _extentMax = tileExtentController.effectiveExtentMax;
final halfSize = _startSize! / 2; final halfSize = _startSize! / 2;
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height)); final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (context) => ScaleOverlay( builder: (context) => _ScaleOverlay(
builder: (scaledTileSize) => SizedBox.fromSize( builder: (scaledTileSize) {
size: scaledTileSize, late final double themeExtent;
child: GridTheme( switch (widget.tileLayout) {
extent: scaledTileSize.width, case TileLayout.grid:
child: widget.scaledBuilder(_metadata!.item, scaledTileSize), themeExtent = scaledTileSize.width;
), break;
), case TileLayout.list:
center: thumbnailCenter, themeExtent = scaledTileSize.height;
break;
}
return SizedBox.fromSize(
size: scaledTileSize,
child: GridTheme(
extent: themeExtent,
child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
),
);
},
tileLayout: widget.tileLayout,
center: tileCenter,
viewportWidth: gridWidth, viewportWidth: gridWidth,
gridBuilder: widget.gridBuilder, gridBuilder: widget.gridBuilder,
scaledSizeNotifier: _scaledSizeNotifier!, scaledSizeNotifier: _scaledSizeNotifier!,
@ -133,8 +148,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
void _onScaleUpdate(ScaleUpdateDetails details) { void _onScaleUpdate(ScaleUpdateDetails details) {
if (_scaledSizeNotifier == null) return; if (_scaledSizeNotifier == null) return;
final s = details.scale; final s = details.scale;
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!); switch (widget.tileLayout) {
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth)); case TileLayout.grid:
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
break;
case TileLayout.list:
final scaledHeight = (_startSize!.height * s).clamp(_extentMin!, _extentMax!);
_scaledSizeNotifier!.value = Size(_startSize!.width, scaledHeight);
break;
}
} }
void _onScaleEnd(ScaleEndDetails details) { void _onScaleEnd(ScaleEndDetails details) {
@ -148,7 +171,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
final tileExtentController = context.read<TileExtentController>(); final tileExtentController = context.read<TileExtentController>();
final oldExtent = tileExtentController.extentNotifier.value; final oldExtent = tileExtentController.extentNotifier.value;
// sanitize and update grid layout if necessary // sanitize and update grid layout if necessary
final newExtent = tileExtentController.setUserPreferredExtent(_scaledSizeNotifier!.value.width); late final double preferredExtent;
switch (widget.tileLayout) {
case TileLayout.grid:
preferredExtent = _scaledSizeNotifier!.value.width;
break;
case TileLayout.list:
preferredExtent = _scaledSizeNotifier!.value.height;
break;
}
final newExtent = tileExtentController.setUserPreferredExtent(preferredExtent);
_scaledSizeNotifier = null; _scaledSizeNotifier = null;
if (newExtent == oldExtent) { if (newExtent == oldExtent) {
_applyingScale = false; _applyingScale = false;
@ -183,16 +215,18 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
} }
} }
class ScaleOverlay extends StatefulWidget { class _ScaleOverlay extends StatefulWidget {
final Widget Function(Size scaledTileSize) builder; final Widget Function(Size scaledTileSize) builder;
final TileLayout tileLayout;
final Offset center; final Offset center;
final double viewportWidth; final double viewportWidth;
final ValueNotifier<Size> scaledSizeNotifier; final ValueNotifier<Size> scaledSizeNotifier;
final Widget Function(Offset center, Size extent, Widget child) gridBuilder; final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
const ScaleOverlay({ const _ScaleOverlay({
Key? key, Key? key,
required this.builder, required this.builder,
required this.tileLayout,
required this.center, required this.center,
required this.viewportWidth, required this.viewportWidth,
required this.scaledSizeNotifier, required this.scaledSizeNotifier,
@ -203,7 +237,7 @@ class ScaleOverlay extends StatefulWidget {
_ScaleOverlayState createState() => _ScaleOverlayState(); _ScaleOverlayState createState() => _ScaleOverlayState();
} }
class _ScaleOverlayState extends State<ScaleOverlay> { class _ScaleOverlayState extends State<_ScaleOverlay> {
bool _init = false; bool _init = false;
Offset get center => widget.center; Offset get center => widget.center;
@ -222,26 +256,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
child: Builder( child: Builder(
builder: (context) => IgnorePointer( builder: (context) => IgnorePointer(
child: AnimatedContainer( child: AnimatedContainer(
decoration: _init decoration: _buildBackgroundDecoration(context),
? BoxDecoration(
gradient: RadialGradient(
center: FractionalOffset.fromOffsetAndSize(center, context.select<MediaQueryData, Size>((mq) => mq.size)),
radius: 1,
colors: const [
Colors.black,
Colors.black54,
],
),
)
: const BoxDecoration(
// provide dummy gradient to lerp to the other one during animation
gradient: RadialGradient(
colors: [
Colors.transparent,
Colors.transparent,
],
),
),
duration: Durations.collectionScalingBackgroundAnimation, duration: Durations.collectionScalingBackgroundAnimation,
child: ValueListenableBuilder<Size>( child: ValueListenableBuilder<Size>(
valueListenable: widget.scaledSizeNotifier, valueListenable: widget.scaledSizeNotifier,
@ -281,17 +296,53 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
), ),
); );
} }
BoxDecoration _buildBackgroundDecoration(BuildContext context) {
late final Offset gradientCenter;
switch (widget.tileLayout) {
case TileLayout.grid:
gradientCenter = center;
break;
case TileLayout.list:
gradientCenter = Offset(0, center.dy);
break;
}
return _init
? BoxDecoration(
gradient: RadialGradient(
center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select<MediaQueryData, Size>((mq) => mq.size)),
radius: 1,
colors: const [
Colors.black,
Colors.black54,
// Colors.amber,
],
),
)
: const BoxDecoration(
// provide dummy gradient to lerp to the other one during animation
gradient: RadialGradient(
colors: [
Colors.transparent,
Colors.transparent,
],
),
);
}
} }
class GridPainter extends CustomPainter { class GridPainter extends CustomPainter {
final Offset center; final TileLayout tileLayout;
final Offset tileCenter;
final Size tileSize; final Size tileSize;
final double spacing, borderWidth; final double spacing, borderWidth;
final Radius borderRadius; final Radius borderRadius;
final Color color; final Color color;
const GridPainter({ const GridPainter({
required this.center, required this.tileLayout,
required this.tileCenter,
required this.tileSize, required this.tileSize,
required this.spacing, required this.spacing,
required this.borderWidth, required this.borderWidth,
@ -301,40 +352,73 @@ class GridPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final tileWidth = tileSize.width; late final Offset chipCenter;
final tileHeight = tileSize.height; late final Size chipSize;
late final int deltaColumn;
late final Shader strokeShader;
switch (tileLayout) {
case TileLayout.grid:
chipCenter = tileCenter;
chipSize = tileSize;
deltaColumn = 2;
strokeShader = ui.Gradient.radial(
tileCenter,
chipSize.shortestSide * 2,
[
color,
Colors.transparent,
],
[
.8,
1,
],
);
break;
case TileLayout.list:
chipSize = Size.square(tileSize.shortestSide);
chipCenter = Offset(chipSize.width / 2, tileCenter.dy);
deltaColumn = 0;
strokeShader = ui.Gradient.linear(
tileCenter - Offset(0, chipSize.shortestSide * 3),
tileCenter + Offset(0, chipSize.shortestSide * 3),
[
Colors.transparent,
color,
color,
Colors.transparent,
],
[
0,
.2,
.8,
1,
],
);
break;
}
final strokePaint = Paint() final strokePaint = Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = borderWidth ..strokeWidth = borderWidth
..shader = ui.Gradient.radial( ..shader = strokeShader;
center,
tileWidth * 2,
[
color,
Colors.transparent,
],
[
.8,
1,
],
);
final fillPaint = Paint() final fillPaint = Paint()
..style = PaintingStyle.fill ..style = PaintingStyle.fill
..color = color.withOpacity(.25); ..color = color.withOpacity(.25);
final deltaX = tileWidth + spacing; final chipWidth = chipSize.width;
final deltaY = tileHeight + spacing; final chipHeight = chipSize.height;
for (var i = -2; i <= 2; i++) {
final deltaX = tileSize.width + spacing;
final deltaY = tileSize.height + spacing;
for (var i = -deltaColumn; i <= deltaColumn; i++) {
final dx = deltaX * i; final dx = deltaX * i;
for (var j = -2; j <= 2; j++) { for (var j = -2; j <= 2; j++) {
if (i == 0 && j == 0) continue; if (i == 0 && j == 0) continue;
final dy = deltaY * j; final dy = deltaY * j;
final rect = RRect.fromRectAndRadius( final rect = RRect.fromRectAndRadius(
Rect.fromCenter( Rect.fromCenter(
center: center + Offset(dx, dy), center: chipCenter + Offset(dx, dy),
width: tileWidth, width: chipWidth - borderWidth,
height: tileHeight, height: chipHeight - borderWidth,
), ),
borderRadius, borderRadius,
); );

View file

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import 'package:aves/model/source/enums.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -12,6 +13,7 @@ import 'package:provider/provider.dart';
abstract class SectionedListLayoutProvider<T> extends StatelessWidget { abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
final double scrollableWidth; final double scrollableWidth;
final TileLayout tileLayout;
final int columnCount; final int columnCount;
final double spacing, tileWidth, tileHeight; final double spacing, tileWidth, tileHeight;
final Widget Function(T item) tileBuilder; final Widget Function(T item) tileBuilder;
@ -21,14 +23,17 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
const SectionedListLayoutProvider({ const SectionedListLayoutProvider({
Key? key, Key? key,
required this.scrollableWidth, required this.scrollableWidth,
required this.columnCount, required this.tileLayout,
required int columnCount,
required this.spacing, required this.spacing,
required this.tileWidth, required double tileWidth,
required this.tileHeight, required this.tileHeight,
required this.tileBuilder, required this.tileBuilder,
required this.tileAnimationDelay, required this.tileAnimationDelay,
required this.child, required this.child,
}) : assert(scrollableWidth != 0), }) : assert(scrollableWidth != 0),
columnCount = tileLayout == TileLayout.list ? 1 : columnCount,
tileWidth = tileLayout == TileLayout.list ? scrollableWidth : tileWidth,
super(key: key); super(key: key);
@override @override

View file

@ -6,7 +6,6 @@ import 'package:aves/utils/math_utils.dart';
import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:aves/widgets/common/grid/section_layout.dart'; import 'package:aves/widgets/common/grid/section_layout.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class GridSelectionGestureDetector<T> extends StatefulWidget { class GridSelectionGestureDetector<T> extends StatefulWidget {

View file

@ -97,13 +97,9 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
double? leadingScrollOffset, double? leadingScrollOffset,
double? trailingScrollOffset, double? trailingScrollOffset,
}) { }) {
return childManager.estimateMaxScrollOffset( // default implementation is an estimation via `childManager.estimateMaxScrollOffset()`
constraints, // but we have the accurate offset via pre-computed section layouts
firstIndex: firstIndex, return _sectionLayouts.last.maxOffset;
lastIndex: lastIndex,
leadingScrollOffset: leadingScrollOffset,
trailingScrollOffset: trailingScrollOffset,
);
} }
double computeMaxScrollOffset(SliverConstraints constraints) { double computeMaxScrollOffset(SliverConstraints constraints) {

View file

@ -37,7 +37,7 @@ class AvesFilterDecoration {
class AvesFilterChip extends StatefulWidget { class AvesFilterChip extends StatefulWidget {
final CollectionFilter filter; final CollectionFilter filter;
final bool removable, showGenericIcon, useFilterColor; final bool removable, showText, showGenericIcon, useFilterColor;
final AvesFilterDecoration? decoration; final AvesFilterDecoration? decoration;
final String? banner; final String? banner;
final Widget? leadingOverride, details; final Widget? leadingOverride, details;
@ -60,6 +60,7 @@ class AvesFilterChip extends StatefulWidget {
Key? key, Key? key,
required this.filter, required this.filter,
this.removable = false, this.removable = false,
this.showText = true,
this.showGenericIcon = true, this.showGenericIcon = true,
this.useFilterColor = true, this.useFilterColor = true,
this.decoration, this.decoration,
@ -160,66 +161,70 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final chipBackground = Theme.of(context).scaffoldBackgroundColor;
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final iconSize = AvesFilterChip.iconSize * textScaleFactor;
final leading = widget.leadingOverride ?? filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon);
final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null;
final decoration = widget.decoration; final decoration = widget.decoration;
Widget content = Row( final chipBackground = Theme.of(context).scaffoldBackgroundColor;
mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (leading != null) ...[
leading,
SizedBox(width: padding),
],
Flexible(
child: Text(
filter.getLabel(context),
style: const TextStyle(
fontSize: AvesFilterChip.fontSize,
),
softWrap: false,
overflow: TextOverflow.fade,
),
),
if (trailing != null) ...[
SizedBox(width: padding),
trailing,
],
],
);
final details = widget.details; Widget? content;
if (details != null) { if (widget.showText) {
content = Column( final textScaleFactor = MediaQuery.textScaleFactorOf(context);
mainAxisSize: MainAxisSize.min, final iconSize = AvesFilterChip.iconSize * textScaleFactor;
final leading = widget.leadingOverride ?? filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon);
final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null;
content = Row(
mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
content, if (leading != null) ...[
Flexible(child: details), leading,
SizedBox(width: padding),
],
Flexible(
child: Text(
filter.getLabel(context),
style: const TextStyle(
fontSize: AvesFilterChip.fontSize,
),
softWrap: false,
overflow: TextOverflow.fade,
),
),
if (trailing != null) ...[
SizedBox(width: padding),
trailing,
],
], ],
); );
}
if (decoration != null) { final details = widget.details;
content = Align( if (details != null) {
alignment: Alignment.bottomCenter, content = Column(
child: ClipRRect( mainAxisSize: MainAxisSize.min,
borderRadius: decoration.textBorderRadius, children: [
child: Container( content,
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: AvesFilterChip.decoratedContentVerticalPadding), Flexible(child: details),
color: chipBackground, ],
child: content, );
}
if (decoration != null) {
content = Align(
alignment: Alignment.bottomCenter,
child: ClipRRect(
borderRadius: decoration.textBorderRadius,
child: Container(
padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: AvesFilterChip.decoratedContentVerticalPadding),
color: chipBackground,
child: content,
),
), ),
), );
); } else {
} else { content = Padding(
content = Padding( padding: EdgeInsets.symmetric(horizontal: padding * 2),
padding: EdgeInsets.symmetric(horizontal: padding * 2), child: content,
child: content, );
); }
} }
final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)); final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
@ -244,7 +249,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
child: InkWell( child: InkWell(
// as of Flutter v1.22.5, `InkWell` does not have `onLongPressStart` like `GestureDetector`, // as of Flutter v2.8.0, `InkWell` does not have `onLongPressStart` like `GestureDetector`,
// so we get the long press details from the tap instead // so we get the long press details from the tap instead
onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null, onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null,
onTap: onTap != null onTap: onTap != null

View file

@ -1,12 +1,8 @@
import 'dart:ui';
import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -203,25 +199,9 @@ class IconUtils {
required BuildContext context, required BuildContext context,
required String albumPath, required String albumPath,
double? size, double? size,
bool embossed = false,
}) { }) {
size ??= IconTheme.of(context).size; size ??= IconTheme.of(context).size;
Widget buildIcon(IconData icon) => embossed Widget buildIcon(IconData icon) => Icon(icon, size: size);
? MediaQuery(
// `DecoratedIcon` internally uses `Text`,
// which size depends on the ambient `textScaleFactor`
// but we already accommodate for it upstream
data: context.read<MediaQueryData>().copyWith(textScaleFactor: 1.0),
child: DecoratedIcon(
icon,
shadows: Constants.embossShadows,
size: size,
),
)
: Icon(
icon,
size: size,
);
switch (androidFileUtils.getAlbumType(albumPath)) { switch (androidFileUtils.getAlbumType(albumPath)) {
case AlbumType.camera: case AlbumType.camera:
return buildIcon(AIcons.cameraAlbum); return buildIcon(AIcons.cameraAlbum);

View file

@ -1,4 +1,6 @@
import 'package:aves/widgets/common/extensions/media_query.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class EmptyContent extends StatelessWidget { class EmptyContent extends StatelessWidget {
final IconData? icon; final IconData? icon;
@ -17,28 +19,33 @@ class EmptyContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const color = Colors.blueGrey; const color = Colors.blueGrey;
return Align( return Padding(
alignment: alignment, padding: EdgeInsets.only(
child: Column( bottom: context.select<MediaQueryData, double>((mq) => mq.effectiveBottomPadding),
mainAxisSize: MainAxisSize.min, ),
children: [ child: Align(
if (icon != null) ...[ alignment: alignment,
Icon( child: Column(
icon, mainAxisSize: MainAxisSize.min,
size: 64, children: [
color: color, if (icon != null) ...[
Icon(
icon,
size: 64,
color: color,
),
const SizedBox(height: 16)
],
Text(
text,
style: TextStyle(
color: color,
fontSize: fontSize,
),
textAlign: TextAlign.center,
), ),
const SizedBox(height: 16)
], ],
Text( ),
text,
style: TextStyle(
color: color,
fontSize: fontSize,
),
textAlign: TextAlign.center,
),
],
), ),
); );
} }

View file

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:aves/widgets/common/magnifier/controller/controller.dart'; import 'package:aves/widgets/common/magnifier/controller/controller.dart';
import 'package:aves/widgets/common/magnifier/controller/state.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart';

View file

@ -1,8 +1,5 @@
import 'dart:ui';
import 'package:aves/widgets/common/magnifier/controller/state.dart'; import 'package:aves/widgets/common/magnifier/controller/state.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@immutable @immutable

View file

@ -22,7 +22,6 @@ import 'package:aves/widgets/common/map/zoomed_bounds.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:fluster/fluster.dart'; import 'package:fluster/fluster.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View file

@ -2,7 +2,6 @@ import 'package:aves/model/settings/enums.dart';
import 'package:aves/widgets/common/basic/outlined_text.dart'; import 'package:aves/widgets/common/basic/outlined_text.dart';
import 'package:aves/widgets/common/map/leaflet/scalebar_utils.dart'; import 'package:aves/widgets/common/map/leaflet/scalebar_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map/plugin_api.dart';
class ScaleLayerOptions extends LayerOptions { class ScaleLayerOptions extends LayerOptions {

View file

@ -2,7 +2,6 @@ import 'package:aves/model/entry.dart';
import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart'; import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class ImageMarker extends StatelessWidget { class ImageMarker extends StatelessWidget {
final AvesEntry? entry; final AvesEntry? entry;

View file

@ -4,7 +4,6 @@ import 'package:aves/model/entry.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/grid/theme.dart'; import 'package:aves/widgets/common/grid/theme.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart'; import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ThumbnailScroller extends StatefulWidget { class ThumbnailScroller extends StatefulWidget {

View file

@ -68,7 +68,7 @@ class DebugSettingsSection extends StatelessWidget {
'searchHistory': toMultiline(settings.searchHistory), 'searchHistory': toMultiline(settings.searchHistory),
'lastVersionCheckDate': '${settings.lastVersionCheckDate}', 'lastVersionCheckDate': '${settings.lastVersionCheckDate}',
'locale': '${settings.locale}', 'locale': '${settings.locale}',
'systemLocale': '${WidgetsBinding.instance!.window.locale}', 'systemLocales': '${WidgetsBinding.instance!.window.locales}',
}, },
), ),
), ),

Some files were not shown because too many files have changed in this diff Show more