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="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 ## <a id="v1.7.8"></a>[v1.7.8] - 2022-12-20
### Added ### 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" [<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/huawei-appgallery-badge-english-black.png"
alt='Get it on Huawei AppGallery' alt='Get it on Huawei AppGallery'
height="80">](https://appgallery.huawei.com/app/C106014023) 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" [<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/amazon-appstore-badge-english-black.png"
alt='Get it on Amazon Appstore' alt='Get it on Amazon Appstore'
height="80">](https://www.amazon.com/dp/B09XQHQQ72) 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. **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 ## Screenshots

View file

@ -191,7 +191,7 @@ dependencies {
implementation 'com.drewnoakes:metadata-extractor:2.18.0' implementation 'com.drewnoakes:metadata-extractor:2.18.0'
implementation 'com.github.bumptech.glide:glide:4.14.2' implementation 'com.github.bumptech.glide:glide:4.14.2'
// SLF4J implementation for `mp4parser` // SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.3' implementation 'org.slf4j:slf4j-simple:2.0.6'
// forked, built by JitPack: // forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory // - 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" /> android:resource="@xml/app_widget_info" />
</receiver> </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 <service
android:name=".AnalysisService" android:name=".AnalysisService"
android:description="@string/analysis_service_description" android:description="@string/analysis_service_description"

View file

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

View file

@ -10,6 +10,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.TransactionTooLargeException
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
@ -280,7 +281,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val title = call.argument<String>("title") val title = call.argument<String>("title")
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType") val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
if (urisByMimeType == null) { if (urisByMimeType == null) {
result.error("setAs-args", "missing arguments", null) result.error("share-args", "missing arguments", null)
return return
} }
@ -288,15 +289,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val mimeTypes = urisByMimeType.keys.toTypedArray() val mimeTypes = urisByMimeType.keys.toTypedArray()
// simplify share intent for a single item, as some apps can handle one item but not more // 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 uri = uriList.first()
val mimeType = mimeTypes.first() val mimeType = mimeTypes.first()
val intent = Intent(Intent.ACTION_SEND) Intent(Intent.ACTION_SEND)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.setType(mimeType) .setType(mimeType)
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri)) .putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri))
safeStartActivityChooser(title, intent)
} else { } else {
var mimeType = "*/*" var mimeType = "*/*"
if (mimeTypes.size == 1) { 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) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList) .putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
.setType(mimeType) .setType(mimeType)
safeStartActivityChooser(title, intent)
} }
try {
val started = safeStartActivityChooser(title, intent)
result.success(started) 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 { 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 com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.metadata.Metadata import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.metadata.MultiPage import deckers.thibault.aves.metadata.XMP.doesPropExist
import deckers.thibault.aves.metadata.XMP.getSafeStructField import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.XMPPropName
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ContentImageProvider import deckers.thibault.aves.model.provider.ContentImageProvider
import deckers.thibault.aves.model.provider.ImageProvider 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.BitmapUtils.getBytes
import deckers.thibault.aves.utils.FileUtils.transferFrom 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.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.extensionFor import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -46,6 +42,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) } "getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) } "extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) } "extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) } "extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
@ -84,6 +81,68 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
result.success(thumbnails) 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) { private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }

