Merge branch 'develop'
26
CHANGELOG.md
|
@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
## <a id="v1.7.9"></a>[v1.7.9] - 2023-01-15
|
||||
|
||||
### Added
|
||||
|
||||
- Viewer: optionally show description on overlay
|
||||
- Collection: unlocated/untagged overlay icons
|
||||
- Video: stop when losing audio focus
|
||||
- Video: stop when becoming noisy
|
||||
- Info: Google camera portrait mode item extraction
|
||||
- TV: handle overscan
|
||||
- TV: improved support for Viewer, Info, Map, Stats
|
||||
- TV: option to use TV layout on any device
|
||||
- Czech translation (thanks vesp)
|
||||
- Polish translation (thanks Piotr K, rehork)
|
||||
|
||||
### Changed
|
||||
|
||||
- editing description writes XMP `dc:description`, and clears Exif `ImageDescription` / `UserComment`
|
||||
- in the tag editor, tapping on applied tag applies it to all items instead of removing it
|
||||
- pin app bar when selecting items
|
||||
|
||||
### Fixed
|
||||
|
||||
- transition between collection and viewer when cutout area is not used
|
||||
- saving video playback state when leaving viewer
|
||||
|
||||
## <a id="v1.7.8"></a>[v1.7.8] - 2022-12-20
|
||||
|
||||
### Added
|
||||
|
|
|
@ -15,9 +15,6 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
|
|||
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/huawei-appgallery-badge-english-black.png"
|
||||
alt='Get it on Huawei AppGallery'
|
||||
height="80">](https://appgallery.huawei.com/app/C106014023)
|
||||
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/samsung-galaxy-store-badge-english.png"
|
||||
alt='Get it on Samsung Galaxy Store'
|
||||
height="80">](https://galaxy.store/aves)
|
||||
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/amazon-appstore-badge-english-black.png"
|
||||
alt='Get it on Amazon Appstore'
|
||||
height="80">](https://www.amazon.com/dp/B09XQHQQ72)
|
||||
|
@ -44,7 +41,7 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka
|
|||
|
||||
**Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
Aves integrates with Android (from **API 19 to 33**, i.e. from KitKat to Android 13) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
|
||||
Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
|
|
@ -191,7 +191,7 @@ dependencies {
|
|||
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.14.2'
|
||||
// SLF4J implementation for `mp4parser`
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.3'
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.6'
|
||||
|
||||
// forked, built by JitPack:
|
||||
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||
|
|
|
@ -205,6 +205,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
|
|||
android:resource="@xml/app_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name="androidx.media.session.MediaButtonReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".AnalysisService"
|
||||
android:description="@string/analysis_service_description"
|
||||
|
|
|
@ -39,6 +39,7 @@ open class MainActivity : FlutterActivity() {
|
|||
private lateinit var analysisStreamHandler: AnalysisStreamHandler
|
||||
internal lateinit var intentDataMap: MutableMap<String, Any?>
|
||||
private lateinit var analysisHandler: AnalysisHandler
|
||||
private lateinit var mediaSessionHandler: MediaSessionHandler
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Log.i(LOG_TAG, "onCreate intent=$intent")
|
||||
|
@ -70,9 +71,21 @@ open class MainActivity : FlutterActivity() {
|
|||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// notification: platform -> dart
|
||||
analysisStreamHandler = AnalysisStreamHandler().apply {
|
||||
EventChannel(messenger, AnalysisStreamHandler.CHANNEL).setStreamHandler(this)
|
||||
}
|
||||
errorStreamHandler = ErrorStreamHandler().apply {
|
||||
EventChannel(messenger, ErrorStreamHandler.CHANNEL).setStreamHandler(this)
|
||||
}
|
||||
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
|
||||
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
|
||||
}
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
analysisHandler = AnalysisHandler(this, ::onAnalysisCompleted)
|
||||
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
|
||||
MethodChannel(messenger, AnalysisHandler.CHANNEL).setMethodCallHandler(analysisHandler)
|
||||
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
|
||||
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
|
||||
|
@ -83,7 +96,7 @@ open class MainActivity : FlutterActivity() {
|
|||
MethodChannel(messenger, HomeWidgetHandler.CHANNEL).setMethodCallHandler(HomeWidgetHandler(this))
|
||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
|
||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
|
||||
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(MediaSessionHandler(this))
|
||||
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||
|
@ -128,16 +141,6 @@ open class MainActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// notification: platform -> dart
|
||||
analysisStreamHandler = AnalysisStreamHandler().apply {
|
||||
EventChannel(messenger, AnalysisStreamHandler.CHANNEL).setStreamHandler(this)
|
||||
}
|
||||
|
||||
// notification: platform -> dart
|
||||
errorStreamHandler = ErrorStreamHandler().apply {
|
||||
EventChannel(messenger, ErrorStreamHandler.CHANNEL).setStreamHandler(this)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
setupShortcuts()
|
||||
}
|
||||
|
@ -166,6 +169,7 @@ open class MainActivity : FlutterActivity() {
|
|||
|
||||
override fun onDestroy() {
|
||||
Log.i(LOG_TAG, "onDestroy")
|
||||
mediaSessionHandler.dispose()
|
||||
mediaStoreChangeStreamHandler.dispose()
|
||||
settingsChangeStreamHandler.dispose()
|
||||
super.onDestroy()
|
||||
|
@ -431,7 +435,7 @@ open class MainActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
var errorStreamHandler: ErrorStreamHandler? = null
|
||||
private var errorStreamHandler: ErrorStreamHandler? = null
|
||||
|
||||
suspend fun notifyError(error: String) {
|
||||
Log.e(LOG_TAG, "notifyError error=$error")
|
||||
|
|
|
@ -10,6 +10,7 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.TransactionTooLargeException
|
||||
import android.util.Log
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
|
@ -280,7 +281,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val title = call.argument<String>("title")
|
||||
val urisByMimeType = call.argument<Map<String, List<String>>>("urisByMimeType")
|
||||
if (urisByMimeType == null) {
|
||||
result.error("setAs-args", "missing arguments", null)
|
||||
result.error("share-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -288,15 +289,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
val mimeTypes = urisByMimeType.keys.toTypedArray()
|
||||
|
||||
// simplify share intent for a single item, as some apps can handle one item but not more
|
||||
val started = if (uriList.size == 1) {
|
||||
val intent = if (uriList.size == 1) {
|
||||
val uri = uriList.first()
|
||||
val mimeType = mimeTypes.first()
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
Intent(Intent.ACTION_SEND)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.setType(mimeType)
|
||||
.putExtra(Intent.EXTRA_STREAM, getShareableUri(context, uri))
|
||||
safeStartActivityChooser(title, intent)
|
||||
} else {
|
||||
var mimeType = "*/*"
|
||||
if (mimeTypes.size == 1) {
|
||||
|
@ -311,14 +311,21 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND_MULTIPLE)
|
||||
Intent(Intent.ACTION_SEND_MULTIPLE)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList)
|
||||
.setType(mimeType)
|
||||
safeStartActivityChooser(title, intent)
|
||||
}
|
||||
|
||||
result.success(started)
|
||||
try {
|
||||
val started = safeStartActivityChooser(title, intent)
|
||||
result.success(started)
|
||||
} catch (e: Exception) {
|
||||
if (e is TransactionTooLargeException || e.cause is TransactionTooLargeException) {
|
||||
result.error("share-large", "transaction too large with ${uriList.size} URIs", e)
|
||||
} else {
|
||||
result.error("share-exception", "failed to share ${uriList.size} URIs", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun safeStartActivity(intent: Intent): Boolean {
|
||||
|
|
|
@ -11,25 +11,21 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
|||
import com.drew.metadata.xmp.XmpDirectory
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||
import deckers.thibault.aves.metadata.Metadata
|
||||
import deckers.thibault.aves.metadata.MultiPage
|
||||
import deckers.thibault.aves.metadata.*
|
||||
import deckers.thibault.aves.metadata.XMP.doesPropExist
|
||||
import deckers.thibault.aves.metadata.XMP.getSafeStructField
|
||||
import deckers.thibault.aves.metadata.XMPPropName
|
||||
import deckers.thibault.aves.metadata.metadataextractor.Helper
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.provider.ContentImageProvider
|
||||
import deckers.thibault.aves.model.provider.ImageProvider
|
||||
import deckers.thibault.aves.utils.BitmapUtils
|
||||
import deckers.thibault.aves.utils.*
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.FileUtils.transferFrom
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
|
||||
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
|
||||
import deckers.thibault.aves.utils.MimeTypes.extensionFor
|
||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
|
@ -46,6 +42,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
|
||||
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
|
||||
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
|
||||
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
|
||||
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
|
||||
|
@ -84,6 +81,68 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(thumbnails)
|
||||
}
|
||||
|
||||
private fun extractGoogleDeviceItem(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
val displayName = call.argument<String>("displayName")
|
||||
val dataUri = call.argument<String>("dataUri")
|
||||
if (mimeType == null || uri == null || sizeBytes == null || dataUri == null) {
|
||||
result.error("extractGoogleDeviceItem-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
var container: GoogleDeviceContainer? = null
|
||||
|
||||
if (canReadWithMetadataExtractor(mimeType)) {
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||
val metadata = Helper.safeRead(input)
|
||||
// data can be large and stored in "Extended XMP",
|
||||
// which is returned as a second XMP directory
|
||||
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
|
||||
try {
|
||||
container = xmpDirs.firstNotNullOfOrNull {
|
||||
val xmpMeta = it.xmpMeta
|
||||
if (xmpMeta.doesPropExist(XMP.GDEVICE_DIRECTORY_PROP_NAME)) {
|
||||
GoogleDeviceContainer().apply { findItems(xmpMeta) }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: XMPException) {
|
||||
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to extract file from XMP", e)
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(LOG_TAG, "failed to extract file from XMP", e)
|
||||
} catch (e: AssertionError) {
|
||||
Log.w(LOG_TAG, "failed to extract file from XMP", e)
|
||||
}
|
||||
}
|
||||
|
||||
container?.let {
|
||||
it.findOffsets(context, uri, mimeType, sizeBytes)
|
||||
|
||||
val index = it.itemIndex(dataUri)
|
||||
val itemStartOffset = it.itemStartOffset(index)
|
||||
val itemLength = it.itemLength(index)
|
||||
val itemMimeType = it.itemMimeType(index)
|
||||
if (itemStartOffset != null && itemLength != null && itemMimeType != null) {
|
||||
StorageUtils.openInputStream(context, uri)?.let { input ->
|
||||
input.skip(itemStartOffset)
|
||||
copyEmbeddedBytes(result, itemMimeType, displayName, input, itemLength)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.error("extractGoogleDeviceItem-empty", "failed to extract item from Google Device XMP at uri=$uri dataUri=$dataUri", null)
|
||||
}
|
||||
|
||||
private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.Context
|
||||
import android.content.*
|
||||
import android.media.AudioManager
|
||||
import android.media.session.PlaybackState
|
||||
import android.net.Uri
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.util.Log
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
|
@ -19,20 +20,36 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MediaSessionHandler(private val context: Context) : MethodCallHandler {
|
||||
class MediaSessionHandler(private val context: Context, private val mediaCommandHandler: MediaCommandStreamHandler) : MethodCallHandler {
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
private val sessions = HashMap<Uri, MediaSessionCompat>()
|
||||
private var session: MediaSessionCompat? = null
|
||||
private var wasPlaying = false
|
||||
private var isNoisyAudioReceiverRegistered = false
|
||||
private val noisyAudioReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
|
||||
mediaCommandHandler.onStop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
if (isNoisyAudioReceiverRegistered) {
|
||||
context.unregisterReceiver(noisyAudioReceiver)
|
||||
isNoisyAudioReceiverRegistered = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"update" -> ioScope.launch { safeSuspend(call, result, ::update) }
|
||||
"release" -> ioScope.launch { safe(call, result, ::release) }
|
||||
"update" -> ioScope.launch { safeSuspend(call, result, ::updateSession) }
|
||||
"release" -> ioScope.launch { safe(call, result, ::releaseSession) }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun update(call: MethodCall, result: MethodChannel.Result) {
|
||||
private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val title = call.argument<String>("title")
|
||||
val durationMillis = call.argument<Number>("durationMillis")?.toLong()
|
||||
|
@ -72,69 +89,51 @@ class MediaSessionHandler(private val context: Context) : MethodCallHandler {
|
|||
.setActions(actions)
|
||||
.build()
|
||||
|
||||
var session = sessions[uri]
|
||||
if (session == null) {
|
||||
session = MediaSessionCompat(context, "aves-$uri")
|
||||
sessions[uri] = session
|
||||
|
||||
val metadata = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
|
||||
.build()
|
||||
session.setMetadata(metadata)
|
||||
|
||||
val callback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() {
|
||||
override fun onPlay() {
|
||||
super.onPlay()
|
||||
Log.d(LOG_TAG, "TLAD onPlay uri=$uri")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.d(LOG_TAG, "TLAD onPause uri=$uri")
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
Log.d(LOG_TAG, "TLAD onStop uri=$uri")
|
||||
}
|
||||
|
||||
override fun onSeekTo(pos: Long) {
|
||||
super.onSeekTo(pos)
|
||||
Log.d(LOG_TAG, "TLAD onSeekTo uri=$uri pos=$pos")
|
||||
FlutterUtils.runOnUiThread {
|
||||
if (session == null) {
|
||||
val mbrIntent = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
|
||||
val mbrName = ComponentName(context, MediaButtonReceiver::class.java)
|
||||
session = MediaSessionCompat(context, "aves", mbrName, mbrIntent).apply {
|
||||
setCallback(mediaCommandHandler)
|
||||
}
|
||||
}
|
||||
FlutterUtils.runOnUiThread {
|
||||
session.setCallback(callback)
|
||||
session!!.apply {
|
||||
val metadata = MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMillis)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, uri.toString())
|
||||
.build()
|
||||
setMetadata(metadata)
|
||||
setPlaybackState(playbackState)
|
||||
if (!isActive) {
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.setPlaybackState(playbackState)
|
||||
|
||||
if (!session.isActive) {
|
||||
session.isActive = true
|
||||
val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
|
||||
if (!wasPlaying && isPlaying) {
|
||||
context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
|
||||
isNoisyAudioReceiverRegistered = true
|
||||
} else if (wasPlaying && !isPlaying) {
|
||||
context.unregisterReceiver(noisyAudioReceiver)
|
||||
isNoisyAudioReceiverRegistered = false
|
||||
}
|
||||
wasPlaying = isPlaying
|
||||
}
|
||||
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun release(call: MethodCall, result: MethodChannel.Result) {
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
|
||||
if (uri == null) {
|
||||
result.error("release-args", "missing arguments", null)
|
||||
return
|
||||
private fun releaseSession(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
session?.let {
|
||||
it.release()
|
||||
session = null
|
||||
}
|
||||
|
||||
sessions[uri]?.release()
|
||||
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<MediaSessionHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/media_session"
|
||||
|
||||
const val STATE_STOPPED = "stopped"
|
||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls
|
|||
import android.content.ContextWrapper
|
||||
import android.net.Uri
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.metadata.Mp4TooLargeException
|
||||
import deckers.thibault.aves.model.ExifOrientationOp
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
|
||||
|
@ -66,10 +67,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
|
|||
return
|
||||
}
|
||||
|
||||
provider.editOrientation(contextWrapper, path, uri, mimeType, op, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
||||
override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation for mimeType=$mimeType uri=$uri", throwable)
|
||||
})
|
||||
val callback = MetadataOpCallback("editOrientation", entryMap, result)
|
||||
provider.editOrientation(contextWrapper, path, uri, mimeType, op, callback)
|
||||
}
|
||||
|
||||
private fun editDate(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
@ -96,10 +95,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
|
|||
return
|
||||
}
|
||||
|
||||
provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
||||
override fun onFailure(throwable: Throwable) = result.error("editDate-failure", "failed to edit date for mimeType=$mimeType uri=$uri", throwable)
|
||||
})
|
||||
val callback = MetadataOpCallback("editDate", entryMap, result)
|
||||
provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, callback)
|
||||
}
|
||||
|
||||
private fun editMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
@ -125,10 +122,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
|
|||
return
|
||||
}
|
||||
|
||||
provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback = object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
||||
override fun onFailure(throwable: Throwable) = result.error("editMetadata-failure", "failed to edit metadata for mimeType=$mimeType uri=$uri", throwable)
|
||||
})
|
||||
val callback = MetadataOpCallback("editMetadata", entryMap, result)
|
||||
provider.editMetadata(contextWrapper, path, uri, mimeType, metadata, autoCorrectTrailerOffset, callback)
|
||||
}
|
||||
|
||||
private fun removeTrailerVideo(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
@ -152,10 +147,8 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
|
|||
return
|
||||
}
|
||||
|
||||
provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
||||
override fun onFailure(throwable: Throwable) = result.error("removeTrailerVideo-failure", "failed to remove trailer video for mimeType=$mimeType uri=$uri", throwable)
|
||||
})
|
||||
val callback = MetadataOpCallback("removeTrailerVideo", entryMap, result)
|
||||
provider.removeTrailerVideo(contextWrapper, path, uri, mimeType, callback)
|
||||
}
|
||||
|
||||
private fun removeTypes(call: MethodCall, result: MethodChannel.Result) {
|
||||
|
@ -180,13 +173,31 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
|
|||
return
|
||||
}
|
||||
|
||||
provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
||||
override fun onFailure(throwable: Throwable) = result.error("removeTypes-failure", "failed to remove metadata for mimeType=$mimeType uri=$uri", throwable)
|
||||
})
|
||||
val callback = MetadataOpCallback("removeTypes", entryMap, result)
|
||||
provider.removeMetadataTypes(contextWrapper, path, uri, mimeType, types.toSet(), callback)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL = "deckers.thibault/aves/metadata_edit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MetadataOpCallback(
|
||||
private val errorCodeBase: String,
|
||||
private val entryMap: FieldMap,
|
||||
private val result: MethodChannel.Result,
|
||||
) : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
||||
override fun onFailure(throwable: Throwable) {
|
||||
val errorCode = if (throwable is Mp4TooLargeException) {
|
||||
if (throwable.type == "moov") {
|
||||
"$errorCodeBase-mp4largemoov"
|
||||
} else {
|
||||
"$errorCodeBase-mp4largeother"
|
||||
}
|
||||
} else {
|
||||
"$errorCodeBase-failure"
|
||||
}
|
||||
result.error(errorCode, "failed for entry=$entryMap", throwable)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
if (prop is XMPPropertyInfo) {
|
||||
val path = prop.path
|
||||
if (path?.isNotEmpty() == true) {
|
||||
val value = if (XMP.isDataPath(path)) "[skipped]" else prop.value
|
||||
val value = if (XMP.isDataPath(path)) VALUE_SKIPPED_DATA else prop.value
|
||||
if (value?.isNotEmpty() == true) {
|
||||
dirMap[path] = value
|
||||
}
|
||||
|
@ -615,7 +615,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
if (!metadataMap.containsKey(KEY_XMP_TITLE) || !metadataMap.containsKey(KEY_XMP_SUBJECTS)) {
|
||||
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
|
||||
if (!metadataMap.containsKey(KEY_XMP_TITLE)) {
|
||||
dir.getSafeString(IptcDirectory.TAG_OBJECT_NAME) { metadataMap[KEY_XMP_TITLE] = it }
|
||||
dir.getSafeString(IptcDirectory.TAG_OBJECT_NAME, acceptBlank = false) { metadataMap[KEY_XMP_TITLE] = it }
|
||||
}
|
||||
if (!metadataMap.containsKey(KEY_XMP_SUBJECTS)) {
|
||||
dir.keywords?.let { metadataMap[KEY_XMP_SUBJECTS] = it.joinToString(XMP_SUBJECTS_SEPARATOR) }
|
||||
|
@ -1151,6 +1151,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
// return description from these fields (by precedence):
|
||||
// - XMP / dc:description
|
||||
// - IPTC / caption-abstract
|
||||
// - Exif / UserComment
|
||||
// - Exif / ImageDescription
|
||||
private fun getDescription(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
|
@ -1171,7 +1172,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
val xmpMeta = dir.xmpMeta
|
||||
try {
|
||||
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
|
||||
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME) { description = it }
|
||||
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it }
|
||||
}
|
||||
} catch (e: XMPException) {
|
||||
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
|
||||
|
@ -1179,12 +1180,23 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
if (description == null) {
|
||||
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
|
||||
dir.getSafeString(IptcDirectory.TAG_CAPTION) { description = it }
|
||||
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
|
||||
}
|
||||
}
|
||||
if (description == null) {
|
||||
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
|
||||
// user comment field specifies encoding, unlike other string fields
|
||||
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
|
||||
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
|
||||
if (string.isNotBlank()) {
|
||||
description = string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (description == null) {
|
||||
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
|
||||
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION) { description = it }
|
||||
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1269,5 +1281,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
|
||||
// additional media key
|
||||
private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture"
|
||||
|
||||
private const val VALUE_SKIPPED_DATA = "[skipped]"
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.channel.calls.window
|
|||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import deckers.thibault.aves.utils.getDisplayCompat
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
@ -42,25 +43,30 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
|
|||
result.success(true)
|
||||
}
|
||||
|
||||
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
}
|
||||
|
||||
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
|
||||
val use = call.argument<Boolean>("use")
|
||||
if (use == null) {
|
||||
result.error("setCutoutMode-args", "missing arguments", null)
|
||||
override fun getCutoutInsets(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
result.error("getCutoutInsets-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null)
|
||||
return
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val mode = if (use) {
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
} else {
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
||||
}
|
||||
activity.window.attributes.layoutInDisplayCutoutMode = mode
|
||||
val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
activity.getDisplayCompat()?.cutout
|
||||
} else {
|
||||
activity.window.decorView.rootWindowInsets.displayCutout
|
||||
}
|
||||
result.success(true)
|
||||
|
||||
val density = activity.resources.displayMetrics.density
|
||||
result.success(
|
||||
hashMapOf(
|
||||
"left" to (cutout?.safeInsetLeft ?: 0) / density,
|
||||
"top" to (cutout?.safeInsetTop ?: 0) / density,
|
||||
"right" to (cutout?.safeInsetRight ?: 0) / density,
|
||||
"bottom" to (cutout?.safeInsetBottom ?: 0) / density,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -17,11 +17,11 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
|
|||
result.success(false)
|
||||
}
|
||||
|
||||
override fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
override fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(false)
|
||||
}
|
||||
|
||||
override fun setCutoutMode(call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(false)
|
||||
override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
|
||||
result.success(HashMap<String, Any>())
|
||||
}
|
||||
}
|
|
@ -15,8 +15,8 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
|
|||
"keepScreenOn" -> Coresult.safe(call, result, ::keepScreenOn)
|
||||
"isRotationLocked" -> Coresult.safe(call, result, ::isRotationLocked)
|
||||
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
|
||||
"canSetCutoutMode" -> Coresult.safe(call, result, ::canSetCutoutMode)
|
||||
"setCutoutMode" -> Coresult.safe(call, result, ::setCutoutMode)
|
||||
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
|
||||
"getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
@ -37,9 +37,9 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
|
|||
|
||||
abstract fun requestOrientation(call: MethodCall, result: MethodChannel.Result)
|
||||
|
||||
abstract fun canSetCutoutMode(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
|
||||
abstract fun isCutoutAware(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result)
|
||||
|
||||
abstract fun setCutoutMode(call: MethodCall, result: MethodChannel.Result)
|
||||
abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result)
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<WindowHandler>()
|
||||
|
|
|
@ -199,7 +199,9 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {}
|
||||
override fun onCancel(arguments: Any?) {
|
||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||
}
|
||||
|
||||
private fun success(result: Any?) {
|
||||
handler.post {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package deckers.thibault.aves.channel.streams
|
||||
|
||||
import android.util.Log
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.EventChannel.EventSink
|
||||
|
||||
|
@ -13,13 +15,16 @@ class AnalysisStreamHandler : EventChannel.StreamHandler {
|
|||
this.eventSink = eventSink
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {}
|
||||
override fun onCancel(arguments: Any?) {
|
||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||
}
|
||||
|
||||
fun notifyCompletion() {
|
||||
eventSink?.success(true)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<AnalysisStreamHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/analysis_events"
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package deckers.thibault.aves.channel.streams
|
||||
|
||||
import android.util.Log
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.EventChannel.EventSink
|
||||
|
||||
|
@ -14,7 +16,9 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
|
|||
this.eventSink = eventSink
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {}
|
||||
override fun onCancel(arguments: Any?) {
|
||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||
}
|
||||
|
||||
suspend fun notifyError(error: String) {
|
||||
FlutterUtils.runOnUiThread {
|
||||
|
@ -23,6 +27,7 @@ class ErrorStreamHandler : EventChannel.StreamHandler {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<ErrorStreamHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/error"
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package deckers.thibault.aves.channel.streams
|
||||
|
||||
import android.util.Log
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.EventChannel.EventSink
|
||||
|
||||
|
@ -13,13 +15,16 @@ class IntentStreamHandler : EventChannel.StreamHandler {
|
|||
this.eventSink = eventSink
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {}
|
||||
override fun onCancel(arguments: Any?) {
|
||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||
}
|
||||
|
||||
fun notifyNewIntent(intentData: MutableMap<String, Any?>?) {
|
||||
eventSink?.success(intentData)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<IntentStreamHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/new_intent_stream"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -41,7 +41,9 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
|
|||
handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {}
|
||||
override fun onCancel(arguments: Any?) {
|
||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||
|
|
|
@ -79,7 +79,9 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
|
|||
handler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {}
|
||||
override fun onCancel(arguments: Any?) {
|
||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||
|
|
|
@ -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) {}
|
|
@ -33,7 +33,7 @@ object Mp4ParserHelper {
|
|||
)
|
||||
setBoxSkipper { type, size ->
|
||||
if (skippedTypes.contains(type)) return@setBoxSkipper true
|
||||
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large")
|
||||
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Mp4TooLargeException(type, "box (type=$type size=$size) is too large")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
@ -232,3 +232,5 @@ object Mp4ParserHelper {
|
|||
return stream.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
class Mp4TooLargeException(val type: String, message: String) : RuntimeException(message)
|
||||
|
|
|
@ -175,14 +175,14 @@ object MultiPage {
|
|||
if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
|
||||
// `GCamera` motion photo
|
||||
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
|
||||
} else if (xmpMeta.doesPropExist(XMP.CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
} else if (xmpMeta.doesPropExist(XMP.GCONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
// `Container` motion photo
|
||||
val count = xmpMeta.countPropArrayItems(XMP.CONTAINER_DIRECTORY_PROP_NAME)
|
||||
val count = xmpMeta.countPropArrayItems(XMP.GCONTAINER_DIRECTORY_PROP_NAME)
|
||||
if (count == 2) {
|
||||
// expect the video to be the second item
|
||||
val i = 2
|
||||
val mime = xmpMeta.getSafeStructField(listOf(XMP.CONTAINER_DIRECTORY_PROP_NAME, i, XMP.CONTAINER_ITEM_PROP_NAME, XMP.CONTAINER_ITEM_MIME_PROP_NAME))?.value
|
||||
val length = xmpMeta.getSafeStructField(listOf(XMP.CONTAINER_DIRECTORY_PROP_NAME, i, XMP.CONTAINER_ITEM_PROP_NAME, XMP.CONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
||||
val mime = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_MIME_PROP_NAME))?.value
|
||||
val length = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
||||
if (MimeTypes.isVideo(mime) && length != null) {
|
||||
offsetFromEnd = length.toLong()
|
||||
}
|
||||
|
|
|
@ -42,17 +42,17 @@ class GSpherical(xmlBytes: ByteArray) {
|
|||
"StitchingSoftware" -> stitchingSoftware = readTag(parser, tag)
|
||||
"ProjectionType" -> projectionType = readTag(parser, tag)
|
||||
"StereoMode" -> stereoMode = readTag(parser, tag)
|
||||
"SourceCount" -> sourceCount = Integer.parseInt(readTag(parser, tag))
|
||||
"InitialViewHeadingDegrees" -> initialViewHeadingDegrees = Integer.parseInt(readTag(parser, tag))
|
||||
"InitialViewPitchDegrees" -> initialViewPitchDegrees = Integer.parseInt(readTag(parser, tag))
|
||||
"InitialViewRollDegrees" -> initialViewRollDegrees = Integer.parseInt(readTag(parser, tag))
|
||||
"Timestamp" -> timestamp = Integer.parseInt(readTag(parser, tag))
|
||||
"FullPanoWidthPixels" -> fullPanoWidthPixels = Integer.parseInt(readTag(parser, tag))
|
||||
"FullPanoHeightPixels" -> fullPanoHeightPixels = Integer.parseInt(readTag(parser, tag))
|
||||
"CroppedAreaImageWidthPixels" -> croppedAreaImageWidthPixels = Integer.parseInt(readTag(parser, tag))
|
||||
"CroppedAreaImageHeightPixels" -> croppedAreaImageHeightPixels = Integer.parseInt(readTag(parser, tag))
|
||||
"CroppedAreaLeftPixels" -> croppedAreaLeftPixels = Integer.parseInt(readTag(parser, tag))
|
||||
"CroppedAreaTopPixels" -> croppedAreaTopPixels = Integer.parseInt(readTag(parser, tag))
|
||||
"SourceCount" -> sourceCount = readTag(parser, tag).toInt()
|
||||
"InitialViewHeadingDegrees" -> initialViewHeadingDegrees = readTag(parser, tag).toInt()
|
||||
"InitialViewPitchDegrees" -> initialViewPitchDegrees = readTag(parser, tag).toInt()
|
||||
"InitialViewRollDegrees" -> initialViewRollDegrees = readTag(parser, tag).toInt()
|
||||
"Timestamp" -> timestamp = readTag(parser, tag).toInt()
|
||||
"FullPanoWidthPixels" -> fullPanoWidthPixels = readTag(parser, tag).toInt()
|
||||
"FullPanoHeightPixels" -> fullPanoHeightPixels = readTag(parser, tag).toInt()
|
||||
"CroppedAreaImageWidthPixels" -> croppedAreaImageWidthPixels = readTag(parser, tag).toInt()
|
||||
"CroppedAreaImageHeightPixels" -> croppedAreaImageHeightPixels = readTag(parser, tag).toInt()
|
||||
"CroppedAreaLeftPixels" -> croppedAreaLeftPixels = readTag(parser, tag).toInt()
|
||||
"CroppedAreaTopPixels" -> croppedAreaTopPixels = readTag(parser, tag).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,11 +43,13 @@ object XMP {
|
|||
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
|
||||
|
||||
// other namespaces
|
||||
private const val CONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
|
||||
private const val CONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
|
||||
private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/"
|
||||
private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/"
|
||||
private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
|
||||
private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
|
||||
private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
|
||||
private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/"
|
||||
private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
|
||||
private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
|
||||
private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
|
||||
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
|
||||
|
@ -75,13 +77,20 @@ object XMP {
|
|||
|
||||
fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it }
|
||||
|
||||
// google portrait
|
||||
|
||||
val GDEVICE_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container/Container:Directory")
|
||||
val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI")
|
||||
val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
|
||||
val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
|
||||
|
||||
// motion photo
|
||||
|
||||
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
|
||||
val CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Directory")
|
||||
val CONTAINER_ITEM_PROP_NAME = XMPPropName(CONTAINER_NS_URI, "Item")
|
||||
val CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Length")
|
||||
val CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(CONTAINER_ITEM_NS_URI, "Mime")
|
||||
val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
|
||||
val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
|
||||
val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
|
||||
val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
|
||||
|
||||
// panorama
|
||||
// cf https://developers.google.com/streetview/spherical-metadata
|
||||
|
@ -189,14 +198,14 @@ object XMP {
|
|||
if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
|
||||
|
||||
// Container motion photo
|
||||
if (doesPropExist(CONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
val count = countPropArrayItems(CONTAINER_DIRECTORY_PROP_NAME)
|
||||
if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
|
||||
val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
|
||||
if (count == 2) {
|
||||
var hasImage = false
|
||||
var hasVideo = false
|
||||
for (i in 1 until count + 1) {
|
||||
val mime = getSafeStructField(listOf(CONTAINER_DIRECTORY_PROP_NAME, i, CONTAINER_ITEM_PROP_NAME, CONTAINER_ITEM_MIME_PROP_NAME))?.value
|
||||
val length = getSafeStructField(listOf(CONTAINER_DIRECTORY_PROP_NAME, i, CONTAINER_ITEM_PROP_NAME, CONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
||||
val mime = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_MIME_PROP_NAME))?.value
|
||||
val length = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
|
||||
hasImage = hasImage || MimeTypes.isImage(mime) && length != null
|
||||
hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
|
||||
}
|
||||
|
|
|
@ -117,8 +117,13 @@ object Helper {
|
|||
|
||||
// extensions
|
||||
|
||||
fun Directory.getSafeString(tag: Int, save: (value: String) -> Unit) {
|
||||
if (this.containsTag(tag)) save(this.getString(tag))
|
||||
fun Directory.getSafeString(tag: Int, acceptBlank: Boolean = true, save: (value: String) -> Unit) {
|
||||
if (this.containsTag(tag)) {
|
||||
val string = this.getString(tag)
|
||||
if (acceptBlank || string.isNotBlank()) {
|
||||
save(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Directory.getSafeBoolean(tag: Int, save: (value: Boolean) -> Unit) {
|
||||
|
|
|
@ -55,7 +55,7 @@ internal class ContentImageProvider : ImageProvider() {
|
|||
if (cursor != null && cursor.moveToFirst()) {
|
||||
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) }
|
||||
cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields["sizeBytes"] = cursor.getLong(it) }
|
||||
cursor.getColumnIndex(PATH).let { if (it != -1) fields["path"] = cursor.getString(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATA).let { if (it != -1) fields["path"] = cursor.getString(it) }
|
||||
cursor.close()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -73,8 +73,5 @@ internal class ContentImageProvider : ImageProvider() {
|
|||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<ContentImageProvider>()
|
||||
|
||||
@Suppress("deprecation")
|
||||
const val PATH = MediaStore.MediaColumns.DATA
|
||||
}
|
||||
}
|
|
@ -761,8 +761,8 @@ abstract class ImageProvider {
|
|||
"${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newTrailerOffset\"",
|
||||
).replace(
|
||||
// Container motion photo
|
||||
"${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"",
|
||||
"${XMP.CONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"",
|
||||
"${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"",
|
||||
"${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -55,10 +55,10 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val relativePathDirectory = ensureTrailingSeparator(directory)
|
||||
val relativePath = PathSegments(context, relativePathDirectory).relativeDir
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && relativePath != null) {
|
||||
selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaColumns.PATH} LIKE ?"
|
||||
selection = "${MediaStore.MediaColumns.RELATIVE_PATH} = ? AND ${MediaStore.MediaColumns.DATA} LIKE ?"
|
||||
selectionArgs = arrayOf(relativePath, "$relativePathDirectory%")
|
||||
} else {
|
||||
selection = "${MediaColumns.PATH} LIKE ?"
|
||||
selection = "${MediaStore.MediaColumns.DATA} LIKE ?"
|
||||
selectionArgs = arrayOf("$relativePathDirectory%")
|
||||
}
|
||||
|
||||
|
@ -139,12 +139,12 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
fun checkObsoletePaths(context: Context, knownPathById: Map<Int?, String?>): List<Int> {
|
||||
val obsoleteIds = ArrayList<Int>()
|
||||
fun check(context: Context, contentUri: Uri) {
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaColumns.PATH)
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||
try {
|
||||
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
|
||||
if (cursor != null) {
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH)
|
||||
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getInt(idColumn)
|
||||
val path = cursor.getString(pathColumn)
|
||||
|
@ -185,7 +185,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
// image & video
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
|
||||
val pathColumn = cursor.getColumnIndexOrThrow(MediaColumns.PATH)
|
||||
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
|
||||
val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
|
||||
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
||||
|
@ -863,7 +863,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
fun getContentUriForPath(context: Context, path: String): Uri? {
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||
val selection = "${MediaColumns.PATH} = ?"
|
||||
val selection = "${MediaStore.MediaColumns.DATA} = ?"
|
||||
val selectionArgs = arrayOf(path)
|
||||
|
||||
fun check(context: Context, contentUri: Uri): Uri? {
|
||||
|
@ -892,7 +892,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
private val BASE_PROJECTION = arrayOf(
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaColumns.PATH,
|
||||
MediaStore.MediaColumns.DATA,
|
||||
MediaStore.MediaColumns.MIME_TYPE,
|
||||
MediaStore.MediaColumns.SIZE,
|
||||
MediaStore.MediaColumns.WIDTH,
|
||||
|
@ -931,9 +931,6 @@ object MediaColumns {
|
|||
|
||||
@SuppressLint("InlinedApi")
|
||||
const val DURATION = MediaStore.MediaColumns.DURATION
|
||||
|
||||
@Suppress("deprecation")
|
||||
const val PATH = MediaStore.MediaColumns.DATA
|
||||
}
|
||||
|
||||
typealias NewEntryHandler = (entry: FieldMap) -> Unit
|
||||
|
|
|
@ -20,7 +20,7 @@ fun <E> MutableList<E>.compatRemoveIf(filter: (t: E) -> Boolean): Boolean {
|
|||
}
|
||||
|
||||
// Boyer-Moore algorithm for pattern searching
|
||||
fun ByteArray.indexOfBytes(pattern: ByteArray): Int {
|
||||
fun ByteArray.indexOfBytes(pattern: ByteArray, start: Int = 0): Int {
|
||||
val n: Int = this.size
|
||||
val m: Int = pattern.size
|
||||
val badChar = Array(256) { 0 }
|
||||
|
@ -30,7 +30,7 @@ fun ByteArray.indexOfBytes(pattern: ByteArray): Int {
|
|||
i += 1
|
||||
}
|
||||
var j: Int = m - 1
|
||||
var s = 0
|
||||
var s = start
|
||||
while (s <= (n - m)) {
|
||||
while (j >= 0 && pattern[j] == this[s + j]) {
|
||||
j -= 1
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package deckers.thibault.aves.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.view.Display
|
||||
|
||||
inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
@ -16,6 +18,14 @@ inline fun <reified T> Intent.getParcelableExtraCompat(name: String): T? {
|
|||
}
|
||||
}
|
||||
|
||||
fun Activity.getDisplayCompat(): Display? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
display
|
||||
} else {
|
||||
@Suppress("deprecation")
|
||||
windowManager.defaultDisplay
|
||||
}
|
||||
}
|
||||
|
||||
fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
|
|
@ -90,7 +90,8 @@ object MimeTypes {
|
|||
|
||||
// as of `metadata-extractor` v2.14.0
|
||||
fun canReadWithMetadataExtractor(mimeType: String) = when (mimeType) {
|
||||
DJVU, WBMP, MKV, MP2T, MP2TS, OGV, WEBM -> false
|
||||
DJVU, WBMP -> false
|
||||
MKV, MP2T, MP2TS, OGV, WEBM -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
|
|
12
android/app/src/main/res/values-cs/strings.xml
Normal 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>
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:configure="deckers.thibault.aves.HomeWidgetSettingsActivity"
|
||||
android:initialLayout="@layout/app_widget"
|
||||
android:minWidth="40dp"
|
||||
|
@ -9,4 +10,5 @@
|
|||
android:targetCellHeight="2"
|
||||
android:updatePeriodMillis="3600000"
|
||||
android:widgetCategory="home_screen"
|
||||
android:widgetFeatures="reconfigurable" />
|
||||
android:widgetFeatures="reconfigurable"
|
||||
tools:targetApi="s" />
|
5
fastlane/metadata/android/cs/full_description.txt
Normal 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>.
|
BIN
fastlane/metadata/android/cs/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
fastlane/metadata/android/cs/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 282 KiB |
BIN
fastlane/metadata/android/cs/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 498 KiB |
BIN
fastlane/metadata/android/cs/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 211 KiB |
BIN
fastlane/metadata/android/cs/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
fastlane/metadata/android/cs/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
fastlane/metadata/android/cs/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 341 KiB |
BIN
fastlane/metadata/android/cs/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 341 KiB |
1
fastlane/metadata/android/cs/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Galerie a prohlížeč metadat
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
In v1.6.3:
|
||||
- enjoy the light theme
|
||||
- rename items in bulk
|
||||
Full changelog available on GitHub
|
|
@ -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
|
|
@ -1,5 +0,0 @@
|
|||
In v1.6.5:
|
||||
- bottom navigation bar
|
||||
- fast scroll with breadcrumbs
|
||||
- settings search
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.6.6:
|
||||
- bottom navigation bar
|
||||
- fast scroll with breadcrumbs
|
||||
- settings search
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.6.7:
|
||||
- bottom navigation bar
|
||||
- fast scroll with breadcrumbs
|
||||
- settings search
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.6.8:
|
||||
- bottom navigation bar
|
||||
- fast scroll with breadcrumbs
|
||||
- settings search
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.6.9:
|
||||
- start slideshows
|
||||
- change your wallpaper
|
||||
- enjoy the app in Turkish
|
||||
Full changelog available on GitHub
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
5
fastlane/metadata/android/en-US/changelogs/89.txt
Normal 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
|
5
fastlane/metadata/android/en-US/changelogs/8901.txt
Normal 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
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
<b>Navigasi dan pencarian</b> merupakan bagian penting dari <i>Aves</i>. Tujuannya adalah agar pengguna dengan mudah mengalir dari album ke foto ke tag ke peta, dll.
|
||||
|
||||
<i>Aves</i> terintegrasi dengan Android (dari <b>API 19 ke 33</b>, yaitu dari KitKat ke Android 13) dengan fitur-fitur seperti <b>pintasan aplikasi</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.
|
||||
<i>Aves</i> mengintegrasi dengan Android (dari Kitkat ke Android 13) dengan fitur-fitur seperti <b>pintasan aplikasi</b>, <b>jalan pintas aplikasi</b>, <b>screen saver</b> dan <b>pencarian global</b> penanganan. Ini juga berfungsi sebagai <b>penampil dan pemilih media</b>.
|
|
@ -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>.
|
BIN
fastlane/metadata/android/pl/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
fastlane/metadata/android/pl/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 282 KiB |
BIN
fastlane/metadata/android/pl/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 499 KiB |
BIN
fastlane/metadata/android/pl/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 213 KiB |
BIN
fastlane/metadata/android/pl/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
fastlane/metadata/android/pl/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
fastlane/metadata/android/pl/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 341 KiB |
BIN
fastlane/metadata/android/pl/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 340 KiB |
|
@ -16,5 +16,57 @@
|
|||
"filePickerShowHiddenFiles": "إظهار الملفات المخفية",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"panoramaEnableSensorControl": "تمكين التحكم في المستشعر",
|
||||
"@panoramaEnableSensorControl": {}
|
||||
"@panoramaEnableSensorControl": {},
|
||||
"saveTooltip": "حفظ",
|
||||
"@saveTooltip": {},
|
||||
"continueButtonLabel": "إستمرار",
|
||||
"@continueButtonLabel": {},
|
||||
"resetTooltip": "إعادة",
|
||||
"@resetTooltip": {},
|
||||
"doNotAskAgain": "عدم السؤال مرة أخرى",
|
||||
"@doNotAskAgain": {},
|
||||
"welcomeTermsToggle": "أوافق على الشروط",
|
||||
"@welcomeTermsToggle": {},
|
||||
"doubleBackExitMessage": "اضغط على \"رجوع\" مرة أخرى للخروج.",
|
||||
"@doubleBackExitMessage": {},
|
||||
"hideButtonLabel": "إخفاء",
|
||||
"@hideButtonLabel": {},
|
||||
"showTooltip": "إظهار",
|
||||
"@showTooltip": {},
|
||||
"clearTooltip": "تنظيف",
|
||||
"@clearTooltip": {},
|
||||
"changeTooltip": "تغيير",
|
||||
"@changeTooltip": {},
|
||||
"actionRemove": "إزالة",
|
||||
"@actionRemove": {},
|
||||
"appName": "Aves",
|
||||
"@appName": {},
|
||||
"welcomeOptional": "اختياري",
|
||||
"@welcomeOptional": {},
|
||||
"deleteButtonLabel": "حذف",
|
||||
"@deleteButtonLabel": {},
|
||||
"nextTooltip": "التالي",
|
||||
"@nextTooltip": {},
|
||||
"cancelTooltip": "إلغاء",
|
||||
"@cancelTooltip": {},
|
||||
"previousTooltip": "السابق",
|
||||
"@previousTooltip": {},
|
||||
"welcomeMessage": "مرحبا بكم في Aves",
|
||||
"@welcomeMessage": {},
|
||||
"applyButtonLabel": "تطبيق",
|
||||
"@applyButtonLabel": {},
|
||||
"nextButtonLabel": "التالي",
|
||||
"@nextButtonLabel": {},
|
||||
"showButtonLabel": "إظهار",
|
||||
"@showButtonLabel": {},
|
||||
"tagEditorSectionRecent": "الأخيرة",
|
||||
"@tagEditorSectionRecent": {},
|
||||
"tagEditorSectionPlaceholders": "العناصر النائبة",
|
||||
"@tagEditorSectionPlaceholders": {},
|
||||
"filePickerUseThisFolder": "إستخدام هذا المجلد",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"hideTooltip": "إخفاء",
|
||||
"@hideTooltip": {},
|
||||
"tagEditorPageAddTagTooltip": "إضافة علامة",
|
||||
"@tagEditorPageAddTagTooltip": {}
|
||||
}
|
||||
|
|
1358
lib/l10n/app_cs.arb
Normal file
|
@ -1196,5 +1196,15 @@
|
|||
"filterNoAddressLabel": "Χωρίς διεύθυνση",
|
||||
"@filterNoAddressLabel": {},
|
||||
"settingsViewerShowRatingTags": "Εμφάνιση βαθμολογίας & ετικετών",
|
||||
"@settingsViewerShowRatingTags": {}
|
||||
"@settingsViewerShowRatingTags": {},
|
||||
"filterLocatedLabel": "Με τοποθεσία",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Με ετικέτα",
|
||||
"@filterTaggedLabel": {},
|
||||
"settingsModificationWarningDialogMessage": "Άλλες ρυθμίσεις θα τροποποιηθούν.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"settingsViewerShowDescription": "Εμφάνιση περιγραφής",
|
||||
"@settingsViewerShowDescription": {}
|
||||
}
|
||||
|
|
|
@ -139,8 +139,10 @@
|
|||
"filterFavouriteLabel": "Favorite",
|
||||
"filterNoDateLabel": "Undated",
|
||||
"filterNoAddressLabel": "No address",
|
||||
"filterLocatedLabel": "Located",
|
||||
"filterNoLocationLabel": "Unlocated",
|
||||
"filterNoRatingLabel": "Unrated",
|
||||
"filterTaggedLabel": "Tagged",
|
||||
"filterNoTagLabel": "Untagged",
|
||||
"filterNoTitleLabel": "Untitled",
|
||||
"filterOnThisDayLabel": "On this day",
|
||||
|
@ -375,13 +377,13 @@
|
|||
"renameProcessorCounter": "Counter",
|
||||
"renameProcessorName": "Name",
|
||||
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and its item?} other{Delete this album and its {count} items?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and the item in it?} other{Delete this album and the {count} items in it?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Delete these albums and their item?} other{Delete these albums and their {count} items?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Delete these albums and the item in them?} other{Delete these albums and the {count} items in them?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -436,6 +438,8 @@
|
|||
"genericFailureFeedback": "Failed",
|
||||
"genericDangerWarningDialogMessage": "Are you sure?",
|
||||
|
||||
"tooManyItemsErrorDialogMessage": "Try again with fewer items.",
|
||||
|
||||
"menuActionConfigureView": "View",
|
||||
"menuActionSelect": "Select",
|
||||
"menuActionSelectAll": "Select all",
|
||||
|
@ -656,6 +660,7 @@
|
|||
"settingsSystemDefault": "System default",
|
||||
"settingsDefault": "Default",
|
||||
"settingsDisabled": "Disabled",
|
||||
"settingsModificationWarningDialogMessage": "Other settings will be modified.",
|
||||
|
||||
"settingsSearchFieldLabel": "Search settings",
|
||||
"settingsSearchEmpty": "No matching setting",
|
||||
|
@ -731,6 +736,7 @@
|
|||
"settingsViewerShowInformationSubtitle": "Show title, date, location, etc.",
|
||||
"settingsViewerShowRatingTags": "Show rating & tags",
|
||||
"settingsViewerShowShootingDetails": "Show shooting details",
|
||||
"settingsViewerShowDescription": "Show description",
|
||||
"settingsViewerShowOverlayThumbnails": "Show thumbnails",
|
||||
"settingsViewerEnableOverlayBlurEffect": "Blur effect",
|
||||
|
||||
|
@ -815,6 +821,7 @@
|
|||
"settingsThemeEnableDynamicColor": "Dynamic color",
|
||||
"settingsDisplayRefreshRateModeTile": "Display refresh rate",
|
||||
"settingsDisplayRefreshRateModeDialogTitle": "Refresh Rate",
|
||||
"settingsDisplayUseTvInterface": "Android TV interface",
|
||||
|
||||
"settingsLanguageSectionTitle": "Language & Formats",
|
||||
"settingsLanguageTile": "Language",
|
||||
|
|
|
@ -371,9 +371,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Nombre",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de que desea borrar este álbum y un elemento?} other{¿Está seguro de que desea borrar este álbum y sus {count} elementos?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{¿Eliminar este álbum y el elemento que contiene?} other{¿Eliminar este álbum y los {count} elementos que contiene?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{¿Está seguro de que desea borrar estos álbumes y un elemento?} other{¿Está seguro de que desea borrar estos álbumes y sus {count} elementos?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{¿Eliminar estos álbumes y el elemento que contienen?} other{¿Eliminar estos álbumes y los {count} elementos que contienen?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "Formato:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -1196,5 +1196,15 @@
|
|||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsViewerShowDescription": "Mostrar la descripción",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"settingsModificationWarningDialogMessage": "Otras configuraciones serán modificadas.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Interfaz de Android TV",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"filterLocatedLabel": "Localizado",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Etiquetado",
|
||||
"@filterTaggedLabel": {}
|
||||
}
|
||||
|
|
|
@ -297,5 +297,86 @@
|
|||
"policyPageTitle": "سیاست حفظ حریم خصوصی",
|
||||
"@policyPageTitle": {},
|
||||
"collectionPickPageTitle": "انتخاب",
|
||||
"@collectionPickPageTitle": {}
|
||||
"@collectionPickPageTitle": {},
|
||||
"videoResumeDialogMessage": "ادامه پخش از زمان {time}؟",
|
||||
"@videoResumeDialogMessage": {
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"type": "String",
|
||||
"example": "13:37"
|
||||
}
|
||||
}
|
||||
},
|
||||
"storageVolumeDescriptionFallbackNonPrimary": "کارت حافظه",
|
||||
"@storageVolumeDescriptionFallbackNonPrimary": {},
|
||||
"videoPlaybackWithSound": "پخش با صدا",
|
||||
"@videoPlaybackWithSound": {},
|
||||
"entryActionCopyToClipboard": "کپی به کلیپ بورد",
|
||||
"@entryActionCopyToClipboard": {},
|
||||
"entryActionShowGeoTiffOnMap": "نمایش بر روی نقشه",
|
||||
"@entryActionShowGeoTiffOnMap": {},
|
||||
"filterOnThisDayLabel": "در امروز",
|
||||
"@filterOnThisDayLabel": {},
|
||||
"mapStyleGoogleNormal": "گوگل مپ",
|
||||
"@mapStyleGoogleNormal": {},
|
||||
"mapStyleGoogleTerrain": "گوگل مپ (نمایش زمین)",
|
||||
"@mapStyleGoogleTerrain": {},
|
||||
"mapStyleGoogleHybrid": "گوگل مپ (نمایش هیبریدی)",
|
||||
"@mapStyleGoogleHybrid": {},
|
||||
"subtitlePositionTop": "بالا",
|
||||
"@subtitlePositionTop": {},
|
||||
"mapStyleStamenWatercolor": "استامن (نمایش نقشه کشیده شده)",
|
||||
"@mapStyleStamenWatercolor": {},
|
||||
"displayRefreshRatePreferLowest": "کمترین مقدار",
|
||||
"@displayRefreshRatePreferLowest": {},
|
||||
"videoPlaybackMuted": "پخش بی صدا",
|
||||
"@videoPlaybackMuted": {},
|
||||
"storageVolumeDescriptionFallbackPrimary": "حافظه داخلی",
|
||||
"@storageVolumeDescriptionFallbackPrimary": {},
|
||||
"columnCount": "{count, plural, =1{1 ستون} other{{count} ستون}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"mapStyleHuaweiNormal": "پتال مپس",
|
||||
"@mapStyleHuaweiNormal": {},
|
||||
"mapStyleHuaweiTerrain": "پتال مپس (نمایش زمین)",
|
||||
"@mapStyleHuaweiTerrain": {},
|
||||
"mapStyleOsmHot": "اوپناستریتمپ",
|
||||
"@mapStyleOsmHot": {},
|
||||
"subtitlePositionBottom": "پایین",
|
||||
"@subtitlePositionBottom": {},
|
||||
"themeBrightnessLight": "روشن",
|
||||
"@themeBrightnessLight": {},
|
||||
"themeBrightnessDark": "تاریک",
|
||||
"@themeBrightnessDark": {},
|
||||
"themeBrightnessBlack": "سیاه",
|
||||
"@themeBrightnessBlack": {},
|
||||
"videoStartOverButtonLabel": "شروع از اول",
|
||||
"@videoStartOverButtonLabel": {},
|
||||
"albumTierApps": "برنامه ها",
|
||||
"@albumTierApps": {},
|
||||
"nameConflictStrategyRename": "تغییر نام",
|
||||
"@nameConflictStrategyRename": {},
|
||||
"nameConflictStrategyReplace": "جایگزین کردن",
|
||||
"@nameConflictStrategyReplace": {},
|
||||
"displayRefreshRatePreferHighest": "بیشترین مقدار",
|
||||
"@displayRefreshRatePreferHighest": {},
|
||||
"storageAccessDialogMessage": "لطفا فولدر {directory} در {volume} را در صفحه بعد انتخاب کنید و اجازه را به برنامه بدهید.",
|
||||
"@storageAccessDialogMessage": {
|
||||
"placeholders": {
|
||||
"directory": {
|
||||
"type": "String",
|
||||
"description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
|
||||
},
|
||||
"volume": {
|
||||
"type": "String",
|
||||
"example": "SD card",
|
||||
"description": "the name of a storage volume"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapStyleStamenToner": "استامن (نمایش رود ها)",
|
||||
"@mapStyleStamenToner": {}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
"@chipActionGoToAlbumPage": {},
|
||||
"chipActionGoToCountryPage": "Afficher dans Pays",
|
||||
"@chipActionGoToCountryPage": {},
|
||||
"chipActionGoToTagPage": "Afficher dans Libellés",
|
||||
"chipActionGoToTagPage": "Afficher dans Étiquettes",
|
||||
"@chipActionGoToTagPage": {},
|
||||
"chipActionFilterOut": "Exclure",
|
||||
"@chipActionFilterOut": {},
|
||||
|
@ -165,7 +165,7 @@
|
|||
"@entryInfoActionEditTitleDescription": {},
|
||||
"entryInfoActionEditRating": "Modifier la notation",
|
||||
"@entryInfoActionEditRating": {},
|
||||
"entryInfoActionEditTags": "Modifier les libellés",
|
||||
"entryInfoActionEditTags": "Modifier les étiquettes",
|
||||
"@entryInfoActionEditTags": {},
|
||||
"entryInfoActionRemoveMetadata": "Retirer les métadonnées",
|
||||
"@entryInfoActionRemoveMetadata": {},
|
||||
|
@ -179,7 +179,7 @@
|
|||
"@filterNoLocationLabel": {},
|
||||
"filterNoRatingLabel": "Sans notation",
|
||||
"@filterNoRatingLabel": {},
|
||||
"filterNoTagLabel": "Sans libellé",
|
||||
"filterNoTagLabel": "Sans étiquette",
|
||||
"@filterNoTagLabel": {},
|
||||
"filterNoTitleLabel": "Sans titre",
|
||||
"@filterNoTitleLabel": {},
|
||||
|
@ -391,9 +391,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Nom",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer cet album et son élément ?} other{Supprimer cet album et ses {count} éléments ?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer cet album et l’élément dedans ?} other{Supprimer cet album et les {count} éléments dedans ?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer ces albums et leur élément ?} other{Supprimer ces albums et leurs {count} éléments ?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Supprimer ces albums et l’élément dedans ?} other{Supprimer ces albums et les {count} éléments dedans ?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "Format :",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -641,7 +641,7 @@
|
|||
"@drawerAlbumPage": {},
|
||||
"drawerCountryPage": "Pays",
|
||||
"@drawerCountryPage": {},
|
||||
"drawerTagPage": "Libellés",
|
||||
"drawerTagPage": "Étiquettes",
|
||||
"@drawerTagPage": {},
|
||||
"sortByDate": "par date",
|
||||
"@sortByDate": {},
|
||||
|
@ -713,9 +713,9 @@
|
|||
"@countryPageTitle": {},
|
||||
"countryEmpty": "Aucun pays",
|
||||
"@countryEmpty": {},
|
||||
"tagPageTitle": "Libellés",
|
||||
"tagPageTitle": "Étiquettes",
|
||||
"@tagPageTitle": {},
|
||||
"tagEmpty": "Aucun libellé",
|
||||
"tagEmpty": "Aucune étiquette",
|
||||
"@tagEmpty": {},
|
||||
"binPageTitle": "Corbeille",
|
||||
"@binPageTitle": {},
|
||||
|
@ -731,7 +731,7 @@
|
|||
"@searchCountriesSectionTitle": {},
|
||||
"searchPlacesSectionTitle": "Lieux",
|
||||
"@searchPlacesSectionTitle": {},
|
||||
"searchTagsSectionTitle": "Libellés",
|
||||
"searchTagsSectionTitle": "Étiquettes",
|
||||
"@searchTagsSectionTitle": {},
|
||||
"searchRatingSectionTitle": "Notations",
|
||||
"@searchRatingSectionTitle": {},
|
||||
|
@ -811,7 +811,7 @@
|
|||
"@settingsThumbnailOverlayPageTitle": {},
|
||||
"settingsThumbnailShowFavouriteIcon": "Afficher l’icône de favori",
|
||||
"@settingsThumbnailShowFavouriteIcon": {},
|
||||
"settingsThumbnailShowTagIcon": "Afficher l’icône de libellé",
|
||||
"settingsThumbnailShowTagIcon": "Afficher l’icône d’étiquette",
|
||||
"@settingsThumbnailShowTagIcon": {},
|
||||
"settingsThumbnailShowLocationIcon": "Afficher l’icône de lieu",
|
||||
"@settingsThumbnailShowLocationIcon": {},
|
||||
|
@ -1043,7 +1043,7 @@
|
|||
"@statsTopCountriesSectionTitle": {},
|
||||
"statsTopPlacesSectionTitle": "Top lieux",
|
||||
"@statsTopPlacesSectionTitle": {},
|
||||
"statsTopTagsSectionTitle": "Top libellés",
|
||||
"statsTopTagsSectionTitle": "Top étiquettes",
|
||||
"@statsTopTagsSectionTitle": {},
|
||||
"statsTopAlbumsSectionTitle": "Top albums",
|
||||
"@statsTopAlbumsSectionTitle": {},
|
||||
|
@ -1123,11 +1123,11 @@
|
|||
"@viewerInfoSearchSuggestionRights": {},
|
||||
"wallpaperUseScrollEffect": "Utiliser l’effet de défilement sur l’écran d’accueil",
|
||||
"@wallpaperUseScrollEffect": {},
|
||||
"tagEditorPageTitle": "Modifier les libellés",
|
||||
"tagEditorPageTitle": "Modifier les étiquettes",
|
||||
"@tagEditorPageTitle": {},
|
||||
"tagEditorPageNewTagFieldLabel": "Nouveau libellé",
|
||||
"tagEditorPageNewTagFieldLabel": "Nouvelle étiquette",
|
||||
"@tagEditorPageNewTagFieldLabel": {},
|
||||
"tagEditorPageAddTagTooltip": "Ajouter le libellé",
|
||||
"tagEditorPageAddTagTooltip": "Ajouter l’étiquette",
|
||||
"@tagEditorPageAddTagTooltip": {},
|
||||
"tagEditorSectionRecent": "Ajouts récents",
|
||||
"@tagEditorSectionRecent": {},
|
||||
|
@ -1149,7 +1149,7 @@
|
|||
"@filePickerUseThisFolder": {},
|
||||
"editEntryLocationDialogSetCustom": "Définir un lieu personnalisé",
|
||||
"@editEntryLocationDialogSetCustom": {},
|
||||
"tagEditorSectionPlaceholders": "Libellés de substitution",
|
||||
"tagEditorSectionPlaceholders": "Étiquettes de substitution",
|
||||
"@tagEditorSectionPlaceholders": {},
|
||||
"tagPlaceholderPlace": "Lieu",
|
||||
"@tagPlaceholderPlace": {},
|
||||
|
@ -1179,7 +1179,7 @@
|
|||
"@filterAspectRatioPortraitLabel": {},
|
||||
"filterAspectRatioLandscapeLabel": "Paysage",
|
||||
"@filterAspectRatioLandscapeLabel": {},
|
||||
"settingsViewerShowRatingTags": "Afficher la notation et les libellés",
|
||||
"settingsViewerShowRatingTags": "Afficher la notation et les étiquettes",
|
||||
"@settingsViewerShowRatingTags": {},
|
||||
"entryActionShareImageOnly": "Partager l’image seulement",
|
||||
"@entryActionShareImageOnly": {},
|
||||
|
@ -1196,5 +1196,17 @@
|
|||
}
|
||||
},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "Afficher des alternatives aux interactions multitactiles",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {}
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"settingsViewerShowDescription": "Afficher la description",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"settingsModificationWarningDialogMessage": "D’autres 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": {}
|
||||
}
|
||||
|
|
|
@ -337,7 +337,7 @@
|
|||
"@binEntriesConfirmationDialogMessage": {},
|
||||
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Anda yakin ingin menghapus benda ini?} other{Apakah Anda yakin ingin menghapus {count} benda?}}",
|
||||
"@deleteEntriesConfirmationDialogMessage": {},
|
||||
"moveUndatedConfirmationDialogMessage": "Beberapa benda tidak mempunyai tanggal metadata. Tanggal mereka sekarang akan diatur ulang dengan operasi ini kecuali ada tanggal metadata yang ditetapkan.",
|
||||
"moveUndatedConfirmationDialogMessage": "Simpan tanggal benda sebelum melanjutkan?",
|
||||
"@moveUndatedConfirmationDialogMessage": {},
|
||||
"moveUndatedConfirmationDialogSetDate": "Atur tanggal",
|
||||
"@moveUndatedConfirmationDialogSetDate": {},
|
||||
|
@ -379,9 +379,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Nama",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Apakah Anda yakin ingin menghapus album ini dan bendanya?} other{Apakah Anda yakin ingin menghapus album ini dan {count} bendanya?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Hapus album ini dan item yang ada di dalam?} other{Hapus album ini dan {count} item yang ada di dalam?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Apakah Anda yakin ingin menghapus album ini dan bendanya?} other{Apakah Anda yakin ingin menghapus album ini dan {count} bendanya?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Hapus album ini dan item yang ada di dalam?} other{Hapus album ini dan {count} item yang ada di dalam?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "Format:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -867,7 +867,7 @@
|
|||
"@settingsSlideshowFillScreen": {},
|
||||
"settingsSlideshowTransitionTile": "Transisi",
|
||||
"@settingsSlideshowTransitionTile": {},
|
||||
"settingsSlideshowIntervalTile": "Interval",
|
||||
"settingsSlideshowIntervalTile": "Jarak waktu",
|
||||
"@settingsSlideshowIntervalTile": {},
|
||||
"settingsSlideshowVideoPlaybackTile": "Putaran ulang video",
|
||||
"@settingsSlideshowVideoPlaybackTile": {},
|
||||
|
@ -1157,7 +1157,7 @@
|
|||
"@tagPlaceholderPlace": {},
|
||||
"editEntryLocationDialogSetCustom": "Terapkan lokasi kustom",
|
||||
"@editEntryLocationDialogSetCustom": {},
|
||||
"subtitlePositionTop": "Atas",
|
||||
"subtitlePositionTop": "Teratas",
|
||||
"@subtitlePositionTop": {},
|
||||
"subtitlePositionBottom": "Bawah",
|
||||
"@subtitlePositionBottom": {},
|
||||
|
@ -1196,5 +1196,17 @@
|
|||
}
|
||||
},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "Tampilkan alternatif gestur multisentuh",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {}
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"settingsViewerShowDescription": "Tampilkan deskripsi",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"settingsModificationWarningDialogMessage": "Pengaturan lain akan diubah.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Antarmuka Android TV",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"filterLocatedLabel": "Terletak",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Dilabel",
|
||||
"@filterTaggedLabel": {},
|
||||
"tooManyItemsErrorDialogMessage": "Coba lagi dengan item yang lebih sedikit.",
|
||||
"@tooManyItemsErrorDialogMessage": {}
|
||||
}
|
||||
|
|
|
@ -389,9 +389,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Nome",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questo album e i suoi elementi?} other{Cancellare questo album e i suoi {count} elementi?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questo album e l’elemento in esso?} other{Cancellare questo album e i {count} elementi in esso?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questi album e i loro elementi?} other{Cancellare questi album e i loro {count} elementi?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Cancellare questi album e l’elemento in essi?} other{Cancellare questi album e i {count} elementi in essi?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "Formato:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -1196,5 +1196,17 @@
|
|||
"entryActionShareVideoOnly": "Condividi solo video",
|
||||
"@entryActionShareVideoOnly": {},
|
||||
"filterNoAddressLabel": "Senza indirizzo",
|
||||
"@filterNoAddressLabel": {}
|
||||
"@filterNoAddressLabel": {},
|
||||
"filterLocatedLabel": "Posizionato",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Etichettato",
|
||||
"@filterTaggedLabel": {},
|
||||
"settingsModificationWarningDialogMessage": "Le altre impostazioni saranno modificate.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Interfaccia Android TV",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"settingsViewerShowDescription": "Mostra la descrizione",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"tooManyItemsErrorDialogMessage": "Riprova con meno elementi.",
|
||||
"@tooManyItemsErrorDialogMessage": {}
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
"@filterFavouriteLabel": {},
|
||||
"filterNoDateLabel": "날짜 없음",
|
||||
"@filterNoDateLabel": {},
|
||||
"filterNoLocationLabel": "장소 없음",
|
||||
"filterNoLocationLabel": "위치 없음",
|
||||
"@filterNoLocationLabel": {},
|
||||
"filterNoRatingLabel": "별점 없음",
|
||||
"@filterNoRatingLabel": {},
|
||||
|
@ -1196,5 +1196,17 @@
|
|||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsViewerShowDescription": "설명 표시",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"settingsModificationWarningDialogMessage": "다른 설정도 변경될 것입니다.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "안드로이드 TV 인터페이스 사용하기",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"filterTaggedLabel": "태그 있음",
|
||||
"@filterTaggedLabel": {},
|
||||
"filterLocatedLabel": "위치 있음",
|
||||
"@filterLocatedLabel": {},
|
||||
"tooManyItemsErrorDialogMessage": "항목 수를 줄이고 다시 시도하세요.",
|
||||
"@tooManyItemsErrorDialogMessage": {}
|
||||
}
|
||||
|
|
|
@ -264,7 +264,7 @@
|
|||
"@keepScreenOnViewerOnly": {},
|
||||
"keepScreenOnAlways": "Heile tida",
|
||||
"@keepScreenOnAlways": {},
|
||||
"accessibilityAnimationsRemove": "Hindra rørsle",
|
||||
"accessibilityAnimationsRemove": "Hindra skjermrørsle",
|
||||
"@accessibilityAnimationsRemove": {},
|
||||
"subtitlePositionTop": "På toppen",
|
||||
"@subtitlePositionTop": {},
|
||||
|
@ -352,5 +352,403 @@
|
|||
"description": "the name of a specific directory"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"keepScreenOnVideoPlayback": "Under videoavspeling",
|
||||
"@keepScreenOnVideoPlayback": {},
|
||||
"newAlbumDialogNameLabel": "Albumsnamn",
|
||||
"@newAlbumDialogNameLabel": {},
|
||||
"durationDialogMinutes": "Minutt",
|
||||
"@durationDialogMinutes": {},
|
||||
"settingsThemeColorHighlights": "Farga framhevjingar",
|
||||
"@settingsThemeColorHighlights": {},
|
||||
"viewerInfoBackToViewerTooltip": "Attende til vising",
|
||||
"@viewerInfoBackToViewerTooltip": {},
|
||||
"mapStyleDialogTitle": "Kartstil",
|
||||
"@mapStyleDialogTitle": {},
|
||||
"notEnoughSpaceDialogMessage": "Denne gjerda tarv {neededSize} unytta rom på «{volume}» for å verta fullgjord, men det er berre {freeSize} att.",
|
||||
"@notEnoughSpaceDialogMessage": {
|
||||
"placeholders": {
|
||||
"neededSize": {
|
||||
"type": "String",
|
||||
"example": "314 MB"
|
||||
},
|
||||
"freeSize": {
|
||||
"type": "String",
|
||||
"example": "123 MB"
|
||||
},
|
||||
"volume": {
|
||||
"type": "String",
|
||||
"example": "SD card",
|
||||
"description": "the name of a storage volume"
|
||||
}
|
||||
}
|
||||
},
|
||||
"missingSystemFilePickerDialogMessage": "Systemfilveljaren er borte eller avslegen. Slå han på og røyn om att.",
|
||||
"@missingSystemFilePickerDialogMessage": {},
|
||||
"videoResumeDialogMessage": "Hald fram avspeling ifrå {time}?",
|
||||
"@videoResumeDialogMessage": {
|
||||
"placeholders": {
|
||||
"time": {
|
||||
"type": "String",
|
||||
"example": "13:37"
|
||||
}
|
||||
}
|
||||
},
|
||||
"videoStartOverButtonLabel": "BYRJA OM ATT",
|
||||
"@videoStartOverButtonLabel": {},
|
||||
"hideFilterConfirmationDialogMessage": "Samsvarande bilete og videoar vil verte skjult ifrå samlinga di. Du kan visa dei att ifrå «Personvern»-innstillingane.\n\nEr du sikker på at du vil skjule dei?",
|
||||
"@hideFilterConfirmationDialogMessage": {},
|
||||
"renameEntrySetPageInsertTooltip": "Innskrivingsområde",
|
||||
"@renameEntrySetPageInsertTooltip": {},
|
||||
"renameEntrySetPagePatternFieldLabel": "Namngjevingsmønster",
|
||||
"@renameEntrySetPagePatternFieldLabel": {},
|
||||
"renameEntrySetPagePreviewSectionTitle": "Førehandsvis",
|
||||
"@renameEntrySetPagePreviewSectionTitle": {},
|
||||
"renameEntryDialogLabel": "Nytt namn",
|
||||
"@renameEntryDialogLabel": {},
|
||||
"editEntryDialogCopyFromItem": "Kopier ifrå anna element",
|
||||
"@editEntryDialogCopyFromItem": {},
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Filbrigdedato",
|
||||
"@editEntryDateDialogSourceFileModifiedDate": {},
|
||||
"durationDialogHours": "Timar",
|
||||
"@durationDialogHours": {},
|
||||
"editEntryLocationDialogChooseOnMap": "Vel på kartet",
|
||||
"@editEntryLocationDialogChooseOnMap": {},
|
||||
"settingsLanguageTile": "Mål",
|
||||
"@settingsLanguageTile": {},
|
||||
"settingsUnitSystemTile": "Einingar",
|
||||
"@settingsUnitSystemTile": {},
|
||||
"settingsCoordinateFormatDialogTitle": "Koordinatformat",
|
||||
"@settingsCoordinateFormatDialogTitle": {},
|
||||
"settingsWidgetDisplayedItem": "Vist element",
|
||||
"@settingsWidgetDisplayedItem": {},
|
||||
"statsTopAlbumsSectionTitle": "Topp-album",
|
||||
"@statsTopAlbumsSectionTitle": {},
|
||||
"statsTopTagsSectionTitle": "Toppmerkelappar",
|
||||
"@statsTopTagsSectionTitle": {},
|
||||
"viewerInfoUnknown": "ukjend",
|
||||
"@viewerInfoUnknown": {},
|
||||
"viewerInfoLabelResolution": "Oppløysing",
|
||||
"@viewerInfoLabelResolution": {},
|
||||
"viewerInfoLabelUri": "URI",
|
||||
"@viewerInfoLabelUri": {},
|
||||
"viewerInfoLabelOwner": "Eigar",
|
||||
"@viewerInfoLabelOwner": {},
|
||||
"viewerInfoLabelCoordinates": "Koordinatar",
|
||||
"@viewerInfoLabelCoordinates": {},
|
||||
"tagEditorPageAddTagTooltip": "Legg til merkelapp",
|
||||
"@tagEditorPageAddTagTooltip": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Ikkje vis skjulte filer",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"panoramaEnableSensorControl": "Slå på sensorstyring",
|
||||
"@panoramaEnableSensorControl": {},
|
||||
"panoramaDisableSensorControl": "Slå av sensorstyring",
|
||||
"@panoramaDisableSensorControl": {},
|
||||
"filePickerOpenFrom": "Opne ifrå",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "Ingen element",
|
||||
"@filePickerNoItems": {},
|
||||
"nameConflictDialogSingleSourceMessage": "Somme filer i målmappa har same namn.",
|
||||
"@nameConflictDialogSingleSourceMessage": {},
|
||||
"nameConflictDialogMultipleSourceMessage": "Somme filer har same namn.",
|
||||
"@nameConflictDialogMultipleSourceMessage": {},
|
||||
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Flytt dette elementet til papirkorga?} other{Flytt desse {count} elementa til papirkorga?}}",
|
||||
"@binEntriesConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Slett dette elementet?} other{Slett desse {count} elementa?}}",
|
||||
"@deleteEntriesConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"videoResumeButtonLabel": "HALD FRAM",
|
||||
"@videoResumeButtonLabel": {},
|
||||
"entryActionShareVideoOnly": "Del berre video",
|
||||
"@entryActionShareVideoOnly": {},
|
||||
"entryActionShareImageOnly": "Del berre bilete",
|
||||
"@entryActionShareImageOnly": {},
|
||||
"unsupportedTypeDialogMessage": "{count, plural, other{Denne gjerda er ustødd for element av fylgjande slag: {types}.}}",
|
||||
"@unsupportedTypeDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {},
|
||||
"types": {
|
||||
"type": "String",
|
||||
"example": "GIF, TIFF, MP4",
|
||||
"description": "a list of unsupported types"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addShortcutDialogLabel": "Snarvegsmerkelapp",
|
||||
"@addShortcutDialogLabel": {},
|
||||
"addShortcutButtonLabel": "LEGG TIL",
|
||||
"@addShortcutButtonLabel": {},
|
||||
"noMatchingAppDialogMessage": "Ingen appar kan handsame dette.",
|
||||
"@noMatchingAppDialogMessage": {},
|
||||
"moveUndatedConfirmationDialogMessage": "Gøym elementdatoar før framhald?",
|
||||
"@moveUndatedConfirmationDialogMessage": {},
|
||||
"moveUndatedConfirmationDialogSetDate": "Gøym datoar",
|
||||
"@moveUndatedConfirmationDialogSetDate": {},
|
||||
"setCoverDialogLatest": "Nyaste element",
|
||||
"@setCoverDialogLatest": {},
|
||||
"setCoverDialogAuto": "Auto",
|
||||
"@setCoverDialogAuto": {},
|
||||
"newAlbumDialogTitle": "Nytt Album",
|
||||
"@newAlbumDialogTitle": {},
|
||||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Mappa finst alt",
|
||||
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
||||
"newAlbumDialogStorageLabel": "Gøyme:",
|
||||
"@newAlbumDialogStorageLabel": {},
|
||||
"renameAlbumDialogLabel": "Nytt namn",
|
||||
"@renameAlbumDialogLabel": {},
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "Mappa finst alt",
|
||||
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
||||
"renameEntrySetPageTitle": "Døyp om",
|
||||
"@renameEntrySetPageTitle": {},
|
||||
"exportEntryDialogWidth": "Breidd",
|
||||
"@exportEntryDialogWidth": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Slett desse albuma og deira element?} other{Slett desse albuma og deira {count} element?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"exportEntryDialogHeight": "Høgd",
|
||||
"@exportEntryDialogHeight": {},
|
||||
"editEntryDateDialogExtractFromTitle": "Tak ut ifrå namn",
|
||||
"@editEntryDateDialogExtractFromTitle": {},
|
||||
"editEntryDateDialogCopyField": "Kopier ifrå annan dato",
|
||||
"@editEntryDateDialogCopyField": {},
|
||||
"editEntryDateDialogShift": "Byt",
|
||||
"@editEntryDateDialogShift": {},
|
||||
"durationDialogSeconds": "Sekund",
|
||||
"@durationDialogSeconds": {},
|
||||
"editEntryRatingDialogTitle": "Omdøme",
|
||||
"@editEntryRatingDialogTitle": {},
|
||||
"removeEntryMetadataDialogTitle": "Metadataborttaking",
|
||||
"@removeEntryMetadataDialogTitle": {},
|
||||
"removeEntryMetadataDialogMore": "Meir",
|
||||
"@removeEntryMetadataDialogMore": {},
|
||||
"statsTopPlacesSectionTitle": "Toppstadar",
|
||||
"@statsTopPlacesSectionTitle": {},
|
||||
"settingsCollectionTile": "Samling",
|
||||
"@settingsCollectionTile": {},
|
||||
"statsPageTitle": "Samandrag",
|
||||
"@statsPageTitle": {},
|
||||
"statsTopCountriesSectionTitle": "Toppland",
|
||||
"@statsTopCountriesSectionTitle": {},
|
||||
"viewerOpenPanoramaButtonLabel": "OPNE PANORAMA",
|
||||
"@viewerOpenPanoramaButtonLabel": {},
|
||||
"viewerInfoLabelSize": "Storleik",
|
||||
"@viewerInfoLabelSize": {},
|
||||
"viewerInfoLabelDate": "Dato",
|
||||
"@viewerInfoLabelDate": {},
|
||||
"viewerInfoLabelDuration": "Lengd",
|
||||
"@viewerInfoLabelDuration": {},
|
||||
"viewerInfoLabelPath": "Sti",
|
||||
"@viewerInfoLabelPath": {},
|
||||
"mapZoomOutTooltip": "Mink",
|
||||
"@mapZoomOutTooltip": {},
|
||||
"mapZoomInTooltip": "Auk",
|
||||
"@mapZoomInTooltip": {},
|
||||
"openMapPageTooltip": "Vis på kartsida",
|
||||
"@openMapPageTooltip": {},
|
||||
"viewerInfoViewXmlLinkText": "Vis XML",
|
||||
"@viewerInfoViewXmlLinkText": {},
|
||||
"viewerInfoSearchFieldLabel": "Søk metadata",
|
||||
"@viewerInfoSearchFieldLabel": {},
|
||||
"viewerInfoSearchEmpty": "Ingen samsvarande lyklar",
|
||||
"@viewerInfoSearchEmpty": {},
|
||||
"viewerInfoSearchSuggestionDate": "Dato og tid",
|
||||
"@viewerInfoSearchSuggestionDate": {},
|
||||
"tagEditorPageTitle": "Brigd merkelappar",
|
||||
"@tagEditorPageTitle": {},
|
||||
"tagEditorPageNewTagFieldLabel": "Ny merkelapp",
|
||||
"@tagEditorPageNewTagFieldLabel": {},
|
||||
"filePickerShowHiddenFiles": "Vis skjulte filer",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"sourceViewerPageTitle": "Kjelde",
|
||||
"@sourceViewerPageTitle": {},
|
||||
"renameProcessorCounter": "Teljar",
|
||||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Namn",
|
||||
"@renameProcessorName": {},
|
||||
"editEntryDateDialogTitle": "Dato og tid",
|
||||
"@editEntryDateDialogTitle": {},
|
||||
"editEntryLocationDialogLatitude": "Breiddegrad",
|
||||
"@editEntryLocationDialogLatitude": {},
|
||||
"editEntryLocationDialogLongitude": "Lengdegrad",
|
||||
"@editEntryLocationDialogLongitude": {},
|
||||
"sourceStateLoading": "Hentar inn",
|
||||
"@sourceStateLoading": {},
|
||||
"filePickerUseThisFolder": "Bruk denne mappa",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"viewerErrorDoesNotExist": "Fila finst ikkje meir.",
|
||||
"@viewerErrorDoesNotExist": {},
|
||||
"filterBinLabel": "Papirkorg",
|
||||
"@filterBinLabel": {},
|
||||
"filterTypeAnimatedLabel": "Animert",
|
||||
"@filterTypeAnimatedLabel": {},
|
||||
"filterTypeMotionPhotoLabel": "Rørslebilete",
|
||||
"@filterTypeMotionPhotoLabel": {},
|
||||
"filterTypePanoramaLabel": "Panorama",
|
||||
"@filterTypePanoramaLabel": {},
|
||||
"mapStyleOsmHot": "Humanitært OSM",
|
||||
"@mapStyleOsmHot": {},
|
||||
"mapStyleStamenToner": "Stamen Toner (svart-kvitt)",
|
||||
"@mapStyleStamenToner": {},
|
||||
"themeBrightnessLight": "Ljos",
|
||||
"@themeBrightnessLight": {},
|
||||
"themeBrightnessDark": "Mørk",
|
||||
"@themeBrightnessDark": {},
|
||||
"themeBrightnessBlack": "Svart",
|
||||
"@themeBrightnessBlack": {},
|
||||
"viewerTransitionSlide": "Skridande",
|
||||
"@viewerTransitionSlide": {},
|
||||
"viewerTransitionParallax": "Parallakse",
|
||||
"@viewerTransitionParallax": {},
|
||||
"viewerTransitionFade": "Ton ut",
|
||||
"@viewerTransitionFade": {},
|
||||
"widgetDisplayedItemRandom": "Tilfeldig",
|
||||
"@widgetDisplayedItemRandom": {},
|
||||
"widgetOpenPageHome": "Opne heimside",
|
||||
"@widgetOpenPageHome": {},
|
||||
"restrictedAccessDialogMessage": "Denne appen har ikkje lov til å brigde filer i «{directory}»-mappa i «{volume}».\n\nBruk ein førehandsinnlagd filhandsamar eller galleriapp til å flytta elementa til ei anna mappe.",
|
||||
"@restrictedAccessDialogMessage": {
|
||||
"placeholders": {
|
||||
"directory": {
|
||||
"type": "String",
|
||||
"description": "the name of a directory, using the output of `rootDirectoryDescription` or `otherDirectoryDescription`"
|
||||
},
|
||||
"volume": {
|
||||
"type": "String",
|
||||
"example": "SD card",
|
||||
"description": "the name of a storage volume"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Slett dette albumet og elementet i det?} other{Slett dette albumet og dei {count} elementa i det?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
},
|
||||
"exportEntryDialogFormat": "Format:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
"tagPlaceholderCountry": "Land",
|
||||
"@tagPlaceholderCountry": {},
|
||||
"tagEditorSectionRecent": "Nyleg",
|
||||
"@tagEditorSectionRecent": {},
|
||||
"tagPlaceholderPlace": "Stad",
|
||||
"@tagPlaceholderPlace": {},
|
||||
"viewerInfoSearchSuggestionRights": "Rettar",
|
||||
"@viewerInfoSearchSuggestionRights": {},
|
||||
"viewerInfoSearchSuggestionResolution": "Oppløysing",
|
||||
"@viewerInfoSearchSuggestionResolution": {},
|
||||
"viewerInfoOpenLinkText": "Opne",
|
||||
"@viewerInfoOpenLinkText": {},
|
||||
"mapStyleTooltip": "Vel kartstil",
|
||||
"@mapStyleTooltip": {},
|
||||
"viewerInfoLabelAddress": "Adresse",
|
||||
"@viewerInfoLabelAddress": {},
|
||||
"viewerInfoLabelTitle": "Namn",
|
||||
"@viewerInfoLabelTitle": {},
|
||||
"viewerErrorUnknown": "Oida.",
|
||||
"@viewerErrorUnknown": {},
|
||||
"viewerSetWallpaperButtonLabel": "SET SOM BAKGRUNNSBILETE",
|
||||
"@viewerSetWallpaperButtonLabel": {},
|
||||
"settingsWidgetShowOutline": "Omrit",
|
||||
"@settingsWidgetShowOutline": {},
|
||||
"settingsWidgetPageTitle": "Bileteramme",
|
||||
"@settingsWidgetPageTitle": {},
|
||||
"settingsScreenSaverPageTitle": "Skjermsparar",
|
||||
"@settingsScreenSaverPageTitle": {},
|
||||
"settingsUnitSystemDialogTitle": "Einingar",
|
||||
"@settingsUnitSystemDialogTitle": {},
|
||||
"settingsCoordinateFormatTile": "Koordinatformat",
|
||||
"@settingsCoordinateFormatTile": {},
|
||||
"settingsLanguagePageTitle": "Mål",
|
||||
"@settingsLanguagePageTitle": {},
|
||||
"settingsLanguageSectionTitle": "Mål og format",
|
||||
"@settingsLanguageSectionTitle": {},
|
||||
"settingsDisplaySectionTitle": "Vising",
|
||||
"@settingsDisplaySectionTitle": {},
|
||||
"videoStreamSelectionDialogTrack": "Spor",
|
||||
"@videoStreamSelectionDialogTrack": {},
|
||||
"genericDangerWarningDialogMessage": "Er du viss?",
|
||||
"@genericDangerWarningDialogMessage": {},
|
||||
"menuActionStats": "Samandrag",
|
||||
"@menuActionStats": {},
|
||||
"viewDialogGroupSectionTitle": "Hop",
|
||||
"@viewDialogGroupSectionTitle": {},
|
||||
"viewDialogLayoutSectionTitle": "Oppsett",
|
||||
"@viewDialogLayoutSectionTitle": {},
|
||||
"tileLayoutMosaic": "Mosaikk",
|
||||
"@tileLayoutMosaic": {},
|
||||
"aboutBugCopyInfoButton": "Kopier",
|
||||
"@aboutBugCopyInfoButton": {},
|
||||
"aboutCreditsWorldAtlas1": "Denne appen nyttar ei TopoJSON-fil ifrå",
|
||||
"@aboutCreditsWorldAtlas1": {},
|
||||
"viewerInfoSearchSuggestionDescription": "Utgreiing",
|
||||
"@viewerInfoSearchSuggestionDescription": {},
|
||||
"aboutCreditsWorldAtlas2": "under ISC-løyve.",
|
||||
"@aboutCreditsWorldAtlas2": {},
|
||||
"videoSpeedDialogLabel": "Avspelingssnøggleik",
|
||||
"@videoSpeedDialogLabel": {},
|
||||
"videoStreamSelectionDialogVideo": "Video",
|
||||
"@videoStreamSelectionDialogVideo": {},
|
||||
"videoStreamSelectionDialogAudio": "Ljod",
|
||||
"@videoStreamSelectionDialogAudio": {},
|
||||
"videoStreamSelectionDialogText": "Undertekster",
|
||||
"@videoStreamSelectionDialogText": {},
|
||||
"videoStreamSelectionDialogOff": "Av",
|
||||
"@videoStreamSelectionDialogOff": {},
|
||||
"videoStreamSelectionDialogNoSelection": "Det er ingen andre spor.",
|
||||
"@videoStreamSelectionDialogNoSelection": {},
|
||||
"genericSuccessFeedback": "Fullgjort",
|
||||
"@genericSuccessFeedback": {},
|
||||
"genericFailureFeedback": "Mislykka",
|
||||
"@genericFailureFeedback": {},
|
||||
"menuActionSelectAll": "Vel alle",
|
||||
"@menuActionSelectAll": {},
|
||||
"menuActionSelectNone": "Tak bort val",
|
||||
"@menuActionSelectNone": {},
|
||||
"menuActionMap": "Kart",
|
||||
"@menuActionMap": {},
|
||||
"menuActionSlideshow": "Ljosbiletevising",
|
||||
"@menuActionSlideshow": {},
|
||||
"menuActionConfigureView": "Vis",
|
||||
"@menuActionConfigureView": {},
|
||||
"menuActionSelect": "Vel",
|
||||
"@menuActionSelect": {},
|
||||
"aboutBugCopyInfoInstruction": "Kopier systemopplysingar",
|
||||
"@aboutBugCopyInfoInstruction": {},
|
||||
"tagEditorSectionPlaceholders": "Førebels",
|
||||
"@tagEditorSectionPlaceholders": {},
|
||||
"tileLayoutGrid": "Rutenett",
|
||||
"@tileLayoutGrid": {},
|
||||
"tileLayoutList": "Liste",
|
||||
"@tileLayoutList": {},
|
||||
"coverDialogTabCover": "Omslag",
|
||||
"@coverDialogTabCover": {},
|
||||
"coverDialogTabApp": "App",
|
||||
"@coverDialogTabApp": {},
|
||||
"coverDialogTabColor": "Let",
|
||||
"@coverDialogTabColor": {},
|
||||
"appPickDialogTitle": "Vel app",
|
||||
"@appPickDialogTitle": {},
|
||||
"aboutPageTitle": "Om",
|
||||
"@aboutPageTitle": {},
|
||||
"aboutLinkLicense": "Løyve",
|
||||
"@aboutLinkLicense": {},
|
||||
"appPickDialogNone": "Ingen",
|
||||
"@appPickDialogNone": {},
|
||||
"aboutBugSectionTitle": "Mistakrapport",
|
||||
"@aboutBugSectionTitle": {},
|
||||
"aboutTranslatorsSectionTitle": "Omsetjarar",
|
||||
"@aboutTranslatorsSectionTitle": {},
|
||||
"viewerInfoOpenEmbeddedFailureFeedback": "Kunne ikkje ta ut innbygde opplysingar",
|
||||
"@viewerInfoOpenEmbeddedFailureFeedback": {}
|
||||
}
|
||||
|
|
1184
lib/l10n/app_pl.arb
|
@ -1156,5 +1156,49 @@
|
|||
"tagPlaceholderPlace": "Lugar",
|
||||
"@tagPlaceholderPlace": {},
|
||||
"editEntryLocationDialogSetCustom": "Definir local personalizado",
|
||||
"@editEntryLocationDialogSetCustom": {}
|
||||
"@editEntryLocationDialogSetCustom": {},
|
||||
"subtitlePositionBottom": "Fundo",
|
||||
"@subtitlePositionBottom": {},
|
||||
"subtitlePositionTop": "Topo",
|
||||
"@subtitlePositionTop": {},
|
||||
"widgetDisplayedItemRandom": "Aleatório",
|
||||
"@widgetDisplayedItemRandom": {},
|
||||
"settingsSubtitleThemeTextPositionTile": "Posição do texto",
|
||||
"@settingsSubtitleThemeTextPositionTile": {},
|
||||
"settingsSubtitleThemeTextPositionDialogTitle": "Posição do Texto",
|
||||
"@settingsSubtitleThemeTextPositionDialogTitle": {},
|
||||
"settingsWidgetDisplayedItem": "Item exibido",
|
||||
"@settingsWidgetDisplayedItem": {},
|
||||
"entryInfoActionRemoveLocation": "Remover localização",
|
||||
"@entryInfoActionRemoveLocation": {},
|
||||
"filterNoAddressLabel": "Sem endereço",
|
||||
"@filterNoAddressLabel": {},
|
||||
"keepScreenOnVideoPlayback": "Durante a reprodução do video",
|
||||
"@keepScreenOnVideoPlayback": {},
|
||||
"settingsViewerShowDescription": "Mostrar descrição",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"entryActionShareImageOnly": "Compartilhar apenas imagem",
|
||||
"@entryActionShareImageOnly": {},
|
||||
"entryActionShareVideoOnly": "Compartilhar apenas video",
|
||||
"@entryActionShareVideoOnly": {},
|
||||
"filterAspectRatioPortraitLabel": "Retrato",
|
||||
"@filterAspectRatioPortraitLabel": {},
|
||||
"filterAspectRatioLandscapeLabel": "Paisagem",
|
||||
"@filterAspectRatioLandscapeLabel": {},
|
||||
"entryInfoActionExportMetadata": "Exportar metadados",
|
||||
"@entryInfoActionExportMetadata": {},
|
||||
"widgetDisplayedItemMostRecent": "Mais recente",
|
||||
"@widgetDisplayedItemMostRecent": {},
|
||||
"filterTaggedLabel": "Marcado",
|
||||
"@filterTaggedLabel": {},
|
||||
"filterLocatedLabel": "Localizado",
|
||||
"@filterLocatedLabel": {},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "Mostrar alternativas de gesto multitoque",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"settingsModificationWarningDialogMessage": "Outras configurações serão modificadas.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Interface de TV Android",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"settingsViewerShowRatingTags": "Mostrar avaliações e tags",
|
||||
"@settingsViewerShowRatingTags": {}
|
||||
}
|
||||
|
|
|
@ -1354,5 +1354,15 @@
|
|||
"filterNoAddressLabel": "Nicio adresă",
|
||||
"@filterNoAddressLabel": {},
|
||||
"entryInfoActionRemoveLocation": "Eliminare locație",
|
||||
"@entryInfoActionRemoveLocation": {}
|
||||
"@entryInfoActionRemoveLocation": {},
|
||||
"settingsViewerShowDescription": "Afișare descriere",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"filterLocatedLabel": "Locație",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Etichetat",
|
||||
"@filterTaggedLabel": {},
|
||||
"settingsModificationWarningDialogMessage": "Alte setări vor fi modificate.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Interfață Android TV",
|
||||
"@settingsDisplayUseTvInterface": {}
|
||||
}
|
||||
|
|
|
@ -1196,5 +1196,7 @@
|
|||
"entryActionShareImageOnly": "Поделиться только изображением",
|
||||
"@entryActionShareImageOnly": {},
|
||||
"entryActionShareVideoOnly": "Поделиться только видео",
|
||||
"@entryActionShareVideoOnly": {}
|
||||
"@entryActionShareVideoOnly": {},
|
||||
"settingsViewerShowDescription": "Показать описание",
|
||||
"@settingsViewerShowDescription": {}
|
||||
}
|
||||
|
|
|
@ -345,9 +345,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Ad",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albüm ve öğesi silinsin mi?} other{Bu albüm ve {count} öğesi silinsin mi?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albüm ve içindeki öge silinsin mi?} other{Bu albüm ve içindeki {count} öge silinsin mi?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albümler ve öğeleri silinsin mi?} other{Bu albümler ve {count} öğesi silinsin mi?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Bu albümler ve içindeki öge silinsin mi?} other{Bu albümler ve içindeki {count} ögesi silinsin mi?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "Biçim:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -1196,5 +1196,17 @@
|
|||
}
|
||||
},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "Çoklu dokunma hareketi alternatiflerini göster",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {}
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"settingsViewerShowDescription": "Açıklamayı göster",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"settingsModificationWarningDialogMessage": "Diğer ayarlar değiştirilecektir.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Android TV arayüzü",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"filterLocatedLabel": "Konumlu",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Etiketli",
|
||||
"@filterTaggedLabel": {},
|
||||
"tooManyItemsErrorDialogMessage": "Daha az ögeyle tekrar deneyin.",
|
||||
"@tooManyItemsErrorDialogMessage": {}
|
||||
}
|
||||
|
|
|
@ -203,7 +203,7 @@
|
|||
"@coordinateDmsWest": {},
|
||||
"unitSystemMetric": "Метричні",
|
||||
"@unitSystemMetric": {},
|
||||
"unitSystemImperial": "Імперські",
|
||||
"unitSystemImperial": "Англійські",
|
||||
"@unitSystemImperial": {},
|
||||
"videoLoopModeNever": "Ніколи",
|
||||
"@videoLoopModeNever": {},
|
||||
|
@ -267,9 +267,9 @@
|
|||
"@wallpaperTargetLock": {},
|
||||
"viewerTransitionNone": "Нічого",
|
||||
"@viewerTransitionNone": {},
|
||||
"widgetDisplayedItemRandom": "Випадкові",
|
||||
"widgetDisplayedItemRandom": "Випадковий",
|
||||
"@widgetDisplayedItemRandom": {},
|
||||
"widgetDisplayedItemMostRecent": "Нещодавні",
|
||||
"widgetDisplayedItemMostRecent": "Нещодавний",
|
||||
"@widgetDisplayedItemMostRecent": {},
|
||||
"widgetOpenPageHome": "Відкрити головну сторінку",
|
||||
"@widgetOpenPageHome": {},
|
||||
|
@ -459,7 +459,7 @@
|
|||
"@coverDialogTabApp": {},
|
||||
"coverDialogTabColor": "Колір",
|
||||
"@coverDialogTabColor": {},
|
||||
"appPickDialogTitle": "Вибрати Додаток",
|
||||
"appPickDialogTitle": "Вибрати додаток",
|
||||
"@appPickDialogTitle": {},
|
||||
"appPickDialogNone": "Нічого",
|
||||
"@appPickDialogNone": {},
|
||||
|
@ -657,7 +657,7 @@
|
|||
"minutes": {}
|
||||
}
|
||||
},
|
||||
"doubleBackExitMessage": "Натисніть «назад» ще раз, щоб вийти.",
|
||||
"doubleBackExitMessage": "Натисніть “назад” ще раз, щоб вийти.",
|
||||
"@doubleBackExitMessage": {},
|
||||
"actionRemove": "Видалити",
|
||||
"@actionRemove": {},
|
||||
|
@ -729,9 +729,9 @@
|
|||
"@widgetOpenPageCollection": {},
|
||||
"accessibilityAnimationsKeep": "Зберегти екранні ефекти",
|
||||
"@accessibilityAnimationsKeep": {},
|
||||
"displayRefreshRatePreferHighest": "Найвищий рейтинг",
|
||||
"displayRefreshRatePreferHighest": "Найвища частота",
|
||||
"@displayRefreshRatePreferHighest": {},
|
||||
"displayRefreshRatePreferLowest": "Найнижчий рейтинг",
|
||||
"displayRefreshRatePreferLowest": "Найнижча частота",
|
||||
"@displayRefreshRatePreferLowest": {},
|
||||
"viewerTransitionSlide": "Ковзання",
|
||||
"@viewerTransitionSlide": {},
|
||||
|
@ -781,9 +781,9 @@
|
|||
},
|
||||
"videoStartOverButtonLabel": "ВІДТВОРИТИ СПОЧАТКУ",
|
||||
"@videoStartOverButtonLabel": {},
|
||||
"newAlbumDialogTitle": "Новий Альбом",
|
||||
"newAlbumDialogTitle": "Новий альбом",
|
||||
"@newAlbumDialogTitle": {},
|
||||
"newAlbumDialogNameLabel": "Назва Альбому",
|
||||
"newAlbumDialogNameLabel": "Назва альбому",
|
||||
"@newAlbumDialogNameLabel": {},
|
||||
"hideFilterConfirmationDialogMessage": "Відповідні фотографії та відео будуть приховані з вашої колекції. Ви можете показати їх знову в налаштуваннях у розділі \"Конфіденційність\".\n\nВи впевнені, що хочете їх приховати?",
|
||||
"@hideFilterConfirmationDialogMessage": {},
|
||||
|
@ -795,7 +795,7 @@
|
|||
"count": {}
|
||||
}
|
||||
},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити цей альбом і його елемент?} few{Видалити цей альбом і {count} елементи?} other{Видалити цей альбом і {count} елементів?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити цей альбом і елемент у ньому?} few{Видалити цей альбом і {count} елементи у ньому?} other{Видалити цей альбом і {count} елементів у ньому?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -803,7 +803,7 @@
|
|||
},
|
||||
"nameConflictDialogSingleSourceMessage": "Деякі файли в папці призначення мають одну й ту саму назву.",
|
||||
"@nameConflictDialogSingleSourceMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити ці альбоми та їх елементи?} few{Видалити ці альбоми та їх {count} елементи?} other{Видалити ці альбоми та їх {count} елементів?}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Видалити ці альбоми та елемент в них?} few{Видалити ці альбоми та {count} елементи в них?} other{Видалити ці альбоми та {count} елементів в них?}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -923,11 +923,11 @@
|
|||
"@settingsShowBottomNavigationBar": {},
|
||||
"settingsConfirmationTile": "Діалоги підтвердження",
|
||||
"@settingsConfirmationTile": {},
|
||||
"settingsConfirmationDialogTitle": "Діалоги Підтвердження",
|
||||
"settingsConfirmationDialogTitle": "Діалоги підтвердження",
|
||||
"@settingsConfirmationDialogTitle": {},
|
||||
"settingsConfirmationBeforeDeleteItems": "Запитати, перш ніж видаляти предмети назавжди",
|
||||
"settingsConfirmationBeforeDeleteItems": "Запитати, перш ніж видаляти елементи назавжди",
|
||||
"@settingsConfirmationBeforeDeleteItems": {},
|
||||
"settingsConfirmationBeforeMoveToBinItems": "Запитати перед тим, як переносити предмети до кошика",
|
||||
"settingsConfirmationBeforeMoveToBinItems": "Запитати перед тим, як переносити елементи до кошика",
|
||||
"@settingsConfirmationBeforeMoveToBinItems": {},
|
||||
"settingsNavigationDrawerTabPages": "Сторінки",
|
||||
"@settingsNavigationDrawerTabPages": {},
|
||||
|
@ -945,7 +945,7 @@
|
|||
"@settingsThumbnailShowRawIcon": {},
|
||||
"settingsThumbnailShowVideoDuration": "Показати тривалість відео",
|
||||
"@settingsThumbnailShowVideoDuration": {},
|
||||
"settingsCollectionQuickActionEditorPageTitle": "Швидкі Дії",
|
||||
"settingsCollectionQuickActionEditorPageTitle": "Швидкі дії",
|
||||
"@settingsCollectionQuickActionEditorPageTitle": {},
|
||||
"settingsCollectionQuickActionTabBrowsing": "Перегляд",
|
||||
"@settingsCollectionQuickActionTabBrowsing": {},
|
||||
|
@ -965,7 +965,7 @@
|
|||
"@settingsImageBackground": {},
|
||||
"settingsViewerQuickActionsTile": "Швидкі дії",
|
||||
"@settingsViewerQuickActionsTile": {},
|
||||
"settingsViewerQuickActionEditorPageTitle": "Швидкі Дії",
|
||||
"settingsViewerQuickActionEditorPageTitle": "Швидкі дії",
|
||||
"@settingsViewerQuickActionEditorPageTitle": {},
|
||||
"settingsViewerQuickActionEditorBanner": "Торкніться і утримуйте для переміщення кнопок і вибору дій, які відображатимуться у переглядачі.",
|
||||
"@settingsViewerQuickActionEditorBanner": {},
|
||||
|
@ -1005,7 +1005,7 @@
|
|||
"@settingsSlideshowIntervalTile": {},
|
||||
"settingsSlideshowVideoPlaybackTile": "Відтворення відео",
|
||||
"@settingsSlideshowVideoPlaybackTile": {},
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Відтворення Відео",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Відтворення відео",
|
||||
"@settingsSlideshowVideoPlaybackDialogTitle": {},
|
||||
"settingsVideoPageTitle": "Налаштування Відео",
|
||||
"@settingsVideoPageTitle": {},
|
||||
|
@ -1015,13 +1015,13 @@
|
|||
"@settingsVideoEnableHardwareAcceleration": {},
|
||||
"settingsVideoAutoPlay": "Автоматичне відтворення",
|
||||
"@settingsVideoAutoPlay": {},
|
||||
"settingsVideoLoopModeDialogTitle": "Циклічний Режим",
|
||||
"settingsVideoLoopModeDialogTitle": "Циклічний режим",
|
||||
"@settingsVideoLoopModeDialogTitle": {},
|
||||
"settingsSubtitleThemeTile": "Субтитри",
|
||||
"@settingsSubtitleThemeTile": {},
|
||||
"settingsSubtitleThemePageTitle": "Субтитри",
|
||||
"@settingsSubtitleThemePageTitle": {},
|
||||
"settingsSubtitleThemeTextAlignmentDialogTitle": "Вирівнювання Тексту",
|
||||
"settingsSubtitleThemeTextAlignmentDialogTitle": "Вирівнювання тексту",
|
||||
"@settingsSubtitleThemeTextAlignmentDialogTitle": {},
|
||||
"settingsSubtitleThemeTextPositionTile": "Положення тексту",
|
||||
"@settingsSubtitleThemeTextPositionTile": {},
|
||||
|
@ -1047,7 +1047,7 @@
|
|||
"@settingsVideoGestureDoubleTapTogglePlay": {},
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Подвійне натискання на краї екрану для переходу назад/вперед",
|
||||
"@settingsVideoGestureSideDoubleTapSeek": {},
|
||||
"settingsAllowErrorReporting": "Дозволити анонімну відправку повідомлення про помилки",
|
||||
"settingsAllowErrorReporting": "Дозволити анонімну відправку повідомлень про помилки",
|
||||
"@settingsAllowErrorReporting": {},
|
||||
"settingsSaveSearchHistory": "Зберігати історію пошуку",
|
||||
"@settingsSaveSearchHistory": {},
|
||||
|
@ -1057,21 +1057,21 @@
|
|||
"@settingsAllowMediaManagement": {},
|
||||
"settingsHiddenItemsTile": "Приховані елементи",
|
||||
"@settingsHiddenItemsTile": {},
|
||||
"settingsHiddenItemsPageTitle": "Приховані Елементи",
|
||||
"settingsHiddenItemsPageTitle": "Приховані елементи",
|
||||
"@settingsHiddenItemsPageTitle": {},
|
||||
"settingsHiddenItemsTabFilters": "Приховані Фільтри",
|
||||
"settingsHiddenItemsTabFilters": "Приховані фільтри",
|
||||
"@settingsHiddenItemsTabFilters": {},
|
||||
"settingsHiddenFiltersBanner": "Фотографії та відео, що відповідають прихованим фільтрам, не з'являться у вашій колекції.",
|
||||
"@settingsHiddenFiltersBanner": {},
|
||||
"settingsHiddenFiltersEmpty": "Немає прихованих фільтрів",
|
||||
"@settingsHiddenFiltersEmpty": {},
|
||||
"settingsHiddenItemsTabPaths": "Приховані Шляхи",
|
||||
"settingsHiddenItemsTabPaths": "Приховані шляхи",
|
||||
"@settingsHiddenItemsTabPaths": {},
|
||||
"addPathTooltip": "Додати шлях",
|
||||
"@addPathTooltip": {},
|
||||
"settingsStorageAccessTile": "Доступ до сховища",
|
||||
"@settingsStorageAccessTile": {},
|
||||
"settingsStorageAccessPageTitle": "Доступ до Сховища",
|
||||
"settingsStorageAccessPageTitle": "Доступ до сховища",
|
||||
"@settingsStorageAccessPageTitle": {},
|
||||
"settingsStorageAccessBanner": "Деякі каталоги вимагають явного надання доступу для зміни файлів в них. Ви можете переглянути тут каталоги, до яких ви раніше надавали доступ.",
|
||||
"@settingsStorageAccessBanner": {},
|
||||
|
@ -1083,7 +1083,7 @@
|
|||
"@settingsAccessibilitySectionTitle": {},
|
||||
"settingsRemoveAnimationsTile": "Видалити анімації",
|
||||
"@settingsRemoveAnimationsTile": {},
|
||||
"settingsRemoveAnimationsDialogTitle": "Видалити Анімації",
|
||||
"settingsRemoveAnimationsDialogTitle": "Видалити анімації",
|
||||
"@settingsRemoveAnimationsDialogTitle": {},
|
||||
"settingsTimeToTakeActionTile": "Час на виконання",
|
||||
"@settingsTimeToTakeActionTile": {},
|
||||
|
@ -1097,7 +1097,7 @@
|
|||
"@settingsThemeEnableDynamicColor": {},
|
||||
"settingsDisplayRefreshRateModeTile": "Частота оновлення дисплея",
|
||||
"@settingsDisplayRefreshRateModeTile": {},
|
||||
"settingsLanguageSectionTitle": "Мова та Формати",
|
||||
"settingsLanguageSectionTitle": "Мова та формати",
|
||||
"@settingsLanguageSectionTitle": {},
|
||||
"settingsLanguageTile": "Мова",
|
||||
"@settingsLanguageTile": {},
|
||||
|
@ -1105,7 +1105,7 @@
|
|||
"@settingsCoordinateFormatTile": {},
|
||||
"settingsUnitSystemTile": "Одиниці виміру",
|
||||
"@settingsUnitSystemTile": {},
|
||||
"settingsUnitSystemDialogTitle": "Одиниці Виміру",
|
||||
"settingsUnitSystemDialogTitle": "Одиниці виміру",
|
||||
"@settingsUnitSystemDialogTitle": {},
|
||||
"settingsWidgetPageTitle": "Фоторамка",
|
||||
"@settingsWidgetPageTitle": {},
|
||||
|
@ -1159,7 +1159,7 @@
|
|||
"@viewerInfoLabelCoordinates": {},
|
||||
"viewerInfoLabelAddress": "Адреса",
|
||||
"@viewerInfoLabelAddress": {},
|
||||
"mapStyleDialogTitle": "Стиль Карти",
|
||||
"mapStyleDialogTitle": "Стиль карти",
|
||||
"@mapStyleDialogTitle": {},
|
||||
"mapStyleTooltip": "Виберіть стиль карти",
|
||||
"@mapStyleTooltip": {},
|
||||
|
@ -1197,7 +1197,7 @@
|
|||
"@viewerInfoSearchSuggestionRights": {},
|
||||
"wallpaperUseScrollEffect": "Використовувати ефект прокрутки на головному екрані",
|
||||
"@wallpaperUseScrollEffect": {},
|
||||
"tagEditorPageTitle": "Редагування Тегів",
|
||||
"tagEditorPageTitle": "Редагування тегів",
|
||||
"@tagEditorPageTitle": {},
|
||||
"tagEditorPageNewTagFieldLabel": "Новий тег",
|
||||
"@tagEditorPageNewTagFieldLabel": {},
|
||||
|
@ -1245,7 +1245,7 @@
|
|||
"@settingsActionImportDialogTitle": {},
|
||||
"appExportFavourites": "Обране",
|
||||
"@appExportFavourites": {},
|
||||
"settingsKeepScreenOnDialogTitle": "Тримати Екран Увімкненим",
|
||||
"settingsKeepScreenOnDialogTitle": "Тримати екран увімкненим",
|
||||
"@settingsKeepScreenOnDialogTitle": {},
|
||||
"settingsActionExportDialogTitle": "Експорт",
|
||||
"@settingsActionExportDialogTitle": {},
|
||||
|
@ -1279,15 +1279,15 @@
|
|||
"@settingsNavigationDrawerTabTypes": {},
|
||||
"settingsThumbnailOverlayPageTitle": "Накладення",
|
||||
"@settingsThumbnailOverlayPageTitle": {},
|
||||
"settingsNavigationDrawerEditorPageTitle": "Навігаційне Меню",
|
||||
"settingsNavigationDrawerEditorPageTitle": "Навігаційне меню",
|
||||
"@settingsNavigationDrawerEditorPageTitle": {},
|
||||
"settingsCollectionSelectionQuickActionEditorBanner": "Торкніться і утримуйте для переміщення кнопок і вибору дій, які будуть відображатися при виборі елементів.",
|
||||
"@settingsCollectionSelectionQuickActionEditorBanner": {},
|
||||
"settingsThumbnailSectionTitle": "Мініатюри",
|
||||
"@settingsThumbnailSectionTitle": {},
|
||||
"settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Відображувані Кнопки",
|
||||
"settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Відображувані кнопки",
|
||||
"@settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": {},
|
||||
"settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Доступні Кнопки",
|
||||
"settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Доступні кнопки",
|
||||
"@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {},
|
||||
"settingsSubtitleThemeTextAlignmentTile": "Вирівнювання тексту",
|
||||
"@settingsSubtitleThemeTextAlignmentTile": {},
|
||||
|
@ -1307,7 +1307,7 @@
|
|||
"@settingsVideoSectionTitle": {},
|
||||
"settingsAllowInstalledAppAccessSubtitle": "Використовується для покращення відображення альбомів",
|
||||
"@settingsAllowInstalledAppAccessSubtitle": {},
|
||||
"settingsSubtitleThemeTextPositionDialogTitle": "Положення Тексту",
|
||||
"settingsSubtitleThemeTextPositionDialogTitle": "Положення тексту",
|
||||
"@settingsSubtitleThemeTextPositionDialogTitle": {},
|
||||
"settingsThumbnailShowRating": "Показати рейтинг",
|
||||
"@settingsThumbnailShowRating": {},
|
||||
|
@ -1337,9 +1337,9 @@
|
|||
"count": {}
|
||||
}
|
||||
},
|
||||
"settingsDisplayRefreshRateModeDialogTitle": "Частота Оновлення",
|
||||
"settingsDisplayRefreshRateModeDialogTitle": "Частота оновлення",
|
||||
"@settingsDisplayRefreshRateModeDialogTitle": {},
|
||||
"settingsCoordinateFormatDialogTitle": "Формат Координат",
|
||||
"settingsCoordinateFormatDialogTitle": "Формат координат",
|
||||
"@settingsCoordinateFormatDialogTitle": {},
|
||||
"settingsScreenSaverPageTitle": "Заставка на Екран",
|
||||
"@settingsScreenSaverPageTitle": {},
|
||||
|
@ -1354,5 +1354,15 @@
|
|||
"placeholders": {
|
||||
"count": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settingsViewerShowDescription": "Показати опис",
|
||||
"@settingsViewerShowDescription": {},
|
||||
"settingsModificationWarningDialogMessage": "Інші параметри будуть змінені.",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
"settingsDisplayUseTvInterface": "Інтерфейс Android TV",
|
||||
"@settingsDisplayUseTvInterface": {},
|
||||
"filterLocatedLabel": "Розташований",
|
||||
"@filterLocatedLabel": {},
|
||||
"filterTaggedLabel": "Позначений тегом",
|
||||
"@filterTaggedLabel": {}
|
||||
}
|
||||
|
|
35
lib/model/actions/map_actions.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,8 +27,6 @@ class Device {
|
|||
|
||||
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
|
||||
|
||||
bool get isReadOnly => _isTelevision;
|
||||
|
||||
bool get isTelevision => _isTelevision;
|
||||
|
||||
bool get showPinShortcutFeedback => _showPinShortcutFeedback;
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:io';
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/geo/countries.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/entry_cache.dart';
|
||||
import 'package:aves/model/entry_dirs.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
|
@ -12,6 +11,7 @@ import 'package:aves/model/metadata/address.dart';
|
|||
import 'package:aves/model/metadata/catalog.dart';
|
||||
import 'package:aves/model/metadata/trash.dart';
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/trash.dart';
|
||||
import 'package:aves/model/video/metadata.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
|
@ -281,7 +281,7 @@ class AvesEntry {
|
|||
|
||||
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
|
||||
|
||||
bool get canEdit => !device.isReadOnly && path != null && !trashed && isMediaStoreContent;
|
||||
bool get canEdit => !settings.isReadOnly && path != null && !trashed && isMediaStoreContent;
|
||||
|
||||
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
||||
|
||||
|
|
|
@ -235,7 +235,10 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
final description = fields[DescriptionField.description];
|
||||
|
||||
if (canEditExif && editDescription) {
|
||||
metadata[MetadataType.exif] = {MetadataField.exifImageDescription.toPlatform!: description};
|
||||
metadata[MetadataType.exif] = {
|
||||
MetadataField.exifImageDescription.toPlatform!: null,
|
||||
MetadataField.exifUserComment.toPlatform!: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (canEditIptc) {
|
||||
|
|