Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2023-01-15 12:22:19 +01:00
commit 1badcf4395
257 changed files with 7140 additions and 3188 deletions

View file

@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.7.9"></a>[v1.7.9] - 2023-01-15
### Added
- Viewer: optionally show description on overlay
- Collection: unlocated/untagged overlay icons
- Video: stop when losing audio focus
- Video: stop when becoming noisy
- Info: Google camera portrait mode item extraction
- TV: handle overscan
- TV: improved support for Viewer, Info, Map, Stats
- TV: option to use TV layout on any device
- Czech translation (thanks vesp)
- Polish translation (thanks Piotr K, rehork)
### Changed
- editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment`
- in the tag editor, tapping on applied tag applies it to all items instead of removing it
- pin app bar when selecting items
### Fixed
- transition between collection and viewer when cutout area is not used
- saving video playback state when leaving viewer
## <a id="v1.7.8"></a>[v1.7.8] - 2022-12-20
### Added

View file

@ -15,9 +15,6 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/huawei-appgallery-badge-english-black.png"
alt='Get it on Huawei AppGallery'
height="80">](https://appgallery.huawei.com/app/C106014023)
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/samsung-galaxy-store-badge-english.png"
alt='Get it on Samsung Galaxy Store'
height="80">](https://galaxy.store/aves)
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/amazon-appstore-badge-english-black.png"
alt='Get it on Amazon Appstore'
height="80">](https://www.amazon.com/dp/B09XQHQQ72)
@ -44,7 +41,7 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka
**Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
Aves integrates with Android (from **API 19 to 33**, i.e. from KitKat to Android 13) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
## Screenshots

View file

@ -191,7 +191,7 @@ dependencies {
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
implementation 'com.github.bumptech.glide:glide:4.14.2'
// SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.3'
implementation 'org.slf4j:slf4j-simple:2.0.6'
// forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory

View file

@ -205,6 +205,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:resource="@xml/app_widget_info" />
</receiver>
<receiver
android:name="androidx.media.session.MediaButtonReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service
android:name=".AnalysisService"
android:description="@string/analysis_service_description"

View file

@ -39,6 +39,7 @@ open class MainActivity : FlutterActivity() {
private lateinit var analysisStreamHandler: AnalysisStreamHandler
internal lateinit var intentDataMap: MutableMap<String, Any?>
private lateinit var analysisHandler: AnalysisHandler
private lateinit var mediaSessionHandler: MediaSessionHandler
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(LOG_TAG, "onCreate intent=$intent")
@ -70,9 +71,21 @@ open class MainActivity : FlutterActivity() {
val messenger = flutterEngine!!.dartExecutor
// notification: platform -> dart
analysisStreamHandler = AnalysisStreamHandler().apply {
EventChannel(messenger, AnalysisStreamHandler.CHANNEL).setStreamHandler(this)
}
errorStreamHandler = ErrorStreamHandler().apply {
EventChannel(messenger, ErrorStreamHandler.CHANNEL).setStreamHandler(this)
}
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
}
// dart -> platform -> dart
// - need Context
analysisHandler = AnalysisHandler(this, ::onAnalysisCompleted)
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
MethodChannel(messenger, AnalysisHandler.CHANNEL).setMethodCallHandler(analysisHandler)
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
@ -83,7 +96,7 @@ open class MainActivity : FlutterActivity() {
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(MediaSessionHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
@ -128,16 +141,6 @@ open class MainActivity : FlutterActivity() {
}
}
// notification: platform -> dart
analysisStreamHandler = AnalysisStreamHandler().apply {
EventChannel(messenger, AnalysisStreamHandler.CHANNEL).setStreamHandler(this)
}
// notification: platform -> dart
errorStreamHandler = ErrorStreamHandler().apply {
EventChannel(messenger, ErrorStreamHandler.CHANNEL).setStreamHandler(this)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
setupShortcuts()
}
@ -166,6 +169,7 @@ open class MainActivity : FlutterActivity() {
override fun onDestroy() {
Log.i(LOG_TAG, "onDestroy")
mediaSessionHandler.dispose()
mediaStoreChangeStreamHandler.dispose()
settingsChangeStreamHandler.dispose()
super.onDestroy()
@ -431,7 +435,7 @@ open class MainActivity : FlutterActivity() {
}
}
var errorStreamHandler: ErrorStreamHandler? = null
private var errorStreamHandler: ErrorStreamHandler? = null
suspend fun notifyError(error: String) {
Log.e(LOG_TAG, "notifyError error=$error")

View file

@ -10,6 +10,7 @@ import android.net.Uri
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.TransactionTooLargeException
import android.util.Log
import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
@ -280,7 +281,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val title = call.argument<String>("title")
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
if (urisByMimeType == null) {
result.error("setAs-args", "missing arguments", null)
result.error("share-args", "missing arguments", null)
return
}
@ -288,15 +289,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val mimeTypes = urisByMimeType.keys.toTypedArray()
// simplify share intent for a single item, as some apps can handle one item but not more
val started = if (uriList.size == 1) {
val intent = if (uriList.size == 1) {
val uri = uriList.first()
val mimeType = mimeTypes.first()
val intent = Intent(Intent.ACTION_SEND)
Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri))
safeStartActivityChooser(title, intent)
} else {
var mimeType = "*/*"
if (mimeTypes.size == 1) {
@ -311,14 +311,21 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
}
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
Intent(Intent.ACTION_SEND_MULTIPLE)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType)
safeStartActivityChooser(title, intent)
}
result.success(started)
try {
val started = safeStartActivityChooser(title, intent)
result.success(started)
} catch (e: Exception) {
if (e is TransactionTooLargeException || e.cause is TransactionTooLargeException) {
result.error("share-large", "transaction too large with ${uriList.size} URIs", e)
} else {
result.error("share-exception", "failed to share ${uriList.size} URIs", e)
}
}
}
private fun safeStartActivity(intent: Intent): Boolean {

View file

@ -11,25 +11,21 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.metadata.XMP.doesPropExist
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.XMPPropName
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ContentImageProvider
import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.*
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -46,6 +42,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
@ -84,6 +81,68 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
result.success(thumbnails)
}
private fun extractGoogleDeviceItem(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName")
val dataUri = call.argument<String>("dataUri")
if (mimeType == null || uri == null || sizeBytes == null || dataUri == null) {
result.error("extractGoogleDeviceItem-args", "missing arguments", null)
return
}
var container: GoogleDeviceContainer? = null
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input)
// data can be large and stored in "Extended XMP",
// which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
try {
container = xmpDirs.firstNotNullOfOrNull {
val xmpMeta = it.xmpMeta
if (xmpMeta.doesPropExist(XMP.GDEVICE_DIRECTORY_PROP_NAME)) {
GoogleDeviceContainer().apply { findItems(xmpMeta) }
} else {
null
}
}
} catch (e: XMPException) {
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
return
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to extract file from XMP", e)
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to extract file from XMP", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to extract file from XMP", e)
}
}
container?.let {
it.findOffsets(context, uri, mimeType, sizeBytes)
val index = it.itemIndex(dataUri)
val itemStartOffset = it.itemStartOffset(index)
val itemLength = it.itemLength(index)
val itemMimeType = it.itemMimeType(index)
if (itemStartOffset != null && itemLength != null && itemMimeType != null) {
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(itemStartOffset)
copyEmbeddedBytes(result, itemMimeType, displayName, input, itemLength)
return
}
}
}
result.error("extractGoogleDeviceItem-empty", "failed to extract item from Google Device XMP at uri=$uri dataUri=$dataUri", null)
}
private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }

View file

@ -1,16 +1,17 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.content.*
import android.media.AudioManager
import android.media.session.PlaybackState
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.media.session.MediaButtonReceiver
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -19,20 +20,36 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class MediaSessionHandler(private val context: Context) : MethodCallHandler {
class MediaSessionHandler(private val context: Context, private val mediaCommandHandler: MediaCommandStreamHandler) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val sessions = HashMap<Uri, MediaSessionCompat>()
private var session: MediaSessionCompat? = null
private var wasPlaying = false
private var isNoisyAudioReceiverRegistered = false
private val noisyAudioReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
mediaCommandHandler.onStop()
}
}
}
fun dispose() {
if (isNoisyAudioReceiverRegistered) {
context.unregisterReceiver(noisyAudioReceiver)
isNoisyAudioReceiverRegistered = false
}
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"update" -> ioScope.launch { safeSuspend(call, result, ::update) }
"release" -> ioScope.launch { safe(call, result, ::release) }
"update" -> ioScope.launch { safeSuspend(call, result, ::updateSession) }
"release" -> ioScope.launch { safe(call, result, ::releaseSession) }
else -> result.notImplemented()
}
}
private suspend fun update(call: MethodCall, result: MethodChannel.Result) {
private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val title = call.argument<String>("title")
val durationMillis = call.argument<Number>("durationMillis")?.toLong()
@ -72,69 +89,51 @@ class MediaSessionHandler(private val context: Context) : MethodCallHandler {
.setActions(actions)
.build()
var session = sessions[uri]
if (session == null) {
session = MediaSessionCompat(context, "aves-$uri")
sessions[uri] = session
val metadata = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
.build()
session.setMetadata(metadata)
val callback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() {
override fun onPlay() {
super.onPlay()
Log.d(LOG_TAG, "TLAD onPlay uri=$uri")
}
override fun onPause() {
super.onPause()
Log.d(LOG_TAG, "TLAD onPause uri=$uri")
}
override fun onStop() {
super.onStop()
Log.d(LOG_TAG, "TLAD onStop uri=$uri")
}
override fun onSeekTo(pos: Long) {
super.onSeekTo(pos)
Log.d(LOG_TAG, "TLAD onSeekTo uri=$uri pos=$pos")
FlutterUtils.runOnUiThread {
if (session == null) {
val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply {
setCallback(mediaCommandHandler)
}
}
FlutterUtils.runOnUiThread {
session.setCallback(callback)
session!!.apply {
val metadata = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
.build()
setMetadata(metadata)
setPlaybackState(playbackState)
if (!isActive) {
isActive = true
}
}
}
session.setPlaybackState(playbackState)
if (!session.isActive) {
session.isActive = true
val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
if (!wasPlaying && isPlaying) {
context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
isNoisyAudioReceiverRegistered = true
} else if (wasPlaying && !isPlaying) {
context.unregisterReceiver(noisyAudioReceiver)
isNoisyAudioReceiverRegistered = false
}
wasPlaying = isPlaying
}
result.success(null)
}
private fun release(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) {
result.error("release-args", "missing arguments", null)
return
private fun releaseSession(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
session?.let {
it.release()
session = null
}
sessions[uri]?.release()
result.success(null)
}
companion object {
private val LOG_TAG = LogUtils.createTag<MediaSessionHandler>()
const val CHANNEL = "deckers.thibault/aves/media_session"
const val STATE_STOPPED = "stopped"

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls
import android.content.ContextWrapper
import android.net.Uri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.Mp4TooLargeException
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
@ -66,10 +67,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}
provider.editOrientation(contextWrapper, path, uri, mimeType, op, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable)
})
val callback = MetadataOpCallback("editOrientation", entryMap, result)
provider.editOrientation(contextWrapper, path, uri, mimeType, op, callback)
}
private fun editDate(call: MethodCall, result: MethodChannel.Result) {
@ -96,10 +95,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}
provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable)
})
val callback = MetadataOpCallback("editDate", entryMap, result)
provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, callback)
}
private fun editMetadata(call: MethodCall, result: MethodChannel.Result) {
@ -125,10 +122,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}
provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable)
})
val callback = MetadataOpCallback("editMetadata", entryMap, result)
provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback)
}
private fun removeTrailerVideo(call: MethodCall, result: MethodChannel.Result) {
@ -152,10 +147,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}
provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable)
})
val callback = MetadataOpCallback("removeTrailerVideo", entryMap, result)
provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, callback)
}
private fun removeTypes(call: MethodCall, result: MethodChannel.Result) {
@ -180,13 +173,31 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return
}
provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable)
})
val callback = MetadataOpCallback("removeTypes", entryMap, result)
provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), callback)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/metadata_edit"
}
}
}
private class MetadataOpCallback(
private val errorCodeBase: String,
private val entryMap: FieldMap,
private val result: MethodChannel.Result,
) : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) {
val errorCode = if (throwable is Mp4TooLargeException) {
if (throwable.type == "moov") {
"$errorCodeBase-mp4largemoov"
} else {
"$errorCodeBase-mp4largeother"
}
} else {
"$errorCodeBase-failure"
}
result.error(errorCode, "failed for entry=$entryMap", throwable)
}
}

View file

@ -134,7 +134,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (prop is XMPPropertyInfo) {
val path = prop.path
if (path?.isNotEmpty() == true) {
val value = if (XMP.isDataPath(path)) "[skipped]" else prop.value
val value = if (XMP.isDataPath(path)) VALUE_SKIPPED_DATA else prop.value
if (value?.isNotEmpty() == true) {
dirMap[path] = value
}
@ -615,7 +615,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (!metadataMap.containsKey(KEY_XMP_TITLE) || !metadataMap.containsKey(KEY_XMP_SUBJECTS)) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
if (!metadataMap.containsKey(KEY_XMP_TITLE)) {
dir.getSafeString(IptcDirectory.TAG_OBJECT_NAME) { metadataMap[KEY_XMP_TITLE] = it }
dir.getSafeString(IptcDirectory.TAG_OBJECT_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE] = it }
}
if (!metadataMap.containsKey(KEY_XMP_SUBJECTS)) {
dir.keywords?.let { metadataMap[KEY_XMP_SUBJECTS] = it.joinToString(XMP_SUBJECTS_SEPARATOR) }
@ -1151,6 +1151,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// return description from these fields (by precedence):
// - XMP / dc:description
// - IPTC / caption-abstract
// - Exif / UserComment
// - Exif / ImageDescription
private fun getDescription(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
@ -1171,7 +1172,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val xmpMeta = dir.xmpMeta
try {
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME) { description = it }
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it }
}
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
@ -1179,12 +1180,23 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
dir.getSafeString(IptcDirectory.TAG_CAPTION) { description = it }
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
// user comment field specifies encoding, unlike other string fields
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
if (string.isNotBlank()) {
description = string
}
}
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION) { description = it }
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = it }
}
}
}
@ -1269,5 +1281,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// additional media key
private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture"
private const val VALUE_SKIPPED_DATA = "[skipped]"
}
}

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls.window
import android.app.Activity
import android.os.Build
import android.view.WindowManager
import deckers.thibault.aves.utils.getDisplayCompat
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -42,25 +43,30 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(true)
}
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
}
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use")
if (use == null) {
result.error("setCutoutMode-args", "missing arguments", null)
override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val mode = if (use) {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
}
activity.window.attributes.layoutInDisplayCutoutMode = mode
val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activity.getDisplayCompat()?.cutout
} else {
activity.window.decorView.rootWindowInsets.displayCutout
}
result.success(true)
val density = activity.resources.displayMetrics.density
result.success(
hashMapOf(
"left" to (cutout?.safeInsetLeft ?: 0) / density,
"top" to (cutout?.safeInsetTop ?: 0) / density,
"right" to (cutout?.safeInsetRight ?: 0) / density,
"bottom" to (cutout?.safeInsetBottom ?: 0) / density,
)
)
}
}

View file

@ -17,11 +17,11 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
result.success(false)
}
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
result.success(HashMap<String, Any>())
}
}

View file

@ -15,8 +15,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode)
"setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode)
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
"getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
else -> result.notImplemented()
}
}
@ -37,9 +37,9 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result)
abstract fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
abstract fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
abstract fun setCutoutMode(call: MethodCall, result: MethodChannel.Result)
abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result)
companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()

View file

@ -199,7 +199,9 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
}
override fun onCancel(arguments: Any?) {}
override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
private fun success(result: Any?) {
handler.post {

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.channel.streams
import android.util.Log
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
@ -13,13 +15,16 @@ class AnalysisStreamHandler : EventChannel.StreamHandler {
this.eventSink = eventSink
}
override fun onCancel(arguments: Any?) {}
override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
fun notifyCompletion() {
eventSink?.success(true)
}
companion object {
private val LOG_TAG = LogUtils.createTag<AnalysisStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/analysis_events"
}
}

View file

@ -1,6 +1,8 @@
package deckers.thibault.aves.channel.streams
import android.util.Log
import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
@ -14,7 +16,9 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
this.eventSink = eventSink
}
override fun onCancel(arguments: Any?) {}
override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
suspend fun notifyError(error: String) {
FlutterUtils.runOnUiThread {
@ -23,6 +27,7 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
}
companion object {
private val LOG_TAG = LogUtils.createTag<ErrorStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/error"
}
}

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.channel.streams
import android.util.Log
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
@ -13,13 +15,16 @@ class IntentStreamHandler : EventChannel.StreamHandler {
this.eventSink = eventSink
}
override fun onCancel(arguments: Any?) {}
override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
fun notifyNewIntent(intentData: MutableMap<String, Any?>?) {
eventSink?.success(intentData)
}
companion object {
private val LOG_TAG = LogUtils.createTag<IntentStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/new_intent_stream"
}
}

View file

@ -0,0 +1,76 @@
package deckers.thibault.aves.channel.streams
import android.os.Handler
import android.os.Looper
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
class MediaCommandStreamHandler : EventChannel.StreamHandler, MediaSessionCompat.Callback() {
// cannot use `lateinit` because we cannot guarantee
// its initialization in `onListen` at the right time
private var eventSink: EventSink? = null
private var handler: Handler? = null
override fun onListen(arguments: Any?, eventSink: EventSink) {
this.eventSink = eventSink
handler = Handler(Looper.getMainLooper())
}
override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
private fun success(fields: FieldMap) {
handler?.post {
try {
eventSink?.success(fields)
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to use event sink", e)
}
}
}
// media session callback
override fun onPlay() {
super.onPlay()
success(hashMapOf(KEY_COMMAND to COMMAND_PLAY))
}
override fun onPause() {
super.onPause()
success(hashMapOf(KEY_COMMAND to COMMAND_PAUSE))
}
override fun onStop() {
super.onStop()
success(hashMapOf(KEY_COMMAND to COMMAND_STOP))
}
override fun onSeekTo(pos: Long) {
super.onSeekTo(pos)
success(
hashMapOf(
KEY_COMMAND to COMMAND_SEEK,
KEY_POSITION to pos,
)
)
}
companion object {
private val LOG_TAG = LogUtils.createTag<MediaCommandStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/media_command"
const val KEY_COMMAND = "command"
const val KEY_POSITION = "position"
const val COMMAND_PLAY = "play"
const val COMMAND_PAUSE = "pause"
const val COMMAND_STOP = "stop"
const val COMMAND_SEEK = "seek"
}
}

View file

@ -41,7 +41,9 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
handler = Handler(Looper.getMainLooper())
}
override fun onCancel(arguments: Any?) {}
override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver)

View file

@ -79,7 +79,9 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
handler = Handler(Looper.getMainLooper())
}
override fun onCancel(arguments: Any?) {}
override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver)

View file

@ -0,0 +1,83 @@
package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import com.adobe.internal.xmp.XMPMeta
import deckers.thibault.aves.metadata.XMP.countPropArrayItems
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.utils.indexOfBytes
import java.io.DataInputStream
class GoogleDeviceContainer {
private val jfifSignature = byteArrayOf(0xFF.toByte(), 0xD8.toByte(), 0xFF.toByte(), 0xE0.toByte(), 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01)
private val items: MutableList<GoogleDeviceContainerItem> = ArrayList()
private val offsets: MutableList<Int> = ArrayList()
fun findItems(xmpMeta: XMPMeta) {
val count = xmpMeta.countPropArrayItems(XMP.GDEVICE_DIRECTORY_PROP_NAME)
for (i in 1 until count + 1) {
val mimeType = xmpMeta.getSafeStructField(listOf(XMP.GDEVICE_DIRECTORY_PROP_NAME, i, XMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = xmpMeta.getSafeStructField(listOf(XMP.GDEVICE_DIRECTORY_PROP_NAME, i, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull()
val dataUri = xmpMeta.getSafeStructField(listOf(XMP.GDEVICE_DIRECTORY_PROP_NAME, i, XMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value
if (mimeType != null && length != null && dataUri != null) {
items.add(
GoogleDeviceContainerItem(
mimeType = mimeType,
length = length,
dataUri = dataUri,
)
)
} else throw Exception("failed to extract Google device container item at index=$i with mimeType=$mimeType, length=$length, dataUri=$dataUri")
}
}
fun findOffsets(context: Context, uri: Uri, mimeType: String, sizeBytes: Long) {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val bytes = ByteArray(sizeBytes.toInt())
DataInputStream(input).use {
it.readFully(bytes)
}
var start = 0
while (start < sizeBytes) {
val offset = bytes.indexOfBytes(jfifSignature, start)
if (offset != -1 && offset >= start) {
start = offset + jfifSignature.size
offsets.add(offset)
} else {
start = sizeBytes.toInt()
}
}
}
// fix first offset as it may refer to included thumbnail instead of primary image
while (offsets.size < items.size) {
offsets.add(0, 0)
}
offsets[0] = 0
}
fun itemIndex(dataUri: String) = items.indexOfFirst { it.dataUri == dataUri }
private fun item(index: Int): GoogleDeviceContainerItem? {
return if (0 <= index && index < items.size) {
items[index]
} else null
}
fun itemStartOffset(index: Int): Long? {
return if (0 <= index && index < offsets.size) {
offsets[index].toLong()
} else null
}
fun itemLength(index: Int): Long? {
val lengthByMeta = item(index)?.length ?: return null
return if (lengthByMeta != 0L) lengthByMeta else itemStartOffset(index + 1)
}
fun itemMimeType(index: Int) = item(index)?.mimeType
}
class GoogleDeviceContainerItem(val mimeType: String, val length: Long, val dataUri: String) {}

View file

@ -33,7 +33,7 @@ object Mp4ParserHelper {
)
setBoxSkipper { type, size ->
if (skippedTypes.contains(type)) return@setBoxSkipper true
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Mp4TooLargeException(type, "box (type=$type size=$size) is too large")
false
}
}
@ -232,3 +232,5 @@ object Mp4ParserHelper {
return stream.toByteArray()
}
}
class Mp4TooLargeException(val type: String, message: String) : RuntimeException(message)

View file

@ -175,14 +175,14 @@ object MultiPage {
if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
// `GCamera` motion photo
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
} else if (xmpMeta.doesPropExist(XMP.CONTAINER_DIRECTORY_PROP_NAME)) {
} else if (xmpMeta.doesPropExist(XMP.GCONTAINER_DIRECTORY_PROP_NAME)) {
// `Container` motion photo
val count = xmpMeta.countPropArrayItems(XMP.CONTAINER_DIRECTORY_PROP_NAME)
val count = xmpMeta.countPropArrayItems(XMP.GCONTAINER_DIRECTORY_PROP_NAME)
if (count == 2) {
// expect the video to be the second item
val i = 2
val mime = xmpMeta.getSafeStructField(listOf(XMP.CONTAINER_DIRECTORY_PROP_NAME, i, XMP.CONTAINER_ITEM_PROP_NAME, XMP.CONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = xmpMeta.getSafeStructField(listOf(XMP.CONTAINER_DIRECTORY_PROP_NAME, i, XMP.CONTAINER_ITEM_PROP_NAME, XMP.CONTAINER_ITEM_LENGTH_PROP_NAME))?.value
val mime = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
if (MimeTypes.isVideo(mime) && length != null) {
offsetFromEnd = length.toLong()
}

View file

@ -42,17 +42,17 @@ class GSpherical(xmlBytes: ByteArray) {
"StitchingSoftware" -> stitchingSoftware = readTag(parser, tag)
"ProjectionType" -> projectionType = readTag(parser, tag)
"StereoMode" -> stereoMode = readTag(parser, tag)
"SourceCount" -> sourceCount = Integer.parseInt(readTag(parser, tag))
"InitialViewHeadingDegrees" -> initialViewHeadingDegrees = Integer.parseInt(readTag(parser, tag))
"InitialViewPitchDegrees" -> initialViewPitchDegrees = Integer.parseInt(readTag(parser, tag))
"InitialViewRollDegrees" -> initialViewRollDegrees = Integer.parseInt(readTag(parser, tag))
"Timestamp" -> timestamp = Integer.parseInt(readTag(parser, tag))
"FullPanoWidthPixels" -> fullPanoWidthPixels = Integer.parseInt(readTag(parser, tag))
"FullPanoHeightPixels" -> fullPanoHeightPixels = Integer.parseInt(readTag(parser, tag))
"CroppedAreaImageWidthPixels" -> croppedAreaImageWidthPixels = Integer.parseInt(readTag(parser, tag))
"CroppedAreaImageHeightPixels" -> croppedAreaImageHeightPixels = Integer.parseInt(readTag(parser, tag))
"CroppedAreaLeftPixels" -> croppedAreaLeftPixels = Integer.parseInt(readTag(parser, tag))
"CroppedAreaTopPixels" -> croppedAreaTopPixels = Integer.parseInt(readTag(parser, tag))
"SourceCount" -> sourceCount = readTag(parser, tag).toInt()
"InitialViewHeadingDegrees" -> initialViewHeadingDegrees = readTag(parser, tag).toInt()
"InitialViewPitchDegrees" -> initialViewPitchDegrees = readTag(parser, tag).toInt()
"InitialViewRollDegrees" -> initialViewRollDegrees = readTag(parser, tag).toInt()
"Timestamp" -> timestamp = readTag(parser, tag).toInt()
"FullPanoWidthPixels" -> fullPanoWidthPixels = readTag(parser, tag).toInt()
"FullPanoHeightPixels" -> fullPanoHeightPixels = readTag(parser, tag).toInt()
"CroppedAreaImageWidthPixels" -> croppedAreaImageWidthPixels = readTag(parser, tag).toInt()
"CroppedAreaImageHeightPixels" -> croppedAreaImageHeightPixels = readTag(parser, tag).toInt()
"CroppedAreaLeftPixels" -> croppedAreaLeftPixels = readTag(parser, tag).toInt()
"CroppedAreaTopPixels" -> croppedAreaTopPixels = readTag(parser, tag).toInt()
}
}
}

View file

@ -43,11 +43,13 @@ object XMP {
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
// other namespaces
private const val CONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
private const val CONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/"
private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/"
private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/"
private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
@ -75,13 +77,20 @@ object XMP {
fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it }
// google portrait
val GDEVICE_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container/Container:Directory")
val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI")
val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
// motion photo
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
val CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Directory")
val CONTAINER_ITEM_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Item")
val CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Length")
val CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Mime")
val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
// panorama
// cf https://developers.google.com/streetview/spherical-metadata
@ -189,14 +198,14 @@ object XMP {
if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
// Container motion photo
if (doesPropExist(CONTAINER_DIRECTORY_PROP_NAME)) {
val count = countPropArrayItems(CONTAINER_DIRECTORY_PROP_NAME)
if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
if (count == 2) {
var hasImage = false
var hasVideo = false
for (i in 1 until count + 1) {
val mime = getSafeStructField(listOf(CONTAINER_DIRECTORY_PROP_NAME, i, CONTAINER_ITEM_PROP_NAME, CONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = getSafeStructField(listOf(CONTAINER_DIRECTORY_PROP_NAME, i, CONTAINER_ITEM_PROP_NAME, CONTAINER_ITEM_LENGTH_PROP_NAME))?.value
val mime = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
hasImage = hasImage || MimeTypes.isImage(mime) && length != null
hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
}

View file

@ -117,8 +117,13 @@ object Helper {
// extensions
fun Directory.getSafeString(tag: Int, save: (value: String) -> Unit) {
if (this.containsTag(tag)) save(this.getString(tag))
fun Directory.getSafeString(tag: Int, acceptBlank: Boolean = true, save: (value: String) -> Unit) {
if (this.containsTag(tag)) {
val string = this.getString(tag)
if (acceptBlank || string.isNotBlank()) {
save(string)
}
}
}
fun Directory.getSafeBoolean(tag: Int, save: (value: Boolean) -> Unit) {

View file

@ -55,7 +55,7 @@ internal class ContentImageProvider : ImageProvider() {
if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) }
cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields["sizeBytes"] = cursor.getLong(it) }
cursor.getColumnIndex(PATH).let { if (it != -1) fields["path"] = cursor.getString(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATA).let { if (it != -1) fields["path"] = cursor.getString(it) }
cursor.close()
}
} catch (e: Exception) {
@ -73,8 +73,5 @@ internal class ContentImageProvider : ImageProvider() {
companion object {
private val LOG_TAG = LogUtils.createTag<ContentImageProvider>()
@Suppress("deprecation")
const val PATH = MediaStore.MediaColumns.DATA
}
}

View file

@ -761,8 +761,8 @@ abstract class ImageProvider {
"${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newTrailerOffset\"",
).replace(
// Container motion photo
"${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"",
"${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"",
"${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"",
"${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"",
)
})
}

View file

@ -55,10 +55,10 @@ class MediaStoreImageProvider : ImageProvider() {
val relativePathDirectory = ensureTrailingSeparator(directory)
val relativePath = PathSegments(context, relativePathDirectory).relativeDir
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && relativePath != null) {
selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaColumns.PATH} LIKE ?"
selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DATA} LIKE ?"
selectionArgs = arrayOf(relativePath, "$relativePathDirectory%")
} else {
selection = "${MediaColumns.PATH} LIKE ?"
selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
selectionArgs = arrayOf("$relativePathDirectory%")
}
@ -139,12 +139,12 @@ class MediaStoreImageProvider : ImageProvider() {
fun checkObsoletePaths(context: Context, knownPathById: Map<Int?, String?>): List<Int> {
val obsoleteIds = ArrayList<Int>()
fun check(context: Context, contentUri: Uri) {
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaColumns.PATH)
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
try {
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
if (cursor != null) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH)
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
while (cursor.moveToNext()) {
val id = cursor.getInt(idColumn)
val path = cursor.getString(pathColumn)
@ -185,7 +185,7 @@ class MediaStoreImageProvider : ImageProvider() {
// image & video
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH)
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
@ -863,7 +863,7 @@ class MediaStoreImageProvider : ImageProvider() {
fun getContentUriForPath(context: Context, path: String): Uri? {
val projection = arrayOf(MediaStore.MediaColumns._ID)
val selection = "${MediaColumns.PATH} = ?"
val selection = "${MediaStore.MediaColumns.DATA} = ?"
val selectionArgs = arrayOf(path)
fun check(context: Context, contentUri: Uri): Uri? {
@ -892,7 +892,7 @@ class MediaStoreImageProvider : ImageProvider() {
private val BASE_PROJECTION = arrayOf(
MediaStore.MediaColumns._ID,
MediaColumns.PATH,
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.WIDTH,
@ -931,9 +931,6 @@ object MediaColumns {
@SuppressLint("InlinedApi")
const val DURATION = MediaStore.MediaColumns.DURATION
@Suppress("deprecation")
const val PATH = MediaStore.MediaColumns.DATA
}
typealias NewEntryHandler = (entry: FieldMap) -> Unit

View file

@ -20,7 +20,7 @@ fun <E> MutableList<E>.compatRemoveIf(filter: (t: E) -> Boolean): Boolean {
}
// Boyer-Moore algorithm for pattern searching
fun ByteArray.indexOfBytes(pattern: ByteArray): Int {
fun ByteArray.indexOfBytes(pattern: ByteArray, start: Int = 0): Int {
val n: Int = this.size
val m: Int = pattern.size
val badChar = Array(256) { 0 }
@ -30,7 +30,7 @@ fun ByteArray.indexOfBytes(pattern: ByteArray): Int {
i += 1
}
var j: Int = m - 1
var s = 0
var s = start
while (s <= (n - m)) {
while (j >= 0 && pattern[j] == this[s + j]) {
j -= 1

View file

@ -1,11 +1,13 @@
package deckers.thibault.aves.utils
import android.app.Activity
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.Build
import android.os.Parcelable
import android.view.Display
inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@ -16,6 +18,14 @@ inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
}
}
fun Activity.getDisplayCompat(): Display? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
display
} else {
@Suppress("deprecation")
windowManager.defaultDisplay
}
}
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

View file

@ -90,7 +90,8 @@ object MimeTypes {
// as of `metadata-extractor` v2.14.0
fun canReadWithMetadataExtractor(mimeType: String) = when (mimeType) {
DJVU, WBMP, MKV, MP2T, MP2TS, OGV, WEBM -> false
DJVU, WBMP -> false
MKV, MP2T, MP2TS, OGV, WEBM -> false
else -> true
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="wallpaper">Tapeta</string>
<string name="search_shortcut_short_label">Hledat</string>
<string name="videos_shortcut_short_label">Videa</string>
<string name="analysis_channel_name">Prohledat média</string>
<string name="analysis_service_description">Prohledat obrázky a videa</string>
<string name="analysis_notification_default_title">Prohledávání médií</string>
<string name="analysis_notification_action_stop">Zastavit</string>
<string name="app_widget_label">Fotorámeček</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:configure="deckers.thibault.aves.HomeWidgetSettingsActivity"
android:initialLayout="@layout/app_widget"
android:minWidth="40dp"
@ -9,4 +10,5 @@
android:targetCellHeight="2"
android:updatePeriodMillis="3600000"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable" />
android:widgetFeatures="reconfigurable"
tools:targetApi="s" />

View file

@ -0,0 +1,5 @@
<i>Aves</i> může pracovat se všemi typy obrázků a videí, jako jsou běžné formáty JPEG a MP4, ale také více exotické jako <b>vícestránkový TIFF, SVG, starý AVI a mnohem více</b>! Prohledává vaši sbírku médií kvůli rozpoznání <b>pohyblivých fotografií</b>, <b>panoramatických snímků</b> (čili fotosféry), <b>360° videí</b>, nebo souborů <b>GeoTIFF</b>.
<b>Navigace a vyhledávání</b> jsou důležitou součástí aplikace <i>Aves</i>. Cílem je, aby uživatelé jednoduše přecházeli z alb k fotografiím, albům, mapám, atd.
<i>Aves</i> podporuje Android (od verze KitKat po Android 13, včetně Android TV) s funkcemi jako jsou <b>widgety</b>, <b>zkratky aplikací</b>, <b>spořič displeje</b> a <b>globální vyhledávání</b>. Rovněž jej lze použít pro <b>prohlížení a výběr médií</b>.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

View file

@ -0,0 +1 @@
Galerie a prohlížeč metadat

View file

@ -1,5 +0,0 @@
In v1.5.10:
- show, search and edit ratings
- add many items to favourites at once
- enjoy the app in Spanish
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.5.11:
- edit locations of images
- export SVGs to convert and resize them
- enjoy the app in Portuguese
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.0:
- recycle bin
- view small and large images at their actual size
- enjoy the app in Indonesian
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.1:
- recycle bin
- view small and large images at their actual size
- enjoy the app in Indonesian
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.2:
- revisited viewer: new layout, thumbnail previews, video gestures
- storage related fixes for Android 10 and older
- enjoy the app in Japanese
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.6.3:
- enjoy the light theme
- rename items in bulk
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.4:
- customize album cover app & color
- explore improved GeoTIFF metadata
- enjoy the app in Italian & Chinese (Simplified)
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.5:
- bottom navigation bar
- fast scroll with breadcrumbs
- settings search
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.6:
- bottom navigation bar
- fast scroll with breadcrumbs
- settings search
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.7:
- bottom navigation bar
- fast scroll with breadcrumbs
- settings search
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.8:
- bottom navigation bar
- fast scroll with breadcrumbs
- settings search
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.9:
- start slideshows
- change your wallpaper
- enjoy the app in Turkish
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.10:
- add the photo frame widget to your home
- use your photos as screen saver
- search photos taken "on this day"
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.11:
- add the photo frame widget to your home
- use your photos as screen saver
- search photos taken "on this day"
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.12:
- play your HEIC motion photos
- find recently downloaded images with the `recently added` filter
- enjoy the app in Dutch
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.6.13:
- play your HEIC motion photos
- find recently downloaded images with the `recently added` filter
- enjoy the app in Dutch
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.7.0:
- change the sort order
- edit image titles
- enjoy the app in Greek
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.7.1:
- view your photos with the mosaic layout
- reverse filters to filter out/in
- set wallpapers with scroll effect
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.7.2:
- tag your MP4, rate your MP4, date your MP4, locate your MP4, rotate your MP4
- give media management access (on Android 12+) to skip some confirmation dialogs
- enjoy higher quality thumbnails
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.7.3:
- tag your MP4, rate your MP4, date your MP4, locate your MP4, rotate your MP4
- give media management access (on Android 12+) to skip some confirmation dialogs
- enjoy higher quality thumbnails
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.7.4:
- tag your MP4, rate your MP4, date your MP4, locate your MP4, rotate your MP4
- give media management access (on Android 12+) to skip some confirmation dialogs
- enjoy higher quality thumbnails
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.7.5:
- use viewer quick actions to rate, tag, locate
- set a default editor
- export metadata to a text file
Full changelog available on GitHub

View file

@ -1,5 +0,0 @@
In v1.7.6:
- use viewer quick actions to rate, tag, locate
- set a default editor
- export metadata to a text file
Full changelog available on GitHub

View file

@ -0,0 +1,5 @@
In v1.7.9:
- Android TV support (cont'd)
- interact with videos via media session controls
- enjoy the app in Czech & Polish
Full changelog available on GitHub

View file

@ -0,0 +1,5 @@
In v1.7.9:
- Android TV support (cont'd)
- interact with videos via media session controls
- enjoy the app in Czech & Polish
Full changelog available on GitHub

View file

@ -2,4 +2,4 @@
<b>Navigasi dan pencarian</b> merupakan bagian penting dari <i>Aves</i>. Tujuannya adalah agar pengguna dengan mudah mengalir dari album ke foto ke tag ke peta, dll.
<i>Aves</i> terintegrasi dengan Android (dari <b>API 19 ke 33</b>, yaitu dari KitKat ke Android 13) dengan fitur-fitur seperti <b>pintasan aplikasi</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.
<i>Aves</i> mengintegrasi dengan Android (dari Kitkat ke Android 13) dengan fitur-fitur seperti <b>pintasan aplikasi</b>, <b>jalan pintas aplikasi</b>, <b>screen saver</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.

View file

@ -1,5 +1,5 @@
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
<i>Aves</i> kan handsama alle slags bileter og videoar, medteken JPEG og MP4, men au meir uvane ting som <b>fleirsida TIFF-ar, SVG-ar, gamle AVI-ar med meir</b>! Aves ser igjennom mediasamlinga di for å gjenkjenne <b>rørslebilete</b>, <b>panorama</b> (bilete med vidt oversyn), <b>360° videoar</b>, og au <b>GeoTIFF</b>-filer.
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
<b>Navigering og søk</b> har mykje å sei i <i>Aves</i>. Målet er at ein skal lett kunne gå ifrå album, til bilete, til merkelappar, til kart, osv.
<i>Aves</i> integrates with Android (from KitKat to Android 13, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

View file

@ -16,5 +16,57 @@
"filePickerShowHiddenFiles": "إظهار الملفات المخفية",
"@filePickerShowHiddenFiles": {},
"panoramaEnableSensorControl": "تمكين التحكم في المستشعر",
"@panoramaEnableSensorControl": {}
"@panoramaEnableSensorControl": {},
"saveTooltip": "حفظ",
"@saveTooltip": {},
"continueButtonLabel": "إستمرار",
"@continueButtonLabel": {},
"resetTooltip": "إعادة",
"@resetTooltip": {},
"doNotAskAgain": "عدم السؤال مرة أخرى",
"@doNotAskAgain": {},
"welcomeTermsToggle": "أوافق على الشروط",
"@welcomeTermsToggle": {},
"doubleBackExitMessage": "اضغط على \"رجوع\" مرة أخرى للخروج.",
"@doubleBackExitMessage": {},
"hideButtonLabel": "إخفاء",
"@hideButtonLabel": {},
"showTooltip": "إظهار",
"@showTooltip": {},
"clearTooltip": "تنظيف",
"@clearTooltip": {},
"changeTooltip": "تغيير",
"@changeTooltip": {},
"actionRemove": "إزالة",
"@actionRemove": {},
"appName": "Aves",
"@appName": {},
"welcomeOptional": "اختياري",
"@welcomeOptional": {},
"deleteButtonLabel": "حذف",
"@deleteButtonLabel": {},
"nextTooltip": "التالي",
"@nextTooltip": {},
"cancelTooltip": "إلغاء",
"@cancelTooltip": {},
"previousTooltip": "السابق",
"@previousTooltip": {},
"welcomeMessage": "مرحبا بكم في Aves",
"@welcomeMessage": {},
"applyButtonLabel": "تطبيق",
"@applyButtonLabel": {},
"nextButtonLabel": "التالي",
"@nextButtonLabel": {},
"showButtonLabel": "إظهار",
"@showButtonLabel": {},
"tagEditorSectionRecent": "الأخيرة",
"@tagEditorSectionRecent": {},
"tagEditorSectionPlaceholders": "العناصر النائبة",
"@tagEditorSectionPlaceholders": {},
"filePickerUseThisFolder": "إستخدام هذا المجلد",
"@filePickerUseThisFolder": {},
"hideTooltip": "إخفاء",
"@hideTooltip": {},
"tagEditorPageAddTagTooltip": "إضافة علامة",
"@tagEditorPageAddTagTooltip": {}
}

1358
lib/l10n/app_cs.arb Normal file

File diff suppressed because it is too large Load diff

View file

@ -1196,5 +1196,15 @@
"filterNoAddressLabel": "Χωρίς διεύθυνση",
"@filterNoAddressLabel": {},
"settingsViewerShowRatingTags": "Εμφάνιση βαθμολογίας & ετικετών",
"@settingsViewerShowRatingTags": {}
"@settingsViewerShowRatingTags": {},
"filterLocatedLabel": "Με τοποθεσία",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Με ετικέτα",
"@filterTaggedLabel": {},
"settingsModificationWarningDialogMessage": "Άλλες ρυθμίσεις θα τροποποιηθούν.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον",
"@settingsDisplayUseTvInterface": {},
"settingsViewerShowDescription": "Εμφάνιση περιγραφής",
"@settingsViewerShowDescription": {}
}

View file

@ -139,8 +139,10 @@
"filterFavouriteLabel": "Favorite",
"filterNoDateLabel": "Undated",
"filterNoAddressLabel": "No address",
"filterLocatedLabel": "Located",
"filterNoLocationLabel": "Unlocated",
"filterNoRatingLabel": "Unrated",
"filterTaggedLabel": "Tagged",
"filterNoTagLabel": "Untagged",
"filterNoTitleLabel": "Untitled",
"filterOnThisDayLabel": "On this day",
@ -375,13 +377,13 @@
"renameProcessorCounter": "Counter",
"renameProcessorName": "Name",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and its item?} other{Delete this album and its {count} items?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and the item in it?} other{Delete this album and the {count} items in it?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
}
},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Delete these albums and their item?} other{Delete these albums and their {count} items?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Delete these albums and the item in them?} other{Delete these albums and the {count} items in them?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -436,6 +438,8 @@
"genericFailureFeedback": "Failed",
"genericDangerWarningDialogMessage": "Are you sure?",
"tooManyItemsErrorDialogMessage": "Try again with fewer items.",
"menuActionConfigureView": "View",
"menuActionSelect": "Select",
"menuActionSelectAll": "Select all",
@ -656,6 +660,7 @@
"settingsSystemDefault": "System default",
"settingsDefault": "Default",
"settingsDisabled": "Disabled",
"settingsModificationWarningDialogMessage": "Other settings will be modified.",
"settingsSearchFieldLabel": "Search settings",
"settingsSearchEmpty": "No matching setting",
@ -731,6 +736,7 @@
"settingsViewerShowInformationSubtitle": "Show title, date, location, etc.",
"settingsViewerShowRatingTags": "Show rating & tags",
"settingsViewerShowShootingDetails": "Show shooting details",
"settingsViewerShowDescription": "Show description",
"settingsViewerShowOverlayThumbnails": "Show thumbnails",
"settingsViewerEnableOverlayBlurEffect": "Blur effect",
@ -815,6 +821,7 @@
"settingsThemeEnableDynamicColor": "Dynamic color",
"settingsDisplayRefreshRateModeTile": "Display refresh rate",
"settingsDisplayRefreshRateModeDialogTitle": "Refresh Rate",
"settingsDisplayUseTvInterface": "Android TV interface",
"settingsLanguageSectionTitle": "Language & Formats",
"settingsLanguageTile": "Language",

View file

@ -371,9 +371,9 @@
"@renameProcessorCounter": {},
"renameProcessorName": "Nombre",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de que desea borrar este álbum y un elemento?} other{¿Está seguro de que desea borrar este álbum y sus {count} elementos?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{¿Eliminar este álbum y el elemento que contiene?} other{¿Eliminar este álbum y los {count} elementos que contiene?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de que desea borrar estos álbumes y un elemento?} other{¿Está seguro de que desea borrar estos álbumes y sus {count} elementos?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{¿Eliminar estos álbumes y el elemento que contienen?} other{¿Eliminar estos álbumes y los {count} elementos que contienen?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Formato:",
"@exportEntryDialogFormat": {},
@ -1196,5 +1196,15 @@
"placeholders": {
"count": {}
}
}
},
"settingsViewerShowDescription": "Mostrar la descripción",
"@settingsViewerShowDescription": {},
"settingsModificationWarningDialogMessage": "Otras configuraciones serán modificadas.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Interfaz de Android TV",
"@settingsDisplayUseTvInterface": {},
"filterLocatedLabel": "Localizado",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Etiquetado",
"@filterTaggedLabel": {}
}

View file

@ -297,5 +297,86 @@
"policyPageTitle": "سیاست حفظ حریم خصوصی",
"@policyPageTitle": {},
"collectionPickPageTitle": "انتخاب",
"@collectionPickPageTitle": {}
"@collectionPickPageTitle": {},
"videoResumeDialogMessage": "ادامه پخش از زمان {time}؟",
"@videoResumeDialogMessage": {
"placeholders": {
"time": {
"type": "String",
"example": "13:37"
}
}
},
"storageVolumeDescriptionFallbackNonPrimary": "کارت حافظه",
"@storageVolumeDescriptionFallbackNonPrimary": {},
"videoPlaybackWithSound": "پخش با صدا",
"@videoPlaybackWithSound": {},
"entryActionCopyToClipboard": "کپی به کلیپ بورد",
"@entryActionCopyToClipboard": {},
"entryActionShowGeoTiffOnMap": "نمایش بر روی نقشه",
"@entryActionShowGeoTiffOnMap": {},
"filterOnThisDayLabel": "در امروز",
"@filterOnThisDayLabel": {},
"mapStyleGoogleNormal": "گوگل مپ",
"@mapStyleGoogleNormal": {},
"mapStyleGoogleTerrain": "گوگل مپ (نمایش زمین)",
"@mapStyleGoogleTerrain": {},
"mapStyleGoogleHybrid": "گوگل مپ (نمایش هیبریدی)",
"@mapStyleGoogleHybrid": {},
"subtitlePositionTop": "بالا",
"@subtitlePositionTop": {},
"mapStyleStamenWatercolor": "استامن (نمایش نقشه کشیده شده)",
"@mapStyleStamenWatercolor": {},
"displayRefreshRatePreferLowest": "کمترین مقدار",
"@displayRefreshRatePreferLowest": {},
"videoPlaybackMuted": "پخش بی صدا",
"@videoPlaybackMuted": {},
"storageVolumeDescriptionFallbackPrimary": "حافظه داخلی",
"@storageVolumeDescriptionFallbackPrimary": {},
"columnCount": "{count, plural, =1{1 ستون} other{{count} ستون}}",
"@columnCount": {
"placeholders": {
"count": {}
}
},
"mapStyleHuaweiNormal": "پتال مپس",
"@mapStyleHuaweiNormal": {},
"mapStyleHuaweiTerrain": "پتال مپس (نمایش زمین)",
"@mapStyleHuaweiTerrain": {},
"mapStyleOsmHot": "اوپن‌استریت‌مپ",
"@mapStyleOsmHot": {},
"subtitlePositionBottom": "پایین",
"@subtitlePositionBottom": {},
"themeBrightnessLight": "روشن",
"@themeBrightnessLight": {},
"themeBrightnessDark": "تاریک",
"@themeBrightnessDark": {},
"themeBrightnessBlack": "سیاه",
"@themeBrightnessBlack": {},
"videoStartOverButtonLabel": "شروع از اول",
"@videoStartOverButtonLabel": {},
"albumTierApps": "برنامه ها",
"@albumTierApps": {},
"nameConflictStrategyRename": "تغییر نام",
"@nameConflictStrategyRename": {},
"nameConflictStrategyReplace": "جایگزین کردن",
"@nameConflictStrategyReplace": {},
"displayRefreshRatePreferHighest": "بیشترین مقدار",
"@displayRefreshRatePreferHighest": {},
"storageAccessDialogMessage": "لطفا فولدر {directory} در {volume} را در صفحه بعد انتخاب کنید و اجازه را به برنامه بدهید.",
"@storageAccessDialogMessage": {
"placeholders": {
"directory": {
"type": "String",
"description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
},
"volume": {
"type": "String",
"example": "SD card",
"description": "the name of a storage volume"
}
}
},
"mapStyleStamenToner": "استامن (نمایش رود ها)",
"@mapStyleStamenToner": {}
}

View file

@ -69,7 +69,7 @@
"@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "Afficher dans Pays",
"@chipActionGoToCountryPage": {},
"chipActionGoToTagPage": "Afficher dans Libellés",
"chipActionGoToTagPage": "Afficher dans Étiquettes",
"@chipActionGoToTagPage": {},
"chipActionFilterOut": "Exclure",
"@chipActionFilterOut": {},
@ -165,7 +165,7 @@
"@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Modifier la notation",
"@entryInfoActionEditRating": {},
"entryInfoActionEditTags": "Modifier les libellés",
"entryInfoActionEditTags": "Modifier les étiquettes",
"@entryInfoActionEditTags": {},
"entryInfoActionRemoveMetadata": "Retirer les métadonnées",
"@entryInfoActionRemoveMetadata": {},
@ -179,7 +179,7 @@
"@filterNoLocationLabel": {},
"filterNoRatingLabel": "Sans notation",
"@filterNoRatingLabel": {},
"filterNoTagLabel": "Sans libellé",
"filterNoTagLabel": "Sans étiquette",
"@filterNoTagLabel": {},
"filterNoTitleLabel": "Sans titre",
"@filterNoTitleLabel": {},
@ -391,9 +391,9 @@
"@renameProcessorCounter": {},
"renameProcessorName": "Nom",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer cet album et son élément ?} other{Supprimer cet album et ses {count} éléments ?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer cet album et lélément dedans ?} other{Supprimer cet album et les {count} éléments dedans ?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer ces albums et leur élément ?} other{Supprimer ces albums et leurs {count} éléments ?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer ces albums et lélément dedans ?} other{Supprimer ces albums et les {count} éléments dedans ?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Format :",
"@exportEntryDialogFormat": {},
@ -641,7 +641,7 @@
"@drawerAlbumPage": {},
"drawerCountryPage": "Pays",
"@drawerCountryPage": {},
"drawerTagPage": "Libellés",
"drawerTagPage": "Étiquettes",
"@drawerTagPage": {},
"sortByDate": "par date",
"@sortByDate": {},
@ -713,9 +713,9 @@
"@countryPageTitle": {},
"countryEmpty": "Aucun pays",
"@countryEmpty": {},
"tagPageTitle": "Libellés",
"tagPageTitle": "Étiquettes",
"@tagPageTitle": {},
"tagEmpty": "Aucun libellé",
"tagEmpty": "Aucune étiquette",
"@tagEmpty": {},
"binPageTitle": "Corbeille",
"@binPageTitle": {},
@ -731,7 +731,7 @@
"@searchCountriesSectionTitle": {},
"searchPlacesSectionTitle": "Lieux",
"@searchPlacesSectionTitle": {},
"searchTagsSectionTitle": "Libellés",
"searchTagsSectionTitle": "Étiquettes",
"@searchTagsSectionTitle": {},
"searchRatingSectionTitle": "Notations",
"@searchRatingSectionTitle": {},
@ -811,7 +811,7 @@
"@settingsThumbnailOverlayPageTitle": {},
"settingsThumbnailShowFavouriteIcon": "Afficher licône de favori",
"@settingsThumbnailShowFavouriteIcon": {},
"settingsThumbnailShowTagIcon": "Afficher licône de libellé",
"settingsThumbnailShowTagIcon": "Afficher licône détiquette",
"@settingsThumbnailShowTagIcon": {},
"settingsThumbnailShowLocationIcon": "Afficher licône de lieu",
"@settingsThumbnailShowLocationIcon": {},
@ -1043,7 +1043,7 @@
"@statsTopCountriesSectionTitle": {},
"statsTopPlacesSectionTitle": "Top lieux",
"@statsTopPlacesSectionTitle": {},
"statsTopTagsSectionTitle": "Top libellés",
"statsTopTagsSectionTitle": "Top étiquettes",
"@statsTopTagsSectionTitle": {},
"statsTopAlbumsSectionTitle": "Top albums",
"@statsTopAlbumsSectionTitle": {},
@ -1123,11 +1123,11 @@
"@viewerInfoSearchSuggestionRights": {},
"wallpaperUseScrollEffect": "Utiliser leffet de défilement sur lécran daccueil",
"@wallpaperUseScrollEffect": {},
"tagEditorPageTitle": "Modifier les libellés",
"tagEditorPageTitle": "Modifier les étiquettes",
"@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "Nouveau libellé",
"tagEditorPageNewTagFieldLabel": "Nouvelle étiquette",
"@tagEditorPageNewTagFieldLabel": {},
"tagEditorPageAddTagTooltip": "Ajouter le libellé",
"tagEditorPageAddTagTooltip": "Ajouter létiquette",
"@tagEditorPageAddTagTooltip": {},
"tagEditorSectionRecent": "Ajouts récents",
"@tagEditorSectionRecent": {},
@ -1149,7 +1149,7 @@
"@filePickerUseThisFolder": {},
"editEntryLocationDialogSetCustom": "Définir un lieu personnalisé",
"@editEntryLocationDialogSetCustom": {},
"tagEditorSectionPlaceholders": "Libellés de substitution",
"tagEditorSectionPlaceholders": "Étiquettes de substitution",
"@tagEditorSectionPlaceholders": {},
"tagPlaceholderPlace": "Lieu",
"@tagPlaceholderPlace": {},
@ -1179,7 +1179,7 @@
"@filterAspectRatioPortraitLabel": {},
"filterAspectRatioLandscapeLabel": "Paysage",
"@filterAspectRatioLandscapeLabel": {},
"settingsViewerShowRatingTags": "Afficher la notation et les libellés",
"settingsViewerShowRatingTags": "Afficher la notation et les étiquettes",
"@settingsViewerShowRatingTags": {},
"entryActionShareImageOnly": "Partager limage seulement",
"@entryActionShareImageOnly": {},
@ -1196,5 +1196,17 @@
}
},
"settingsAccessibilityShowPinchGestureAlternatives": "Afficher des alternatives aux interactions multitactiles",
"@settingsAccessibilityShowPinchGestureAlternatives": {}
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"settingsViewerShowDescription": "Afficher la description",
"@settingsViewerShowDescription": {},
"settingsModificationWarningDialogMessage": "Dautres réglages seront modifiés.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Interface Android TV",
"@settingsDisplayUseTvInterface": {},
"filterTaggedLabel": "Étiqueté",
"@filterTaggedLabel": {},
"filterLocatedLabel": "Localisé",
"@filterLocatedLabel": {},
"tooManyItemsErrorDialogMessage": "Réessayez avec moins déléments.",
"@tooManyItemsErrorDialogMessage": {}
}

View file

@ -337,7 +337,7 @@
"@binEntriesConfirmationDialogMessage": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus benda ini?} other{Apakah Anda yakin ingin menghapus {count} benda?}}",
"@deleteEntriesConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogMessage": "Beberapa benda tidak mempunyai tanggal metadata. Tanggal mereka sekarang akan diatur ulang dengan operasi ini kecuali ada tanggal metadata yang ditetapkan.",
"moveUndatedConfirmationDialogMessage": "Simpan tanggal benda sebelum melanjutkan?",
"@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "Atur tanggal",
"@moveUndatedConfirmationDialogSetDate": {},
@ -379,9 +379,9 @@
"@renameProcessorCounter": {},
"renameProcessorName": "Nama",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Apakah Anda yakin ingin menghapus album ini dan bendanya?} other{Apakah Anda yakin ingin menghapus album ini dan {count} bendanya?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Hapus album ini dan item yang ada di dalam?} other{Hapus album ini dan {count} item yang ada di dalam?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Apakah Anda yakin ingin menghapus album ini dan bendanya?} other{Apakah Anda yakin ingin menghapus album ini dan {count} bendanya?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Hapus album ini dan item yang ada di dalam?} other{Hapus album ini dan {count} item yang ada di dalam?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {},
@ -867,7 +867,7 @@
"@settingsSlideshowFillScreen": {},
"settingsSlideshowTransitionTile": "Transisi",
"@settingsSlideshowTransitionTile": {},
"settingsSlideshowIntervalTile": "Interval",
"settingsSlideshowIntervalTile": "Jarak waktu",
"@settingsSlideshowIntervalTile": {},
"settingsSlideshowVideoPlaybackTile": "Putaran ulang video",
"@settingsSlideshowVideoPlaybackTile": {},
@ -1157,7 +1157,7 @@
"@tagPlaceholderPlace": {},
"editEntryLocationDialogSetCustom": "Terapkan lokasi kustom",
"@editEntryLocationDialogSetCustom": {},
"subtitlePositionTop": "Atas",
"subtitlePositionTop": "Teratas",
"@subtitlePositionTop": {},
"subtitlePositionBottom": "Bawah",
"@subtitlePositionBottom": {},
@ -1196,5 +1196,17 @@
}
},
"settingsAccessibilityShowPinchGestureAlternatives": "Tampilkan alternatif gestur multisentuh",
"@settingsAccessibilityShowPinchGestureAlternatives": {}
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"settingsViewerShowDescription": "Tampilkan deskripsi",
"@settingsViewerShowDescription": {},
"settingsModificationWarningDialogMessage": "Pengaturan lain akan diubah.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Antarmuka Android TV",
"@settingsDisplayUseTvInterface": {},
"filterLocatedLabel": "Terletak",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Dilabel",
"@filterTaggedLabel": {},
"tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.",
"@tooManyItemsErrorDialogMessage": {}
}

View file

@ -389,9 +389,9 @@
"@renameProcessorCounter": {},
"renameProcessorName": "Nome",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questo album e i suoi elementi?} other{Cancellare questo album e i suoi {count} elementi?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questo album e lelemento in esso?} other{Cancellare questo album e i {count} elementi in esso?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questi album e i loro elementi?} other{Cancellare questi album e i loro {count} elementi?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questi album e lelemento in essi?} other{Cancellare questi album e i {count} elementi in essi?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Formato:",
"@exportEntryDialogFormat": {},
@ -1196,5 +1196,17 @@
"entryActionShareVideoOnly": "Condividi solo video",
"@entryActionShareVideoOnly": {},
"filterNoAddressLabel": "Senza indirizzo",
"@filterNoAddressLabel": {}
"@filterNoAddressLabel": {},
"filterLocatedLabel": "Posizionato",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Etichettato",
"@filterTaggedLabel": {},
"settingsModificationWarningDialogMessage": "Le altre impostazioni saranno modificate.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Interfaccia Android TV",
"@settingsDisplayUseTvInterface": {},
"settingsViewerShowDescription": "Mostra la descrizione",
"@settingsViewerShowDescription": {},
"tooManyItemsErrorDialogMessage": "Riprova con meno elementi.",
"@tooManyItemsErrorDialogMessage": {}
}

View file

@ -175,7 +175,7 @@
"@filterFavouriteLabel": {},
"filterNoDateLabel": "날짜 없음",
"@filterNoDateLabel": {},
"filterNoLocationLabel": "장소 없음",
"filterNoLocationLabel": "위치 없음",
"@filterNoLocationLabel": {},
"filterNoRatingLabel": "별점 없음",
"@filterNoRatingLabel": {},
@ -1196,5 +1196,17 @@
"placeholders": {
"count": {}
}
}
},
"settingsViewerShowDescription": "설명 표시",
"@settingsViewerShowDescription": {},
"settingsModificationWarningDialogMessage": "다른 설정도 변경될 것입니다.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "안드로이드 TV 인터페이스 사용하기",
"@settingsDisplayUseTvInterface": {},
"filterTaggedLabel": "태그 있음",
"@filterTaggedLabel": {},
"filterLocatedLabel": "위치 있음",
"@filterLocatedLabel": {},
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
"@tooManyItemsErrorDialogMessage": {}
}

View file

@ -264,7 +264,7 @@
"@keepScreenOnViewerOnly": {},
"keepScreenOnAlways": "Heile tida",
"@keepScreenOnAlways": {},
"accessibilityAnimationsRemove": "Hindra rørsle",
"accessibilityAnimationsRemove": "Hindra skjermrørsle",
"@accessibilityAnimationsRemove": {},
"subtitlePositionTop": "På toppen",
"@subtitlePositionTop": {},
@ -352,5 +352,403 @@
"description": "the name of a specific directory"
}
}
}
},
"keepScreenOnVideoPlayback": "Under videoavspeling",
"@keepScreenOnVideoPlayback": {},
"newAlbumDialogNameLabel": "Albumsnamn",
"@newAlbumDialogNameLabel": {},
"durationDialogMinutes": "Minutt",
"@durationDialogMinutes": {},
"settingsThemeColorHighlights": "Farga framhevjingar",
"@settingsThemeColorHighlights": {},
"viewerInfoBackToViewerTooltip": "Attende til vising",
"@viewerInfoBackToViewerTooltip": {},
"mapStyleDialogTitle": "Kartstil",
"@mapStyleDialogTitle": {},
"notEnoughSpaceDialogMessage": "Denne gjerda tarv {neededSize} unytta rom på «{volume}» for å verta fullgjord, men det er berre {freeSize} att.",
"@notEnoughSpaceDialogMessage": {
"placeholders": {
"neededSize": {
"type": "String",
"example": "314 MB"
},
"freeSize": {
"type": "String",
"example": "123 MB"
},
"volume": {
"type": "String",
"example": "SD card",
"description": "the name of a storage volume"
}
}
},
"missingSystemFilePickerDialogMessage": "Systemfilveljaren er borte eller avslegen. Slå han på og røyn om att.",
"@missingSystemFilePickerDialogMessage": {},
"videoResumeDialogMessage": "Hald fram avspeling ifrå {time}?",
"@videoResumeDialogMessage": {
"placeholders": {
"time": {
"type": "String",
"example": "13:37"
}
}
},
"videoStartOverButtonLabel": "BYRJA OM ATT",
"@videoStartOverButtonLabel": {},
"hideFilterConfirmationDialogMessage": "Samsvarande bilete og videoar vil verte skjult ifrå samlinga di. Du kan visa dei att ifrå «Personvern»-innstillingane.\n\nEr du sikker på at du vil skjule dei?",
"@hideFilterConfirmationDialogMessage": {},
"renameEntrySetPageInsertTooltip": "Innskrivingsområde",
"@renameEntrySetPageInsertTooltip": {},
"renameEntrySetPagePatternFieldLabel": "Namngjevingsmønster",
"@renameEntrySetPagePatternFieldLabel": {},
"renameEntrySetPagePreviewSectionTitle": "Førehandsvis",
"@renameEntrySetPagePreviewSectionTitle": {},
"renameEntryDialogLabel": "Nytt namn",
"@renameEntryDialogLabel": {},
"editEntryDialogCopyFromItem": "Kopier ifrå anna element",
"@editEntryDialogCopyFromItem": {},
"editEntryDateDialogSourceFileModifiedDate": "Filbrigdedato",
"@editEntryDateDialogSourceFileModifiedDate": {},
"durationDialogHours": "Timar",
"@durationDialogHours": {},
"editEntryLocationDialogChooseOnMap": "Vel på kartet",
"@editEntryLocationDialogChooseOnMap": {},
"settingsLanguageTile": "Mål",
"@settingsLanguageTile": {},
"settingsUnitSystemTile": "Einingar",
"@settingsUnitSystemTile": {},
"settingsCoordinateFormatDialogTitle": "Koordinatformat",
"@settingsCoordinateFormatDialogTitle": {},
"settingsWidgetDisplayedItem": "Vist element",
"@settingsWidgetDisplayedItem": {},
"statsTopAlbumsSectionTitle": "Topp-album",
"@statsTopAlbumsSectionTitle": {},
"statsTopTagsSectionTitle": "Toppmerkelappar",
"@statsTopTagsSectionTitle": {},
"viewerInfoUnknown": "ukjend",
"@viewerInfoUnknown": {},
"viewerInfoLabelResolution": "Oppløysing",
"@viewerInfoLabelResolution": {},
"viewerInfoLabelUri": "URI",
"@viewerInfoLabelUri": {},
"viewerInfoLabelOwner": "Eigar",
"@viewerInfoLabelOwner": {},
"viewerInfoLabelCoordinates": "Koordinatar",
"@viewerInfoLabelCoordinates": {},
"tagEditorPageAddTagTooltip": "Legg til merkelapp",
"@tagEditorPageAddTagTooltip": {},
"filePickerDoNotShowHiddenFiles": "Ikkje vis skjulte filer",
"@filePickerDoNotShowHiddenFiles": {},
"panoramaEnableSensorControl": "Slå på sensorstyring",
"@panoramaEnableSensorControl": {},
"panoramaDisableSensorControl": "Slå av sensorstyring",
"@panoramaDisableSensorControl": {},
"filePickerOpenFrom": "Opne ifrå",
"@filePickerOpenFrom": {},
"filePickerNoItems": "Ingen element",
"@filePickerNoItems": {},
"nameConflictDialogSingleSourceMessage": "Somme filer i målmappa har same namn.",
"@nameConflictDialogSingleSourceMessage": {},
"nameConflictDialogMultipleSourceMessage": "Somme filer har same namn.",
"@nameConflictDialogMultipleSourceMessage": {},
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Flytt dette elementet til papirkorga?} other{Flytt desse {count} elementa til papirkorga?}}",
"@binEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {}
}
},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Slett dette elementet?} other{Slett desse {count} elementa?}}",
"@deleteEntriesConfirmationDialogMessage": {
"placeholders": {
"count": {}
}
},
"videoResumeButtonLabel": "HALD FRAM",
"@videoResumeButtonLabel": {},
"entryActionShareVideoOnly": "Del berre video",
"@entryActionShareVideoOnly": {},
"entryActionShareImageOnly": "Del berre bilete",
"@entryActionShareImageOnly": {},
"unsupportedTypeDialogMessage": "{count, plural, other{Denne gjerda er ustødd for element av fylgjande slag: {types}.}}",
"@unsupportedTypeDialogMessage": {
"placeholders": {
"count": {},
"types": {
"type": "String",
"example": "GIF, TIFF, MP4",
"description": "a list of unsupported types"
}
}
},
"addShortcutDialogLabel": "Snarvegsmerkelapp",
"@addShortcutDialogLabel": {},
"addShortcutButtonLabel": "LEGG TIL",
"@addShortcutButtonLabel": {},
"noMatchingAppDialogMessage": "Ingen appar kan handsame dette.",
"@noMatchingAppDialogMessage": {},
"moveUndatedConfirmationDialogMessage": "Gøym elementdatoar før framhald?",
"@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "Gøym datoar",
"@moveUndatedConfirmationDialogSetDate": {},
"setCoverDialogLatest": "Nyaste element",
"@setCoverDialogLatest": {},
"setCoverDialogAuto": "Auto",
"@setCoverDialogAuto": {},
"newAlbumDialogTitle": "Nytt Album",
"@newAlbumDialogTitle": {},
"newAlbumDialogNameLabelAlreadyExistsHelper": "Mappa finst alt",
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
"newAlbumDialogStorageLabel": "Gøyme:",
"@newAlbumDialogStorageLabel": {},
"renameAlbumDialogLabel": "Nytt namn",
"@renameAlbumDialogLabel": {},
"renameAlbumDialogLabelAlreadyExistsHelper": "Mappa finst alt",
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
"renameEntrySetPageTitle": "Døyp om",
"@renameEntrySetPageTitle": {},
"exportEntryDialogWidth": "Breidd",
"@exportEntryDialogWidth": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett desse albuma og deira element?} other{Slett desse albuma og deira {count} element?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
}
},
"exportEntryDialogHeight": "Høgd",
"@exportEntryDialogHeight": {},
"editEntryDateDialogExtractFromTitle": "Tak ut ifrå namn",
"@editEntryDateDialogExtractFromTitle": {},
"editEntryDateDialogCopyField": "Kopier ifrå annan dato",
"@editEntryDateDialogCopyField": {},
"editEntryDateDialogShift": "Byt",
"@editEntryDateDialogShift": {},
"durationDialogSeconds": "Sekund",
"@durationDialogSeconds": {},
"editEntryRatingDialogTitle": "Omdøme",
"@editEntryRatingDialogTitle": {},
"removeEntryMetadataDialogTitle": "Metadataborttaking",
"@removeEntryMetadataDialogTitle": {},
"removeEntryMetadataDialogMore": "Meir",
"@removeEntryMetadataDialogMore": {},
"statsTopPlacesSectionTitle": "Toppstadar",
"@statsTopPlacesSectionTitle": {},
"settingsCollectionTile": "Samling",
"@settingsCollectionTile": {},
"statsPageTitle": "Samandrag",
"@statsPageTitle": {},
"statsTopCountriesSectionTitle": "Toppland",
"@statsTopCountriesSectionTitle": {},
"viewerOpenPanoramaButtonLabel": "OPNE PANORAMA",
"@viewerOpenPanoramaButtonLabel": {},
"viewerInfoLabelSize": "Storleik",
"@viewerInfoLabelSize": {},
"viewerInfoLabelDate": "Dato",
"@viewerInfoLabelDate": {},
"viewerInfoLabelDuration": "Lengd",
"@viewerInfoLabelDuration": {},
"viewerInfoLabelPath": "Sti",
"@viewerInfoLabelPath": {},
"mapZoomOutTooltip": "Mink",
"@mapZoomOutTooltip": {},
"mapZoomInTooltip": "Auk",
"@mapZoomInTooltip": {},
"openMapPageTooltip": "Vis på kartsida",
"@openMapPageTooltip": {},
"viewerInfoViewXmlLinkText": "Vis XML",
"@viewerInfoViewXmlLinkText": {},
"viewerInfoSearchFieldLabel": "Søk metadata",
"@viewerInfoSearchFieldLabel": {},
"viewerInfoSearchEmpty": "Ingen samsvarande lyklar",
"@viewerInfoSearchEmpty": {},
"viewerInfoSearchSuggestionDate": "Dato og tid",
"@viewerInfoSearchSuggestionDate": {},
"tagEditorPageTitle": "Brigd merkelappar",
"@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "Ny merkelapp",
"@tagEditorPageNewTagFieldLabel": {},
"filePickerShowHiddenFiles": "Vis skjulte filer",
"@filePickerShowHiddenFiles": {},
"sourceViewerPageTitle": "Kjelde",
"@sourceViewerPageTitle": {},
"renameProcessorCounter": "Teljar",
"@renameProcessorCounter": {},
"renameProcessorName": "Namn",
"@renameProcessorName": {},
"editEntryDateDialogTitle": "Dato og tid",
"@editEntryDateDialogTitle": {},
"editEntryLocationDialogLatitude": "Breiddegrad",
"@editEntryLocationDialogLatitude": {},
"editEntryLocationDialogLongitude": "Lengdegrad",
"@editEntryLocationDialogLongitude": {},
"sourceStateLoading": "Hentar inn",
"@sourceStateLoading": {},
"filePickerUseThisFolder": "Bruk denne mappa",
"@filePickerUseThisFolder": {},
"viewerErrorDoesNotExist": "Fila finst ikkje meir.",
"@viewerErrorDoesNotExist": {},
"filterBinLabel": "Papirkorg",
"@filterBinLabel": {},
"filterTypeAnimatedLabel": "Animert",
"@filterTypeAnimatedLabel": {},
"filterTypeMotionPhotoLabel": "Rørslebilete",
"@filterTypeMotionPhotoLabel": {},
"filterTypePanoramaLabel": "Panorama",
"@filterTypePanoramaLabel": {},
"mapStyleOsmHot": "Humanitært OSM",
"@mapStyleOsmHot": {},
"mapStyleStamenToner": "Stamen Toner (svart-kvitt)",
"@mapStyleStamenToner": {},
"themeBrightnessLight": "Ljos",
"@themeBrightnessLight": {},
"themeBrightnessDark": "Mørk",
"@themeBrightnessDark": {},
"themeBrightnessBlack": "Svart",
"@themeBrightnessBlack": {},
"viewerTransitionSlide": "Skridande",
"@viewerTransitionSlide": {},
"viewerTransitionParallax": "Parallakse",
"@viewerTransitionParallax": {},
"viewerTransitionFade": "Ton ut",
"@viewerTransitionFade": {},
"widgetDisplayedItemRandom": "Tilfeldig",
"@widgetDisplayedItemRandom": {},
"widgetOpenPageHome": "Opne heimside",
"@widgetOpenPageHome": {},
"restrictedAccessDialogMessage": "Denne appen har ikkje lov til å brigde filer i «{directory}»-mappa i «{volume}».\n\nBruk ein førehandsinnlagd filhandsamar eller galleriapp til å flytta elementa til ei anna mappe.",
"@restrictedAccessDialogMessage": {
"placeholders": {
"directory": {
"type": "String",
"description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
},
"volume": {
"type": "String",
"example": "SD card",
"description": "the name of a storage volume"
}
}
},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Slett dette albumet og elementet i det?} other{Slett dette albumet og dei {count} elementa i det?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
}
},
"exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {},
"tagPlaceholderCountry": "Land",
"@tagPlaceholderCountry": {},
"tagEditorSectionRecent": "Nyleg",
"@tagEditorSectionRecent": {},
"tagPlaceholderPlace": "Stad",
"@tagPlaceholderPlace": {},
"viewerInfoSearchSuggestionRights": "Rettar",
"@viewerInfoSearchSuggestionRights": {},
"viewerInfoSearchSuggestionResolution": "Oppløysing",
"@viewerInfoSearchSuggestionResolution": {},
"viewerInfoOpenLinkText": "Opne",
"@viewerInfoOpenLinkText": {},
"mapStyleTooltip": "Vel kartstil",
"@mapStyleTooltip": {},
"viewerInfoLabelAddress": "Adresse",
"@viewerInfoLabelAddress": {},
"viewerInfoLabelTitle": "Namn",
"@viewerInfoLabelTitle": {},
"viewerErrorUnknown": "Oida.",
"@viewerErrorUnknown": {},
"viewerSetWallpaperButtonLabel": "SET SOM BAKGRUNNSBILETE",
"@viewerSetWallpaperButtonLabel": {},
"settingsWidgetShowOutline": "Omrit",
"@settingsWidgetShowOutline": {},
"settingsWidgetPageTitle": "Bileteramme",
"@settingsWidgetPageTitle": {},
"settingsScreenSaverPageTitle": "Skjermsparar",
"@settingsScreenSaverPageTitle": {},
"settingsUnitSystemDialogTitle": "Einingar",
"@settingsUnitSystemDialogTitle": {},
"settingsCoordinateFormatTile": "Koordinatformat",
"@settingsCoordinateFormatTile": {},
"settingsLanguagePageTitle": "Mål",
"@settingsLanguagePageTitle": {},
"settingsLanguageSectionTitle": "Mål og format",
"@settingsLanguageSectionTitle": {},
"settingsDisplaySectionTitle": "Vising",
"@settingsDisplaySectionTitle": {},
"videoStreamSelectionDialogTrack": "Spor",
"@videoStreamSelectionDialogTrack": {},
"genericDangerWarningDialogMessage": "Er du viss?",
"@genericDangerWarningDialogMessage": {},
"menuActionStats": "Samandrag",
"@menuActionStats": {},
"viewDialogGroupSectionTitle": "Hop",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Oppsett",
"@viewDialogLayoutSectionTitle": {},
"tileLayoutMosaic": "Mosaikk",
"@tileLayoutMosaic": {},
"aboutBugCopyInfoButton": "Kopier",
"@aboutBugCopyInfoButton": {},
"aboutCreditsWorldAtlas1": "Denne appen nyttar ei TopoJSON-fil ifrå",
"@aboutCreditsWorldAtlas1": {},
"viewerInfoSearchSuggestionDescription": "Utgreiing",
"@viewerInfoSearchSuggestionDescription": {},
"aboutCreditsWorldAtlas2": "under ISC-løyve.",
"@aboutCreditsWorldAtlas2": {},
"videoSpeedDialogLabel": "Avspelingssnøggleik",
"@videoSpeedDialogLabel": {},
"videoStreamSelectionDialogVideo": "Video",
"@videoStreamSelectionDialogVideo": {},
"videoStreamSelectionDialogAudio": "Ljod",
"@videoStreamSelectionDialogAudio": {},
"videoStreamSelectionDialogText": "Undertekster",
"@videoStreamSelectionDialogText": {},
"videoStreamSelectionDialogOff": "Av",
"@videoStreamSelectionDialogOff": {},
"videoStreamSelectionDialogNoSelection": "Det er ingen andre spor.",
"@videoStreamSelectionDialogNoSelection": {},
"genericSuccessFeedback": "Fullgjort",
"@genericSuccessFeedback": {},
"genericFailureFeedback": "Mislykka",
"@genericFailureFeedback": {},
"menuActionSelectAll": "Vel alle",
"@menuActionSelectAll": {},
"menuActionSelectNone": "Tak bort val",
"@menuActionSelectNone": {},
"menuActionMap": "Kart",
"@menuActionMap": {},
"menuActionSlideshow": "Ljosbiletevising",
"@menuActionSlideshow": {},
"menuActionConfigureView": "Vis",
"@menuActionConfigureView": {},
"menuActionSelect": "Vel",
"@menuActionSelect": {},
"aboutBugCopyInfoInstruction": "Kopier systemopplysingar",
"@aboutBugCopyInfoInstruction": {},
"tagEditorSectionPlaceholders": "Førebels",
"@tagEditorSectionPlaceholders": {},
"tileLayoutGrid": "Rutenett",
"@tileLayoutGrid": {},
"tileLayoutList": "Liste",
"@tileLayoutList": {},
"coverDialogTabCover": "Omslag",
"@coverDialogTabCover": {},
"coverDialogTabApp": "App",
"@coverDialogTabApp": {},
"coverDialogTabColor": "Let",
"@coverDialogTabColor": {},
"appPickDialogTitle": "Vel app",
"@appPickDialogTitle": {},
"aboutPageTitle": "Om",
"@aboutPageTitle": {},
"aboutLinkLicense": "Løyve",
"@aboutLinkLicense": {},
"appPickDialogNone": "Ingen",
"@appPickDialogNone": {},
"aboutBugSectionTitle": "Mistakrapport",
"@aboutBugSectionTitle": {},
"aboutTranslatorsSectionTitle": "Omsetjarar",
"@aboutTranslatorsSectionTitle": {},
"viewerInfoOpenEmbeddedFailureFeedback": "Kunne ikkje ta ut innbygde opplysingar",
"@viewerInfoOpenEmbeddedFailureFeedback": {}
}

File diff suppressed because it is too large Load diff

View file

@ -1156,5 +1156,49 @@
"tagPlaceholderPlace": "Lugar",
"@tagPlaceholderPlace": {},
"editEntryLocationDialogSetCustom": "Definir local personalizado",
"@editEntryLocationDialogSetCustom": {}
"@editEntryLocationDialogSetCustom": {},
"subtitlePositionBottom": "Fundo",
"@subtitlePositionBottom": {},
"subtitlePositionTop": "Topo",
"@subtitlePositionTop": {},
"widgetDisplayedItemRandom": "Aleatório",
"@widgetDisplayedItemRandom": {},
"settingsSubtitleThemeTextPositionTile": "Posição do texto",
"@settingsSubtitleThemeTextPositionTile": {},
"settingsSubtitleThemeTextPositionDialogTitle": "Posição do Texto",
"@settingsSubtitleThemeTextPositionDialogTitle": {},
"settingsWidgetDisplayedItem": "Item exibido",
"@settingsWidgetDisplayedItem": {},
"entryInfoActionRemoveLocation": "Remover localização",
"@entryInfoActionRemoveLocation": {},
"filterNoAddressLabel": "Sem endereço",
"@filterNoAddressLabel": {},
"keepScreenOnVideoPlayback": "Durante a reprodução do video",
"@keepScreenOnVideoPlayback": {},
"settingsViewerShowDescription": "Mostrar descrição",
"@settingsViewerShowDescription": {},
"entryActionShareImageOnly": "Compartilhar apenas imagem",
"@entryActionShareImageOnly": {},
"entryActionShareVideoOnly": "Compartilhar apenas video",
"@entryActionShareVideoOnly": {},
"filterAspectRatioPortraitLabel": "Retrato",
"@filterAspectRatioPortraitLabel": {},
"filterAspectRatioLandscapeLabel": "Paisagem",
"@filterAspectRatioLandscapeLabel": {},
"entryInfoActionExportMetadata": "Exportar metadados",
"@entryInfoActionExportMetadata": {},
"widgetDisplayedItemMostRecent": "Mais recente",
"@widgetDisplayedItemMostRecent": {},
"filterTaggedLabel": "Marcado",
"@filterTaggedLabel": {},
"filterLocatedLabel": "Localizado",
"@filterLocatedLabel": {},
"settingsAccessibilityShowPinchGestureAlternatives": "Mostrar alternativas de gesto multitoque",
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"settingsModificationWarningDialogMessage": "Outras configurações serão modificadas.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Interface de TV Android",
"@settingsDisplayUseTvInterface": {},
"settingsViewerShowRatingTags": "Mostrar avaliações e tags",
"@settingsViewerShowRatingTags": {}
}

View file

@ -1354,5 +1354,15 @@
"filterNoAddressLabel": "Nicio adresă",
"@filterNoAddressLabel": {},
"entryInfoActionRemoveLocation": "Eliminare locație",
"@entryInfoActionRemoveLocation": {}
"@entryInfoActionRemoveLocation": {},
"settingsViewerShowDescription": "Afișare descriere",
"@settingsViewerShowDescription": {},
"filterLocatedLabel": "Locație",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Etichetat",
"@filterTaggedLabel": {},
"settingsModificationWarningDialogMessage": "Alte setări vor fi modificate.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Interfață Android TV",
"@settingsDisplayUseTvInterface": {}
}

View file

@ -1196,5 +1196,7 @@
"entryActionShareImageOnly": "Поделиться только изображением",
"@entryActionShareImageOnly": {},
"entryActionShareVideoOnly": "Поделиться только видео",
"@entryActionShareVideoOnly": {}
"@entryActionShareVideoOnly": {},
"settingsViewerShowDescription": "Показать описание",
"@settingsViewerShowDescription": {}
}

View file

@ -345,9 +345,9 @@
"@renameProcessorCounter": {},
"renameProcessorName": "Ad",
"@renameProcessorName": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albüm ve öğesi silinsin mi?} other{Bu albüm ve {count} öğesi silinsin mi?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albüm ve içindeki öge silinsin mi?} other{Bu albüm ve içindeki {count} öge silinsin mi?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albümler ve öğeleri silinsin mi?} other{Bu albümler ve {count} öğesi silinsin mi?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albümler ve içindeki öge silinsin mi?} other{Bu albümler ve içindeki {count} ögesi silinsin mi?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Biçim:",
"@exportEntryDialogFormat": {},
@ -1196,5 +1196,17 @@
}
},
"settingsAccessibilityShowPinchGestureAlternatives": "Çoklu dokunma hareketi alternatiflerini göster",
"@settingsAccessibilityShowPinchGestureAlternatives": {}
"@settingsAccessibilityShowPinchGestureAlternatives": {},
"settingsViewerShowDescription": "Açıklamayı göster",
"@settingsViewerShowDescription": {},
"settingsModificationWarningDialogMessage": "Diğer ayarlar değiştirilecektir.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Android TV arayüzü",
"@settingsDisplayUseTvInterface": {},
"filterLocatedLabel": "Konumlu",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Etiketli",
"@filterTaggedLabel": {},
"tooManyItemsErrorDialogMessage": "Daha az ögeyle tekrar deneyin.",
"@tooManyItemsErrorDialogMessage": {}
}

View file

@ -203,7 +203,7 @@
"@coordinateDmsWest": {},
"unitSystemMetric": "Метричні",
"@unitSystemMetric": {},
"unitSystemImperial": "Імперські",
"unitSystemImperial": "Англійські",
"@unitSystemImperial": {},
"videoLoopModeNever": "Ніколи",
"@videoLoopModeNever": {},
@ -267,9 +267,9 @@
"@wallpaperTargetLock": {},
"viewerTransitionNone": "Нічого",
"@viewerTransitionNone": {},
"widgetDisplayedItemRandom": "Випадкові",
"widgetDisplayedItemRandom": "Випадковий",
"@widgetDisplayedItemRandom": {},
"widgetDisplayedItemMostRecent": "Нещодавні",
"widgetDisplayedItemMostRecent": "Нещодавний",
"@widgetDisplayedItemMostRecent": {},
"widgetOpenPageHome": "Відкрити головну сторінку",
"@widgetOpenPageHome": {},
@ -459,7 +459,7 @@
"@coverDialogTabApp": {},
"coverDialogTabColor": "Колір",
"@coverDialogTabColor": {},
"appPickDialogTitle": "Вибрати Додаток",
"appPickDialogTitle": "Вибрати додаток",
"@appPickDialogTitle": {},
"appPickDialogNone": "Нічого",
"@appPickDialogNone": {},
@ -657,7 +657,7 @@
"minutes": {}
}
},
"doubleBackExitMessage": "Натисніть «назад» ще раз, щоб вийти.",
"doubleBackExitMessage": "Натисніть “назад” ще раз, щоб вийти.",
"@doubleBackExitMessage": {},
"actionRemove": "Видалити",
"@actionRemove": {},
@ -729,9 +729,9 @@
"@widgetOpenPageCollection": {},
"accessibilityAnimationsKeep": "Зберегти екранні ефекти",
"@accessibilityAnimationsKeep": {},
"displayRefreshRatePreferHighest": "Найвищий рейтинг",
"displayRefreshRatePreferHighest": "Найвища частота",
"@displayRefreshRatePreferHighest": {},
"displayRefreshRatePreferLowest": "Найнижчий рейтинг",
"displayRefreshRatePreferLowest": "Найнижча частота",
"@displayRefreshRatePreferLowest": {},
"viewerTransitionSlide": "Ковзання",
"@viewerTransitionSlide": {},
@ -781,9 +781,9 @@
},
"videoStartOverButtonLabel": "ВІДТВОРИТИ СПОЧАТКУ",
"@videoStartOverButtonLabel": {},
"newAlbumDialogTitle": "Новий Альбом",
"newAlbumDialogTitle": "Новий альбом",
"@newAlbumDialogTitle": {},
"newAlbumDialogNameLabel": "Назва Альбому",
"newAlbumDialogNameLabel": "Назва альбому",
"@newAlbumDialogNameLabel": {},
"hideFilterConfirmationDialogMessage": "Відповідні фотографії та відео будуть приховані з вашої колекції. Ви можете показати їх знову в налаштуваннях у розділі \"Конфіденційність\".\n\nВи впевнені, що хочете їх приховати?",
"@hideFilterConfirmationDialogMessage": {},
@ -795,7 +795,7 @@
"count": {}
}
},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити цей альбом і його елемент?} few{Видалити цей альбом і {count} елементи?} other{Видалити цей альбом і {count} елементів?}}",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити цей альбом і елемент у ньому?} few{Видалити цей альбом і {count} елементи у ньому?} other{Видалити цей альбом і {count} елементів у ньому?}}",
"@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -803,7 +803,7 @@
},
"nameConflictDialogSingleSourceMessage": "Деякі файли в папці призначення мають одну й ту саму назву.",
"@nameConflictDialogSingleSourceMessage": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити ці альбоми та їх елементи?} few{Видалити ці альбоми та їх {count} елементи?} other{Видалити ці альбоми та їх {count} елементів?}}",
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити ці альбоми та елемент в них?} few{Видалити ці альбоми та {count} елементи в них?} other{Видалити ці альбоми та {count} елементів в них?}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": {
"count": {}
@ -923,11 +923,11 @@
"@settingsShowBottomNavigationBar": {},
"settingsConfirmationTile": "Діалоги підтвердження",
"@settingsConfirmationTile": {},
"settingsConfirmationDialogTitle": "Діалоги Підтвердження",
"settingsConfirmationDialogTitle": "Діалоги підтвердження",
"@settingsConfirmationDialogTitle": {},
"settingsConfirmationBeforeDeleteItems": "Запитати, перш ніж видаляти предмети назавжди",
"settingsConfirmationBeforeDeleteItems": "Запитати, перш ніж видаляти елементи назавжди",
"@settingsConfirmationBeforeDeleteItems": {},
"settingsConfirmationBeforeMoveToBinItems": "Запитати перед тим, як переносити предмети до кошика",
"settingsConfirmationBeforeMoveToBinItems": "Запитати перед тим, як переносити елементи до кошика",
"@settingsConfirmationBeforeMoveToBinItems": {},
"settingsNavigationDrawerTabPages": "Сторінки",
"@settingsNavigationDrawerTabPages": {},
@ -945,7 +945,7 @@
"@settingsThumbnailShowRawIcon": {},
"settingsThumbnailShowVideoDuration": "Показати тривалість відео",
"@settingsThumbnailShowVideoDuration": {},
"settingsCollectionQuickActionEditorPageTitle": "Швидкі Дії",
"settingsCollectionQuickActionEditorPageTitle": "Швидкі дії",
"@settingsCollectionQuickActionEditorPageTitle": {},
"settingsCollectionQuickActionTabBrowsing": "Перегляд",
"@settingsCollectionQuickActionTabBrowsing": {},
@ -965,7 +965,7 @@
"@settingsImageBackground": {},
"settingsViewerQuickActionsTile": "Швидкі дії",
"@settingsViewerQuickActionsTile": {},
"settingsViewerQuickActionEditorPageTitle": "Швидкі Дії",
"settingsViewerQuickActionEditorPageTitle": "Швидкі дії",
"@settingsViewerQuickActionEditorPageTitle": {},
"settingsViewerQuickActionEditorBanner": "Торкніться і утримуйте для переміщення кнопок і вибору дій, які відображатимуться у переглядачі.",
"@settingsViewerQuickActionEditorBanner": {},
@ -1005,7 +1005,7 @@
"@settingsSlideshowIntervalTile": {},
"settingsSlideshowVideoPlaybackTile": "Відтворення відео",
"@settingsSlideshowVideoPlaybackTile": {},
"settingsSlideshowVideoPlaybackDialogTitle": "Відтворення Відео",
"settingsSlideshowVideoPlaybackDialogTitle": "Відтворення відео",
"@settingsSlideshowVideoPlaybackDialogTitle": {},
"settingsVideoPageTitle": "Налаштування Відео",
"@settingsVideoPageTitle": {},
@ -1015,13 +1015,13 @@
"@settingsVideoEnableHardwareAcceleration": {},
"settingsVideoAutoPlay": "Автоматичне відтворення",
"@settingsVideoAutoPlay": {},
"settingsVideoLoopModeDialogTitle": "Циклічний Режим",
"settingsVideoLoopModeDialogTitle": "Циклічний режим",
"@settingsVideoLoopModeDialogTitle": {},
"settingsSubtitleThemeTile": "Субтитри",
"@settingsSubtitleThemeTile": {},
"settingsSubtitleThemePageTitle": "Субтитри",
"@settingsSubtitleThemePageTitle": {},
"settingsSubtitleThemeTextAlignmentDialogTitle": "Вирівнювання Тексту",
"settingsSubtitleThemeTextAlignmentDialogTitle": "Вирівнювання тексту",
"@settingsSubtitleThemeTextAlignmentDialogTitle": {},
"settingsSubtitleThemeTextPositionTile": "Положення тексту",
"@settingsSubtitleThemeTextPositionTile": {},
@ -1047,7 +1047,7 @@
"@settingsVideoGestureDoubleTapTogglePlay": {},
"settingsVideoGestureSideDoubleTapSeek": "Подвійне натискання на краї екрану для переходу назад/вперед",
"@settingsVideoGestureSideDoubleTapSeek": {},
"settingsAllowErrorReporting": "Дозволити анонімну відправку повідомлення про помилки",
"settingsAllowErrorReporting": "Дозволити анонімну відправку повідомлень про помилки",
"@settingsAllowErrorReporting": {},
"settingsSaveSearchHistory": "Зберігати історію пошуку",
"@settingsSaveSearchHistory": {},
@ -1057,21 +1057,21 @@
"@settingsAllowMediaManagement": {},
"settingsHiddenItemsTile": "Приховані елементи",
"@settingsHiddenItemsTile": {},
"settingsHiddenItemsPageTitle": "Приховані Елементи",
"settingsHiddenItemsPageTitle": "Приховані елементи",
"@settingsHiddenItemsPageTitle": {},
"settingsHiddenItemsTabFilters": "Приховані Фільтри",
"settingsHiddenItemsTabFilters": "Приховані фільтри",
"@settingsHiddenItemsTabFilters": {},
"settingsHiddenFiltersBanner": "Фотографії та відео, що відповідають прихованим фільтрам, не з'являться у вашій колекції.",
"@settingsHiddenFiltersBanner": {},
"settingsHiddenFiltersEmpty": "Немає прихованих фільтрів",
"@settingsHiddenFiltersEmpty": {},
"settingsHiddenItemsTabPaths": "Приховані Шляхи",
"settingsHiddenItemsTabPaths": "Приховані шляхи",
"@settingsHiddenItemsTabPaths": {},
"addPathTooltip": "Додати шлях",
"@addPathTooltip": {},
"settingsStorageAccessTile": "Доступ до сховища",
"@settingsStorageAccessTile": {},
"settingsStorageAccessPageTitle": "Доступ до Сховища",
"settingsStorageAccessPageTitle": "Доступ до сховища",
"@settingsStorageAccessPageTitle": {},
"settingsStorageAccessBanner": "Деякі каталоги вимагають явного надання доступу для зміни файлів в них. Ви можете переглянути тут каталоги, до яких ви раніше надавали доступ.",
"@settingsStorageAccessBanner": {},
@ -1083,7 +1083,7 @@
"@settingsAccessibilitySectionTitle": {},
"settingsRemoveAnimationsTile": "Видалити анімації",
"@settingsRemoveAnimationsTile": {},
"settingsRemoveAnimationsDialogTitle": "Видалити Анімації",
"settingsRemoveAnimationsDialogTitle": "Видалити анімації",
"@settingsRemoveAnimationsDialogTitle": {},
"settingsTimeToTakeActionTile": "Час на виконання",
"@settingsTimeToTakeActionTile": {},
@ -1097,7 +1097,7 @@
"@settingsThemeEnableDynamicColor": {},
"settingsDisplayRefreshRateModeTile": "Частота оновлення дисплея",
"@settingsDisplayRefreshRateModeTile": {},
"settingsLanguageSectionTitle": "Мова та Формати",
"settingsLanguageSectionTitle": "Мова та формати",
"@settingsLanguageSectionTitle": {},
"settingsLanguageTile": "Мова",
"@settingsLanguageTile": {},
@ -1105,7 +1105,7 @@
"@settingsCoordinateFormatTile": {},
"settingsUnitSystemTile": "Одиниці виміру",
"@settingsUnitSystemTile": {},
"settingsUnitSystemDialogTitle": "Одиниці Виміру",
"settingsUnitSystemDialogTitle": "Одиниці виміру",
"@settingsUnitSystemDialogTitle": {},
"settingsWidgetPageTitle": "Фоторамка",
"@settingsWidgetPageTitle": {},
@ -1159,7 +1159,7 @@
"@viewerInfoLabelCoordinates": {},
"viewerInfoLabelAddress": "Адреса",
"@viewerInfoLabelAddress": {},
"mapStyleDialogTitle": "Стиль Карти",
"mapStyleDialogTitle": "Стиль карти",
"@mapStyleDialogTitle": {},
"mapStyleTooltip": "Виберіть стиль карти",
"@mapStyleTooltip": {},
@ -1197,7 +1197,7 @@
"@viewerInfoSearchSuggestionRights": {},
"wallpaperUseScrollEffect": "Використовувати ефект прокрутки на головному екрані",
"@wallpaperUseScrollEffect": {},
"tagEditorPageTitle": "Редагування Тегів",
"tagEditorPageTitle": "Редагування тегів",
"@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "Новий тег",
"@tagEditorPageNewTagFieldLabel": {},
@ -1245,7 +1245,7 @@
"@settingsActionImportDialogTitle": {},
"appExportFavourites": "Обране",
"@appExportFavourites": {},
"settingsKeepScreenOnDialogTitle": "Тримати Екран Увімкненим",
"settingsKeepScreenOnDialogTitle": "Тримати екран увімкненим",
"@settingsKeepScreenOnDialogTitle": {},
"settingsActionExportDialogTitle": "Експорт",
"@settingsActionExportDialogTitle": {},
@ -1279,15 +1279,15 @@
"@settingsNavigationDrawerTabTypes": {},
"settingsThumbnailOverlayPageTitle": "Накладення",
"@settingsThumbnailOverlayPageTitle": {},
"settingsNavigationDrawerEditorPageTitle": "Навігаційне Меню",
"settingsNavigationDrawerEditorPageTitle": "Навігаційне меню",
"@settingsNavigationDrawerEditorPageTitle": {},
"settingsCollectionSelectionQuickActionEditorBanner": "Торкніться і утримуйте для переміщення кнопок і вибору дій, які будуть відображатися при виборі елементів.",
"@settingsCollectionSelectionQuickActionEditorBanner": {},
"settingsThumbnailSectionTitle": "Мініатюри",
"@settingsThumbnailSectionTitle": {},
"settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Відображувані Кнопки",
"settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Відображувані кнопки",
"@settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": {},
"settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Доступні Кнопки",
"settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Доступні кнопки",
"@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {},
"settingsSubtitleThemeTextAlignmentTile": "Вирівнювання тексту",
"@settingsSubtitleThemeTextAlignmentTile": {},
@ -1307,7 +1307,7 @@
"@settingsVideoSectionTitle": {},
"settingsAllowInstalledAppAccessSubtitle": "Використовується для покращення відображення альбомів",
"@settingsAllowInstalledAppAccessSubtitle": {},
"settingsSubtitleThemeTextPositionDialogTitle": "Положення Тексту",
"settingsSubtitleThemeTextPositionDialogTitle": "Положення тексту",
"@settingsSubtitleThemeTextPositionDialogTitle": {},
"settingsThumbnailShowRating": "Показати рейтинг",
"@settingsThumbnailShowRating": {},
@ -1337,9 +1337,9 @@
"count": {}
}
},
"settingsDisplayRefreshRateModeDialogTitle": "Частота Оновлення",
"settingsDisplayRefreshRateModeDialogTitle": "Частота оновлення",
"@settingsDisplayRefreshRateModeDialogTitle": {},
"settingsCoordinateFormatDialogTitle": "Формат Координат",
"settingsCoordinateFormatDialogTitle": "Формат координат",
"@settingsCoordinateFormatDialogTitle": {},
"settingsScreenSaverPageTitle": "Заставка на Екран",
"@settingsScreenSaverPageTitle": {},
@ -1354,5 +1354,15 @@
"placeholders": {
"count": {}
}
}
},
"settingsViewerShowDescription": "Показати опис",
"@settingsViewerShowDescription": {},
"settingsModificationWarningDialogMessage": "Інші параметри будуть змінені.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Інтерфейс Android TV",
"@settingsDisplayUseTvInterface": {},
"filterLocatedLabel": "Розташований",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Позначений тегом",
"@filterTaggedLabel": {}
}

View file

@ -0,0 +1,35 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
enum MapAction {
selectStyle,
zoomIn,
zoomOut,
}
extension ExtraMapAction on MapAction {
String getText(BuildContext context) {
switch (this) {
case MapAction.selectStyle:
return context.l10n.mapStyleTooltip;
case MapAction.zoomIn:
return context.l10n.mapZoomInTooltip;
case MapAction.zoomOut:
return context.l10n.mapZoomOutTooltip;
}
}
Widget getIcon() => Icon(_getIconData());
IconData _getIconData() {
switch (this) {
case MapAction.selectStyle:
return AIcons.layers;
case MapAction.zoomIn:
return AIcons.zoomIn;
case MapAction.zoomOut:
return AIcons.zoomOut;
}
}
}

View file

@ -27,8 +27,6 @@ class Device {
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
bool get isReadOnly => _isTelevision;
bool get isTelevision => _isTelevision;
bool get showPinShortcutFeedback => _showPinShortcutFeedback;

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui';
import 'package:aves/geo/countries.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry_cache.dart';
import 'package:aves/model/entry_dirs.dart';
import 'package:aves/model/favourites.dart';
@ -12,6 +11,7 @@ import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/multipage.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/trash.dart';
import 'package:aves/model/video/metadata.dart';
import 'package:aves/ref/mime_types.dart';
@ -281,7 +281,7 @@ class AvesEntry {
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
bool get canEdit => !device.isReadOnly && path != null && !trashed && isMediaStoreContent;
bool get canEdit => !settings.isReadOnly && path != null && !trashed && isMediaStoreContent;
bool get canEditDate => canEdit && (canEditExif || canEditXmp);

View file

@ -235,7 +235,10 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
final description = fields[DescriptionField.description];
if (canEditExif && editDescription) {
metadata[MetadataType.exif] = {MetadataField.exifImageDescription.toPlatform!: description};
metadata[MetadataType.exif] = {
MetadataField.exifImageDescription.toPlatform!: null,
MetadataField.exifUserComment.toPlatform!: null,
};
}
if (canEditIptc) {

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