View file

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

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe 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.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
@ -66,10 +67,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
provider.editOrientation(contextWrapper, path, uri, mimeType, op, object : ImageOpCallback { val callback = MetadataOpCallback("editOrientation", entryMap, result)
override fun onSuccess(fields: FieldMap) = result.success(fields) provider.editOrientation(contextWrapper, path, uri, mimeType, op, callback)
override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable)
})
} }
private fun editDate(call: MethodCall, result: MethodChannel.Result) { private fun editDate(call: MethodCall, result: MethodChannel.Result) {
@ -96,10 +95,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback { val callback = MetadataOpCallback("editDate", entryMap, result)
override fun onSuccess(fields: FieldMap) = result.success(fields) provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, callback)
override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable)
})
} }
private fun editMetadata(call: MethodCall, result: MethodChannel.Result) { private fun editMetadata(call: MethodCall, result: MethodChannel.Result) {
@ -125,10 +122,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback { val callback = MetadataOpCallback("editMetadata", entryMap, result)
override fun onSuccess(fields: FieldMap) = result.success(fields) provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback)
override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable)
})
} }
private fun removeTrailerVideo(call: MethodCall, result: MethodChannel.Result) { private fun removeTrailerVideo(call: MethodCall, result: MethodChannel.Result) {
@ -152,10 +147,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, object : ImageOpCallback { val callback = MetadataOpCallback("removeTrailerVideo", entryMap, result)
override fun onSuccess(fields: FieldMap) = result.success(fields) provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, callback)
override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable)
})
} }
private fun removeTypes(call: MethodCall, result: MethodChannel.Result) { private fun removeTypes(call: MethodCall, result: MethodChannel.Result) {
@ -180,13 +173,31 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), object : ImageOpCallback { val callback = MetadataOpCallback("removeTypes", entryMap, result)
override fun onSuccess(fields: FieldMap) = result.success(fields) provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), callback)
override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable)
})
} }
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/metadata_edit" 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) { if (prop is XMPPropertyInfo) {
val path = prop.path val path = prop.path
if (path?.isNotEmpty() == true) { 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) { if (value?.isNotEmpty() == true) {
dirMap[path] = value 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)) { if (!metadataMap.containsKey(KEY_XMP_TITLE) || !metadataMap.containsKey(KEY_XMP_SUBJECTS)) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
if (!metadataMap.containsKey(KEY_XMP_TITLE)) { 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)) { if (!metadataMap.containsKey(KEY_XMP_SUBJECTS)) {
dir.keywords?.let { metadataMap[KEY_XMP_SUBJECTS] = it.joinToString(XMP_SUBJECTS_SEPARATOR) } 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): // return description from these fields (by precedence):
// - XMP / dc:description // - XMP / dc:description
// - IPTC / caption-abstract // - IPTC / caption-abstract
// - Exif / UserComment
// - Exif / ImageDescription // - Exif / ImageDescription
private fun getDescription(call: MethodCall, result: MethodChannel.Result) { private fun getDescription(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
@ -1171,7 +1172,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val xmpMeta = dir.xmpMeta val xmpMeta = dir.xmpMeta
try { try {
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) { 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) { } catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e) 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) { if (description == null) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) { 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) { if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { 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 // additional media key
private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture" 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.app.Activity
import android.os.Build import android.os.Build
import android.view.WindowManager import android.view.WindowManager
import deckers.thibault.aves.utils.getDisplayCompat
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -42,25 +43,30 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
result.success(true) 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) result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
} }
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val use = call.argument<Boolean>("use") if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
if (use == null) { result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
result.error("setCutoutMode-args", "missing arguments", null)
return return
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val mode = if (use) { activity.getDisplayCompat()?.cutout
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
} else { } else {
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER activity.window.decorView.rootWindowInsets.displayCutout
} }
activity.window.attributes.layoutInDisplayCutoutMode = mode
} val density = activity.resources.displayMetrics.density
result.success(true) 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) 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) result.success(false)
} }
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) { override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
result.success(false) 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) "keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked) "isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode) "isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
"setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode) "getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
else -> result.notImplemented() 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 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 { companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>() 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) 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?) { private fun success(result: Any?) {
handler.post { handler.post {

View file

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

View file

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

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.channel.streams 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
import io.flutter.plugin.common.EventChannel.EventSink import io.flutter.plugin.common.EventChannel.EventSink
@ -13,13 +15,16 @@ class IntentStreamHandler : EventChannel.StreamHandler {
this.eventSink = eventSink 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?>?) { fun notifyNewIntent(intentData: MutableMap<String, Any?>?) {
eventSink?.success(intentData) eventSink?.success(intentData)
} }
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<IntentStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/new_intent_stream" 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()) handler = Handler(Looper.getMainLooper())
} }
override fun onCancel(arguments: Any?) {} override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
fun dispose() { fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver) context.contentResolver.unregisterContentObserver(contentObserver)

View file

@ -79,7 +79,9 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
handler = Handler(Looper.getMainLooper()) handler = Handler(Looper.getMainLooper())
} }
override fun onCancel(arguments: Any?) {} override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments")
}
fun dispose() { fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver) 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 -> setBoxSkipper { type, size ->
if (skippedTypes.contains(type)) return@setBoxSkipper true 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 false
} }
} }
@ -232,3 +232,5 @@ object Mp4ParserHelper {
return stream.toByteArray() 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)) { if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
// `GCamera` motion photo // `GCamera` motion photo
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it } 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 // `Container` motion photo
val count = xmpMeta.countPropArrayItems(XMP.CONTAINER_DIRECTORY_PROP_NAME) val count = xmpMeta.countPropArrayItems(XMP.GCONTAINER_DIRECTORY_PROP_NAME)
if (count == 2) { if (count == 2) {
// expect the video to be the second item // expect the video to be the second item
val i = 2 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 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.CONTAINER_DIRECTORY_PROP_NAME, i, XMP.CONTAINER_ITEM_PROP_NAME, XMP.CONTAINER_ITEM_LENGTH_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) { if (MimeTypes.isVideo(mime) && length != null) {
offsetFromEnd = length.toLong() offsetFromEnd = length.toLong()
} }

View file

@ -42,17 +42,17 @@ class GSpherical(xmlBytes: ByteArray) {
"StitchingSoftware" -> stitchingSoftware = readTag(parser, tag) "StitchingSoftware" -> stitchingSoftware = readTag(parser, tag)
"ProjectionType" -> projectionType = readTag(parser, tag) "ProjectionType" -> projectionType = readTag(parser, tag)
"StereoMode" -> stereoMode = readTag(parser, tag) "StereoMode" -> stereoMode = readTag(parser, tag)
"SourceCount" -> sourceCount = Integer.parseInt(readTag(parser, tag)) "SourceCount" -> sourceCount = readTag(parser, tag).toInt()
"InitialViewHeadingDegrees" -> initialViewHeadingDegrees = Integer.parseInt(readTag(parser, tag)) "InitialViewHeadingDegrees" -> initialViewHeadingDegrees = readTag(parser, tag).toInt()
"InitialViewPitchDegrees" -> initialViewPitchDegrees = Integer.parseInt(readTag(parser, tag)) "InitialViewPitchDegrees" -> initialViewPitchDegrees = readTag(parser, tag).toInt()
"InitialViewRollDegrees" -> initialViewRollDegrees = Integer.parseInt(readTag(parser, tag)) "InitialViewRollDegrees" -> initialViewRollDegrees = readTag(parser, tag).toInt()
"Timestamp" -> timestamp = Integer.parseInt(readTag(parser, tag)) "Timestamp" -> timestamp = readTag(parser, tag).toInt()
"FullPanoWidthPixels" -> fullPanoWidthPixels = Integer.parseInt(readTag(parser, tag)) "FullPanoWidthPixels" -> fullPanoWidthPixels = readTag(parser, tag).toInt()
"FullPanoHeightPixels" -> fullPanoHeightPixels = Integer.parseInt(readTag(parser, tag)) "FullPanoHeightPixels" -> fullPanoHeightPixels = readTag(parser, tag).toInt()
"CroppedAreaImageWidthPixels" -> croppedAreaImageWidthPixels = Integer.parseInt(readTag(parser, tag)) "CroppedAreaImageWidthPixels" -> croppedAreaImageWidthPixels = readTag(parser, tag).toInt()
"CroppedAreaImageHeightPixels" -> croppedAreaImageHeightPixels = Integer.parseInt(readTag(parser, tag)) "CroppedAreaImageHeightPixels" -> croppedAreaImageHeightPixels = readTag(parser, tag).toInt()
"CroppedAreaLeftPixels" -> croppedAreaLeftPixels = Integer.parseInt(readTag(parser, tag)) "CroppedAreaLeftPixels" -> croppedAreaLeftPixels = readTag(parser, tag).toInt()
"CroppedAreaTopPixels" -> croppedAreaTopPixels = Integer.parseInt(readTag(parser, tag)) "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/" private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
// other namespaces // 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 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 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 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 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 GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01" 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 } 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 // motion photo
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset") val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
val CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Directory") val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
val CONTAINER_ITEM_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Item") val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
val CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Length") val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
val CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Mime") val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
// panorama // panorama
// cf https://developers.google.com/streetview/spherical-metadata // cf https://developers.google.com/streetview/spherical-metadata
@ -189,14 +198,14 @@ object XMP {
if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
// Container motion photo // Container motion photo
if (doesPropExist(CONTAINER_DIRECTORY_PROP_NAME)) { if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
val count = countPropArrayItems(CONTAINER_DIRECTORY_PROP_NAME) val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
if (count == 2) { if (count == 2) {
var hasImage = false var hasImage = false
var hasVideo = false var hasVideo = false
for (i in 1 until count + 1) { 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 mime = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_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 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 hasImage = hasImage || MimeTypes.isImage(mime) && length != null
hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
} }

View file

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

View file

@ -55,7 +55,7 @@ internal class ContentImageProvider : ImageProvider() {
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) } 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(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() cursor.close()
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -73,8 +73,5 @@ internal class ContentImageProvider : ImageProvider() {
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ContentImageProvider>() 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\"", "${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newTrailerOffset\"",
).replace( ).replace(
// Container motion photo // Container motion photo
"${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"", "${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"",
"${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"", "${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"",
) )
}) })
} }

