Merge branch 'develop'
This commit is contained in:
commit
369647555f
175 changed files with 3326 additions and 1668 deletions
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
@ -15,7 +15,7 @@ jobs:
|
|||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
channel: stable
|
||||
flutter-version: '2.5.3'
|
||||
flutter-version: '2.8.1'
|
||||
|
||||
- name: Clone the repository.
|
||||
uses: actions/checkout@v2
|
||||
|
|
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
- uses: subosito/flutter-action@v1
|
||||
with:
|
||||
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):
|
||||
# https://issuetracker.google.com/issues/144111441
|
||||
|
@ -52,12 +52,12 @@ jobs:
|
|||
rm release.keystore.asc
|
||||
mkdir outputs
|
||||
(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
|
||||
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
|
||||
(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
|
||||
rm $AVES_STORE_FILE
|
||||
env:
|
||||
|
@ -71,6 +71,7 @@ jobs:
|
|||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "outputs/*"
|
||||
body: "[Changelog](https://github.com/${{ github.repository }}/blob/develop/CHANGELOG.md#${{ github.ref_name }})"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload app bundle
|
||||
|
|
26
CHANGELOG.md
26
CHANGELOG.md
|
@ -2,9 +2,27 @@
|
|||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
- 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
|
||||
|
||||
|
@ -502,4 +520,4 @@ upgraded libtiff to 4.2.0 for TIFF decoding
|
|||
|
||||
## [v1.2.3] - 2020-10-22
|
||||
|
||||
...
|
||||
...
|
||||
|
|
|
@ -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" />
|
||||
|
||||
## Changelog
|
||||
|
||||
The list of changes for past and future releases is available [here](https://github.com/deckerst/aves/blob/develop/CHANGELOG.md).
|
||||
|
||||
## Permissions
|
||||
|
||||
Aves requires a few permissions to do its job:
|
||||
|
|
|
@ -56,6 +56,9 @@ android {
|
|||
// minSdkVersion constraints:
|
||||
// - Flutter & other plugins: 16
|
||||
// - 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
|
||||
targetSdkVersion 31
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
|
|
|
@ -155,7 +155,7 @@ class MainActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
@SuppressLint("WrongConstant", "ObsoleteSdkInt")
|
||||
private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
|
||||
val treeUri = data?.data
|
||||
if (resultCode != RESULT_OK || treeUri == null) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
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) {
|
||||
var removed = false
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
try {
|
||||
removed = Settings.Global.getFloat(activity.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE) == 0f
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
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
|
||||
// so we get their names in English as well as the current locale
|
||||
val englishConfig = Configuration().apply {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
setLocale(Locale.ENGLISH)
|
||||
} else {
|
||||
|
@ -272,13 +274,13 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
} else {
|
||||
var mimeType = "*/*"
|
||||
if (mimeTypes.size == 1) {
|
||||
// items have the same mime type & subtype
|
||||
// items have the same MIME type & subtype
|
||||
mimeType = mimeTypes.first()
|
||||
} else {
|
||||
// items have different subtypes
|
||||
val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct()
|
||||
if (mimeTypeTypes.size == 1) {
|
||||
// items have the same mime type
|
||||
// items have the same MIME type
|
||||
mimeType = "${mimeTypeTypes.first()}/*"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
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.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
|
@ -14,6 +16,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
|
|||
when (call.method) {
|
||||
"getCapabilities" -> safe(call, result, ::getCapabilities)
|
||||
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
|
||||
"getLocales" -> safe(call, result, ::getLocales)
|
||||
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
|
@ -41,6 +44,32 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
|
|||
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) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val performanceClass = Build.VERSION.MEDIA_PERFORMANCE_CLASS
|
||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls
|
|||
import android.app.Activity
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Glide
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
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.provider.ImageProvider.ImageOpCallback
|
||||
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.StorageUtils.ensureTrailingSeparator
|
||||
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) }
|
||||
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) }
|
||||
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) }
|
||||
"cancelFileOp" -> safe(call, result, ::cancelFileOp)
|
||||
"captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) }
|
||||
"clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) }
|
||||
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) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val desiredName = call.argument<String>("desiredName")
|
||||
|
@ -169,6 +185,9 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<MediaFileHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/media_file"
|
||||
|
||||
val cancelledOps = HashSet<String>()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
|
@ -191,6 +192,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val key = kv.key
|
||||
// `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) {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
StandardCharsets.UTF_8
|
||||
} else {
|
||||
|
@ -409,19 +411,19 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
|
||||
// File type
|
||||
for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) {
|
||||
// * `metadata-extractor` sometimes detects the wrong mime type (e.g. `pef` file as `tiff`)
|
||||
// * the content resolver / media store sometimes reports the wrong mime type (e.g. `png` file as `jpeg`, `tiff` as `srw`)
|
||||
// * `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`)
|
||||
// * `context.getContentResolver().getType()` sometimes returns an incorrect value
|
||||
// * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000`
|
||||
// * 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
|
||||
// cf https://github.com/drewnoakes/metadata-extractor/issues/296
|
||||
if (path?.matches(TIFF_EXTENSION_PATTERN) == true) {
|
||||
metadataMap[KEY_MIME_TYPE] = MimeTypes.TIFF
|
||||
} else {
|
||||
dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) {
|
||||
if (it != MimeTypes.TIFF) {
|
||||
if (it != MimeTypes.TIFF && it != MimeTypes.DVD) {
|
||||
metadataMap[KEY_MIME_TYPE] = it
|
||||
}
|
||||
}
|
||||
|
@ -584,6 +586,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
|
||||
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
|
||||
try {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { metadataMap[KEY_ROTATION_DEGREES] = it }
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.net.Uri
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import deckers.thibault.aves.channel.calls.MediaFileHandler.Companion.cancelledOps
|
||||
import deckers.thibault.aves.model.AvesEntry
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
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 var op: String? = null
|
||||
private var opId: String? = null
|
||||
private val entryMapList = ArrayList<FieldMap>()
|
||||
|
||||
init {
|
||||
if (arguments is Map<*, *>) {
|
||||
op = arguments["op"] as String?
|
||||
opId = arguments["id"] as String?
|
||||
@Suppress("unchecked_cast")
|
||||
val rawEntries = arguments["entries"] as List<FieldMap>?
|
||||
if (rawEntries != null) {
|
||||
|
@ -74,6 +77,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
}
|
||||
|
||||
private fun endOfStream() {
|
||||
cancelledOps.remove(opId)
|
||||
handler.post {
|
||||
try {
|
||||
eventSink.endOfStream()
|
||||
|
@ -97,14 +101,18 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
return
|
||||
}
|
||||
|
||||
for (entryMap in entryMapList) {
|
||||
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) }
|
||||
val path = entryMap["path"] as String?
|
||||
val mimeType = entryMap["mimeType"] as String?
|
||||
if (uri != null && mimeType != null) {
|
||||
val result: FieldMap = hashMapOf(
|
||||
"uri" to uri.toString(),
|
||||
)
|
||||
val entries = entryMapList.map(::AvesEntry)
|
||||
for (entry in entries) {
|
||||
val uri = entry.uri
|
||||
val path = entry.path
|
||||
val mimeType = entry.mimeType
|
||||
|
||||
val result: FieldMap = hashMapOf(
|
||||
"uri" to uri.toString(),
|
||||
)
|
||||
if (isCancelledOp()) {
|
||||
result["skipped"] = true
|
||||
} else {
|
||||
try {
|
||||
provider.delete(activity, uri, path, mimeType)
|
||||
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)
|
||||
result["success"] = false
|
||||
}
|
||||
success(result)
|
||||
}
|
||||
success(result)
|
||||
}
|
||||
endOfStream()
|
||||
}
|
||||
|
@ -173,7 +181,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
|||
|
||||
destinationDir = StorageUtils.ensureTrailingSeparator(destinationDir)
|
||||
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 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)
|
||||
provider.renameMultiple(activity, newName, entries, object : ImageOpCallback {
|
||||
provider.renameMultiple(activity, newName, entries, ::isCancelledOp, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
||||
})
|
||||
endOfStream()
|
||||
}
|
||||
|
||||
private fun isCancelledOp() = cancelledOps.contains(opId)
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/media_op_stream"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.channel.streams
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.net.Uri
|
||||
|
@ -36,6 +37,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
|
|||
val settings: FieldMap = hashMapOf(
|
||||
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
|
||||
)
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
settings[Settings.Global.TRANSITION_ANIMATION_SCALE] = transitionAnimationScale
|
||||
}
|
||||
|
@ -51,6 +53,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
|
|||
accelerometerRotation = newAccelerometerRotation
|
||||
changed = true
|
||||
}
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
val newTransitionAnimationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.TRANSITION_ANIMATION_SCALE)
|
||||
if (transitionAnimationScale != newTransitionAnimationScale) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.channel.streams
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
@ -10,6 +11,7 @@ import android.util.Log
|
|||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.PendingStorageAccessResultHandler
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.PermissionManager
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.EventChannel.EventSink
|
||||
|
@ -91,8 +93,8 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
}
|
||||
|
||||
private fun createFile() {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
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)
|
||||
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) {
|
||||
// TODO TLAD [<=API18] open file
|
||||
error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
|
||||
return
|
||||
}
|
||||
|
||||
val mimeType = args["mimeType"] as String?
|
||||
if (mimeType == null) {
|
||||
error("openFile-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
val mimeType = args["mimeType"] as String? // optional
|
||||
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = mimeType
|
||||
}
|
||||
MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri ->
|
||||
fun onGranted(uri: Uri) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
activity.contentResolver.openInputStream(uri)?.use { input ->
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
|
@ -161,11 +155,24 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any?
|
|||
endOfStream()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
}
|
||||
|
||||
fun onDenied() {
|
||||
success(ByteArray(0))
|
||||
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?) {}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.metadata
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.media.MediaFormat
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Build
|
||||
|
@ -31,6 +32,7 @@ object MediaMetadataRetrieverHelper {
|
|||
MediaMetadataRetriever.METADATA_KEY_WRITER to "Writer",
|
||||
MediaMetadataRetriever.METADATA_KEY_YEAR to "Year",
|
||||
).apply {
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
put(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION, "Video Rotation")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.metadata
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaFormat
|
||||
|
@ -56,6 +57,7 @@ object MultiPage {
|
|||
|
||||
format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it }
|
||||
format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it }
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
format.getSafeInt(MediaFormat.KEY_IS_DEFAULT) { track[KEY_IS_DEFAULT] = it != 0 }
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.model
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
|
@ -139,6 +140,7 @@ class SourceEntry {
|
|||
retriever.getSafeLong(MediaMetadataRetriever.METADATA_KEY_DURATION) { durationMillis = it }
|
||||
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { sourceDateTakenMillis = it }
|
||||
retriever.getSafeString(MediaMetadataRetriever.METADATA_KEY_TITLE) { title = it }
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION) { sourceRotationDegrees = it }
|
||||
}
|
||||
|
@ -161,7 +163,7 @@ class SourceEntry {
|
|||
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { 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)
|
||||
if (isVideo) {
|
||||
for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) {
|
||||
|
|
|
@ -47,11 +47,25 @@ abstract class ImageProvider {
|
|||
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"))
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
|
@ -937,3 +951,5 @@ abstract class ImageProvider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias CancelCheck = () -> Boolean
|
||||
|
|
|
@ -175,7 +175,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
// but for single items, `contentUri` already contains the ID
|
||||
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)
|
||||
// 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 width = cursor.getInt(widthColumn)
|
||||
val height = cursor.getInt(heightColumn)
|
||||
|
@ -331,6 +331,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetDir: String,
|
||||
nameConflictStrategy: NameConflictStrategy,
|
||||
entries: List<AvesEntry>,
|
||||
isCancelledOp: CancelCheck,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
|
||||
|
@ -366,7 +367,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
// - 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
|
||||
try {
|
||||
val newFields = moveSingle(
|
||||
val newFields = if (isCancelledOp()) skippedFieldMap else moveSingle(
|
||||
activity = activity,
|
||||
sourcePath = sourcePath,
|
||||
sourceUri = sourceUri,
|
||||
|
@ -505,6 +506,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
activity: Activity,
|
||||
newFileName: String,
|
||||
entries: List<AvesEntry>,
|
||||
isCancelledOp: CancelCheck,
|
||||
callback: ImageOpCallback,
|
||||
) {
|
||||
for (entry in entries) {
|
||||
|
@ -519,7 +521,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
if (sourcePath != null) {
|
||||
try {
|
||||
val newFields = renameSingle(
|
||||
val newFields = if (isCancelledOp()) skippedFieldMap else renameSingle(
|
||||
activity = activity,
|
||||
mimeType = mimeType,
|
||||
oldMediaUri = sourceUri,
|
||||
|
@ -563,6 +565,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
throw Exception("unsupported Android version")
|
||||
}
|
||||
|
||||
Log.d(LOG_TAG, "rename content at uri=$mediaUri")
|
||||
val uri = StorageUtils.getMediaStoreScopedStorageSafeUri(mediaUri, mimeType)
|
||||
|
||||
// `IS_PENDING` is necessary for `TITLE`, not for `DISPLAY_NAME`
|
||||
|
|
|
@ -3,7 +3,7 @@ package deckers.thibault.aves.utils
|
|||
import androidx.exifinterface.media.ExifInterface
|
||||
|
||||
object MimeTypes {
|
||||
private const val IMAGE = "image"
|
||||
const val ANY = "*/*"
|
||||
|
||||
// generic raster
|
||||
const val BMP = "image/bmp"
|
||||
|
@ -45,10 +45,9 @@ object MimeTypes {
|
|||
// vector
|
||||
const val SVG = "image/svg+xml"
|
||||
|
||||
private const val VIDEO = "video"
|
||||
|
||||
private const val AVI = "video/avi"
|
||||
private const val AVI_VND = "video/vnd.avi"
|
||||
const val DVD = "video/dvd"
|
||||
private const val MKV = "video/x-matroska"
|
||||
private const val MOV = "video/quicktime"
|
||||
private const val MP2T = "video/mp2t"
|
||||
|
@ -57,9 +56,9 @@ object MimeTypes {
|
|||
private const val OGV = "video/ogg"
|
||||
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)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
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 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
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
accessibleDirs.addAll(StorageUtils.getVolumePaths(context))
|
||||
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
|
|
|
@ -93,7 +93,6 @@ object StorageUtils {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun findVolumePaths(context: Context): Array<String> {
|
||||
// Final set of paths
|
||||
val paths = HashSet<String>()
|
||||
|
|
10
android/app/src/main/res/values-de/strings.xml
Normal file
10
android/app/src/main/res/values-de/strings.xml
Normal 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 & Videos scannen</string>
|
||||
<string name="analysis_notification_default_title">Medien scannen</string>
|
||||
<string name="analysis_notification_action_stop">Abbrechen</string>
|
||||
</resources>
|
|
@ -1,16 +1,16 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.6.0'
|
||||
ext.kotlin_version = '1.6.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
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"
|
||||
// GMS & Firebase Crashlytics are not actually used by all flavors
|
||||
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
526
lib/l10n/app_de.arb
Normal 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"
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
{
|
||||
"appName": "Aves",
|
||||
"@appName": {},
|
||||
"welcomeMessage": "Welcome to Aves",
|
||||
"@welcomeMessage": {},
|
||||
"welcomeOptional": "Optional",
|
||||
"welcomeTermsToggle": "I agree to the terms and conditions",
|
||||
"@welcomeTermsToggle": {},
|
||||
"itemCount": "{count, plural, =1{1 item} other{{count} items}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
|
@ -27,158 +24,87 @@
|
|||
},
|
||||
|
||||
"applyButtonLabel": "APPLY",
|
||||
"@applyButtonLabel": {},
|
||||
"deleteButtonLabel": "DELETE",
|
||||
"@deleteButtonLabel": {},
|
||||
"nextButtonLabel": "NEXT",
|
||||
"@nextButtonLabel": {},
|
||||
"showButtonLabel": "SHOW",
|
||||
"@showButtonLabel": {},
|
||||
"hideButtonLabel": "HIDE",
|
||||
"@hideButtonLabel": {},
|
||||
"continueButtonLabel": "CONTINUE",
|
||||
"@continueButtonLabel": {},
|
||||
|
||||
"cancelTooltip": "Cancel",
|
||||
"changeTooltip": "Change",
|
||||
"@changeTooltip": {},
|
||||
"clearTooltip": "Clear",
|
||||
"@clearTooltip": {},
|
||||
"previousTooltip": "Previous",
|
||||
"@previousTooltip": {},
|
||||
"nextTooltip": "Next",
|
||||
"@nextTooltip": {},
|
||||
"showTooltip": "Show",
|
||||
"@showTooltip": {},
|
||||
"hideTooltip": "Hide",
|
||||
"@hideTooltip": {},
|
||||
"removeTooltip": "Remove",
|
||||
"@removeTooltip": {},
|
||||
"resetButtonTooltip": "Reset",
|
||||
"@resetButtonTooltip": {},
|
||||
|
||||
"doubleBackExitMessage": "Tap “back” again to exit.",
|
||||
"@doubleBackExitMessage": {},
|
||||
|
||||
"sourceStateLoading": "Loading",
|
||||
"@sourceStateLoading": {},
|
||||
"sourceStateCataloguing": "Cataloguing",
|
||||
"@sourceStateCataloguing": {},
|
||||
"sourceStateLocatingCountries": "Locating countries",
|
||||
"@sourceStateLocatingCountries": {},
|
||||
"sourceStateLocatingPlaces": "Locating places",
|
||||
"@sourceStateLocatingPlaces": {},
|
||||
|
||||
"chipActionDelete": "Delete",
|
||||
"@chipActionDelete": {},
|
||||
"chipActionGoToAlbumPage": "Show in Albums",
|
||||
"@chipActionGoToAlbumPage": {},
|
||||
"chipActionGoToCountryPage": "Show in Countries",
|
||||
"@chipActionGoToCountryPage": {},
|
||||
"chipActionGoToTagPage": "Show in Tags",
|
||||
"@chipActionGoToTagPage": {},
|
||||
"chipActionHide": "Hide",
|
||||
"@chipActionHide": {},
|
||||
"chipActionPin": "Pin to top",
|
||||
"@chipActionPin": {},
|
||||
"chipActionUnpin": "Unpin from top",
|
||||
"@chipActionUnpin": {},
|
||||
"chipActionRename": "Rename",
|
||||
"@chipActionRename": {},
|
||||
"chipActionSetCover": "Set cover",
|
||||
"@chipActionSetCover": {},
|
||||
"chipActionCreateAlbum": "Create album",
|
||||
"@chipActionCreateAlbum": {},
|
||||
|
||||
"entryActionCopyToClipboard": "Copy to clipboard",
|
||||
"@entryActionCopyToClipboard": {},
|
||||
"entryActionDelete": "Delete",
|
||||
"@entryActionDelete": {},
|
||||
"entryActionExport": "Export",
|
||||
"@entryActionExport": {},
|
||||
"entryActionInfo": "Info",
|
||||
"@entryActionInfo": {},
|
||||
"entryActionRename": "Rename",
|
||||
"@entryActionRename": {},
|
||||
"entryActionRotateCCW": "Rotate counterclockwise",
|
||||
"@entryActionRotateCCW": {},
|
||||
"entryActionRotateCW": "Rotate clockwise",
|
||||
"@entryActionRotateCW": {},
|
||||
"entryActionFlip": "Flip horizontally",
|
||||
"@entryActionFlip": {},
|
||||
"entryActionPrint": "Print",
|
||||
"@entryActionPrint": {},
|
||||
"entryActionShare": "Share",
|
||||
"@entryActionShare": {},
|
||||
"entryActionViewSource": "View source",
|
||||
"@entryActionViewSource": {},
|
||||
"entryActionViewMotionPhotoVideo": "Open Motion Photo",
|
||||
"@entryActionViewMotionPhotoVideo": {},
|
||||
"entryActionEdit": "Edit with…",
|
||||
"@entryActionEdit": {},
|
||||
"entryActionOpen": "Open with…",
|
||||
"@entryActionOpen": {},
|
||||
"entryActionSetAs": "Set as…",
|
||||
"@entryActionSetAs": {},
|
||||
"entryActionOpenMap": "Show in map app…",
|
||||
"@entryActionOpenMap": {},
|
||||
"entryActionRotateScreen": "Rotate screen",
|
||||
"@entryActionRotateScreen": {},
|
||||
"entryActionAddFavourite": "Add to favourites",
|
||||
"@entryActionAddFavourite": {},
|
||||
"entryActionRemoveFavourite": "Remove from favourites",
|
||||
"@entryActionRemoveFavourite": {},
|
||||
|
||||
"videoActionCaptureFrame": "Capture frame",
|
||||
"@videoActionCaptureFrame": {},
|
||||
"videoActionPause": "Pause",
|
||||
"@videoActionPause": {},
|
||||
"videoActionPlay": "Play",
|
||||
"@videoActionPlay": {},
|
||||
"videoActionReplay10": "Seek backward 10 seconds",
|
||||
"@videoActionReplay10": {},
|
||||
"videoActionSkip10": "Seek forward 10 seconds",
|
||||
"@videoActionSkip10": {},
|
||||
"videoActionSelectStreams": "Select tracks",
|
||||
"@videoActionSelectStreams": {},
|
||||
"videoActionSetSpeed": "Playback speed",
|
||||
"@videoActionSetSpeed": {},
|
||||
"videoActionSettings": "Settings",
|
||||
"@videoActionSettings": {},
|
||||
|
||||
"entryInfoActionEditDate": "Edit date & time",
|
||||
"@entryInfoActionEditDate": {},
|
||||
"entryInfoActionEditTags": "Edit tags",
|
||||
"@entryInfoActionEditTags": {},
|
||||
"entryInfoActionRemoveMetadata": "Remove metadata",
|
||||
"@entryInfoActionRemoveMetadata": {},
|
||||
|
||||
"filterFavouriteLabel": "Favourite",
|
||||
"@filterFavouriteLabel": {},
|
||||
"filterLocationEmptyLabel": "Unlocated",
|
||||
"@filterLocationEmptyLabel": {},
|
||||
"filterTagEmptyLabel": "Untagged",
|
||||
"@filterTagEmptyLabel": {},
|
||||
"filterTypeAnimatedLabel": "Animated",
|
||||
"@filterTypeAnimatedLabel": {},
|
||||
"filterTypeMotionPhotoLabel": "Motion Photo",
|
||||
"@filterTypeMotionPhotoLabel": {},
|
||||
"filterTypePanoramaLabel": "Panorama",
|
||||
"@filterTypePanoramaLabel": {},
|
||||
"filterTypeRawLabel": "Raw",
|
||||
"@filterTypeRawLabel": {},
|
||||
"filterTypeSphericalVideoLabel": "360° Video",
|
||||
"@filterTypeSphericalVideoLabel": {},
|
||||
"filterTypeGeotiffLabel": "GeoTIFF",
|
||||
"@filterTypeGeotiffLabel": {},
|
||||
"filterMimeImageLabel": "Image",
|
||||
"@filterMimeImageLabel": {},
|
||||
"filterMimeVideoLabel": "Video",
|
||||
"@filterMimeVideoLabel": {},
|
||||
|
||||
"coordinateFormatDms": "DMS",
|
||||
"@coordinateFormatDms": {},
|
||||
"coordinateFormatDecimal": "Decimal degrees",
|
||||
"@coordinateFormatDecimal": {},
|
||||
"coordinateDms": "{coordinate} {direction}",
|
||||
"@coordinateDms": {
|
||||
"placeholders": {
|
||||
|
@ -191,75 +117,44 @@
|
|||
}
|
||||
},
|
||||
"coordinateDmsNorth": "N",
|
||||
"@coordinateDmsNorth": {},
|
||||
"coordinateDmsSouth": "S",
|
||||
"@coordinateDmsSouth": {},
|
||||
"coordinateDmsEast": "E",
|
||||
"@coordinateDmsEast": {},
|
||||
"coordinateDmsWest": "W",
|
||||
"@coordinateDmsWest": {},
|
||||
|
||||
"unitSystemMetric": "Metric",
|
||||
"@unitSystemMetric": {},
|
||||
"unitSystemImperial": "Imperial",
|
||||
"@unitSystemImperial": {},
|
||||
|
||||
"videoLoopModeNever": "Never",
|
||||
"@videoLoopModeNever": {},
|
||||
"videoLoopModeShortOnly": "Short videos only",
|
||||
"@videoLoopModeShortOnly": {},
|
||||
"videoLoopModeAlways": "Always",
|
||||
"@videoLoopModeAlways": {},
|
||||
|
||||
"mapStyleGoogleNormal": "Google Maps",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleHybrid": "Google Maps (Hybrid)",
|
||||
"@mapStyleGoogleHybrid": {},
|
||||
"mapStyleGoogleTerrain": "Google Maps (Terrain)",
|
||||
"@mapStyleGoogleTerrain": {},
|
||||
"mapStyleOsmHot": "Humanitarian OSM",
|
||||
"@mapStyleOsmHot": {},
|
||||
"mapStyleStamenToner": "Stamen Toner",
|
||||
"@mapStyleStamenToner": {},
|
||||
"mapStyleStamenWatercolor": "Stamen Watercolor",
|
||||
"@mapStyleStamenWatercolor": {},
|
||||
|
||||
"nameConflictStrategyRename": "Rename",
|
||||
"@nameConflictStrategyRename": {},
|
||||
"nameConflictStrategyReplace": "Replace",
|
||||
"@nameConflictStrategyReplace": {},
|
||||
"nameConflictStrategySkip": "Skip",
|
||||
"@nameConflictStrategySkip": {},
|
||||
|
||||
"keepScreenOnNever": "Never",
|
||||
"@keepScreenOnNever": {},
|
||||
"keepScreenOnViewerOnly": "Viewer page only",
|
||||
"@keepScreenOnViewerOnly": {},
|
||||
"keepScreenOnAlways": "Always",
|
||||
"@keepScreenOnAlways": {},
|
||||
|
||||
"accessibilityAnimationsRemove": "Prevent screen effects",
|
||||
"@accessibilityAnimationsRemove": {},
|
||||
"accessibilityAnimationsKeep": "Keep screen effects",
|
||||
"@accessibilityAnimationsKeep": {},
|
||||
|
||||
"albumTierNew": "New",
|
||||
"@albumTierNew": {},
|
||||
"albumTierPinned": "Pinned",
|
||||
"@albumTierPinned": {},
|
||||
"albumTierSpecial": "Common",
|
||||
"@albumTierSpecial": {},
|
||||
"albumTierApps": "Apps",
|
||||
"@albumTierApps": {},
|
||||
"albumTierRegular": "Others",
|
||||
"@albumTierRegular": {},
|
||||
|
||||
"storageVolumeDescriptionFallbackPrimary": "Internal storage",
|
||||
"@storageVolumeDescriptionFallbackPrimary": {},
|
||||
"storageVolumeDescriptionFallbackNonPrimary": "SD card",
|
||||
"@storageVolumeDescriptionFallbackNonPrimary": {},
|
||||
"rootDirectoryDescription": "root directory",
|
||||
"@rootDirectoryDescription": {},
|
||||
"otherDirectoryDescription": "“{name}” directory",
|
||||
"@otherDirectoryDescription": {
|
||||
"placeholders": {
|
||||
|
@ -269,7 +164,6 @@
|
|||
}
|
||||
},
|
||||
"storageAccessDialogTitle": "Storage Access",
|
||||
"@storageAccessDialogTitle": {},
|
||||
"storageAccessDialogMessage": "Please select the {directory} of “{volume}” in the next screen to give this app access to it.",
|
||||
"@storageAccessDialogMessage": {
|
||||
"placeholders": {
|
||||
|
@ -282,7 +176,6 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"placeholders": {
|
||||
|
@ -295,7 +188,6 @@
|
|||
}
|
||||
},
|
||||
"notEnoughSpaceDialogTitle": "Not Enough Space",
|
||||
"@notEnoughSpaceDialogTitle": {},
|
||||
"notEnoughSpaceDialogMessage": "This operation needs {neededSize} of free space on “{volume}” to complete, but there is only {freeSize} left.",
|
||||
"@notEnoughSpaceDialogMessage": {
|
||||
"placeholders": {
|
||||
|
@ -312,7 +204,6 @@
|
|||
},
|
||||
|
||||
"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": {
|
||||
"placeholders": {
|
||||
|
@ -324,19 +215,13 @@
|
|||
},
|
||||
|
||||
"nameConflictDialogSingleSourceMessage": "Some files in the destination folder have the same name.",
|
||||
"@nameConflictDialogSingleSourceMessage": {},
|
||||
"nameConflictDialogMultipleSourceMessage": "Some files have the same name.",
|
||||
"@nameConflictDialogMultipleSourceMessage": {},
|
||||
|
||||
"addShortcutDialogLabel": "Shortcut label",
|
||||
"@addShortcutDialogLabel": {},
|
||||
"addShortcutButtonLabel": "ADD",
|
||||
"@addShortcutButtonLabel": {},
|
||||
|
||||
"noMatchingAppDialogTitle": "No Matching App",
|
||||
"@noMatchingAppDialogTitle": {},
|
||||
"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": {
|
||||
|
@ -352,33 +237,21 @@
|
|||
}
|
||||
},
|
||||
"videoStartOverButtonLabel": "START OVER",
|
||||
"@videoStartOverButtonLabel": {},
|
||||
"videoResumeButtonLabel": "RESUME",
|
||||
"@videoResumeButtonLabel": {},
|
||||
|
||||
"setCoverDialogTitle": "Set Cover",
|
||||
"@setCoverDialogTitle": {},
|
||||
"setCoverDialogLatest": "Latest item",
|
||||
"@setCoverDialogLatest": {},
|
||||
"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": {},
|
||||
|
||||
"newAlbumDialogTitle": "New Album",
|
||||
"@newAlbumDialogTitle": {},
|
||||
"newAlbumDialogNameLabel": "Album name",
|
||||
"@newAlbumDialogNameLabel": {},
|
||||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Directory already exists",
|
||||
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
||||
"newAlbumDialogStorageLabel": "Storage:",
|
||||
"@newAlbumDialogStorageLabel": {},
|
||||
|
||||
"renameAlbumDialogLabel": "New name",
|
||||
"@renameAlbumDialogLabel": {},
|
||||
"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": {
|
||||
|
@ -394,117 +267,73 @@
|
|||
},
|
||||
|
||||
"exportEntryDialogFormat": "Format:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
||||
"renameEntryDialogLabel": "New name",
|
||||
"@renameEntryDialogLabel": {},
|
||||
|
||||
"editEntryDateDialogTitle": "Date & Time",
|
||||
"@editEntryDateDialogTitle": {},
|
||||
"editEntryDateDialogSet": "Set",
|
||||
"@editEntryDateDialogSet": {},
|
||||
"editEntryDateDialogShift": "Shift",
|
||||
"@editEntryDateDialogShift": {},
|
||||
"editEntryDateDialogExtractFromTitle": "Extract from title",
|
||||
"@editEntryDateDialogExtractFromTitle": {},
|
||||
"editEntryDateDialogClear": "Clear",
|
||||
"@editEntryDateDialogClear": {},
|
||||
"editEntryDateDialogFieldSelection": "Field selection",
|
||||
"@editEntryDateDialogFieldSelection": {},
|
||||
"editEntryDateDialogHours": "Hours",
|
||||
"@editEntryDateDialogHours": {},
|
||||
"editEntryDateDialogMinutes": "Minutes",
|
||||
"@editEntryDateDialogMinutes": {},
|
||||
|
||||
"removeEntryMetadataDialogTitle": "Metadata Removal",
|
||||
"@removeEntryMetadataDialogTitle": {},
|
||||
"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": {},
|
||||
|
||||
"videoSpeedDialogLabel": "Playback speed",
|
||||
"@videoSpeedDialogLabel": {},
|
||||
|
||||
"videoStreamSelectionDialogVideo": "Video",
|
||||
"@videoStreamSelectionDialogVideo": {},
|
||||
"videoStreamSelectionDialogAudio": "Audio",
|
||||
"@videoStreamSelectionDialogAudio": {},
|
||||
"videoStreamSelectionDialogText": "Subtitles",
|
||||
"@videoStreamSelectionDialogText": {},
|
||||
"videoStreamSelectionDialogOff": "Off",
|
||||
"@videoStreamSelectionDialogOff": {},
|
||||
"videoStreamSelectionDialogTrack": "Track",
|
||||
"@videoStreamSelectionDialogTrack": {},
|
||||
"videoStreamSelectionDialogNoSelection": "There are no other tracks.",
|
||||
"@videoStreamSelectionDialogNoSelection": {},
|
||||
|
||||
"genericSuccessFeedback": "Done!",
|
||||
"@genericSuccessFeedback": {},
|
||||
"genericFailureFeedback": "Failed",
|
||||
"@genericFailureFeedback": {},
|
||||
|
||||
"menuActionSort": "Sort",
|
||||
"@menuActionSort": {},
|
||||
"menuActionGroup": "Group",
|
||||
"@menuActionGroup": {},
|
||||
"menuActionConfigureView": "View",
|
||||
"menuActionSelect": "Select",
|
||||
"@menuActionSelect": {},
|
||||
"menuActionSelectAll": "Select all",
|
||||
"@menuActionSelectAll": {},
|
||||
"menuActionSelectNone": "Select none",
|
||||
"@menuActionSelectNone": {},
|
||||
"menuActionMap": "Map",
|
||||
"@menuActionMap": {},
|
||||
"menuActionStats": "Stats",
|
||||
"@menuActionStats": {},
|
||||
|
||||
"viewDialogTabSort": "Sort",
|
||||
"viewDialogTabGroup": "Group",
|
||||
"viewDialogTabLayout": "Layout",
|
||||
|
||||
"tileLayoutGrid": "Grid",
|
||||
"tileLayoutList": "List",
|
||||
|
||||
"aboutPageTitle": "About",
|
||||
"@aboutPageTitle": {},
|
||||
"aboutLinkSources": "Sources",
|
||||
"@aboutLinkSources": {},
|
||||
"aboutLinkLicense": "License",
|
||||
"@aboutLinkLicense": {},
|
||||
"aboutLinkPolicy": "Privacy Policy",
|
||||
"@aboutLinkPolicy": {},
|
||||
|
||||
"aboutUpdate": "New Version Available",
|
||||
"@aboutUpdate": {},
|
||||
"aboutUpdateLinks1": "A new version of Aves is available on",
|
||||
"@aboutUpdateLinks1": {},
|
||||
"aboutUpdateLinks2": "and",
|
||||
"@aboutUpdateLinks2": {},
|
||||
"aboutUpdateLinks3": ".",
|
||||
"@aboutUpdateLinks3": {},
|
||||
"aboutUpdateGitHub": "GitHub",
|
||||
"@aboutUpdateGitHub": {},
|
||||
"aboutUpdateGooglePlay": "Google Play",
|
||||
"@aboutUpdateGooglePlay": {},
|
||||
|
||||
"aboutBug": "Bug Report",
|
||||
"@aboutBug": {},
|
||||
"aboutBugSaveLogInstruction": "Save app logs to a file",
|
||||
"@aboutBugSaveLogInstruction": {},
|
||||
"aboutBugSaveLogButton": "Save",
|
||||
"@aboutBugSaveLogButton": {},
|
||||
"aboutBugCopyInfoInstruction": "Copy system information",
|
||||
"@aboutBugCopyInfoInstruction": {},
|
||||
"aboutBugCopyInfoButton": "Copy",
|
||||
"@aboutBugCopyInfoButton": {},
|
||||
"aboutBugReportInstruction": "Report on GitHub with the logs and system information",
|
||||
"@aboutBugReportInstruction": {},
|
||||
"aboutBugReportButton": "Report",
|
||||
"@aboutBugReportButton": {},
|
||||
|
||||
"aboutCredits": "Credits",
|
||||
"@aboutCredits": {},
|
||||
"aboutCreditsWorldAtlas1": "This app uses a TopoJSON file from",
|
||||
"@aboutCreditsWorldAtlas1": {},
|
||||
"aboutCreditsWorldAtlas2": "under ISC License.",
|
||||
"@aboutCreditsWorldAtlas2": {},
|
||||
"aboutCreditsTranslators": "Translators:",
|
||||
"@aboutCreditsTranslators": {},
|
||||
"aboutCreditsTranslatorLine": "{language}: {names}",
|
||||
"@aboutCreditsTranslatorLine": {
|
||||
"placeholders": {
|
||||
|
@ -518,27 +347,17 @@
|
|||
},
|
||||
|
||||
"aboutLicenses": "Open-Source Licenses",
|
||||
"@aboutLicenses": {},
|
||||
"aboutLicensesBanner": "This app uses the following open-source packages and libraries.",
|
||||
"@aboutLicensesBanner": {},
|
||||
"aboutLicensesAndroidLibraries": "Android Libraries",
|
||||
"@aboutLicensesAndroidLibraries": {},
|
||||
"aboutLicensesFlutterPlugins": "Flutter Plugins",
|
||||
"@aboutLicensesFlutterPlugins": {},
|
||||
"aboutLicensesFlutterPackages": "Flutter Packages",
|
||||
"@aboutLicensesFlutterPackages": {},
|
||||
"aboutLicensesDartPackages": "Dart Packages",
|
||||
"@aboutLicensesDartPackages": {},
|
||||
"aboutLicensesShowAllButtonLabel": "Show All Licenses",
|
||||
"@aboutLicensesShowAllButtonLabel": {},
|
||||
|
||||
"policyPageTitle": "Privacy Policy",
|
||||
"@policyPageTitle": {},
|
||||
|
||||
"collectionPageTitle": "Collection",
|
||||
"@collectionPageTitle": {},
|
||||
"collectionPickPageTitle": "Pick",
|
||||
"@collectionPickPageTitle": {},
|
||||
"collectionSelectionPageTitle": "{count, plural, =0{Select items} =1{1 item} other{{count} items}}",
|
||||
"@collectionSelectionPageTitle": {
|
||||
"placeholders": {
|
||||
|
@ -547,51 +366,28 @@
|
|||
},
|
||||
|
||||
"collectionActionShowTitleSearch": "Show title filter",
|
||||
"@collectionActionShowTitleSearch": {},
|
||||
"collectionActionHideTitleSearch": "Hide title filter",
|
||||
"@collectionActionHideTitleSearch": {},
|
||||
"collectionActionAddShortcut": "Add shortcut",
|
||||
"@collectionActionAddShortcut": {},
|
||||
"collectionActionCopy": "Copy to album",
|
||||
"@collectionActionCopy": {},
|
||||
"collectionActionMove": "Move to album",
|
||||
"@collectionActionMove": {},
|
||||
"collectionActionRescan": "Rescan",
|
||||
"@collectionActionRescan": {},
|
||||
"collectionActionEdit": "Edit",
|
||||
"@collectionActionEdit": {},
|
||||
|
||||
"collectionSearchTitlesHintText": "Search titles",
|
||||
"@collectionSearchTitlesHintText": {},
|
||||
|
||||
"collectionSortTitle": "Sort",
|
||||
"@collectionSortTitle": {},
|
||||
"collectionSortDate": "By date",
|
||||
"@collectionSortDate": {},
|
||||
"collectionSortSize": "By size",
|
||||
"@collectionSortSize": {},
|
||||
"collectionSortName": "By album & file name",
|
||||
"@collectionSortName": {},
|
||||
|
||||
"collectionGroupTitle": "Group",
|
||||
"@collectionGroupTitle": {},
|
||||
"collectionGroupAlbum": "By album",
|
||||
"@collectionGroupAlbum": {},
|
||||
"collectionGroupMonth": "By month",
|
||||
"@collectionGroupMonth": {},
|
||||
"collectionGroupDay": "By day",
|
||||
"@collectionGroupDay": {},
|
||||
"collectionGroupNone": "Do not group",
|
||||
"@collectionGroupNone": {},
|
||||
|
||||
"sectionUnknown": "Unknown",
|
||||
"@sectionUnknown": {},
|
||||
"dateToday": "Today",
|
||||
"@dateToday": {},
|
||||
"dateYesterday": "Yesterday",
|
||||
"@dateYesterday": {},
|
||||
"dateThisMonth": "This month",
|
||||
"@dateThisMonth": {},
|
||||
"collectionDeleteFailureFeedback": "{count, plural, =1{Failed to delete 1 item} other{Failed to delete {count} items}}",
|
||||
"@collectionDeleteFailureFeedback": {
|
||||
"placeholders": {
|
||||
|
@ -642,324 +438,178 @@
|
|||
},
|
||||
|
||||
"collectionEmptyFavourites": "No favourites",
|
||||
"@collectionEmptyFavourites": {},
|
||||
"collectionEmptyVideos": "No videos",
|
||||
"@collectionEmptyVideos": {},
|
||||
"collectionEmptyImages": "No images",
|
||||
"@collectionEmptyImages": {},
|
||||
|
||||
"collectionSelectSectionTooltip": "Select section",
|
||||
"@collectionSelectSectionTooltip": {},
|
||||
"collectionDeselectSectionTooltip": "Deselect section",
|
||||
"@collectionDeselectSectionTooltip": {},
|
||||
|
||||
"drawerCollectionAll": "All collection",
|
||||
"@drawerCollectionAll": {},
|
||||
"drawerCollectionFavourites": "Favourites",
|
||||
"@drawerCollectionFavourites": {},
|
||||
"drawerCollectionImages": "Images",
|
||||
"@drawerCollectionImages": {},
|
||||
"drawerCollectionVideos": "Videos",
|
||||
"@drawerCollectionVideos": {},
|
||||
"drawerCollectionAnimated": "Animated",
|
||||
"@drawerCollectionAnimated": {},
|
||||
"drawerCollectionMotionPhotos": "Motion photos",
|
||||
"@drawerCollectionMotionPhotos": {},
|
||||
"drawerCollectionPanoramas": "Panoramas",
|
||||
"@drawerCollectionPanoramas": {},
|
||||
"drawerCollectionRaws": "Raw photos",
|
||||
"@drawerCollectionRaws": {},
|
||||
"drawerCollectionSphericalVideos": "360° Videos",
|
||||
"@drawerCollectionSphericalVideos": {},
|
||||
|
||||
"chipSortTitle": "Sort",
|
||||
"@chipSortTitle": {},
|
||||
"chipSortDate": "By date",
|
||||
"@chipSortDate": {},
|
||||
"chipSortName": "By name",
|
||||
"@chipSortName": {},
|
||||
"chipSortCount": "By item count",
|
||||
"@chipSortCount": {},
|
||||
|
||||
"albumGroupTitle": "Group",
|
||||
"@albumGroupTitle": {},
|
||||
"albumGroupTier": "By tier",
|
||||
"@albumGroupTier": {},
|
||||
"albumGroupVolume": "By storage volume",
|
||||
"@albumGroupVolume": {},
|
||||
"albumGroupNone": "Do not group",
|
||||
"@albumGroupNone": {},
|
||||
|
||||
"albumPickPageTitleCopy": "Copy to Album",
|
||||
"@albumPickPageTitleCopy": {},
|
||||
"albumPickPageTitleExport": "Export to Album",
|
||||
"@albumPickPageTitleExport": {},
|
||||
"albumPickPageTitleMove": "Move to Album",
|
||||
"@albumPickPageTitleMove": {},
|
||||
"albumPickPageTitlePick": "Pick Album",
|
||||
"@albumPickPageTitlePick": {},
|
||||
|
||||
"albumCamera": "Camera",
|
||||
"@albumCamera": {},
|
||||
"albumDownload": "Download",
|
||||
"@albumDownload": {},
|
||||
"albumScreenshots": "Screenshots",
|
||||
"@albumScreenshots": {},
|
||||
"albumScreenRecordings": "Screen recordings",
|
||||
"@albumScreenRecordings": {},
|
||||
"albumVideoCaptures": "Video Captures",
|
||||
"@albumVideoCaptures": {},
|
||||
|
||||
"albumPageTitle": "Albums",
|
||||
"@albumPageTitle": {},
|
||||
"albumEmpty": "No albums",
|
||||
"@albumEmpty": {},
|
||||
"createAlbumTooltip": "Create album",
|
||||
"@createAlbumTooltip": {},
|
||||
"createAlbumButtonLabel": "CREATE",
|
||||
"@createAlbumButtonLabel": {},
|
||||
"newFilterBanner": "new",
|
||||
"@newFilterBanner": {},
|
||||
|
||||
"countryPageTitle": "Countries",
|
||||
"@countryPageTitle": {},
|
||||
"countryEmpty": "No countries",
|
||||
"@countryEmpty": {},
|
||||
|
||||
"tagPageTitle": "Tags",
|
||||
"@tagPageTitle": {},
|
||||
"tagEmpty": "No tags",
|
||||
"@tagEmpty": {},
|
||||
|
||||
"searchCollectionFieldHint": "Search collection",
|
||||
"@searchCollectionFieldHint": {},
|
||||
"searchSectionRecent": "Recent",
|
||||
"@searchSectionRecent": {},
|
||||
"searchSectionAlbums": "Albums",
|
||||
"@searchSectionAlbums": {},
|
||||
"searchSectionCountries": "Countries",
|
||||
"@searchSectionCountries": {},
|
||||
"searchSectionPlaces": "Places",
|
||||
"@searchSectionPlaces": {},
|
||||
"searchSectionTags": "Tags",
|
||||
"@searchSectionTags": {},
|
||||
|
||||
"settingsPageTitle": "Settings",
|
||||
"@settingsPageTitle": {},
|
||||
"settingsSystemDefault": "System",
|
||||
"@settingsSystemDefault": {},
|
||||
"settingsDefault": "Default",
|
||||
"@settingsDefault": {},
|
||||
|
||||
"settingsActionExport": "Export",
|
||||
"@settingsActionExport": {},
|
||||
"settingsActionImport": "Import",
|
||||
"@settingsActionImport": {},
|
||||
|
||||
"settingsSectionNavigation": "Navigation",
|
||||
"@settingsSectionNavigation": {},
|
||||
"settingsHome": "Home",
|
||||
"@settingsHome": {},
|
||||
"settingsKeepScreenOnTile": "Keep screen on",
|
||||
"@settingsKeepScreenOnTile": {},
|
||||
"settingsKeepScreenOnTitle": "Keep Screen On",
|
||||
"@settingsKeepScreenOnTitle": {},
|
||||
"settingsDoubleBackExit": "Tap “back” twice to exit",
|
||||
"@settingsDoubleBackExit": {},
|
||||
|
||||
"settingsNavigationDrawerTile": "Navigation menu",
|
||||
"@settingsNavigationDrawerTile": {},
|
||||
"settingsNavigationDrawerEditorTitle": "Navigation Menu",
|
||||
"@settingsNavigationDrawerEditorTitle": {},
|
||||
"settingsNavigationDrawerBanner": "Touch and hold to move and reorder menu items.",
|
||||
"@settingsNavigationDrawerBanner": {},
|
||||
"settingsNavigationDrawerTabTypes": "Types",
|
||||
"@settingsNavigationDrawerTabTypes": {},
|
||||
"settingsNavigationDrawerTabAlbums": "Albums",
|
||||
"@settingsNavigationDrawerTabAlbums": {},
|
||||
"settingsNavigationDrawerTabPages": "Pages",
|
||||
"@settingsNavigationDrawerTabPages": {},
|
||||
"settingsNavigationDrawerAddAlbum": "Add album",
|
||||
"@settingsNavigationDrawerAddAlbum": {},
|
||||
|
||||
"settingsSectionThumbnails": "Thumbnails",
|
||||
"@settingsSectionThumbnails": {},
|
||||
"settingsThumbnailShowLocationIcon": "Show location icon",
|
||||
"@settingsThumbnailShowLocationIcon": {},
|
||||
"settingsThumbnailShowMotionPhotoIcon": "Show motion photo icon",
|
||||
"@settingsThumbnailShowMotionPhotoIcon": {},
|
||||
"settingsThumbnailShowRawIcon": "Show raw icon",
|
||||
"@settingsThumbnailShowRawIcon": {},
|
||||
"settingsThumbnailShowVideoDuration": "Show video duration",
|
||||
"@settingsThumbnailShowVideoDuration": {},
|
||||
|
||||
"settingsCollectionQuickActionsTile": "Quick actions",
|
||||
"@settingsCollectionQuickActionsTile": {},
|
||||
"settingsCollectionQuickActionEditorTitle": "Quick Actions",
|
||||
"@settingsCollectionQuickActionEditorTitle": {},
|
||||
"settingsCollectionQuickActionTabBrowsing": "Browsing",
|
||||
"@settingsCollectionQuickActionTabBrowsing": {},
|
||||
"settingsCollectionQuickActionTabSelecting": "Selecting",
|
||||
"@settingsCollectionQuickActionTabSelecting": {},
|
||||
"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": {},
|
||||
|
||||
"settingsSectionViewer": "Viewer",
|
||||
"@settingsSectionViewer": {},
|
||||
"settingsViewerUseCutout": "Use cutout area",
|
||||
"@settingsViewerUseCutout": {},
|
||||
"settingsViewerMaximumBrightness": "Maximum brightness",
|
||||
"@settingsViewerMaximumBrightness": {},
|
||||
"settingsMotionPhotoAutoPlay": "Auto play motion photos",
|
||||
"settingsImageBackground": "Image background",
|
||||
"@settingsImageBackground": {},
|
||||
|
||||
"settingsViewerQuickActionsTile": "Quick actions",
|
||||
"@settingsViewerQuickActionsTile": {},
|
||||
"settingsViewerQuickActionEditorTitle": "Quick Actions",
|
||||
"@settingsViewerQuickActionEditorTitle": {},
|
||||
"settingsViewerQuickActionEditorBanner": "Touch and hold to move buttons and select which actions are displayed in the viewer.",
|
||||
"@settingsViewerQuickActionEditorBanner": {},
|
||||
"settingsViewerQuickActionEditorDisplayedButtons": "Displayed Buttons",
|
||||
"@settingsViewerQuickActionEditorDisplayedButtons": {},
|
||||
"settingsViewerQuickActionEditorAvailableButtons": "Available Buttons",
|
||||
"@settingsViewerQuickActionEditorAvailableButtons": {},
|
||||
"settingsViewerQuickActionEmpty": "No buttons",
|
||||
"@settingsViewerQuickActionEmpty": {},
|
||||
|
||||
"settingsViewerOverlayTile": "Overlay",
|
||||
"@settingsViewerOverlayTile": {},
|
||||
"settingsViewerOverlayTitle": "Overlay",
|
||||
"@settingsViewerOverlayTitle": {},
|
||||
"settingsViewerShowOverlayOnOpening": "Show on opening",
|
||||
"@settingsViewerShowOverlayOnOpening": {},
|
||||
"settingsViewerShowMinimap": "Show minimap",
|
||||
"@settingsViewerShowMinimap": {},
|
||||
"settingsViewerShowInformation": "Show information",
|
||||
"@settingsViewerShowInformation": {},
|
||||
"settingsViewerShowInformationSubtitle": "Show title, date, location, etc.",
|
||||
"@settingsViewerShowInformationSubtitle": {},
|
||||
"settingsViewerShowShootingDetails": "Show shooting details",
|
||||
"@settingsViewerShowShootingDetails": {},
|
||||
"settingsViewerEnableOverlayBlurEffect": "Blur effect",
|
||||
"@settingsViewerEnableOverlayBlurEffect": {},
|
||||
|
||||
"settingsVideoPageTitle": "Video Settings",
|
||||
"@settingsVideoPageTitle": {},
|
||||
"settingsSectionVideo": "Video",
|
||||
"@settingsSectionVideo": {},
|
||||
"settingsVideoShowVideos": "Show videos",
|
||||
"@settingsVideoShowVideos": {},
|
||||
"settingsVideoEnableHardwareAcceleration": "Hardware acceleration",
|
||||
"@settingsVideoEnableHardwareAcceleration": {},
|
||||
"settingsVideoEnableAutoPlay": "Auto play",
|
||||
"@settingsVideoEnableAutoPlay": {},
|
||||
"settingsVideoLoopModeTile": "Loop mode",
|
||||
"@settingsVideoLoopModeTile": {},
|
||||
"settingsVideoLoopModeTitle": "Loop Mode",
|
||||
"@settingsVideoLoopModeTitle": {},
|
||||
"settingsVideoQuickActionsTile": "Quick actions for videos",
|
||||
"@settingsVideoQuickActionsTile": {},
|
||||
"settingsVideoQuickActionEditorTitle": "Quick Actions",
|
||||
"@settingsVideoQuickActionEditorTitle": {},
|
||||
|
||||
"settingsSubtitleThemeTile": "Subtitles",
|
||||
"@settingsSubtitleThemeTile": {},
|
||||
"settingsSubtitleThemeTitle": "Subtitles",
|
||||
"@settingsSubtitleThemeTitle": {},
|
||||
"settingsSubtitleThemeSample": "This is a sample.",
|
||||
"@settingsSubtitleThemeSample": {},
|
||||
"settingsSubtitleThemeTextAlignmentTile": "Text alignment",
|
||||
"@settingsSubtitleThemeTextAlignmentTile": {},
|
||||
"settingsSubtitleThemeTextAlignmentTitle": "Text Alignment",
|
||||
"@settingsSubtitleThemeTextAlignmentTitle": {},
|
||||
"settingsSubtitleThemeTextSize": "Text size",
|
||||
"@settingsSubtitleThemeTextSize": {},
|
||||
"settingsSubtitleThemeShowOutline": "Show outline and shadow",
|
||||
"@settingsSubtitleThemeShowOutline": {},
|
||||
"settingsSubtitleThemeTextColor": "Text color",
|
||||
"@settingsSubtitleThemeTextColor": {},
|
||||
"settingsSubtitleThemeTextOpacity": "Text opacity",
|
||||
"@settingsSubtitleThemeTextOpacity": {},
|
||||
"settingsSubtitleThemeBackgroundColor": "Background color",
|
||||
"@settingsSubtitleThemeBackgroundColor": {},
|
||||
"settingsSubtitleThemeBackgroundOpacity": "Background opacity",
|
||||
"@settingsSubtitleThemeBackgroundOpacity": {},
|
||||
"settingsSubtitleThemeTextAlignmentLeft": "Left",
|
||||
"@settingsSubtitleThemeTextAlignmentLeft": {},
|
||||
"settingsSubtitleThemeTextAlignmentCenter": "Center",
|
||||
"@settingsSubtitleThemeTextAlignmentCenter": {},
|
||||
"settingsSubtitleThemeTextAlignmentRight": "Right",
|
||||
"@settingsSubtitleThemeTextAlignmentRight": {},
|
||||
|
||||
"settingsSectionPrivacy": "Privacy",
|
||||
"@settingsSectionPrivacy": {},
|
||||
"settingsAllowInstalledAppAccess": "Allow access to app inventory",
|
||||
"@settingsAllowInstalledAppAccess": {},
|
||||
"settingsAllowInstalledAppAccessSubtitle": "Used to improve album display",
|
||||
"@settingsAllowInstalledAppAccessSubtitle": {},
|
||||
"settingsAllowErrorReporting": "Allow anonymous error reporting",
|
||||
"@settingsAllowErrorReporting": {},
|
||||
"settingsSaveSearchHistory": "Save search history",
|
||||
"@settingsSaveSearchHistory": {},
|
||||
|
||||
"settingsHiddenItemsTile": "Hidden items",
|
||||
"@settingsHiddenItemsTile": {},
|
||||
"settingsHiddenItemsTitle": "Hidden Items",
|
||||
"@settingsHiddenItemsTitle": {},
|
||||
|
||||
"settingsHiddenFiltersTitle": "Hidden Filters",
|
||||
"@settingsHiddenFiltersTitle": {},
|
||||
"settingsHiddenFiltersBanner": "Photos and videos matching hidden filters will not appear in your collection.",
|
||||
"@settingsHiddenFiltersBanner": {},
|
||||
"settingsHiddenFiltersEmpty": "No hidden filters",
|
||||
"@settingsHiddenFiltersEmpty": {},
|
||||
|
||||
"settingsHiddenPathsTitle": "Hidden Paths",
|
||||
"@settingsHiddenPathsTitle": {},
|
||||
"settingsHiddenPathsBanner": "Photos and videos in these folders, or any of their subfolders, will not appear in your collection.",
|
||||
"@settingsHiddenPathsBanner": {},
|
||||
"addPathTooltip": "Add path",
|
||||
"@addPathTooltip": {},
|
||||
|
||||
"settingsStorageAccessTile": "Storage access",
|
||||
"@settingsStorageAccessTile": {},
|
||||
"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": {},
|
||||
"settingsStorageAccessEmpty": "No access grants",
|
||||
"@settingsStorageAccessEmpty": {},
|
||||
"settingsStorageAccessRevokeTooltip": "Revoke",
|
||||
"@settingsStorageAccessRevokeTooltip": {},
|
||||
|
||||
"settingsSectionAccessibility": "Accessibility",
|
||||
"@settingsSectionAccessibility": {},
|
||||
"settingsRemoveAnimationsTile": "Remove animations",
|
||||
"@settingsRemoveAnimationsTile": {},
|
||||
"settingsRemoveAnimationsTitle": "Remove Animations",
|
||||
"@settingsRemoveAnimationsTitle": {},
|
||||
"settingsTimeToTakeActionTile": "Time to take action",
|
||||
"@settingsTimeToTakeActionTile": {},
|
||||
"settingsTimeToTakeActionTitle": "Time to Take Action",
|
||||
"@settingsTimeToTakeActionTitle": {},
|
||||
|
||||
"settingsSectionLanguage": "Language & Formats",
|
||||
"@settingsSectionLanguage": {},
|
||||
"settingsLanguage": "Language",
|
||||
"@settingsLanguage": {},
|
||||
"settingsCoordinateFormatTile": "Coordinate format",
|
||||
"@settingsCoordinateFormatTile": {},
|
||||
"settingsCoordinateFormatTitle": "Coordinate Format",
|
||||
"@settingsCoordinateFormatTitle": {},
|
||||
"settingsUnitSystemTile": "Units",
|
||||
"@settingsUnitSystemTile": {},
|
||||
"settingsUnitSystemTitle": "Units",
|
||||
"@settingsUnitSystemTitle": {},
|
||||
|
||||
"statsPageTitle": "Stats",
|
||||
"@statsPageTitle": {},
|
||||
"statsWithGps": "{count, plural, =1{1 item with location} other{{count} items with location}}",
|
||||
"@statsWithGps": {
|
||||
"placeholders": {
|
||||
|
@ -967,113 +617,64 @@
|
|||
}
|
||||
},
|
||||
"statsTopCountries": "Top Countries",
|
||||
"@statsTopCountries": {},
|
||||
"statsTopPlaces": "Top Places",
|
||||
"@statsTopPlaces": {},
|
||||
"statsTopTags": "Top Tags",
|
||||
"@statsTopTags": {},
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
|
||||
"@viewerOpenPanoramaButtonLabel": {},
|
||||
"viewerErrorUnknown": "Oops!",
|
||||
"@viewerErrorUnknown": {},
|
||||
"viewerErrorDoesNotExist": "The file no longer exists.",
|
||||
"@viewerErrorDoesNotExist": {},
|
||||
|
||||
"viewerInfoPageTitle": "Info",
|
||||
"@viewerInfoPageTitle": {},
|
||||
"viewerInfoBackToViewerTooltip": "Back to viewer",
|
||||
"@viewerInfoBackToViewerTooltip": {},
|
||||
|
||||
"viewerInfoUnknown": "unknown",
|
||||
"@viewerInfoUnknown": {},
|
||||
"viewerInfoLabelTitle": "Title",
|
||||
"@viewerInfoLabelTitle": {},
|
||||
"viewerInfoLabelDate": "Date",
|
||||
"@viewerInfoLabelDate": {},
|
||||
"viewerInfoLabelResolution": "Resolution",
|
||||
"@viewerInfoLabelResolution": {},
|
||||
"viewerInfoLabelSize": "Size",
|
||||
"@viewerInfoLabelSize": {},
|
||||
"viewerInfoLabelUri": "URI",
|
||||
"@viewerInfoLabelUri": {},
|
||||
"viewerInfoLabelPath": "Path",
|
||||
"@viewerInfoLabelPath": {},
|
||||
"viewerInfoLabelDuration": "Duration",
|
||||
"@viewerInfoLabelDuration": {},
|
||||
"viewerInfoLabelOwner": "Owned by",
|
||||
"@viewerInfoLabelOwner": {},
|
||||
"viewerInfoLabelCoordinates": "Coordinates",
|
||||
"@viewerInfoLabelCoordinates": {},
|
||||
"viewerInfoLabelAddress": "Address",
|
||||
"@viewerInfoLabelAddress": {},
|
||||
|
||||
"mapStyleTitle": "Map Style",
|
||||
"@mapStyleTitle": {},
|
||||
"mapStyleTooltip": "Select map style",
|
||||
"@mapStyleTooltip": {},
|
||||
"mapZoomInTooltip": "Zoom in",
|
||||
"@mapZoomInTooltip": {},
|
||||
"mapZoomOutTooltip": "Zoom out",
|
||||
"@mapZoomOutTooltip": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"mapEmptyRegion": "No images in this region",
|
||||
"@mapEmpty": {},
|
||||
|
||||
"viewerInfoOpenEmbeddedFailureFeedback": "Failed to extract embedded data",
|
||||
"@viewerInfoOpenEmbeddedFailureFeedback": {},
|
||||
"viewerInfoOpenLinkText": "Open",
|
||||
"@viewerInfoOpenLinkText": {},
|
||||
"viewerInfoViewXmlLinkText": "View XML",
|
||||
"@viewerInfoViewXmlLinkText": {},
|
||||
|
||||
"viewerInfoSearchFieldLabel": "Search metadata",
|
||||
"@viewerInfoSearchFieldLabel": {},
|
||||
"viewerInfoSearchEmpty": "No matching keys",
|
||||
"@viewerInfoSearchEmpty": {},
|
||||
"viewerInfoSearchSuggestionDate": "Date & time",
|
||||
"@viewerInfoSearchSuggestionDate": {},
|
||||
"viewerInfoSearchSuggestionDescription": "Description",
|
||||
"@viewerInfoSearchSuggestionDescription": {},
|
||||
"viewerInfoSearchSuggestionDimensions": "Dimensions",
|
||||
"@viewerInfoSearchSuggestionDimensions": {},
|
||||
"viewerInfoSearchSuggestionResolution": "Resolution",
|
||||
"@viewerInfoSearchSuggestionResolution": {},
|
||||
"viewerInfoSearchSuggestionRights": "Rights",
|
||||
"@viewerInfoSearchSuggestionRights": {},
|
||||
|
||||
"tagEditorPageTitle": "Edit Tags",
|
||||
"@tagEditorPageTitle": {},
|
||||
"tagEditorPageNewTagFieldLabel": "New tag",
|
||||
"@tagEditorPageNewTagFieldLabel": {},
|
||||
"tagEditorPageAddTagTooltip": "Add tag",
|
||||
"@tagEditorPageAddTagTooltip": {},
|
||||
"tagEditorSectionRecent": "Recent",
|
||||
"@tagEditorSectionRecent": {},
|
||||
|
||||
"panoramaEnableSensorControl": "Enable sensor control",
|
||||
"@panoramaEnableSensorControl": {},
|
||||
"panoramaDisableSensorControl": "Disable sensor control",
|
||||
"@panoramaDisableSensorControl": {},
|
||||
|
||||
"sourceViewerPageTitle": "Source",
|
||||
"@sourceViewerPageTitle": {},
|
||||
|
||||
"filePickerShowHiddenFiles": "Show hidden files",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Don’t show hidden files",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"filePickerOpenFrom": "Open from",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "No items",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerUseThisFolder": "Use this folder",
|
||||
"@filePickerUseThisFolder": {}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"hideButtonLabel": "MASQUER",
|
||||
"continueButtonLabel": "CONTINUER",
|
||||
|
||||
"cancelTooltip": "Annuler",
|
||||
"changeTooltip": "Modifier",
|
||||
"clearTooltip": "Effacer",
|
||||
"previousTooltip": "Précédent",
|
||||
|
@ -202,14 +203,20 @@
|
|||
"genericSuccessFeedback": "Succès !",
|
||||
"genericFailureFeedback": "Échec",
|
||||
|
||||
"menuActionSort": "Trier",
|
||||
"menuActionGroup": "Grouper",
|
||||
"menuActionConfigureView": "Vue",
|
||||
"menuActionSelect": "Sélectionner",
|
||||
"menuActionSelectAll": "Tout sélectionner",
|
||||
"menuActionSelectNone": "Tout désélectionner",
|
||||
"menuActionMap": "Carte",
|
||||
"menuActionStats": "Statistiques",
|
||||
|
||||
"viewDialogTabSort": "Tri",
|
||||
"viewDialogTabGroup": "Groupes",
|
||||
"viewDialogTabLayout": "Vue",
|
||||
|
||||
"tileLayoutGrid": "Grille",
|
||||
"tileLayoutList": "Liste",
|
||||
|
||||
"aboutPageTitle": "À propos",
|
||||
"aboutLinkSources": "Sources",
|
||||
"aboutLinkLicense": "Licence",
|
||||
|
@ -260,12 +267,10 @@
|
|||
|
||||
"collectionSearchTitlesHintText": "Recherche de titres",
|
||||
|
||||
"collectionSortTitle": "Trier",
|
||||
"collectionSortDate": "par date",
|
||||
"collectionSortSize": "par taille",
|
||||
"collectionSortName": "alphabétiquement",
|
||||
"collectionSortName": "alphabétique",
|
||||
|
||||
"collectionGroupTitle": "Grouper",
|
||||
"collectionGroupAlbum": "par album",
|
||||
"collectionGroupMonth": "par mois",
|
||||
"collectionGroupDay": "par jour",
|
||||
|
@ -301,12 +306,10 @@
|
|||
"drawerCollectionRaws": "Photos Raw",
|
||||
"drawerCollectionSphericalVideos": "Vidéos à 360°",
|
||||
|
||||
"chipSortTitle": "Trier",
|
||||
"chipSortDate": "par date",
|
||||
"chipSortName": "par nom",
|
||||
"chipSortName": "alphabétique",
|
||||
"chipSortCount": "par nombre d’éléments",
|
||||
|
||||
"albumGroupTitle": "Grouper",
|
||||
"albumGroupTier": "par importance",
|
||||
"albumGroupVolume": "par volume de stockage",
|
||||
"albumGroupNone": "ne pas grouper",
|
||||
|
@ -378,6 +381,7 @@
|
|||
"settingsSectionViewer": "Visionneuse",
|
||||
"settingsViewerUseCutout": "Utiliser la zone d’encoche",
|
||||
"settingsViewerMaximumBrightness": "Luminosité maximale",
|
||||
"settingsMotionPhotoAutoPlay": "Lecture automatique des photos animées",
|
||||
"settingsImageBackground": "Arrière-plan de l’image",
|
||||
|
||||
"settingsViewerQuickActionsTile": "Actions rapides",
|
||||
|
@ -513,7 +517,6 @@
|
|||
"panoramaDisableSensorControl": "Désactiver le contrôle par capteurs",
|
||||
|
||||
"sourceViewerPageTitle": "Code source",
|
||||
"@sourceViewerPageTitle": {},
|
||||
|
||||
"filePickerShowHiddenFiles": "Afficher les fichiers masqués",
|
||||
"filePickerDoNotShowHiddenFiles": "Ne pas afficher les fichiers masqués",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"hideButtonLabel": "숨기기",
|
||||
"continueButtonLabel": "다음",
|
||||
|
||||
"cancelTooltip": "취소",
|
||||
"changeTooltip": "변경",
|
||||
"clearTooltip": "초기화",
|
||||
"previousTooltip": "이전",
|
||||
|
@ -202,14 +203,20 @@
|
|||
"genericSuccessFeedback": "정상 처리됐습니다",
|
||||
"genericFailureFeedback": "오류가 발생했습니다",
|
||||
|
||||
"menuActionSort": "정렬",
|
||||
"menuActionGroup": "묶음",
|
||||
"menuActionConfigureView": "보기 설정",
|
||||
"menuActionSelect": "선택",
|
||||
"menuActionSelectAll": "모두 선택",
|
||||
"menuActionSelectNone": "모두 해제",
|
||||
"menuActionMap": "지도",
|
||||
"menuActionStats": "통계",
|
||||
|
||||
"viewDialogTabSort": "정렬",
|
||||
"viewDialogTabGroup": "묶음",
|
||||
"viewDialogTabLayout": "배치",
|
||||
|
||||
"tileLayoutGrid": "바둑판",
|
||||
"tileLayoutList": "목록",
|
||||
|
||||
"aboutPageTitle": "앱 정보",
|
||||
"aboutLinkSources": "소스 코드",
|
||||
"aboutLinkLicense": "라이선스",
|
||||
|
@ -260,12 +267,10 @@
|
|||
|
||||
"collectionSearchTitlesHintText": "제목 검색",
|
||||
|
||||
"collectionSortTitle": "정렬",
|
||||
"collectionSortDate": "날짜",
|
||||
"collectionSortSize": "크기",
|
||||
"collectionSortName": "이름",
|
||||
|
||||
"collectionGroupTitle": "묶음",
|
||||
"collectionGroupAlbum": "앨범별로",
|
||||
"collectionGroupMonth": "월별로",
|
||||
"collectionGroupDay": "날짜별로",
|
||||
|
@ -301,12 +306,10 @@
|
|||
"drawerCollectionRaws": "Raw 이미지",
|
||||
"drawerCollectionSphericalVideos": "360° 동영상",
|
||||
|
||||
"chipSortTitle": "정렬",
|
||||
"chipSortDate": "날짜",
|
||||
"chipSortName": "이름",
|
||||
"chipSortCount": "항목수",
|
||||
|
||||
"albumGroupTitle": "묶음",
|
||||
"albumGroupTier": "단계별로",
|
||||
"albumGroupVolume": "저장공간별로",
|
||||
"albumGroupNone": "묶음 없음",
|
||||
|
@ -378,6 +381,7 @@
|
|||
"settingsSectionViewer": "뷰어",
|
||||
"settingsViewerUseCutout": "컷아웃 영역 사용",
|
||||
"settingsViewerMaximumBrightness": "최대 밝기",
|
||||
"settingsMotionPhotoAutoPlay": "모션 포토 자동 재생",
|
||||
"settingsImageBackground": "이미지 배경",
|
||||
|
||||
"settingsViewerQuickActionsTile": "빠른 작업",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"hideButtonLabel": "СКРЫТЬ",
|
||||
"continueButtonLabel": "ПРОДОЛЖИТЬ",
|
||||
|
||||
"cancelTooltip": "Отмена",
|
||||
"changeTooltip": "Изменить",
|
||||
"clearTooltip": "Очистить",
|
||||
"previousTooltip": "Предыдущий",
|
||||
|
@ -22,6 +23,7 @@
|
|||
"showTooltip": "Показать",
|
||||
"hideTooltip": "Скрыть",
|
||||
"removeTooltip": "Удалить",
|
||||
"resetButtonTooltip": "Сбросить",
|
||||
|
||||
"doubleBackExitMessage": "Нажмите «назад» еще раз, чтобы выйти.",
|
||||
|
||||
|
@ -71,6 +73,7 @@
|
|||
"videoActionSettings": "Настройки",
|
||||
|
||||
"entryInfoActionEditDate": "Изменить дату и время",
|
||||
"entryInfoActionEditTags": "Изменить теги",
|
||||
"entryInfoActionRemoveMetadata": "Удалить метаданные",
|
||||
|
||||
"filterFavouriteLabel": "Избранное",
|
||||
|
@ -126,10 +129,10 @@
|
|||
|
||||
"storageVolumeDescriptionFallbackPrimary": "Внутренняя память",
|
||||
"storageVolumeDescriptionFallbackNonPrimary": "SD-карта",
|
||||
"rootDirectoryDescription": "корень",
|
||||
"otherDirectoryDescription": "“{name}” каталог",
|
||||
"rootDirectoryDescription": "корневой каталог",
|
||||
"otherDirectoryDescription": "каталог «{name}»",
|
||||
"storageAccessDialogTitle": "Доступ к хранилищу",
|
||||
"storageAccessDialogMessage": "Пожалуйста, выберите каталог {directory} на накопителе «{volume}» на следующем экране, чтобы предоставить этому приложению доступ к нему.",
|
||||
"storageAccessDialogMessage": "Пожалуйста, выберите {directory} на накопителе «{volume}» на следующем экране, чтобы предоставить этому приложению доступ к нему.",
|
||||
"restrictedAccessDialogTitle": "Ограниченный доступ",
|
||||
"restrictedAccessDialogMessage": "Этому приложению не разрешается изменять файлы в каталоге {directory} накопителя «{volume}».\n\nПожалуйста, используйте предустановленный файловый менеджер или галерею, чтобы переместить элементы в другой каталог.",
|
||||
"notEnoughSpaceDialogTitle": "Недостаточно свободного места.",
|
||||
|
@ -200,14 +203,20 @@
|
|||
"genericSuccessFeedback": "Выполнено!",
|
||||
"genericFailureFeedback": "Не удалось",
|
||||
|
||||
"menuActionSort": "Сортировка",
|
||||
"menuActionGroup": "Группировка",
|
||||
"menuActionConfigureView": "Вид",
|
||||
"menuActionSelect": "Выбрать",
|
||||
"menuActionSelectAll": "Выбрать все",
|
||||
"menuActionSelectNone": "Снять выделение",
|
||||
"menuActionMap": "Карта",
|
||||
"menuActionStats": "Статистика",
|
||||
|
||||
"viewDialogTabSort": "Сортировка",
|
||||
"viewDialogTabGroup": "Группировка",
|
||||
"viewDialogTabLayout": "Макет",
|
||||
|
||||
"tileLayoutGrid": "Сетка",
|
||||
"tileLayoutList": "Список",
|
||||
|
||||
"aboutPageTitle": "О нас",
|
||||
"aboutLinkSources": "Исходники",
|
||||
"aboutLinkLicense": "Лицензия",
|
||||
|
@ -258,12 +267,10 @@
|
|||
|
||||
"collectionSearchTitlesHintText": "Поиск заголовков",
|
||||
|
||||
"collectionSortTitle": "Сортировка",
|
||||
"collectionSortDate": "По дате",
|
||||
"collectionSortSize": "По размеру",
|
||||
"collectionSortName": "По имени альбома и файла",
|
||||
|
||||
"collectionGroupTitle": "Группировка",
|
||||
"collectionGroupAlbum": "По альбому",
|
||||
"collectionGroupMonth": "По месяцу",
|
||||
"collectionGroupDay": "По дню",
|
||||
|
@ -299,12 +306,10 @@
|
|||
"drawerCollectionRaws": "RAW",
|
||||
"drawerCollectionSphericalVideos": "360° видео",
|
||||
|
||||
"chipSortTitle": "Сортировка",
|
||||
"chipSortDate": "По дате",
|
||||
"chipSortName": "По названию",
|
||||
"chipSortCount": "По количеству объектов",
|
||||
|
||||
"albumGroupTitle": "Группировка",
|
||||
"albumGroupTier": "По уровню",
|
||||
"albumGroupVolume": "По накопителю",
|
||||
"albumGroupNone": "Не группировать",
|
||||
|
@ -375,6 +380,8 @@
|
|||
|
||||
"settingsSectionViewer": "Просмотрщик",
|
||||
"settingsViewerUseCutout": "Использовать область выреза",
|
||||
"settingsViewerMaximumBrightness": "Максимальная яркость",
|
||||
"settingsMotionPhotoAutoPlay": "Автовоспроизведение «Живых фото»",
|
||||
"settingsImageBackground": "Фон изображения",
|
||||
|
||||
"settingsViewerQuickActionsTile": "Быстрые действия",
|
||||
|
@ -501,6 +508,9 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Разрешение",
|
||||
"viewerInfoSearchSuggestionRights": "Права",
|
||||
|
||||
"tagEditorPageTitle": "Изменить теги",
|
||||
"tagEditorPageNewTagFieldLabel": "Новый тег",
|
||||
"tagEditorPageAddTagTooltip": "Добавить тег",
|
||||
"tagEditorSectionRecent": "Недавние",
|
||||
|
||||
"panoramaEnableSensorControl": "Включить сенсорное управление",
|
||||
|
|
1
lib/l10n/l10n.dart
Normal file
1
lib/l10n/l10n.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
enum ChipSetAction {
|
||||
// general
|
||||
sort,
|
||||
group,
|
||||
configureView,
|
||||
select,
|
||||
selectAll,
|
||||
selectNone,
|
||||
|
@ -27,8 +26,7 @@ enum ChipSetAction {
|
|||
|
||||
class ChipSetActions {
|
||||
static const general = [
|
||||
ChipSetAction.sort,
|
||||
ChipSetAction.group,
|
||||
ChipSetAction.configureView,
|
||||
ChipSetAction.select,
|
||||
ChipSetAction.selectAll,
|
||||
ChipSetAction.selectNone,
|
||||
|
@ -57,10 +55,8 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
return context.l10n.menuActionSort;
|
||||
case ChipSetAction.group:
|
||||
return context.l10n.menuActionGroup;
|
||||
case ChipSetAction.configureView:
|
||||
return context.l10n.menuActionConfigureView;
|
||||
case ChipSetAction.select:
|
||||
return context.l10n.menuActionSelect;
|
||||
case ChipSetAction.selectAll:
|
||||
|
@ -101,10 +97,8 @@ extension ExtraChipSetAction on ChipSetAction {
|
|||
IconData _getIconData() {
|
||||
switch (this) {
|
||||
// general
|
||||
case ChipSetAction.sort:
|
||||
return AIcons.sort;
|
||||
case ChipSetAction.group:
|
||||
return AIcons.group;
|
||||
case ChipSetAction.configureView:
|
||||
return AIcons.view;
|
||||
case ChipSetAction.select:
|
||||
return AIcons.select;
|
||||
case ChipSetAction.selectAll:
|
||||
|
|
|
@ -4,8 +4,7 @@ import 'package:flutter/material.dart';
|
|||
|
||||
enum EntrySetAction {
|
||||
// general
|
||||
sort,
|
||||
group,
|
||||
configureView,
|
||||
select,
|
||||
selectAll,
|
||||
selectNone,
|
||||
|
@ -32,8 +31,7 @@ enum EntrySetAction {
|
|||
|
||||
class EntrySetActions {
|
||||
static const general = [
|
||||
EntrySetAction.sort,
|
||||
EntrySetAction.group,
|
||||
EntrySetAction.configureView,
|
||||
EntrySetAction.select,
|
||||
EntrySetAction.selectAll,
|
||||
EntrySetAction.selectNone,
|
||||
|
@ -63,10 +61,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
String getText(BuildContext context) {
|
||||
switch (this) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
return context.l10n.menuActionSort;
|
||||
case EntrySetAction.group:
|
||||
return context.l10n.menuActionGroup;
|
||||
case EntrySetAction.configureView:
|
||||
return context.l10n.menuActionConfigureView;
|
||||
case EntrySetAction.select:
|
||||
return context.l10n.menuActionSelect;
|
||||
case EntrySetAction.selectAll:
|
||||
|
@ -119,10 +115,8 @@ extension ExtraEntrySetAction on EntrySetAction {
|
|||
IconData _getIconData() {
|
||||
switch (this) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
return AIcons.sort;
|
||||
case EntrySetAction.group:
|
||||
return AIcons.group;
|
||||
case EntrySetAction.configureView:
|
||||
return AIcons.view;
|
||||
case EntrySetAction.select:
|
||||
return AIcons.select;
|
||||
case EntrySetAction.selectAll:
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:github/github.dart';
|
||||
import 'package:google_api_availability/google_api_availability.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
|
|
@ -703,8 +703,8 @@ class AvesEntry {
|
|||
|
||||
Future<bool> delete() {
|
||||
final completer = Completer<bool>();
|
||||
mediaFileService.delete([this]).listen(
|
||||
(event) => completer.complete(event.success),
|
||||
mediaFileService.delete(entries: {this}).listen(
|
||||
(event) => completer.complete(event.success && !event.skipped),
|
||||
onError: completer.completeError,
|
||||
onDone: () {
|
||||
if (!completer.isCompleted) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/image_providers/region_provider.dart';
|
||||
import 'package:aves/image_providers/thumbnail_provider.dart';
|
||||
|
|
|
@ -44,12 +44,11 @@ class AlbumFilter extends CollectionFilter {
|
|||
String getTooltip(BuildContext context) => album;
|
||||
|
||||
@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(
|
||||
context: context,
|
||||
albumPath: album,
|
||||
size: size,
|
||||
embossed: embossed,
|
||||
) ??
|
||||
(showGenericIcon ? Icon(AIcons.album, size: size) : null);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/coordinate_format.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:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:latlong2/latlong.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);
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -3,7 +3,6 @@ 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/widgets.dart';
|
||||
|
||||
class FavouriteFilter extends CollectionFilter {
|
||||
static const type = 'favourite';
|
||||
|
@ -30,7 +29,7 @@ class FavouriteFilter extends CollectionFilter {
|
|||
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
|
||||
|
||||
@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
|
||||
Future<Color> color(BuildContext context) => SynchronousFuture(Colors.red);
|
||||
|
|
|
@ -81,7 +81,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
|
||||
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)));
|
||||
|
||||
|
|
|
@ -58,15 +58,13 @@ class LocationFilter extends CollectionFilter {
|
|||
String getLabel(BuildContext context) => _location.isEmpty ? context.l10n.filterLocationEmptyLabel : _location;
|
||||
|
||||
@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) {
|
||||
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) {
|
||||
return Text(
|
||||
flag,
|
||||
style: TextStyle(fontSize: size, shadows: const []),
|
||||
style: TextStyle(fontSize: size),
|
||||
textScaleFactor: 1.0,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ class MimeFilter extends CollectionFilter {
|
|||
}
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -64,7 +64,7 @@ class QueryFilter extends CollectionFilter {
|
|||
String get universalLabel => query;
|
||||
|
||||
@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
|
||||
Future<Color> color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor);
|
||||
|
|
|
@ -44,7 +44,7 @@ class TagFilter extends CollectionFilter {
|
|||
String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterTagEmptyLabel : tag;
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -94,7 +94,7 @@ class TypeFilter extends CollectionFilter {
|
|||
}
|
||||
|
||||
@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
|
||||
String get category => type;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:aves/utils/geo_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ class SettingsDefaults {
|
|||
static const mustBackTwiceToExit = true;
|
||||
static const keepScreenOn = KeepScreenOn.viewerOnly;
|
||||
static const homePage = HomePageSetting.collection;
|
||||
static const tileLayout = TileLayout.grid;
|
||||
|
||||
// drawer
|
||||
static final drawerTypeBookmarks = [
|
||||
|
@ -66,6 +67,7 @@ class SettingsDefaults {
|
|||
static const enableOverlayBlurEffect = true; // `enableOverlayBlurEffect` has a contextual default value
|
||||
static const viewerUseCutout = true;
|
||||
static const viewerMaxBrightness = false;
|
||||
static const enableMotionPhotoAutoPlay = false;
|
||||
|
||||
// video
|
||||
static const videoQuickActions = [
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry_set_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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
final Settings settings = Settings._private();
|
||||
|
@ -49,6 +49,7 @@ class Settings extends ChangeNotifier {
|
|||
static const homePageKey = 'home_page';
|
||||
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||
static const tileExtentPrefixKey = 'tile_extent_';
|
||||
static const tileLayoutPrefixKey = 'tile_layout_';
|
||||
|
||||
// drawer
|
||||
static const drawerTypeBookmarksKey = 'drawer_type_bookmarks';
|
||||
|
@ -82,6 +83,8 @@ class Settings extends ChangeNotifier {
|
|||
static const enableOverlayBlurEffectKey = 'enable_overlay_blur_effect';
|
||||
static const viewerUseCutoutKey = 'viewer_use_cutout';
|
||||
static const viewerMaxBrightnessKey = 'viewer_max_brightness';
|
||||
static const enableMotionPhotoAutoPlayKey = 'motion_photo_auto_play';
|
||||
static const imageBackgroundKey = 'image_background';
|
||||
|
||||
// video
|
||||
static const videoQuickActionsKey = 'video_quick_actions';
|
||||
|
@ -103,9 +106,6 @@ class Settings extends ChangeNotifier {
|
|||
static const coordinateFormatKey = 'coordinates_format';
|
||||
static const unitSystemKey = 'unit_system';
|
||||
|
||||
// rendering
|
||||
static const imageBackgroundKey = 'image_background';
|
||||
|
||||
// search
|
||||
static const saveSearchHistoryKey = 'save_search_history';
|
||||
static const searchHistoryKey = 'search_history';
|
||||
|
@ -217,12 +217,26 @@ class Settings extends ChangeNotifier {
|
|||
_appliedLocale = null;
|
||||
}
|
||||
|
||||
List<Locale> _systemLocalesFallback = [];
|
||||
|
||||
set systemLocalesFallback(List<Locale> locales) => _systemLocalesFallback = locales;
|
||||
|
||||
Locale? _appliedLocale;
|
||||
|
||||
Locale get appliedLocale {
|
||||
if (_appliedLocale == null) {
|
||||
final preferredLocale = locale;
|
||||
_appliedLocale = basicLocaleListResolution(preferredLocale != null ? [preferredLocale] : null, AppLocalizations.supportedLocales);
|
||||
final _locale = locale;
|
||||
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!;
|
||||
}
|
||||
|
@ -247,6 +261,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
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
|
||||
|
||||
List<CollectionFilter?> get drawerTypeBookmarks =>
|
||||
|
@ -360,6 +378,14 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
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
|
||||
|
||||
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());
|
||||
|
||||
// rendering
|
||||
|
||||
EntryBackground get imageBackground => getEnumOrDefault(imageBackgroundKey, SettingsDefaults.imageBackground, EntryBackground.values);
|
||||
|
||||
set imageBackground(EntryBackground newValue) => setAndNotify(imageBackgroundKey, newValue.toString());
|
||||
|
||||
// search
|
||||
|
||||
bool get saveSearchHistory => getBoolOrDefault(saveSearchHistoryKey, SettingsDefaults.saveSearchHistory);
|
||||
|
@ -570,6 +590,12 @@ class Settings extends ChangeNotifier {
|
|||
} else {
|
||||
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 {
|
||||
switch (key) {
|
||||
case subtitleTextColorKey:
|
||||
|
@ -602,6 +628,7 @@ class Settings extends ChangeNotifier {
|
|||
case enableOverlayBlurEffectKey:
|
||||
case viewerUseCutoutKey:
|
||||
case viewerMaxBrightnessKey:
|
||||
case enableMotionPhotoAutoPlayKey:
|
||||
case enableVideoHardwareAccelerationKey:
|
||||
case enableVideoAutoPlayKey:
|
||||
case subtitleShowOutlineKey:
|
||||
|
@ -622,12 +649,12 @@ class Settings extends ChangeNotifier {
|
|||
case albumSortFactorKey:
|
||||
case countrySortFactorKey:
|
||||
case tagSortFactorKey:
|
||||
case imageBackgroundKey:
|
||||
case videoLoopModeKey:
|
||||
case subtitleTextAlignmentKey:
|
||||
case infoMapStyleKey:
|
||||
case coordinateFormatKey:
|
||||
case unitSystemKey:
|
||||
case imageBackgroundKey:
|
||||
case accessibilityAnimationsKey:
|
||||
case timeToTakeActionKey:
|
||||
if (value is String) {
|
||||
|
|
|
@ -176,7 +176,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
processed.add,
|
||||
onError: (error) => reportService.recordError('renameEntry failed with error=$error', null),
|
||||
onDone: () async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final successOps = processed.where((e) => e.success && !e.skipped).toSet();
|
||||
if (successOps.isEmpty) {
|
||||
completer.complete(false);
|
||||
return;
|
||||
|
|
|
@ -7,3 +7,5 @@ enum AlbumChipGroupFactor { none, importance, volume }
|
|||
enum EntrySortFactor { date, size, name }
|
||||
|
||||
enum EntryGroupFactor { none, album, month, day }
|
||||
|
||||
enum TileLayout { grid, list }
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
extension ExtraSourceState on SourceState {
|
||||
String? getName(AppLocalizations l10n) {
|
||||
|
|
|
@ -14,6 +14,7 @@ class MimeTypes {
|
|||
|
||||
static const art = 'image/x-jg';
|
||||
static const djvu = 'image/vnd.djvu';
|
||||
static const jxl = 'image/jxl';
|
||||
static const psdVnd = 'image/vnd.adobe.photoshop';
|
||||
static const psdX = 'image/x-photoshop';
|
||||
|
||||
|
@ -47,6 +48,7 @@ class MimeTypes {
|
|||
static const mp2t = 'video/mp2t'; // .m2ts, .ts
|
||||
static const mp2ts = 'video/mp2ts'; // .ts (prefer `mp2t` when possible)
|
||||
static const mp4 = 'video/mp4';
|
||||
static const mpeg = 'video/mpeg';
|
||||
static const ogv = 'video/ogg';
|
||||
static const webm = 'video/webm';
|
||||
|
||||
|
@ -55,6 +57,7 @@ class MimeTypes {
|
|||
|
||||
// JB2, JPC, JPX?
|
||||
static const octetStream = 'application/octet-stream';
|
||||
static const zip = 'application/zip';
|
||||
|
||||
// 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};
|
||||
|
||||
// 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> _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};
|
||||
|
||||
|
|
|
@ -9,10 +9,13 @@ class XMP {
|
|||
'aux': 'Exif Aux',
|
||||
'avm': 'Astronomy Visualization',
|
||||
'Camera': 'Camera',
|
||||
'cc': 'Creative Commons',
|
||||
'crd': 'Camera Raw Defaults',
|
||||
'creatorAtom': 'After Effects',
|
||||
'crs': 'Camera Raw Settings',
|
||||
'dc': 'Dublin Core',
|
||||
'drone-dji': 'DJI Drone',
|
||||
'dwc': 'Darwin Core',
|
||||
'exif': 'Exif',
|
||||
'exifEX': 'Exif Ex',
|
||||
'GettyImagesGIFT': 'Getty Images',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/analysis_controller.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:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class AnalysisService {
|
||||
static const platform = MethodChannel('deckers.thibault/aves/analysis');
|
||||
|
@ -112,6 +112,7 @@ class Analyzer {
|
|||
stopSignal: ValueNotifier(false),
|
||||
);
|
||||
|
||||
settings.systemLocalesFallback = await deviceService.getLocales();
|
||||
_l10n = await AppLocalizations.delegate.load(settings.appliedLocale);
|
||||
_serviceStateNotifier.value = AnalyzerState.running;
|
||||
await _source.init();
|
||||
|
|
|
@ -44,6 +44,10 @@ class PlatformAndroidAppService implements AndroidAppService {
|
|||
if (kakaoTalk != null) {
|
||||
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;
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
|
@ -140,7 +144,7 @@ class PlatformAndroidAppService implements AndroidAppService {
|
|||
|
||||
@override
|
||||
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
|
||||
final urisByMimeType = groupBy<AvesEntry, String>(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
|
||||
try {
|
||||
|
|
|
@ -133,7 +133,7 @@ class AndroidDebugService {
|
|||
|
||||
static Future<Map> getMetadataExtractorSummary(AvesEntry entry) async {
|
||||
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>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
|
|
|
@ -3,65 +3,87 @@ import 'package:flutter/foundation.dart';
|
|||
|
||||
@immutable
|
||||
class ImageOpEvent extends Equatable {
|
||||
final bool success;
|
||||
final bool success, skipped;
|
||||
final String uri;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [success, uri];
|
||||
List<Object?> get props => [success, skipped, uri];
|
||||
|
||||
const ImageOpEvent({
|
||||
required this.success,
|
||||
required this.skipped,
|
||||
required this.uri,
|
||||
});
|
||||
|
||||
factory ImageOpEvent.fromMap(Map map) {
|
||||
final skipped = map['skipped'] ?? false;
|
||||
return ImageOpEvent(
|
||||
success: map['success'] ?? false,
|
||||
success: (map['success'] ?? false) || skipped,
|
||||
skipped: skipped,
|
||||
uri: map['uri'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class MoveOpEvent extends ImageOpEvent {
|
||||
final Map newFields;
|
||||
|
||||
const MoveOpEvent({required bool success, required String uri, required this.newFields})
|
||||
: super(
|
||||
@override
|
||||
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,
|
||||
skipped: skipped,
|
||||
uri: uri,
|
||||
);
|
||||
|
||||
factory MoveOpEvent.fromMap(Map map) {
|
||||
final newFields = map['newFields'] ?? {};
|
||||
final skipped = (map['skipped'] ?? false) || (newFields['skipped'] ?? false);
|
||||
return MoveOpEvent(
|
||||
success: map['success'] ?? false,
|
||||
success: (map['success'] ?? false) || skipped,
|
||||
skipped: skipped,
|
||||
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 {
|
||||
final int? pageId;
|
||||
|
||||
@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})
|
||||
: super(
|
||||
const ExportOpEvent({
|
||||
required bool success,
|
||||
required bool skipped,
|
||||
required String uri,
|
||||
this.pageId,
|
||||
required Map newFields,
|
||||
}) : super(
|
||||
success: success,
|
||||
skipped: skipped,
|
||||
uri: uri,
|
||||
newFields: newFields,
|
||||
);
|
||||
|
||||
factory ExportOpEvent.fromMap(Map map) {
|
||||
final newFields = map['newFields'] ?? {};
|
||||
final skipped = (map['skipped'] ?? false) || (newFields['skipped'] ?? false);
|
||||
return ExportOpEvent(
|
||||
success: map['success'] ?? false,
|
||||
success: (map['success'] ?? false) || skipped,
|
||||
skipped: skipped,
|
||||
uri: map['uri'],
|
||||
pageId: map['pageId'],
|
||||
newFields: map['newFields'] ?? {},
|
||||
newFields: newFields,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
|
@ -6,6 +8,8 @@ abstract class DeviceService {
|
|||
|
||||
Future<String?> getDefaultTimeZone();
|
||||
|
||||
Future<List<Locale>> getLocales();
|
||||
|
||||
Future<int> getPerformanceClass();
|
||||
}
|
||||
|
||||
|
@ -33,6 +37,26 @@ class PlatformDeviceService implements DeviceService {
|
|||
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
|
||||
Future<int> getPerformanceClass() async {
|
||||
try {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// names should match possible values on platform
|
||||
enum NameConflictStrategy { rename, replace, skip }
|
||||
|
||||
extension ExtraNameConflictStrategy on NameConflictStrategy {
|
||||
// TODO TLAD [dart 2.15] replace `describeEnum()` by `enum.name`
|
||||
String toPlatform() => describeEnum(this);
|
||||
String toPlatform() => name;
|
||||
|
||||
String getName(BuildContext context) {
|
||||
switch (this) {
|
||||
|
|
|
@ -15,6 +15,8 @@ import 'package:flutter/services.dart';
|
|||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
||||
abstract class MediaFileService {
|
||||
String get newOpId;
|
||||
|
||||
Future<AvesEntry?> getEntry(String uri, String? mimeType);
|
||||
|
||||
Future<Uint8List> getSvg(
|
||||
|
@ -68,10 +70,16 @@ abstract class MediaFileService {
|
|||
|
||||
Future<T>? resumeLoading<T>(Object taskKey);
|
||||
|
||||
Stream<ImageOpEvent> delete(Iterable<AvesEntry> entries);
|
||||
Future<void> cancelFileOp(String opId);
|
||||
|
||||
Stream<MoveOpEvent> move(
|
||||
Iterable<AvesEntry> entries, {
|
||||
Stream<ImageOpEvent> delete({
|
||||
String? opId,
|
||||
required Iterable<AvesEntry> entries,
|
||||
});
|
||||
|
||||
Stream<MoveOpEvent> move({
|
||||
String? opId,
|
||||
required Iterable<AvesEntry> entries,
|
||||
required bool copy,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
|
@ -120,6 +128,9 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String get newOpId => DateTime.now().millisecondsSinceEpoch.toString();
|
||||
|
||||
@override
|
||||
Future<AvesEntry?> getEntry(String uri, String? mimeType) async {
|
||||
try {
|
||||
|
@ -194,7 +205,9 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
// `await` here, so that `completeError` will be caught below
|
||||
return await completer.future;
|
||||
} on PlatformException catch (e, stack) {
|
||||
await reportService.recordError(e, stack);
|
||||
if (!MimeTypes.knownMediaTypes.contains(mimeType)) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
@ -296,10 +309,25 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
Future<T>? resumeLoading<T>(Object taskKey) => servicePolicy.resume<T>(taskKey);
|
||||
|
||||
@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 {
|
||||
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'delete',
|
||||
'id': opId,
|
||||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
}).map((event) => ImageOpEvent.fromMap(event));
|
||||
} on PlatformException catch (e, stack) {
|
||||
|
@ -309,8 +337,9 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
}
|
||||
|
||||
@override
|
||||
Stream<MoveOpEvent> move(
|
||||
Iterable<AvesEntry> entries, {
|
||||
Stream<MoveOpEvent> move({
|
||||
String? opId,
|
||||
required Iterable<AvesEntry> entries,
|
||||
required bool copy,
|
||||
required String destinationAlbum,
|
||||
required NameConflictStrategy nameConflictStrategy,
|
||||
|
@ -318,6 +347,7 @@ class PlatformMediaFileService implements MediaFileService {
|
|||
try {
|
||||
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'move',
|
||||
'id': opId,
|
||||
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||
'copy': copy,
|
||||
'destinationPath': destinationAlbum,
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:aves/model/entry.dart';
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/string_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ abstract class StorageService {
|
|||
// return whether operation succeeded (`null` if user cancelled)
|
||||
Future<bool?> createFile(String name, String mimeType, Uint8List bytes);
|
||||
|
||||
Future<Uint8List> openFile(String mimeType);
|
||||
Future<Uint8List> openFile([String? mimeType]);
|
||||
}
|
||||
|
||||
class PlatformStorageService implements StorageService {
|
||||
|
@ -231,7 +231,7 @@ class PlatformStorageService implements StorageService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<Uint8List> openFile(String mimeType) async {
|
||||
Future<Uint8List> openFile([String? mimeType]) async {
|
||||
try {
|
||||
final completer = Completer<Uint8List>.sync();
|
||||
final sink = OutputBuffer();
|
||||
|
|
|
@ -59,6 +59,7 @@ class Durations {
|
|||
static const collectionScrollMonitoringTimerDelay = Duration(milliseconds: 100);
|
||||
static const highlightJumpDelay = Duration(milliseconds: 400);
|
||||
static const highlightScrollInitDelay = Duration(milliseconds: 800);
|
||||
static const motionPhotoAutoPlayDelay = Duration(milliseconds: 700);
|
||||
static const videoOverlayHideDelay = Duration(milliseconds: 500);
|
||||
static const videoProgressTimerInterval = Duration(milliseconds: 300);
|
||||
static const doubleBackTimerDelay = Duration(milliseconds: 1000);
|
||||
|
|
|
@ -32,10 +32,16 @@ class AIcons {
|
|||
static const IconData tag = Icons.local_offer_outlined;
|
||||
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
|
||||
static const IconData add = Icons.add_circle_outline;
|
||||
static const IconData addShortcut = Icons.add_to_home_screen_outlined;
|
||||
static const IconData addTag = MdiIcons.tagPlusOutline;
|
||||
static const IconData cancel = Icons.cancel_outlined;
|
||||
static const IconData replay10 = Icons.replay_10_outlined;
|
||||
static const IconData skip10 = Icons.forward_10_outlined;
|
||||
static const IconData captureFrame = Icons.screenshot_outlined;
|
||||
|
@ -53,7 +59,6 @@ class AIcons {
|
|||
static const IconData filterOff = MdiIcons.filterOffOutline;
|
||||
static const IconData geoBounds = Icons.public_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 import = MdiIcons.fileImportOutline;
|
||||
static const IconData info = Icons.info_outlined;
|
||||
|
@ -79,7 +84,6 @@ class AIcons {
|
|||
static const IconData setCover = MdiIcons.imageEditOutline;
|
||||
static const IconData share = Icons.share_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 stats = Icons.pie_chart_outlined;
|
||||
static const IconData streams = Icons.translate_outlined;
|
||||
|
@ -87,6 +91,7 @@ class AIcons {
|
|||
static const IconData streamAudio = Icons.audiotrack_outlined;
|
||||
static const IconData streamText = Icons.closed_caption_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 zoomOut = Icons.remove_outlined;
|
||||
static const IconData collapse = Icons.expand_less_outlined;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class Themes {
|
||||
static const _accentColor = Colors.indigoAccent;
|
||||
|
|
|
@ -2,11 +2,10 @@ import 'dart:ui';
|
|||
|
||||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
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
|
||||
static const overflowStrutStyle = StrutStyle(height: 1.3);
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/basic/link_chip.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);
|
||||
|
||||
static const translators = {
|
||||
'Deutsch': 'JanWaldhorn',
|
||||
'Русский': 'D3ZOXY',
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/app_mode.dart';
|
||||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/settings/accessibility_animations.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/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:overlay_support/overlay_support.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
@ -122,9 +121,7 @@ class _AvesAppState extends State<AvesApp> {
|
|||
darkTheme: Themes.darkTheme,
|
||||
themeMode: ThemeMode.dark,
|
||||
locale: settingsLocale,
|
||||
localizationsDelegates: const [
|
||||
...AppLocalizations.localizationsDelegates,
|
||||
],
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
// checkerboardRasterCacheImages: true,
|
||||
// checkerboardOffscreenLayers: true,
|
||||
|
@ -200,7 +197,7 @@ class _AvesAppState extends State<AvesApp> {
|
|||
? 'profile'
|
||||
: 'debug',
|
||||
'has_play_services': hasPlayServices,
|
||||
'locales': window.locales.join(', '),
|
||||
'locales': WidgetsBinding.instance!.window.locales.join(', '),
|
||||
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
|
||||
});
|
||||
_navigatorObservers = [
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.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/filter_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/basic/menu.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:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.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')
|
||||
// TODO TLAD [dart 2.15] replace `describeEnum()` by `enum.name`
|
||||
Key _getActionKey(EntrySetAction action) => Key('menu-${describeEnum(action)}');
|
||||
// key is expected by test driver (e.g. 'menu-configureView', 'menu-map')
|
||||
Key _getActionKey(EntrySetAction action) => Key('menu-${action.name}');
|
||||
|
||||
Widget _toActionButton(EntrySetAction action, {required bool enabled}) {
|
||||
final onPressed = enabled ? () => _onActionSelected(action) : null;
|
||||
|
@ -397,11 +396,8 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
Future<void> _onActionSelected(EntrySetAction action) async {
|
||||
switch (action) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
await _sort();
|
||||
break;
|
||||
case EntrySetAction.group:
|
||||
await _group();
|
||||
case EntrySetAction.configureView:
|
||||
await _configureView();
|
||||
break;
|
||||
case EntrySetAction.select:
|
||||
context.read<Selection<AvesEntry>>().select();
|
||||
|
@ -436,47 +432,42 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _sort() async {
|
||||
final value = await showDialog<EntrySortFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<EntrySortFactor>(
|
||||
initialValue: settings.collectionSortFactor,
|
||||
options: {
|
||||
EntrySortFactor.date: context.l10n.collectionSortDate,
|
||||
EntrySortFactor.size: context.l10n.collectionSortSize,
|
||||
EntrySortFactor.name: context.l10n.collectionSortName,
|
||||
},
|
||||
title: context.l10n.collectionSortTitle,
|
||||
),
|
||||
Future<void> _configureView() async {
|
||||
final initialValue = Tuple3(
|
||||
settings.collectionSortFactor,
|
||||
settings.collectionSectionFactor,
|
||||
settings.getTileLayout(CollectionPage.routeName),
|
||||
);
|
||||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.collectionSortFactor = value;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _group() async {
|
||||
final value = await showDialog<EntryGroupFactor>(
|
||||
final value = await showDialog<Tuple3<EntrySortFactor?, EntryGroupFactor?, TileLayout?>>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return AvesSelectionDialog<EntryGroupFactor>(
|
||||
initialValue: settings.collectionSectionFactor,
|
||||
options: {
|
||||
return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
|
||||
initialValue: initialValue,
|
||||
sortOptions: {
|
||||
EntrySortFactor.date: l10n.collectionSortDate,
|
||||
EntrySortFactor.size: l10n.collectionSortSize,
|
||||
EntrySortFactor.name: l10n.collectionSortName,
|
||||
},
|
||||
groupOptions: {
|
||||
EntryGroupFactor.album: l10n.collectionGroupAlbum,
|
||||
EntryGroupFactor.month: l10n.collectionGroupMonth,
|
||||
EntryGroupFactor.day: l10n.collectionGroupDay,
|
||||
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
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.collectionSectionFactor = value;
|
||||
if (value != null && initialValue != value) {
|
||||
settings.collectionSortFactor = value.item1!;
|
||||
settings.collectionSectionFactor = value.item2!;
|
||||
settings.setTileLayout(CollectionPage.routeName, value.item3!);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/app_mode.dart';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/favourite.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/enums.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/widgets/collection/app_bar.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/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/insets.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/media_query.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/sliver.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/identity/empty.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/scaling.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -74,11 +76,13 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsRouteKey = context.read<TileExtentController>().settingsRouteKey;
|
||||
final tileLayout = context.select<Settings, TileLayout>((s) => s.getTileLayout(settingsRouteKey));
|
||||
return Consumer<CollectionLens>(
|
||||
builder: (context, collection, child) {
|
||||
final sectionedListLayoutProvider = ValueListenableBuilder<double>(
|
||||
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
|
||||
builder: (context, tileExtent, child) {
|
||||
builder: (context, thumbnailExtent, child) {
|
||||
return Selector<TileExtentController, Tuple3<double, int, double>>(
|
||||
selector: (context, c) => Tuple3(c.viewportSize.width, c.columnCount, c.spacing),
|
||||
builder: (context, c, child) {
|
||||
|
@ -89,22 +93,27 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
final target = context.read<DurationsData>().staggeredAnimationPageTarget;
|
||||
final tileAnimationDelay = context.read<TileExtentController>().getTileAnimationDelay(target);
|
||||
return GridTheme(
|
||||
extent: tileExtent,
|
||||
child: SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
scrollableWidth: scrollableWidth,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
tileExtent: tileExtent,
|
||||
tileBuilder: (entry) => InteractiveThumbnail(
|
||||
key: ValueKey(entry.contentId),
|
||||
extent: thumbnailExtent,
|
||||
child: EntryListDetailsTheme(
|
||||
extent: thumbnailExtent,
|
||||
child: SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileLayout: tileLayout,
|
||||
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,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
scrollController: PrimaryScrollController.of(context)!,
|
||||
tileLayout: tileLayout,
|
||||
),
|
||||
);
|
||||
return sectionedListLayoutProvider;
|
||||
|
@ -127,27 +137,28 @@ class _CollectionSectionedContent extends StatefulWidget {
|
|||
final CollectionLens collection;
|
||||
final ValueNotifier<bool> isScrollingNotifier;
|
||||
final ScrollController scrollController;
|
||||
final TileLayout tileLayout;
|
||||
|
||||
const _CollectionSectionedContent({
|
||||
required this.collection,
|
||||
required this.isScrollingNotifier,
|
||||
required this.scrollController,
|
||||
required this.tileLayout,
|
||||
});
|
||||
|
||||
@override
|
||||
_CollectionSectionedContentState createState() => _CollectionSectionedContentState();
|
||||
}
|
||||
|
||||
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> with WidgetsBindingObserver, GridItemTrackerMixin<AvesEntry, _CollectionSectionedContent> {
|
||||
class _CollectionSectionedContentState extends State<_CollectionSectionedContent> {
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
@override
|
||||
TileLayout get tileLayout => widget.tileLayout;
|
||||
|
||||
ScrollController get scrollController => widget.scrollController;
|
||||
|
||||
@override
|
||||
final ValueNotifier<double> appBarHeightNotifier = ValueNotifier(0);
|
||||
|
||||
@override
|
||||
final GlobalKey scrollableKey = GlobalKey(debugLabel: 'thumbnail-collection-scrollable');
|
||||
|
||||
@override
|
||||
|
@ -169,6 +180,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
|
|||
final scaler = _CollectionScaler(
|
||||
scrollableKey: scrollableKey,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
tileLayout: tileLayout,
|
||||
child: scrollView,
|
||||
);
|
||||
|
||||
|
@ -181,18 +193,26 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent
|
|||
child: scaler,
|
||||
);
|
||||
|
||||
return selector;
|
||||
return GridItemTracker<AvesEntry>(
|
||||
scrollableKey: scrollableKey,
|
||||
tileLayout: tileLayout,
|
||||
appBarHeightNotifier: appBarHeightNotifier,
|
||||
scrollController: scrollController,
|
||||
child: selector,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CollectionScaler extends StatelessWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final ValueNotifier<double> appBarHeightNotifier;
|
||||
final TileLayout tileLayout;
|
||||
final Widget child;
|
||||
|
||||
const _CollectionScaler({
|
||||
required this.scrollableKey,
|
||||
required this.appBarHeightNotifier,
|
||||
required this.tileLayout,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
|
@ -201,10 +221,12 @@ class _CollectionScaler extends StatelessWidget {
|
|||
final tileSpacing = context.select<TileExtentController, double>((controller) => controller.spacing);
|
||||
return GridScaleGestureDetector<AvesEntry>(
|
||||
scrollableKey: scrollableKey,
|
||||
tileLayout: tileLayout,
|
||||
heightForWidth: (width) => width,
|
||||
gridBuilder: (center, tileSize, child) => CustomPaint(
|
||||
painter: GridPainter(
|
||||
center: center,
|
||||
tileLayout: tileLayout,
|
||||
tileCenter: center,
|
||||
tileSize: tileSize,
|
||||
spacing: tileSpacing,
|
||||
borderWidth: DecoratedThumbnail.borderWidth,
|
||||
|
@ -213,11 +235,13 @@ class _CollectionScaler extends StatelessWidget {
|
|||
),
|
||||
child: child,
|
||||
),
|
||||
scaledBuilder: (entry, tileSize) => DecoratedThumbnail(
|
||||
entry: entry,
|
||||
tileExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
selectable: false,
|
||||
highlightable: false,
|
||||
scaledBuilder: (entry, tileSize) => EntryListDetailsTheme(
|
||||
extent: tileSize.height,
|
||||
child: Tile(
|
||||
entry: entry,
|
||||
thumbnailExtent: context.read<TileExtentController>().effectiveExtentMax,
|
||||
tileLayout: tileLayout,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
|
|
|
@ -36,7 +36,6 @@ import 'package:aves/widgets/search/search_delegate.dart';
|
|||
import 'package:aves/widgets/stats/stats_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
|
@ -51,10 +50,8 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
}) {
|
||||
switch (action) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
case EntrySetAction.configureView:
|
||||
return true;
|
||||
case EntrySetAction.group:
|
||||
return sortFactor == EntrySortFactor.date;
|
||||
case EntrySetAction.select:
|
||||
return appMode.canSelect && !isSelecting;
|
||||
case EntrySetAction.selectAll:
|
||||
|
@ -98,8 +95,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
final hasSelection = selectedItemCount > 0;
|
||||
|
||||
switch (action) {
|
||||
case EntrySetAction.sort:
|
||||
case EntrySetAction.group:
|
||||
case EntrySetAction.configureView:
|
||||
return true;
|
||||
case EntrySetAction.select:
|
||||
return hasItems;
|
||||
|
@ -133,8 +129,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
void onActionSelected(BuildContext context, EntrySetAction action) {
|
||||
switch (action) {
|
||||
// general
|
||||
case EntrySetAction.sort:
|
||||
case EntrySetAction.group:
|
||||
case EntrySetAction.configureView:
|
||||
case EntrySetAction.select:
|
||||
case EntrySetAction.selectAll:
|
||||
case EntrySetAction.selectNone:
|
||||
|
@ -227,7 +222,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.deleteEntriesConfirmationDialogMessage(todoCount)),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
@ -247,19 +241,23 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
if (!await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
|
||||
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
showOpReport<ImageOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.delete(selectedItems),
|
||||
opStream: mediaFileService.delete(opId: opId, entries: selectedItems),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => mediaFileService.cancelFileOp(opId),
|
||||
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);
|
||||
selection.browse();
|
||||
source.resumeMonitoring();
|
||||
|
||||
final deletedCount = deletedUris.length;
|
||||
if (deletedCount < todoCount) {
|
||||
final count = todoCount - deletedCount;
|
||||
final successCount = successOps.length;
|
||||
if (successCount < todoCount) {
|
||||
final count = todoCount - successCount;
|
||||
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 {
|
||||
final l10n = context.l10n;
|
||||
final source = context.read<CollectionSource>();
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
final selectedItems = _getExpandedSelectedItems(selection);
|
||||
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
||||
|
||||
final destinationAlbum = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<String>(
|
||||
settings: const RouteSettings(name: AlbumPickPage.routeName),
|
||||
builder: (context) => AlbumPickPage(source: source, moveType: moveType),
|
||||
),
|
||||
);
|
||||
if (destinationAlbum == null || destinationAlbum.isEmpty) return;
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
|
||||
if (destinationAlbum == null) return;
|
||||
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
|
||||
|
||||
if (moveType == MoveType.move && !await checkStoragePermissionForAlbums(context, selectionDirs, entries: selectedItems)) return;
|
||||
|
@ -323,19 +314,23 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
nameConflictStrategy = value;
|
||||
}
|
||||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
final opId = mediaFileService.newOpId;
|
||||
showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
opStream: mediaFileService.move(
|
||||
todoItems,
|
||||
opId: opId,
|
||||
entries: todoItems,
|
||||
copy: copy,
|
||||
destinationAlbum: destinationAlbum,
|
||||
nameConflictStrategy: nameConflictStrategy,
|
||||
),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => mediaFileService.cancelFileOp(opId),
|
||||
onDone: (processed) async {
|
||||
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(
|
||||
todoEntries: todoItems,
|
||||
copy: copy,
|
||||
|
@ -414,18 +409,25 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
var cancelled = false;
|
||||
showOpReport<ImageOpEvent>(
|
||||
context: context,
|
||||
opStream: Stream.fromIterable(todoItems).asyncMap((entry) async {
|
||||
final dataTypes = await op(entry);
|
||||
return ImageOpEvent(success: dataTypes.isNotEmpty, uri: entry.uri);
|
||||
if (cancelled) {
|
||||
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(),
|
||||
itemCount: todoCount,
|
||||
onCancel: () => cancelled = true,
|
||||
onDone: (processed) async {
|
||||
final successOps = processed.where((e) => e.success).toSet();
|
||||
final editedOps = successOps.where((e) => !e.skipped).toSet();
|
||||
selection.browse();
|
||||
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 successCount = successOps.length;
|
||||
|
@ -433,7 +435,7 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
final count = todoCount - successCount;
|
||||
showFeedback(context, l10n.collectionEditFailureFeedback(count));
|
||||
} else {
|
||||
final count = successCount;
|
||||
final count = editedOps.length;
|
||||
showFeedback(context, l10n.collectionEditSuccessFeedback(count));
|
||||
}
|
||||
},
|
||||
|
@ -457,7 +459,6 @@ class EntrySetActionDelegate with EntryEditorMixin, FeedbackMixin, PermissionAwa
|
|||
builder: (context) {
|
||||
final l10n = context.l10n;
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: l10n.unsupportedTypeDialogTitle,
|
||||
content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))),
|
||||
actions: [
|
||||
|
|
95
lib/widgets/collection/grid/list_details.dart
Normal file
95
lib/widgets/collection/grid/list_details.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
99
lib/widgets/collection/grid/list_details_theme.dart
Normal file
99
lib/widgets/collection/grid/list_details_theme.dart
Normal 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,
|
||||
});
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:aves/model/entry.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/widgets/collection/grid/headers/any.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
|
@ -12,6 +13,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
|
|||
Key? key,
|
||||
required this.collection,
|
||||
required double scrollableWidth,
|
||||
required TileLayout tileLayout,
|
||||
required int columnCount,
|
||||
required double spacing,
|
||||
required double tileExtent,
|
||||
|
@ -21,6 +23,7 @@ class SectionedEntryListLayoutProvider extends SectionedListLayoutProvider<AvesE
|
|||
}) : super(
|
||||
key: key,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileLayout: tileLayout,
|
||||
columnCount: columnCount,
|
||||
spacing: spacing,
|
||||
tileWidth: tileExtent,
|
||||
|
|
|
@ -2,32 +2,36 @@ import 'package:aves/app_mode.dart';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/selection.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/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/scaling.dart';
|
||||
import 'package:aves/widgets/common/grid/scaling.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class InteractiveThumbnail extends StatelessWidget {
|
||||
class InteractiveTile extends StatelessWidget {
|
||||
final CollectionLens collection;
|
||||
final AvesEntry entry;
|
||||
final double tileExtent;
|
||||
final double thumbnailExtent;
|
||||
final TileLayout tileLayout;
|
||||
final ValueNotifier<bool>? isScrollingNotifier;
|
||||
|
||||
const InteractiveThumbnail({
|
||||
const InteractiveTile({
|
||||
Key? key,
|
||||
required this.collection,
|
||||
required this.entry,
|
||||
required this.tileExtent,
|
||||
required this.thumbnailExtent,
|
||||
required this.tileLayout,
|
||||
this.isScrollingNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
key: ValueKey(entry.uri),
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
final appMode = context.read<ValueNotifier<AppMode>>().value;
|
||||
switch (appMode) {
|
||||
|
@ -51,13 +55,13 @@ class InteractiveThumbnail extends StatelessWidget {
|
|||
},
|
||||
child: MetaData(
|
||||
metaData: ScalerMetadata(entry),
|
||||
child: DecoratedThumbnail(
|
||||
child: Tile(
|
||||
entry: entry,
|
||||
tileExtent: tileExtent,
|
||||
// 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,
|
||||
thumbnailExtent: thumbnailExtent,
|
||||
tileLayout: tileLayout,
|
||||
selectable: true,
|
||||
highlightable: true,
|
||||
isScrollingNotifier: isScrollingNotifier,
|
||||
// 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)
|
||||
// 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,
|
||||
);
|
||||
}
|
|
@ -55,7 +55,6 @@ mixin EntryEditorMixin {
|
|||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
|
|
@ -6,6 +6,9 @@ import 'package:aves/model/settings/enums.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/accessibility_service.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:intl/intl.dart';
|
||||
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||
|
@ -15,7 +18,17 @@ mixin FeedbackMixin {
|
|||
void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
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
|
||||
|
@ -60,32 +73,36 @@ mixin FeedbackMixin {
|
|||
required BuildContext context,
|
||||
required Stream<T> opStream,
|
||||
required int itemCount,
|
||||
VoidCallback? onCancel,
|
||||
void Function(Set<T> processed)? onDone,
|
||||
}) {
|
||||
late OverlayEntry _opReportOverlayEntry;
|
||||
_opReportOverlayEntry = OverlayEntry(
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => ReportOverlay<T>(
|
||||
opStream: opStream,
|
||||
itemCount: itemCount,
|
||||
onCancel: onCancel,
|
||||
onDone: (processed) {
|
||||
_opReportOverlayEntry.remove();
|
||||
Navigator.of(context).pop();
|
||||
onDone?.call(processed);
|
||||
},
|
||||
),
|
||||
);
|
||||
Overlay.of(context)!.insert(_opReportOverlayEntry);
|
||||
}
|
||||
}
|
||||
|
||||
class ReportOverlay<T> extends StatefulWidget {
|
||||
final Stream<T> opStream;
|
||||
final int itemCount;
|
||||
final VoidCallback? onCancel;
|
||||
final void Function(Set<T> processed) onDone;
|
||||
|
||||
const ReportOverlay({
|
||||
Key? key,
|
||||
required this.opStream,
|
||||
required this.itemCount,
|
||||
required this.onCancel,
|
||||
required this.onDone,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -100,8 +117,9 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
|
|||
|
||||
Stream<T> get opStream => widget.opStream;
|
||||
|
||||
static const fontSize = 18.0;
|
||||
static const radius = 160.0;
|
||||
static const strokeWidth = 16.0;
|
||||
static const strokeWidth = 8.0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -136,52 +154,70 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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>(
|
||||
stream: opStream,
|
||||
builder: (context, snapshot) {
|
||||
final processedCount = processed.length.toDouble();
|
||||
final total = widget.itemCount;
|
||||
assert(processedCount <= total);
|
||||
final percent = min(1.0, processedCount / total);
|
||||
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate);
|
||||
return FadeTransition(
|
||||
opacity: _animation,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
Colors.black,
|
||||
Colors.black54,
|
||||
],
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: radius + 2,
|
||||
height: radius + 2,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xBB000000),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
if (animate)
|
||||
Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
padding: const EdgeInsets.all(strokeWidth / 2),
|
||||
child: CircularProgressIndicator(
|
||||
color: progressColor.withOpacity(.1),
|
||||
strokeWidth: strokeWidth,
|
||||
if (animate)
|
||||
Container(
|
||||
width: radius,
|
||||
height: radius,
|
||||
padding: const EdgeInsets.all(strokeWidth / 2),
|
||||
child: CircularProgressIndicator(
|
||||
color: progressColor.withOpacity(.1),
|
||||
strokeWidth: strokeWidth,
|
||||
),
|
||||
),
|
||||
CircularPercentIndicator(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -50,7 +50,6 @@ mixin PermissionAwareMixin {
|
|||
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
|
||||
final volume = dir.getVolumeDescription(context);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: context.l10n.storageAccessDialogTitle,
|
||||
content: Text(context.l10n.storageAccessDialogMessage(directory, volume)),
|
||||
actions: [
|
||||
|
@ -84,7 +83,6 @@ mixin PermissionAwareMixin {
|
|||
final directory = dir.relativeDir.isEmpty ? context.l10n.rootDirectoryDescription : context.l10n.otherDirectoryDescription(dir.relativeDir);
|
||||
final volume = dir.getVolumeDescription(context);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: context.l10n.restrictedAccessDialogTitle,
|
||||
content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)),
|
||||
actions: [
|
||||
|
|
|
@ -11,7 +11,6 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
mixin SizeAwareMixin {
|
||||
Future<bool> checkFreeSpaceForMove(
|
||||
|
@ -81,7 +80,6 @@ mixin SizeAwareMixin {
|
|||
final freeSize = formatFileSize(locale, free);
|
||||
final volume = destinationVolume.getDescription(context);
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
title: l10n.notEnoughSpaceDialogTitle,
|
||||
content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)),
|
||||
actions: [
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:highlight/highlight.dart' show highlight, Node;
|
||||
|
||||
// adapted from package `flutter_highlight` v0.7.0 `HighlightView`
|
||||
|
|
|
@ -71,7 +71,6 @@ class _ColorPickerDialogState extends State<ColorPickerDialog> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AvesDialog(
|
||||
context: context,
|
||||
scrollableContent: [
|
||||
ColorPicker(
|
||||
color: color,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
|
|
@ -2,9 +2,7 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/debouncer.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class QueryBar extends StatefulWidget {
|
||||
final ValueNotifier<String> queryNotifier;
|
||||
|
|
|
@ -132,18 +132,20 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
Matrix4? _lastTransform;
|
||||
|
||||
late Offset _initialFocalPoint;
|
||||
late Offset _currentFocalPoint;
|
||||
Offset? _currentFocalPoint;
|
||||
late double _initialSpan;
|
||||
late double _currentSpan;
|
||||
late double _initialHorizontalSpan;
|
||||
late double _currentHorizontalSpan;
|
||||
late double _initialVerticalSpan;
|
||||
late double _currentVerticalSpan;
|
||||
late Offset _localFocalPoint;
|
||||
_LineBetweenPointers? _initialLine;
|
||||
_LineBetweenPointers? _currentLine;
|
||||
late Map<int, Offset> _pointerLocations;
|
||||
late List<int> _pointerQueue; // A queue to sort pointers in order of entrance
|
||||
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
||||
late Offset _delta;
|
||||
|
||||
double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
|
||||
|
||||
|
@ -222,11 +224,28 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
void _update() {
|
||||
final int count = _pointerLocations.keys.length;
|
||||
|
||||
final Offset? previousFocalPoint = _currentFocalPoint;
|
||||
|
||||
// Compute the focal point
|
||||
Offset focalPoint = Offset.zero;
|
||||
for (final int pointer in _pointerLocations.keys) focalPoint += _pointerLocations[pointer]!;
|
||||
_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
|
||||
// spans are the average deviations from the focal point's horizontal and
|
||||
// vertical coordinates, respectively.
|
||||
|
@ -234,9 +253,9 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
double totalHorizontalDeviation = 0.0;
|
||||
double totalVerticalDeviation = 0.0;
|
||||
for (final int pointer in _pointerLocations.keys) {
|
||||
totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]!).distance;
|
||||
totalHorizontalDeviation += (_currentFocalPoint.dx - _pointerLocations[pointer]!.dx).abs();
|
||||
totalVerticalDeviation += (_currentFocalPoint.dy - _pointerLocations[pointer]!.dy).abs();
|
||||
totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance;
|
||||
totalHorizontalDeviation += (_currentFocalPoint!.dx - _pointerLocations[pointer]!.dx).abs();
|
||||
totalVerticalDeviation += (_currentFocalPoint!.dy - _pointerLocations[pointer]!.dy).abs();
|
||||
}
|
||||
_currentSpan = count > 0 ? totalDeviation / count : 0.0;
|
||||
_currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
|
||||
|
@ -273,7 +292,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
}
|
||||
|
||||
bool _reconfigure(int pointer) {
|
||||
_initialFocalPoint = _currentFocalPoint;
|
||||
_initialFocalPoint = _currentFocalPoint!;
|
||||
_initialSpan = _currentSpan;
|
||||
_initialLine = _currentLine;
|
||||
_initialHorizontalSpan = _currentHorizontalSpan;
|
||||
|
@ -288,7 +307,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length)));
|
||||
} else {
|
||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: Velocity.zero, pointerCount: _pointerQueue.length)));
|
||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerQueue.length)));
|
||||
}
|
||||
}
|
||||
_state = _ScaleState.accepted;
|
||||
|
@ -308,8 +327,8 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
|
||||
if (_state == _ScaleState.possible) {
|
||||
final double spanDelta = (_currentSpan - _initialSpan).abs();
|
||||
final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
|
||||
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind)) resolve(GestureDisposition.accepted);
|
||||
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
|
||||
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings)) resolve(GestureDisposition.accepted);
|
||||
} else if (_state.index >= _ScaleState.accepted.index) {
|
||||
resolve(GestureDisposition.accepted);
|
||||
}
|
||||
|
@ -325,11 +344,11 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
scale: _scaleFactor,
|
||||
horizontalScale: _horizontalScaleFactor,
|
||||
verticalScale: _verticalScaleFactor,
|
||||
focalPoint: _currentFocalPoint,
|
||||
localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
|
||||
focalPoint: _currentFocalPoint!,
|
||||
localFocalPoint: _localFocalPoint,
|
||||
rotation: _computeRotationFactor(),
|
||||
pointerCount: _pointerQueue.length,
|
||||
delta: _currentFocalPoint - _initialFocalPoint,
|
||||
focalPointDelta: _delta,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
@ -339,8 +358,8 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
if (onStart != null)
|
||||
invokeCallback<void>('onStart', () {
|
||||
onStart!(ScaleStartDetails(
|
||||
focalPoint: _currentFocalPoint,
|
||||
localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
|
||||
focalPoint: _currentFocalPoint!,
|
||||
localFocalPoint: _localFocalPoint,
|
||||
pointerCount: _pointerQueue.length,
|
||||
));
|
||||
});
|
||||
|
@ -352,7 +371,7 @@ class EagerScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||
_state = _ScaleState.started;
|
||||
_dispatchOnStartCallbackIfNeeded();
|
||||
if (dragStartBehavior == DragStartBehavior.start) {
|
||||
_initialFocalPoint = _currentFocalPoint;
|
||||
_initialFocalPoint = _currentFocalPoint!;
|
||||
_initialSpan = _currentSpan;
|
||||
_initialLine = _currentLine;
|
||||
_initialHorizontalSpan = _currentHorizontalSpan;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/l10n/l10n.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
extension ExtraContext on BuildContext {
|
||||
String? get currentRouteName => ModalRoute.of(this)?.settings.name;
|
||||
|
|
|
@ -2,22 +2,45 @@ import 'dart:async';
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBindingObserver {
|
||||
ValueNotifier<double> get appBarHeightNotifier;
|
||||
class GridItemTracker<T> extends StatefulWidget {
|
||||
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 {
|
||||
final scrollableContext = scrollableKey.currentContext!;
|
||||
final scrollableContext = widget.scrollableKey.currentContext!;
|
||||
return (scrollableContext.findRenderObject() as RenderBox).size;
|
||||
}
|
||||
|
||||
|
@ -44,11 +67,27 @@ mixin GridItemTrackerMixin<T, U extends StatefulWidget> on State<U>, WidgetsBind
|
|||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant oldWidget) {
|
||||
void didUpdateWidget(covariant GridItemTracker<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.tileLayout != widget.tileLayout) {
|
||||
_onLayoutChange();
|
||||
}
|
||||
_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
|
||||
void dispose() {
|
||||
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 {
|
||||
// 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
|
||||
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
|
||||
// w.r.t. the `MediaQuery` update, and consequentially to this widget update
|
||||
// `WidgetsBindingObserver` is notified mostly before, sometimes after, the widget update
|
||||
void _onWindowOrientationChange() {
|
||||
void _onLayoutChange() {
|
||||
// do not track when view shows top edge
|
||||
if (scrollController.offset == 0) return;
|
||||
|
||||
final layout = _lastSectionedListLayout;
|
||||
final halfSize = _lastScrollableSize / 2;
|
||||
final center = Offset(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/behaviour/eager_scale_gesture_recognizer.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
|
@ -21,6 +22,7 @@ class ScalerMetadata<T> {
|
|||
|
||||
class GridScaleGestureDetector<T> extends StatefulWidget {
|
||||
final GlobalKey scrollableKey;
|
||||
final TileLayout tileLayout;
|
||||
final double Function(double width) heightForWidth;
|
||||
final Widget Function(Offset center, Size tileSize, Widget child) gridBuilder;
|
||||
final Widget Function(T item, Size tileSize) scaledBuilder;
|
||||
|
@ -30,6 +32,7 @@ class GridScaleGestureDetector<T> extends StatefulWidget {
|
|||
const GridScaleGestureDetector({
|
||||
Key? key,
|
||||
required this.scrollableKey,
|
||||
required this.tileLayout,
|
||||
required this.heightForWidth,
|
||||
required this.gridBuilder,
|
||||
required this.scaledBuilder,
|
||||
|
@ -111,17 +114,29 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
_extentMax = tileExtentController.effectiveExtentMax;
|
||||
|
||||
final halfSize = _startSize! / 2;
|
||||
final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
||||
final tileCenter = renderMetaData.localToGlobal(Offset(halfSize.width, halfSize.height));
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => ScaleOverlay(
|
||||
builder: (scaledTileSize) => SizedBox.fromSize(
|
||||
size: scaledTileSize,
|
||||
child: GridTheme(
|
||||
extent: scaledTileSize.width,
|
||||
child: widget.scaledBuilder(_metadata!.item, scaledTileSize),
|
||||
),
|
||||
),
|
||||
center: thumbnailCenter,
|
||||
builder: (context) => _ScaleOverlay(
|
||||
builder: (scaledTileSize) {
|
||||
late final double themeExtent;
|
||||
switch (widget.tileLayout) {
|
||||
case TileLayout.grid:
|
||||
themeExtent = scaledTileSize.width;
|
||||
break;
|
||||
case TileLayout.list:
|
||||
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,
|
||||
gridBuilder: widget.gridBuilder,
|
||||
scaledSizeNotifier: _scaledSizeNotifier!,
|
||||
|
@ -133,8 +148,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
void _onScaleUpdate(ScaleUpdateDetails details) {
|
||||
if (_scaledSizeNotifier == null) return;
|
||||
final s = details.scale;
|
||||
final scaledWidth = (_startSize!.width * s).clamp(_extentMin!, _extentMax!);
|
||||
_scaledSizeNotifier!.value = Size(scaledWidth, widget.heightForWidth(scaledWidth));
|
||||
switch (widget.tileLayout) {
|
||||
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) {
|
||||
|
@ -148,7 +171,16 @@ class _GridScaleGestureDetectorState<T> extends State<GridScaleGestureDetector<T
|
|||
final tileExtentController = context.read<TileExtentController>();
|
||||
final oldExtent = tileExtentController.extentNotifier.value;
|
||||
// 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;
|
||||
if (newExtent == oldExtent) {
|
||||
_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 TileLayout tileLayout;
|
||||
final Offset center;
|
||||
final double viewportWidth;
|
||||
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,
|
||||
required this.builder,
|
||||
required this.tileLayout,
|
||||
required this.center,
|
||||
required this.viewportWidth,
|
||||
required this.scaledSizeNotifier,
|
||||
|
@ -203,7 +237,7 @@ class ScaleOverlay extends StatefulWidget {
|
|||
_ScaleOverlayState createState() => _ScaleOverlayState();
|
||||
}
|
||||
|
||||
class _ScaleOverlayState extends State<ScaleOverlay> {
|
||||
class _ScaleOverlayState extends State<_ScaleOverlay> {
|
||||
bool _init = false;
|
||||
|
||||
Offset get center => widget.center;
|
||||
|
@ -222,26 +256,7 @@ class _ScaleOverlayState extends State<ScaleOverlay> {
|
|||
child: Builder(
|
||||
builder: (context) => IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
decoration: _init
|
||||
? 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,
|
||||
],
|
||||
),
|
||||
),
|
||||
decoration: _buildBackgroundDecoration(context),
|
||||
duration: Durations.collectionScalingBackgroundAnimation,
|
||||
child: ValueListenableBuilder<Size>(
|
||||
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 {
|
||||
final Offset center;
|
||||
final TileLayout tileLayout;
|
||||
final Offset tileCenter;
|
||||
final Size tileSize;
|
||||
final double spacing, borderWidth;
|
||||
final Radius borderRadius;
|
||||
final Color color;
|
||||
|
||||
const GridPainter({
|
||||
required this.center,
|
||||
required this.tileLayout,
|
||||
required this.tileCenter,
|
||||
required this.tileSize,
|
||||
required this.spacing,
|
||||
required this.borderWidth,
|
||||
|
@ -301,40 +352,73 @@ class GridPainter extends CustomPainter {
|
|||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final tileWidth = tileSize.width;
|
||||
final tileHeight = tileSize.height;
|
||||
|
||||
late final Offset chipCenter;
|
||||
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()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = borderWidth
|
||||
..shader = ui.Gradient.radial(
|
||||
center,
|
||||
tileWidth * 2,
|
||||
[
|
||||
color,
|
||||
Colors.transparent,
|
||||
],
|
||||
[
|
||||
.8,
|
||||
1,
|
||||
],
|
||||
);
|
||||
..shader = strokeShader;
|
||||
final fillPaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = color.withOpacity(.25);
|
||||
|
||||
final deltaX = tileWidth + spacing;
|
||||
final deltaY = tileHeight + spacing;
|
||||
for (var i = -2; i <= 2; i++) {
|
||||
final chipWidth = chipSize.width;
|
||||
final chipHeight = chipSize.height;
|
||||
|
||||
final deltaX = tileSize.width + spacing;
|
||||
final deltaY = tileSize.height + spacing;
|
||||
for (var i = -deltaColumn; i <= deltaColumn; i++) {
|
||||
final dx = deltaX * i;
|
||||
for (var j = -2; j <= 2; j++) {
|
||||
if (i == 0 && j == 0) continue;
|
||||
final dy = deltaY * j;
|
||||
final rect = RRect.fromRectAndRadius(
|
||||
Rect.fromCenter(
|
||||
center: center + Offset(dx, dy),
|
||||
width: tileWidth,
|
||||
height: tileHeight,
|
||||
center: chipCenter + Offset(dx, dy),
|
||||
width: chipWidth - borderWidth,
|
||||
height: chipHeight - borderWidth,
|
||||
),
|
||||
borderRadius,
|
||||
);
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/source/enums.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -12,6 +13,7 @@ import 'package:provider/provider.dart';
|
|||
|
||||
abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
||||
final double scrollableWidth;
|
||||
final TileLayout tileLayout;
|
||||
final int columnCount;
|
||||
final double spacing, tileWidth, tileHeight;
|
||||
final Widget Function(T item) tileBuilder;
|
||||
|
@ -21,14 +23,17 @@ abstract class SectionedListLayoutProvider<T> extends StatelessWidget {
|
|||
const SectionedListLayoutProvider({
|
||||
Key? key,
|
||||
required this.scrollableWidth,
|
||||
required this.columnCount,
|
||||
required this.tileLayout,
|
||||
required int columnCount,
|
||||
required this.spacing,
|
||||
required this.tileWidth,
|
||||
required double tileWidth,
|
||||
required this.tileHeight,
|
||||
required this.tileBuilder,
|
||||
required this.tileAnimationDelay,
|
||||
required this.child,
|
||||
}) : assert(scrollableWidth != 0),
|
||||
columnCount = tileLayout == TileLayout.list ? 1 : columnCount,
|
||||
tileWidth = tileLayout == TileLayout.list ? scrollableWidth : tileWidth,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
|
|
|
@ -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/grid/section_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GridSelectionGestureDetector<T> extends StatefulWidget {
|
||||
|
|
|
@ -97,13 +97,9 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
|
|||
double? leadingScrollOffset,
|
||||
double? trailingScrollOffset,
|
||||
}) {
|
||||
return childManager.estimateMaxScrollOffset(
|
||||
constraints,
|
||||
firstIndex: firstIndex,
|
||||
lastIndex: lastIndex,
|
||||
leadingScrollOffset: leadingScrollOffset,
|
||||
trailingScrollOffset: trailingScrollOffset,
|
||||
);
|
||||
// default implementation is an estimation via `childManager.estimateMaxScrollOffset()`
|
||||
// but we have the accurate offset via pre-computed section layouts
|
||||
return _sectionLayouts.last.maxOffset;
|
||||
}
|
||||
|
||||
double computeMaxScrollOffset(SliverConstraints constraints) {
|
||||
|
|
|
@ -37,7 +37,7 @@ class AvesFilterDecoration {
|
|||
|
||||
class AvesFilterChip extends StatefulWidget {
|
||||
final CollectionFilter filter;
|
||||
final bool removable, showGenericIcon, useFilterColor;
|
||||
final bool removable, showText, showGenericIcon, useFilterColor;
|
||||
final AvesFilterDecoration? decoration;
|
||||
final String? banner;
|
||||
final Widget? leadingOverride, details;
|
||||
|
@ -60,6 +60,7 @@ class AvesFilterChip extends StatefulWidget {
|
|||
Key? key,
|
||||
required this.filter,
|
||||
this.removable = false,
|
||||
this.showText = true,
|
||||
this.showGenericIcon = true,
|
||||
this.useFilterColor = true,
|
||||
this.decoration,
|
||||
|
@ -160,66 +161,70 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
|
||||
@override
|
||||
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;
|
||||
Widget content = Row(
|
||||
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 chipBackground = Theme.of(context).scaffoldBackgroundColor;
|
||||
|
||||
final details = widget.details;
|
||||
if (details != null) {
|
||||
content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
Widget? content;
|
||||
if (widget.showText) {
|
||||
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;
|
||||
|
||||
content = Row(
|
||||
mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
content,
|
||||
Flexible(child: details),
|
||||
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,
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
final details = widget.details;
|
||||
if (details != null) {
|
||||
content = Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
content,
|
||||
Flexible(child: details),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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 {
|
||||
content = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding * 2),
|
||||
child: content,
|
||||
);
|
||||
);
|
||||
} else {
|
||||
content = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding * 2),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final borderRadius = decoration?.chipBorderRadius ?? const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius));
|
||||
|
@ -244,7 +249,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
|||
borderRadius: borderRadius,
|
||||
),
|
||||
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
|
||||
onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null,
|
||||
onTap: onTap != null
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/theme/icons.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:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -203,25 +199,9 @@ class IconUtils {
|
|||
required BuildContext context,
|
||||
required String albumPath,
|
||||
double? size,
|
||||
bool embossed = false,
|
||||
}) {
|
||||
size ??= IconTheme.of(context).size;
|
||||
Widget buildIcon(IconData icon) => embossed
|
||||
? 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,
|
||||
);
|
||||
Widget buildIcon(IconData icon) => Icon(icon, size: size);
|
||||
switch (androidFileUtils.getAlbumType(albumPath)) {
|
||||
case AlbumType.camera:
|
||||
return buildIcon(AIcons.cameraAlbum);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EmptyContent extends StatelessWidget {
|
||||
final IconData? icon;
|
||||
|
@ -17,28 +19,33 @@ class EmptyContent extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const color = Colors.blueGrey;
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
size: 64,
|
||||
color: color,
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: context.select<MediaQueryData, double>((mq) => mq.effectiveBottomPadding),
|
||||
),
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/widgets/common/magnifier/controller/controller.dart';
|
||||
import 'package:aves/widgets/common/magnifier/controller/state.dart';
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/widgets/common/magnifier/controller/state.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@immutable
|
||||
|
|
|
@ -22,7 +22,6 @@ import 'package:aves/widgets/common/map/zoomed_bounds.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:fluster/fluster.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
|
|
@ -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/map/leaflet/scalebar_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map/plugin_api.dart';
|
||||
|
||||
class ScaleLayerOptions extends LayerOptions {
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:aves/model/entry.dart';
|
|||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class ImageMarker extends StatelessWidget {
|
||||
final AvesEntry? entry;
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:aves/model/entry.dart';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThumbnailScroller extends StatefulWidget {
|
||||
|
|
|
@ -68,7 +68,7 @@ class DebugSettingsSection extends StatelessWidget {
|
|||
'searchHistory': toMultiline(settings.searchHistory),
|
||||
'lastVersionCheckDate': '${settings.lastVersionCheckDate}',
|
||||
'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
Loading…
Reference in a new issue