View file

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

View file

@ -1,11 +1,13 @@
package deckers.thibault.aves.utils package deckers.thibault.aves.utils
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import android.view.Display
inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? { inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 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 { fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 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 // as of `metadata-extractor` v2.14.0
fun canReadWithMetadataExtractor(mimeType: String) = when (mimeType) { 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 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"?> <?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <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:configure="deckers.thibault.aves.HomeWidgetSettingsActivity"
android:initialLayout="@layout/app_widget" android:initialLayout="@layout/app_widget"
android:minWidth="40dp" android:minWidth="40dp"
@ -9,4 +10,5 @@
android:targetCellHeight="2" android:targetCellHeight="2"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:widgetCategory="home_screen" 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. <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>. <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": "إظهار الملفات المخفية",
"@filePickerShowHiddenFiles": {}, "@filePickerShowHiddenFiles": {},
"panoramaEnableSensorControl": "تمكين التحكم في المستشعر", "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": "Χωρίς διεύθυνση",
"@filterNoAddressLabel": {}, "@filterNoAddressLabel": {},
"settingsViewerShowRatingTags": "Εμφάνιση βαθμολογίας & ετικετών", "settingsViewerShowRatingTags": "Εμφάνιση βαθμολογίας & ετικετών",
"@settingsViewerShowRatingTags": {} "@settingsViewerShowRatingTags": {},
"filterLocatedLabel": "Με τοποθεσία",
"@filterLocatedLabel": {},
"filterTaggedLabel": "Με ετικέτα",
"@filterTaggedLabel": {},
"settingsModificationWarningDialogMessage": "Άλλες ρυθμίσεις θα τροποποιηθούν.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον",
"@settingsDisplayUseTvInterface": {},
"settingsViewerShowDescription": "Εμφάνιση περιγραφής",
"@settingsViewerShowDescription": {}
} }

View file

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

View file

@ -371,9 +371,9 @@
"@renameProcessorCounter": {}, "@renameProcessorCounter": {},
"renameProcessorName": "Nombre", "renameProcessorName": "Nombre",
"@renameProcessorName": {}, "@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": {}, "@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": {}, "@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Formato:", "exportEntryDialogFormat": "Formato:",
"@exportEntryDialogFormat": {}, "@exportEntryDialogFormat": {},
@ -1196,5 +1196,15 @@
"placeholders": { "placeholders": {
"count": {} "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": "سیاست حفظ حریم خصوصی",
"@policyPageTitle": {}, "@policyPageTitle": {},
"collectionPickPageTitle": "انتخاب", "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": {}, "@chipActionGoToAlbumPage": {},
"chipActionGoToCountryPage": "Afficher dans Pays", "chipActionGoToCountryPage": "Afficher dans Pays",
"@chipActionGoToCountryPage": {}, "@chipActionGoToCountryPage": {},
"chipActionGoToTagPage": "Afficher dans Libellés", "chipActionGoToTagPage": "Afficher dans Étiquettes",
"@chipActionGoToTagPage": {}, "@chipActionGoToTagPage": {},
"chipActionFilterOut": "Exclure", "chipActionFilterOut": "Exclure",
"@chipActionFilterOut": {}, "@chipActionFilterOut": {},
@ -165,7 +165,7 @@
"@entryInfoActionEditTitleDescription": {}, "@entryInfoActionEditTitleDescription": {},
"entryInfoActionEditRating": "Modifier la notation", "entryInfoActionEditRating": "Modifier la notation",
"@entryInfoActionEditRating": {}, "@entryInfoActionEditRating": {},
"entryInfoActionEditTags": "Modifier les libellés", "entryInfoActionEditTags": "Modifier les étiquettes",
"@entryInfoActionEditTags": {}, "@entryInfoActionEditTags": {},
"entryInfoActionRemoveMetadata": "Retirer les métadonnées", "entryInfoActionRemoveMetadata": "Retirer les métadonnées",
"@entryInfoActionRemoveMetadata": {}, "@entryInfoActionRemoveMetadata": {},
@ -179,7 +179,7 @@
"@filterNoLocationLabel": {}, "@filterNoLocationLabel": {},
"filterNoRatingLabel": "Sans notation", "filterNoRatingLabel": "Sans notation",
"@filterNoRatingLabel": {}, "@filterNoRatingLabel": {},
"filterNoTagLabel": "Sans libellé", "filterNoTagLabel": "Sans étiquette",
"@filterNoTagLabel": {}, "@filterNoTagLabel": {},
"filterNoTitleLabel": "Sans titre", "filterNoTitleLabel": "Sans titre",
"@filterNoTitleLabel": {}, "@filterNoTitleLabel": {},
@ -391,9 +391,9 @@
"@renameProcessorCounter": {}, "@renameProcessorCounter": {},
"renameProcessorName": "Nom", "renameProcessorName": "Nom",
"@renameProcessorName": {}, "@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": {}, "@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": {}, "@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Format :", "exportEntryDialogFormat": "Format :",
"@exportEntryDialogFormat": {}, "@exportEntryDialogFormat": {},
@ -641,7 +641,7 @@
"@drawerAlbumPage": {}, "@drawerAlbumPage": {},
"drawerCountryPage": "Pays", "drawerCountryPage": "Pays",
"@drawerCountryPage": {}, "@drawerCountryPage": {},
"drawerTagPage": "Libellés", "drawerTagPage": "Étiquettes",
"@drawerTagPage": {}, "@drawerTagPage": {},
"sortByDate": "par date", "sortByDate": "par date",
"@sortByDate": {}, "@sortByDate": {},
@ -713,9 +713,9 @@
"@countryPageTitle": {}, "@countryPageTitle": {},
"countryEmpty": "Aucun pays", "countryEmpty": "Aucun pays",
"@countryEmpty": {}, "@countryEmpty": {},
"tagPageTitle": "Libellés", "tagPageTitle": "Étiquettes",
"@tagPageTitle": {}, "@tagPageTitle": {},
"tagEmpty": "Aucun libellé", "tagEmpty": "Aucune étiquette",
"@tagEmpty": {}, "@tagEmpty": {},
"binPageTitle": "Corbeille", "binPageTitle": "Corbeille",
"@binPageTitle": {}, "@binPageTitle": {},
@ -731,7 +731,7 @@
"@searchCountriesSectionTitle": {}, "@searchCountriesSectionTitle": {},
"searchPlacesSectionTitle": "Lieux", "searchPlacesSectionTitle": "Lieux",
"@searchPlacesSectionTitle": {}, "@searchPlacesSectionTitle": {},
"searchTagsSectionTitle": "Libellés", "searchTagsSectionTitle": "Étiquettes",
"@searchTagsSectionTitle": {}, "@searchTagsSectionTitle": {},
"searchRatingSectionTitle": "Notations", "searchRatingSectionTitle": "Notations",
"@searchRatingSectionTitle": {}, "@searchRatingSectionTitle": {},
@ -811,7 +811,7 @@
"@settingsThumbnailOverlayPageTitle": {}, "@settingsThumbnailOverlayPageTitle": {},
"settingsThumbnailShowFavouriteIcon": "Afficher licône de favori", "settingsThumbnailShowFavouriteIcon": "Afficher licône de favori",
"@settingsThumbnailShowFavouriteIcon": {}, "@settingsThumbnailShowFavouriteIcon": {},
"settingsThumbnailShowTagIcon": "Afficher licône de libellé", "settingsThumbnailShowTagIcon": "Afficher licône détiquette",
"@settingsThumbnailShowTagIcon": {}, "@settingsThumbnailShowTagIcon": {},
"settingsThumbnailShowLocationIcon": "Afficher licône de lieu", "settingsThumbnailShowLocationIcon": "Afficher licône de lieu",
"@settingsThumbnailShowLocationIcon": {}, "@settingsThumbnailShowLocationIcon": {},
@ -1043,7 +1043,7 @@
"@statsTopCountriesSectionTitle": {}, "@statsTopCountriesSectionTitle": {},
"statsTopPlacesSectionTitle": "Top lieux", "statsTopPlacesSectionTitle": "Top lieux",
"@statsTopPlacesSectionTitle": {}, "@statsTopPlacesSectionTitle": {},
"statsTopTagsSectionTitle": "Top libellés", "statsTopTagsSectionTitle": "Top étiquettes",
"@statsTopTagsSectionTitle": {}, "@statsTopTagsSectionTitle": {},
"statsTopAlbumsSectionTitle": "Top albums", "statsTopAlbumsSectionTitle": "Top albums",
"@statsTopAlbumsSectionTitle": {}, "@statsTopAlbumsSectionTitle": {},
@ -1123,11 +1123,11 @@
"@viewerInfoSearchSuggestionRights": {}, "@viewerInfoSearchSuggestionRights": {},
"wallpaperUseScrollEffect": "Utiliser leffet de défilement sur lécran daccueil", "wallpaperUseScrollEffect": "Utiliser leffet de défilement sur lécran daccueil",
"@wallpaperUseScrollEffect": {}, "@wallpaperUseScrollEffect": {},
"tagEditorPageTitle": "Modifier les libellés", "tagEditorPageTitle": "Modifier les étiquettes",
"@tagEditorPageTitle": {}, "@tagEditorPageTitle": {},
"tagEditorPageNewTagFieldLabel": "Nouveau libellé", "tagEditorPageNewTagFieldLabel": "Nouvelle étiquette",
"@tagEditorPageNewTagFieldLabel": {}, "@tagEditorPageNewTagFieldLabel": {},
"tagEditorPageAddTagTooltip": "Ajouter le libellé", "tagEditorPageAddTagTooltip": "Ajouter létiquette",
"@tagEditorPageAddTagTooltip": {}, "@tagEditorPageAddTagTooltip": {},
"tagEditorSectionRecent": "Ajouts récents", "tagEditorSectionRecent": "Ajouts récents",
"@tagEditorSectionRecent": {}, "@tagEditorSectionRecent": {},
@ -1149,7 +1149,7 @@
"@filePickerUseThisFolder": {}, "@filePickerUseThisFolder": {},
"editEntryLocationDialogSetCustom": "Définir un lieu personnalisé", "editEntryLocationDialogSetCustom": "Définir un lieu personnalisé",
"@editEntryLocationDialogSetCustom": {}, "@editEntryLocationDialogSetCustom": {},
"tagEditorSectionPlaceholders": "Libellés de substitution", "tagEditorSectionPlaceholders": "Étiquettes de substitution",
"@tagEditorSectionPlaceholders": {}, "@tagEditorSectionPlaceholders": {},
"tagPlaceholderPlace": "Lieu", "tagPlaceholderPlace": "Lieu",
"@tagPlaceholderPlace": {}, "@tagPlaceholderPlace": {},
@ -1179,7 +1179,7 @@
"@filterAspectRatioPortraitLabel": {}, "@filterAspectRatioPortraitLabel": {},
"filterAspectRatioLandscapeLabel": "Paysage", "filterAspectRatioLandscapeLabel": "Paysage",
"@filterAspectRatioLandscapeLabel": {}, "@filterAspectRatioLandscapeLabel": {},
"settingsViewerShowRatingTags": "Afficher la notation et les libellés", "settingsViewerShowRatingTags": "Afficher la notation et les étiquettes",
"@settingsViewerShowRatingTags": {}, "@settingsViewerShowRatingTags": {},
"entryActionShareImageOnly": "Partager limage seulement", "entryActionShareImageOnly": "Partager limage seulement",
"@entryActionShareImageOnly": {}, "@entryActionShareImageOnly": {},
@ -1196,5 +1196,17 @@
} }
}, },
"settingsAccessibilityShowPinchGestureAlternatives": "Afficher des alternatives aux interactions multitactiles", "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": {}, "@binEntriesConfirmationDialogMessage": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus benda ini?} other{Apakah Anda yakin ingin menghapus {count} benda?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus benda ini?} other{Apakah Anda yakin ingin menghapus {count} benda?}}",
"@deleteEntriesConfirmationDialogMessage": {}, "@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": {}, "@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "Atur tanggal", "moveUndatedConfirmationDialogSetDate": "Atur tanggal",
"@moveUndatedConfirmationDialogSetDate": {}, "@moveUndatedConfirmationDialogSetDate": {},
@ -379,9 +379,9 @@
"@renameProcessorCounter": {}, "@renameProcessorCounter": {},
"renameProcessorName": "Nama", "renameProcessorName": "Nama",
"@renameProcessorName": {}, "@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": {}, "@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": {}, "@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Format:", "exportEntryDialogFormat": "Format:",
"@exportEntryDialogFormat": {}, "@exportEntryDialogFormat": {},
@ -867,7 +867,7 @@
"@settingsSlideshowFillScreen": {}, "@settingsSlideshowFillScreen": {},
"settingsSlideshowTransitionTile": "Transisi", "settingsSlideshowTransitionTile": "Transisi",
"@settingsSlideshowTransitionTile": {}, "@settingsSlideshowTransitionTile": {},
"settingsSlideshowIntervalTile": "Interval", "settingsSlideshowIntervalTile": "Jarak waktu",
"@settingsSlideshowIntervalTile": {}, "@settingsSlideshowIntervalTile": {},
"settingsSlideshowVideoPlaybackTile": "Putaran ulang video", "settingsSlideshowVideoPlaybackTile": "Putaran ulang video",
"@settingsSlideshowVideoPlaybackTile": {}, "@settingsSlideshowVideoPlaybackTile": {},
@ -1157,7 +1157,7 @@
"@tagPlaceholderPlace": {}, "@tagPlaceholderPlace": {},
"editEntryLocationDialogSetCustom": "Terapkan lokasi kustom", "editEntryLocationDialogSetCustom": "Terapkan lokasi kustom",
"@editEntryLocationDialogSetCustom": {}, "@editEntryLocationDialogSetCustom": {},
"subtitlePositionTop": "Atas", "subtitlePositionTop": "Teratas",
"@subtitlePositionTop": {}, "@subtitlePositionTop": {},
"subtitlePositionBottom": "Bawah", "subtitlePositionBottom": "Bawah",
"@subtitlePositionBottom": {}, "@subtitlePositionBottom": {},
@ -1196,5 +1196,17 @@
} }
}, },
"settingsAccessibilityShowPinchGestureAlternatives": "Tampilkan alternatif gestur multisentuh", "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": {}, "@renameProcessorCounter": {},
"renameProcessorName": "Nome", "renameProcessorName": "Nome",
"@renameProcessorName": {}, "@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": {}, "@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": {}, "@deleteMultiAlbumConfirmationDialogMessage": {},
"exportEntryDialogFormat": "Formato:", "exportEntryDialogFormat": "Formato:",
"@exportEntryDialogFormat": {}, "@exportEntryDialogFormat": {},
@ -1196,5 +1196,17 @@
"entryActionShareVideoOnly": "Condividi solo video", "entryActionShareVideoOnly": "Condividi solo video",
"@entryActionShareVideoOnly": {}, "@entryActionShareVideoOnly": {},
"filterNoAddressLabel": "Senza indirizzo", "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": {}, "@filterFavouriteLabel": {},
"filterNoDateLabel": "날짜 없음", "filterNoDateLabel": "날짜 없음",
"@filterNoDateLabel": {}, "@filterNoDateLabel": {},
"filterNoLocationLabel": "장소 없음", "filterNoLocationLabel": "위치 없음",
"@filterNoLocationLabel": {}, "@filterNoLocationLabel": {},
"filterNoRatingLabel": "별점 없음", "filterNoRatingLabel": "별점 없음",
"@filterNoRatingLabel": {}, "@filterNoRatingLabel": {},
@ -1196,5 +1196,17 @@
"placeholders": { "placeholders": {
"count": {} "count": {}
} }
} },
"settingsViewerShowDescription": "설명 표시",
"@settingsViewerShowDescription": {},
"settingsModificationWarningDialogMessage": "다른 설정도 변경될 것입니다.",
"@settingsModificationWarningDialogMessage": {},
"settingsDisplayUseTvInterface": "안드로이드 TV 인터페이스 사용하기",
"@settingsDisplayUseTvInterface": {},
"filterTaggedLabel": "태그 있음",
"@filterTaggedLabel": {},
"filterLocatedLabel": "위치 있음",
"@filterLocatedLabel": {},
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
"@tooManyItemsErrorDialogMessage": {}
} }

View file

@ -264,7 +264,7 @@
"@keepScreenOnViewerOnly": {}, "@keepScreenOnViewerOnly": {},
"keepScreenOnAlways": "Heile tida", "keepScreenOnAlways": "Heile tida",
"@keepScreenOnAlways": {}, "@keepScreenOnAlways": {},
"accessibilityAnimationsRemove": "Hindra rørsle", "accessibilityAnimationsRemove": "Hindra skjermrørsle",
"@accessibilityAnimationsRemove": {}, "@accessibilityAnimationsRemove": {},
"subtitlePositionTop": "På toppen", "subtitlePositionTop": "På toppen",
"@subtitlePositionTop": {}, "@subtitlePositionTop": {},
@ -352,5 +352,403 @@
"description": "the name of a specific directory" "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": "Lugar",
"@tagPlaceholderPlace": {}, "@tagPlaceholderPlace": {},
"editEntryLocationDialogSetCustom": "Definir local personalizado", "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": "Nicio adresă",
"@filterNoAddressLabel": {}, "@filterNoAddressLabel": {},
"entryInfoActionRemoveLocation": "Eliminare locație", "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": "Поделиться только изображением",
"@entryActionShareImageOnly": {}, "@entryActionShareImageOnly": {},
"entryActionShareVideoOnly": "Поделиться только видео", "entryActionShareVideoOnly": "Поделиться только видео",
"@entryActionShareVideoOnly": {} "@entryActionShareVideoOnly": {},
"settingsViewerShowDescription": "Показать описание",
"@settingsViewerShowDescription": {}
} }

View file

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

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/geo/countries.dart'; import 'package:aves/geo/countries.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/entry_cache.dart';
import 'package:aves/model/entry_dirs.dart'; import 'package:aves/model/entry_dirs.dart';
import 'package:aves/model/favourites.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/catalog.dart';
import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/multipage.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/source/trash.dart';
import 'package:aves/model/video/metadata.dart'; import 'package:aves/model/video/metadata.dart';
import 'package:aves/ref/mime_types.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 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); bool get canEditDate => canEdit && (canEditExif || canEditXmp);

View file

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

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