diff --git a/CHANGELOG.md b/CHANGELOG.md index 91bdb7498..8de3150aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [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 + ## [v1.7.8] - 2022-12-20 ### Added diff --git a/README.md b/README.md index 9b7a7b0d4..98f3f5833 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,6 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt [Get it on Huawei AppGallery](https://appgallery.huawei.com/app/C106014023) -[Get it on Samsung Galaxy Store](https://galaxy.store/aves) [Get it on Amazon Appstore](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 diff --git a/android/app/build.gradle b/android/app/build.gradle index abcc4d0ef..051231057 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9988d4941..64f920264 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -205,6 +205,14 @@ This change eventually prevents building the app with Flutter v3.3.3. android:resource="@xml/app_widget_info" /> + + + + + + 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") diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index bf68b220b..c044f710f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -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("title") val urisByMimeType = call.argument>>("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 { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index 62f13accd..715b155b5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -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("mimeType") + val uri = call.argument("uri")?.let { Uri.parse(it) } + val sizeBytes = call.argument("sizeBytes")?.toLong() + val displayName = call.argument("displayName") + val dataUri = call.argument("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("mimeType") val uri = call.argument("uri")?.let { Uri.parse(it) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt index f2713b79c..e3373bcce 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MediaSessionHandler.kt @@ -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() + 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("uri")?.let { Uri.parse(it) } val title = call.argument("title") val durationMillis = call.argument("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("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() const val CHANNEL = "deckers.thibault/aves/media_session" const val STATE_STOPPED = "stopped" diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt index df2c1c86e..bb349855a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataEditHandler.kt @@ -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" } -} \ No newline at end of file +} + +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) + } +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index c23c7d85a..291de3940 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -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("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]" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt index 789f57f41..258682b00 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ActivityWindowHandler.kt @@ -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("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, + ) + ) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt index 55794ade4..46d1e43b8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/ServiceWindowHandler.kt @@ -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()) } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt index 184d2398d..0a6f41249 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/window/WindowHandler.kt @@ -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() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt index cd30bf1a7..c2e9d364d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ActivityResultStreamHandler.kt @@ -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 { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt index 2e199a30d..3cbaad3e2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/AnalysisStreamHandler.kt @@ -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() const val CHANNEL = "deckers.thibault/aves/analysis_events" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt index 1896fd0ef..a71e16b88 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ErrorStreamHandler.kt @@ -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() const val CHANNEL = "deckers.thibault/aves/error" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt index e1734338c..a5626196b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/IntentStreamHandler.kt @@ -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?) { eventSink?.success(intentData) } companion object { + private val LOG_TAG = LogUtils.createTag() const val CHANNEL = "deckers.thibault/aves/new_intent_stream" } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaCommandStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaCommandStreamHandler.kt new file mode 100644 index 000000000..b81815622 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaCommandStreamHandler.kt @@ -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() + 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" + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt index 7fdcddffc..05ede13b6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt @@ -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) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt index c61c18ad7..77a753bfc 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/SettingsChangeStreamHandler.kt @@ -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) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt new file mode 100644 index 000000000..590b1f65d --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/GoogleDeviceContainer.kt @@ -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 = ArrayList() + private val offsets: MutableList = 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) {} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt index 5f86a11a6..8edb548df 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Mp4ParserHelper.kt @@ -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) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt index 711d62cad..1693c4bab 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/MultiPage.kt @@ -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() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt index e2a1c45e4..783e4b2a1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt @@ -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() } } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt index a1f0055a7..737c862da 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/XMP.kt @@ -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 } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt index 8f6672ce9..ed247f4cf 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/metadataextractor/Helper.kt @@ -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) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt index 6d5bb2315..7ff773d21 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -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() - - @Suppress("deprecation") - const val PATH = MediaStore.MediaColumns.DATA } } \ No newline at end of file diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index dd06a2e0c..9e2e9c9eb 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -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\"", ) }) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 1e92c2e86..3b29bcf55 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -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): List { val obsoleteIds = ArrayList() 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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/CollectionUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/CollectionUtils.kt index 9fc81e5c7..55ed6ce6c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/CollectionUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/CollectionUtils.kt @@ -20,7 +20,7 @@ fun MutableList.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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt index 9be2fbe67..4aa362a1b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/Compat.kt @@ -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 Intent.getParcelableExtraCompat(name: String): T? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -16,6 +18,14 @@ inline fun 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) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index af0e471c2..df4ea5f75 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -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 } diff --git a/android/app/src/main/res/values-cs/strings.xml b/android/app/src/main/res/values-cs/strings.xml new file mode 100644 index 000000000..171ff70fa --- /dev/null +++ b/android/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,12 @@ + + + Aves + Tapeta + Hledat + Videa + Prohledat média + Prohledat obrázky a videa + Prohledávání médií + Zastavit + Fotorámeček + \ No newline at end of file diff --git a/android/app/src/main/res/xml/app_widget_info.xml b/android/app/src/main/res/xml/app_widget_info.xml index af3ae7c12..eb1d68c0d 100644 --- a/android/app/src/main/res/xml/app_widget_info.xml +++ b/android/app/src/main/res/xml/app_widget_info.xml @@ -1,5 +1,6 @@ \ No newline at end of file + android:widgetFeatures="reconfigurable" + tools:targetApi="s" /> \ No newline at end of file diff --git a/fastlane/metadata/android/cs/full_description.txt b/fastlane/metadata/android/cs/full_description.txt new file mode 100644 index 000000000..2ca7137cc --- /dev/null +++ b/fastlane/metadata/android/cs/full_description.txt @@ -0,0 +1,5 @@ +Aves 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 vícestránkový TIFF, SVG, starý AVI a mnohem více! Prohledává vaši sbírku médií kvůli rozpoznání pohyblivých fotografií, panoramatických snímků (čili fotosféry), 360° videí, nebo souborů GeoTIFF. + +Navigace a vyhledávání jsou důležitou součástí aplikace Aves. Cílem je, aby uživatelé jednoduše přecházeli z alb k fotografiím, albům, mapám, atd. + +Aves podporuje Android (od verze KitKat po Android 13, včetně Android TV) s funkcemi jako jsou widgety, zkratky aplikací, spořič displeje a globální vyhledávání. Rovněž jej lze použít pro prohlížení a výběr médií. \ No newline at end of file diff --git a/fastlane/metadata/android/cs/images/featureGraphic.png b/fastlane/metadata/android/cs/images/featureGraphic.png new file mode 100644 index 000000000..a0b3a3e77 Binary files /dev/null and b/fastlane/metadata/android/cs/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/cs/images/phoneScreenshots/1.png b/fastlane/metadata/android/cs/images/phoneScreenshots/1.png new file mode 100644 index 000000000..ea8be8f13 Binary files /dev/null and b/fastlane/metadata/android/cs/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/cs/images/phoneScreenshots/2.png b/fastlane/metadata/android/cs/images/phoneScreenshots/2.png new file mode 100644 index 000000000..f684259cf Binary files /dev/null and b/fastlane/metadata/android/cs/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/cs/images/phoneScreenshots/3.png b/fastlane/metadata/android/cs/images/phoneScreenshots/3.png new file mode 100644 index 000000000..7940af98c Binary files /dev/null and b/fastlane/metadata/android/cs/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/cs/images/phoneScreenshots/4.png b/fastlane/metadata/android/cs/images/phoneScreenshots/4.png new file mode 100644 index 000000000..3ea278277 Binary files /dev/null and b/fastlane/metadata/android/cs/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/cs/images/phoneScreenshots/5.png b/fastlane/metadata/android/cs/images/phoneScreenshots/5.png new file mode 100644 index 000000000..6fbd3a037 Binary files /dev/null and b/fastlane/metadata/android/cs/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/cs/images/phoneScreenshots/6.png b/fastlane/metadata/android/cs/images/phoneScreenshots/6.png new file mode 100644 index 000000000..e63823d65 Binary files /dev/null and b/fastlane/metadata/android/cs/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/cs/images/phoneScreenshots/7.png b/fastlane/metadata/android/cs/images/phoneScreenshots/7.png new file mode 100644 index 000000000..0c8d9c0e7 Binary files /dev/null and b/fastlane/metadata/android/cs/images/phoneScreenshots/7.png differ diff --git a/fastlane/metadata/android/cs/short_description.txt b/fastlane/metadata/android/cs/short_description.txt new file mode 100644 index 000000000..48cbd5539 --- /dev/null +++ b/fastlane/metadata/android/cs/short_description.txt @@ -0,0 +1 @@ +Galerie a prohlížeč metadat \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1064.txt b/fastlane/metadata/android/en-US/changelogs/1064.txt deleted file mode 100644 index 3ce411869..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1064.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1065.txt b/fastlane/metadata/android/en-US/changelogs/1065.txt deleted file mode 100644 index c75dc2346..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1065.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1066.txt b/fastlane/metadata/android/en-US/changelogs/1066.txt deleted file mode 100644 index f1cc67dc3..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1066.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1067.txt b/fastlane/metadata/android/en-US/changelogs/1067.txt deleted file mode 100644 index 9a00bb1be..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1067.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1068.txt b/fastlane/metadata/android/en-US/changelogs/1068.txt deleted file mode 100644 index decd4dce4..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1068.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1069.txt b/fastlane/metadata/android/en-US/changelogs/1069.txt deleted file mode 100644 index 93bf3bf8b..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1069.txt +++ /dev/null @@ -1,4 +0,0 @@ -In v1.6.3: -- enjoy the light theme -- rename items in bulk -Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/changelogs/1070.txt b/fastlane/metadata/android/en-US/changelogs/1070.txt deleted file mode 100644 index 64a893886..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1070.txt +++ /dev/null @@ -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 diff --git a/fastlane/metadata/android/en-US/changelogs/1071.txt b/fastlane/metadata/android/en-US/changelogs/1071.txt deleted file mode 100644 index 38d163888..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1071.txt +++ /dev/null @@ -1,5 +0,0 @@ -In v1.6.5: -- bottom navigation bar -- fast scroll with breadcrumbs -- settings search -Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/changelogs/1072.txt b/fastlane/metadata/android/en-US/changelogs/1072.txt deleted file mode 100644 index b8aa57b1e..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1072.txt +++ /dev/null @@ -1,5 +0,0 @@ -In v1.6.6: -- bottom navigation bar -- fast scroll with breadcrumbs -- settings search -Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/changelogs/1073.txt b/fastlane/metadata/android/en-US/changelogs/1073.txt deleted file mode 100644 index 75aa6e973..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1073.txt +++ /dev/null @@ -1,5 +0,0 @@ -In v1.6.7: -- bottom navigation bar -- fast scroll with breadcrumbs -- settings search -Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/changelogs/1074.txt b/fastlane/metadata/android/en-US/changelogs/1074.txt deleted file mode 100644 index 8b9f35460..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1074.txt +++ /dev/null @@ -1,5 +0,0 @@ -In v1.6.8: -- bottom navigation bar -- fast scroll with breadcrumbs -- settings search -Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/changelogs/1075.txt b/fastlane/metadata/android/en-US/changelogs/1075.txt deleted file mode 100644 index c852da8ed..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1075.txt +++ /dev/null @@ -1,5 +0,0 @@ -In v1.6.9: -- start slideshows -- change your wallpaper -- enjoy the app in Turkish -Full changelog available on GitHub diff --git a/fastlane/metadata/android/en-US/changelogs/1076.txt b/fastlane/metadata/android/en-US/changelogs/1076.txt deleted file mode 100644 index cce1f4f7d..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1076.txt +++ /dev/null @@ -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 diff --git a/fastlane/metadata/android/en-US/changelogs/1077.txt b/fastlane/metadata/android/en-US/changelogs/1077.txt deleted file mode 100644 index 546d1be97..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1077.txt +++ /dev/null @@ -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 diff --git a/fastlane/metadata/android/en-US/changelogs/1078.txt b/fastlane/metadata/android/en-US/changelogs/1078.txt deleted file mode 100644 index 756c775df..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1078.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1079.txt b/fastlane/metadata/android/en-US/changelogs/1079.txt deleted file mode 100644 index 30000cd00..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1079.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1080.txt b/fastlane/metadata/android/en-US/changelogs/1080.txt deleted file mode 100644 index af4200674..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1080.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1081.txt b/fastlane/metadata/android/en-US/changelogs/1081.txt deleted file mode 100644 index e5b6ac839..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1081.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1082.txt b/fastlane/metadata/android/en-US/changelogs/1082.txt deleted file mode 100644 index 5905035c9..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1082.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1083.txt b/fastlane/metadata/android/en-US/changelogs/1083.txt deleted file mode 100644 index b9b547a16..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1083.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1084.txt b/fastlane/metadata/android/en-US/changelogs/1084.txt deleted file mode 100644 index b434e87d9..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1084.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1085.txt b/fastlane/metadata/android/en-US/changelogs/1085.txt deleted file mode 100644 index 3ae743b04..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1085.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1086.txt b/fastlane/metadata/android/en-US/changelogs/1086.txt deleted file mode 100644 index 55493c9f4..000000000 --- a/fastlane/metadata/android/en-US/changelogs/1086.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/89.txt b/fastlane/metadata/android/en-US/changelogs/89.txt new file mode 100644 index 000000000..ec10c2286 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/89.txt @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/8901.txt b/fastlane/metadata/android/en-US/changelogs/8901.txt new file mode 100644 index 000000000..ec10c2286 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/8901.txt @@ -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 \ No newline at end of file diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt index 8eda6d0ba..3c18c062f 100644 --- a/fastlane/metadata/android/id/full_description.txt +++ b/fastlane/metadata/android/id/full_description.txt @@ -2,4 +2,4 @@ Navigasi dan pencarian merupakan bagian penting dari Aves. Tujuannya adalah agar pengguna dengan mudah mengalir dari album ke foto ke tag ke peta, dll. -Aves terintegrasi dengan Android (dari API 19 ke 33, yaitu dari KitKat ke Android 13) dengan fitur-fitur seperti pintasan aplikasi dan pencarian global penanganan. Ini juga berfungsi sebagai penampil dan pemilih media. \ No newline at end of file +Aves mengintegrasi dengan Android (dari Kitkat ke Android 13) dengan fitur-fitur seperti pintasan aplikasi, jalan pintas aplikasi, screen saver dan pencarian global penanganan. Ini juga berfungsi sebagai penampil dan pemilih media. \ No newline at end of file diff --git a/fastlane/metadata/android/nn/full_description.txt b/fastlane/metadata/android/nn/full_description.txt index 6c92748f8..ed514a8df 100644 --- a/fastlane/metadata/android/nn/full_description.txt +++ b/fastlane/metadata/android/nn/full_description.txt @@ -1,5 +1,5 @@ -Aves can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like multi-page TIFFs, SVGs, old AVIs and more! It scans your media collection to identify motion photos, panoramas (aka photo spheres), 360° videos, as well as GeoTIFF files. +Aves kan handsama alle slags bileter og videoar, medteken JPEG og MP4, men au meir uvane ting som fleirsida TIFF-ar, SVG-ar, gamle AVI-ar med meir! Aves ser igjennom mediasamlinga di for å gjenkjenne rørslebilete, panorama (bilete med vidt oversyn), 360° videoar, og au GeoTIFF-filer. -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. +Navigering og søk har mykje å sei i Aves. Målet er at ein skal lett kunne gå ifrå album, til bilete, til merkelappar, til kart, osv. 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. \ No newline at end of file diff --git a/fastlane/metadata/android/pl/images/featureGraphic.png b/fastlane/metadata/android/pl/images/featureGraphic.png new file mode 100644 index 000000000..ad29dfe39 Binary files /dev/null and b/fastlane/metadata/android/pl/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/pl/images/phoneScreenshots/1.png b/fastlane/metadata/android/pl/images/phoneScreenshots/1.png new file mode 100644 index 000000000..01e38dc7f Binary files /dev/null and b/fastlane/metadata/android/pl/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/pl/images/phoneScreenshots/2.png b/fastlane/metadata/android/pl/images/phoneScreenshots/2.png new file mode 100644 index 000000000..a1665b69e Binary files /dev/null and b/fastlane/metadata/android/pl/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/pl/images/phoneScreenshots/3.png b/fastlane/metadata/android/pl/images/phoneScreenshots/3.png new file mode 100644 index 000000000..b3e2d334c Binary files /dev/null and b/fastlane/metadata/android/pl/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/pl/images/phoneScreenshots/4.png b/fastlane/metadata/android/pl/images/phoneScreenshots/4.png new file mode 100644 index 000000000..d66d85bbf Binary files /dev/null and b/fastlane/metadata/android/pl/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/pl/images/phoneScreenshots/5.png b/fastlane/metadata/android/pl/images/phoneScreenshots/5.png new file mode 100644 index 000000000..d3f82ec46 Binary files /dev/null and b/fastlane/metadata/android/pl/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/pl/images/phoneScreenshots/6.png b/fastlane/metadata/android/pl/images/phoneScreenshots/6.png new file mode 100644 index 000000000..cf3ff0987 Binary files /dev/null and b/fastlane/metadata/android/pl/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/pl/images/phoneScreenshots/7.png b/fastlane/metadata/android/pl/images/phoneScreenshots/7.png new file mode 100644 index 000000000..ed1aa3e5c Binary files /dev/null and b/fastlane/metadata/android/pl/images/phoneScreenshots/7.png differ diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 1453cb0df..557e12b6a 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -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": {} } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb new file mode 100644 index 000000000..b487cf3b0 --- /dev/null +++ b/lib/l10n/app_cs.arb @@ -0,0 +1,1358 @@ +{ + "welcomeOptional": "Volitelné", + "@welcomeOptional": {}, + "welcomeTermsToggle": "Souhlasím s podmínkami použivání", + "@welcomeTermsToggle": {}, + "appName": "Aves", + "@appName": {}, + "welcomeMessage": "Vítá tě Aves", + "@welcomeMessage": {}, + "applyButtonLabel": "POUŽÍT", + "@applyButtonLabel": {}, + "deleteButtonLabel": "SMAZAT", + "@deleteButtonLabel": {}, + "nextButtonLabel": "DALŠÍ", + "@nextButtonLabel": {}, + "showButtonLabel": "ZOBRAZIT", + "@showButtonLabel": {}, + "continueButtonLabel": "POKRAČOVAT", + "@continueButtonLabel": {}, + "cancelTooltip": "Zrušit", + "@cancelTooltip": {}, + "changeTooltip": "Upravit", + "@changeTooltip": {}, + "clearTooltip": "Vyčistit", + "@clearTooltip": {}, + "previousTooltip": "Předchozí", + "@previousTooltip": {}, + "nextTooltip": "Další", + "@nextTooltip": {}, + "showTooltip": "Zobrazit", + "@showTooltip": {}, + "actionRemove": "Odstranit", + "@actionRemove": {}, + "resetTooltip": "Resetovat", + "@resetTooltip": {}, + "saveTooltip": "Uložit", + "@saveTooltip": {}, + "focalLength": "{length} mm", + "@focalLength": { + "placeholders": { + "length": { + "type": "String", + "example": "5.4" + } + } + }, + "timeDays": "{days, plural, =1{1 den} =2..4{{days} dny} other{{days} dnů}}", + "@timeDays": { + "placeholders": { + "days": {} + } + }, + "itemCount": "{count, plural, =1{1 položka} =2..4{{count} položky} other{{count} položek}}", + "@itemCount": { + "placeholders": { + "count": {} + } + }, + "columnCount": "{count, plural, =1{1 sloupec} =2..4{{count} sloupce} other{{count} sloupců}}", + "@columnCount": { + "placeholders": { + "count": {} + } + }, + "hideButtonLabel": "SKRÝT", + "@hideButtonLabel": {}, + "hideTooltip": "Skrýt", + "@hideTooltip": {}, + "doNotAskAgain": "Znovu nezobrazovat", + "@doNotAskAgain": {}, + "sourceStateLoading": "Nahrávání", + "@sourceStateLoading": {}, + "sourceStateCataloguing": "Katalogizace", + "@sourceStateCataloguing": {}, + "sourceStateLocatingCountries": "Vyhledání zemí", + "@sourceStateLocatingCountries": {}, + "sourceStateLocatingPlaces": "Vyhledávání míst", + "@sourceStateLocatingPlaces": {}, + "chipActionDelete": "Smazat", + "@chipActionDelete": {}, + "chipActionGoToAlbumPage": "Zobrazit v albech", + "@chipActionGoToAlbumPage": {}, + "chipActionGoToCountryPage": "Zobrazit v zemích", + "@chipActionGoToCountryPage": {}, + "chipActionGoToTagPage": "Zobrazit ve štítcích", + "@chipActionGoToTagPage": {}, + "chipActionFilterOut": "Odfiltrovat", + "@chipActionFilterOut": {}, + "chipActionFilterIn": "Filtrovat", + "@chipActionFilterIn": {}, + "chipActionPin": "Připnout nahoru", + "@chipActionPin": {}, + "chipActionUnpin": "Odepnout seshora", + "@chipActionUnpin": {}, + "chipActionHide": "Skrýt", + "@chipActionHide": {}, + "chipActionRename": "Přejmenovat", + "@chipActionRename": {}, + "chipActionCreateAlbum": "Vytvořit album", + "@chipActionCreateAlbum": {}, + "entryActionCopyToClipboard": "Kopírovat do paměti", + "@entryActionCopyToClipboard": {}, + "entryActionDelete": "Smazat", + "@entryActionDelete": {}, + "entryActionConvert": "Konvertovat", + "@entryActionConvert": {}, + "entryActionExport": "Exportovat", + "@entryActionExport": {}, + "entryActionInfo": "Informace", + "@entryActionInfo": {}, + "entryActionRename": "Přejmenovat", + "@entryActionRename": {}, + "entryActionRestore": "Obnovit", + "@entryActionRestore": {}, + "entryActionRotateCCW": "Otočit doleva", + "@entryActionRotateCCW": {}, + "entryActionRotateCW": "Otočit doprava", + "@entryActionRotateCW": {}, + "entryActionFlip": "Převrátit vodorovně", + "@entryActionFlip": {}, + "entryActionPrint": "Tisknout", + "@entryActionPrint": {}, + "entryActionShare": "Sdílet", + "@entryActionShare": {}, + "entryActionShareImageOnly": "Sdílet pouze obrázek", + "@entryActionShareImageOnly": {}, + "entryActionShareVideoOnly": "Sdílet pouze video", + "@entryActionShareVideoOnly": {}, + "entryActionViewSource": "Zobrazit zdroj", + "@entryActionViewSource": {}, + "entryActionShowGeoTiffOnMap": "Zobrazit jako překryv mapy", + "@entryActionShowGeoTiffOnMap": {}, + "entryActionConvertMotionPhotoToStillImage": "Konvertovat na statický obrázek", + "@entryActionConvertMotionPhotoToStillImage": {}, + "entryActionViewMotionPhotoVideo": "Otevřít video", + "@entryActionViewMotionPhotoVideo": {}, + "entryActionEdit": "Upravit", + "@entryActionEdit": {}, + "entryActionOpen": "Otevřít s", + "@entryActionOpen": {}, + "entryActionSetAs": "Nastavit jako", + "@entryActionSetAs": {}, + "entryActionOpenMap": "Zobrazit na mapě", + "@entryActionOpenMap": {}, + "entryActionRotateScreen": "Otočit obrazovku", + "@entryActionRotateScreen": {}, + "entryActionAddFavourite": "Přidat do oblíbených", + "@entryActionAddFavourite": {}, + "entryActionRemoveFavourite": "Odebrat z oblíbených", + "@entryActionRemoveFavourite": {}, + "videoActionCaptureFrame": "Zachytit rámeček", + "@videoActionCaptureFrame": {}, + "videoActionMute": "Ztlumit", + "@videoActionMute": {}, + "videoActionUnmute": "Zrušit ztlumení", + "@videoActionUnmute": {}, + "chipActionSetCover": "Nastavit přebal", + "@chipActionSetCover": {}, + "videoActionPlay": "Spustit", + "@videoActionPlay": {}, + "videoActionPause": "Pozastavit", + "@videoActionPause": {}, + "videoActionReplay10": "Přetočit zpět o 10 sekund", + "@videoActionReplay10": {}, + "videoActionSkip10": "Posunout vpřed o 10 sekund", + "@videoActionSkip10": {}, + "videoActionSelectStreams": "Vybrat stopy", + "@videoActionSelectStreams": {}, + "videoActionSetSpeed": "Rychlost přehrávání", + "@videoActionSetSpeed": {}, + "videoActionSettings": "Nastavení", + "@videoActionSettings": {}, + "slideshowActionResume": "Pokračovat", + "@slideshowActionResume": {}, + "slideshowActionShowInCollection": "Zobrazit ve sbírce", + "@slideshowActionShowInCollection": {}, + "entryInfoActionEditDate": "Upravit datum a čas", + "@entryInfoActionEditDate": {}, + "entryInfoActionEditLocation": "Upravit polohu", + "@entryInfoActionEditLocation": {}, + "entryInfoActionEditTitleDescription": "Upravit název a popis", + "@entryInfoActionEditTitleDescription": {}, + "entryInfoActionEditRating": "Upravit hodnocení", + "@entryInfoActionEditRating": {}, + "entryInfoActionEditTags": "Upravit štítky", + "@entryInfoActionEditTags": {}, + "entryInfoActionRemoveMetadata": "Odstranit metadata", + "@entryInfoActionRemoveMetadata": {}, + "entryInfoActionExportMetadata": "Exportovat metadata", + "@entryInfoActionExportMetadata": {}, + "entryInfoActionRemoveLocation": "Odstranit polohu", + "@entryInfoActionRemoveLocation": {}, + "filterAspectRatioPortraitLabel": "Na výšku", + "@filterAspectRatioPortraitLabel": {}, + "filterAspectRatioLandscapeLabel": "Na šířku", + "@filterAspectRatioLandscapeLabel": {}, + "filterBinLabel": "Koš", + "@filterBinLabel": {}, + "filterFavouriteLabel": "Oblíbené", + "@filterFavouriteLabel": {}, + "filterNoDateLabel": "Bez data", + "@filterNoDateLabel": {}, + "filterNoAddressLabel": "Bez adresy", + "@filterNoAddressLabel": {}, + "filterNoLocationLabel": "Bez polohy", + "@filterNoLocationLabel": {}, + "filterNoRatingLabel": "Nehodnocený", + "@filterNoRatingLabel": {}, + "filterNoTagLabel": "Bez štítků", + "@filterNoTagLabel": {}, + "filterNoTitleLabel": "Bez názvu", + "@filterNoTitleLabel": {}, + "filterOnThisDayLabel": "V tento den", + "@filterOnThisDayLabel": {}, + "filterRecentlyAddedLabel": "Naposled přidané", + "@filterRecentlyAddedLabel": {}, + "filterRatingRejectedLabel": "Zamítnutý", + "@filterRatingRejectedLabel": {}, + "filterTypeAnimatedLabel": "Animovaný", + "@filterTypeAnimatedLabel": {}, + "filterTypeMotionPhotoLabel": "Pohyblivá fotografie", + "@filterTypeMotionPhotoLabel": {}, + "filterTypePanoramaLabel": "Panoráma", + "@filterTypePanoramaLabel": {}, + "filterTypeRawLabel": "Raw", + "@filterTypeRawLabel": {}, + "filterTypeSphericalVideoLabel": "360° video", + "@filterTypeSphericalVideoLabel": {}, + "filterTypeGeotiffLabel": "GeoTIFF", + "@filterTypeGeotiffLabel": {}, + "filterMimeImageLabel": "Obrázek", + "@filterMimeImageLabel": {}, + "filterMimeVideoLabel": "Video", + "@filterMimeVideoLabel": {}, + "coordinateDmsNorth": "S", + "@coordinateDmsNorth": {}, + "coordinateDmsSouth": "J", + "@coordinateDmsSouth": {}, + "coordinateDmsEast": "V", + "@coordinateDmsEast": {}, + "coordinateDmsWest": "Z", + "@coordinateDmsWest": {}, + "unitSystemMetric": "Metrická soustava", + "@unitSystemMetric": {}, + "unitSystemImperial": "Imperiální jednotky", + "@unitSystemImperial": {}, + "videoLoopModeNever": "Nikdy", + "@videoLoopModeNever": {}, + "videoLoopModeShortOnly": "Pouze krátká videa", + "@videoLoopModeShortOnly": {}, + "videoLoopModeAlways": "Vždy", + "@videoLoopModeAlways": {}, + "videoControlsPlay": "Přehrát", + "@videoControlsPlay": {}, + "videoControlsPlaySeek": "Přehrávat a vyhledávat vzad/vpřed", + "@videoControlsPlaySeek": {}, + "videoControlsPlayOutside": "Otevřít jiným přehrávačem", + "@videoControlsPlayOutside": {}, + "videoControlsNone": "Žádný", + "@videoControlsNone": {}, + "coordinateFormatDms": "Stupně, minuty, vteřiny", + "@coordinateFormatDms": {}, + "coordinateFormatDecimal": "Stupně s desetinnými místy", + "@coordinateFormatDecimal": {}, + "coordinateDms": "{coordinate} {direction}", + "@coordinateDms": { + "placeholders": { + "coordinate": { + "type": "String", + "example": "38° 41′ 47.72″" + }, + "direction": { + "type": "String", + "example": "S" + } + } + }, + "mapStyleGoogleTerrain": "Mapy Google (terén)", + "@mapStyleGoogleTerrain": {}, + "mapStyleHuaweiNormal": "Mapy Petal", + "@mapStyleHuaweiNormal": {}, + "mapStyleHuaweiTerrain": "Mapy Petal (terénní)", + "@mapStyleHuaweiTerrain": {}, + "mapStyleOsmHot": "Humanitární OSM", + "@mapStyleOsmHot": {}, + "mapStyleStamenToner": "Stamen Toner (černobílé)", + "@mapStyleStamenToner": {}, + "mapStyleStamenWatercolor": "Stamen Watercolor (vodové barvy)", + "@mapStyleStamenWatercolor": {}, + "nameConflictStrategyRename": "Přejmenovat", + "@nameConflictStrategyRename": {}, + "nameConflictStrategyReplace": "Nahradit", + "@nameConflictStrategyReplace": {}, + "keepScreenOnVideoPlayback": "Při přehrávání videa", + "@keepScreenOnVideoPlayback": {}, + "keepScreenOnAlways": "Vždy", + "@keepScreenOnAlways": {}, + "accessibilityAnimationsRemove": "Zakázat vizuální efekty", + "@accessibilityAnimationsRemove": {}, + "accessibilityAnimationsKeep": "Povolit vizuální efekty", + "@accessibilityAnimationsKeep": {}, + "displayRefreshRatePreferHighest": "Nejvyšší", + "@displayRefreshRatePreferHighest": {}, + "subtitlePositionTop": "Nahoře", + "@subtitlePositionTop": {}, + "keepScreenOnViewerOnly": "Pouze v zobrazení prohlížeče", + "@keepScreenOnViewerOnly": {}, + "subtitlePositionBottom": "Dole", + "@subtitlePositionBottom": {}, + "videoPlaybackSkip": "Přeskočit", + "@videoPlaybackSkip": {}, + "videoPlaybackMuted": "Přehrát ztlumené", + "@videoPlaybackMuted": {}, + "videoPlaybackWithSound": "Přehrát se zvukem", + "@videoPlaybackWithSound": {}, + "themeBrightnessLight": "Svetlé", + "@themeBrightnessLight": {}, + "themeBrightnessDark": "Tmavé", + "@themeBrightnessDark": {}, + "themeBrightnessBlack": "Černé", + "@themeBrightnessBlack": {}, + "viewerTransitionSlide": "Posun", + "@viewerTransitionSlide": {}, + "viewerTransitionParallax": "Parallax", + "@viewerTransitionParallax": {}, + "viewerTransitionFade": "Zeslábnutí", + "@viewerTransitionFade": {}, + "viewerTransitionZoomIn": "Přiblížení", + "@viewerTransitionZoomIn": {}, + "viewerTransitionNone": "Žádný", + "@viewerTransitionNone": {}, + "wallpaperTargetHome": "Domovská obrazovka", + "@wallpaperTargetHome": {}, + "wallpaperTargetLock": "Zamykací obrazovka", + "@wallpaperTargetLock": {}, + "wallpaperTargetHomeLock": "Domovská i zamykací obrazovka", + "@wallpaperTargetHomeLock": {}, + "widgetDisplayedItemRandom": "Náhodně", + "@widgetDisplayedItemRandom": {}, + "widgetDisplayedItemMostRecent": "Nejnovější", + "@widgetDisplayedItemMostRecent": {}, + "widgetOpenPageHome": "Otevřít domovskou stránku", + "@widgetOpenPageHome": {}, + "widgetOpenPageCollection": "Otevřít sbírku", + "@widgetOpenPageCollection": {}, + "widgetOpenPageViewer": "Otevřít prohlížeč", + "@widgetOpenPageViewer": {}, + "albumTierNew": "Nové", + "@albumTierNew": {}, + "albumTierPinned": "Připnuté", + "@albumTierPinned": {}, + "albumTierSpecial": "Společné", + "@albumTierSpecial": {}, + "albumTierApps": "Aplikace", + "@albumTierApps": {}, + "albumTierRegular": "Ostatní", + "@albumTierRegular": {}, + "storageVolumeDescriptionFallbackPrimary": "Interní úložiště", + "@storageVolumeDescriptionFallbackPrimary": {}, + "storageVolumeDescriptionFallbackNonPrimary": "SD karta", + "@storageVolumeDescriptionFallbackNonPrimary": {}, + "rootDirectoryDescription": "kořenový adresář", + "@rootDirectoryDescription": {}, + "otherDirectoryDescription": "„{name}“ adresář", + "@otherDirectoryDescription": { + "placeholders": { + "name": { + "type": "String", + "example": "Pictures", + "description": "the name of a specific directory" + } + } + }, + "storageAccessDialogMessage": "Prosím zvolte adresář {directory} v „{volume}“ na další obrazovce, abyste k němu povolili aplikaci přístup.", + "@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" + } + } + }, + "restrictedAccessDialogMessage": "Tato aplikace nemá povolené úpravy souborů v adresáři {directory} v „{volume}“.\n\nPro přesun položek do jiného adresáře prosím použijte předinstalovaného správce souborů nebo galerii.", + "@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" + } + } + }, + "notEnoughSpaceDialogMessage": "Pro dokončení této operace je vyžadováno {neededSize} volného místa na „{volume}“, ale k dispozici je pouze {freeSize}.", + "@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" + } + } + }, + "unsupportedTypeDialogMessage": "{count, plural, =1{Tato operace není dostupná pro položky tohoto typu: {types}.} other{Tato operace není dostupná pro položky těchto typů: {types}.}}", + "@unsupportedTypeDialogMessage": { + "placeholders": { + "count": {}, + "types": { + "type": "String", + "example": "GIF, TIFF, MP4", + "description": "a list of unsupported types" + } + } + }, + "nameConflictDialogSingleSourceMessage": "Některé soubory v cílovém umístění mají stejný název.", + "@nameConflictDialogSingleSourceMessage": {}, + "nameConflictDialogMultipleSourceMessage": "Některé soubory mají stejný název.", + "@nameConflictDialogMultipleSourceMessage": {}, + "addShortcutButtonLabel": "PŘIDAT", + "@addShortcutButtonLabel": {}, + "noMatchingAppDialogMessage": "Pro tuto operaci není k dispozici žádná aplikace.", + "@noMatchingAppDialogMessage": {}, + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Smazat tuto položku?} =2..4{Smazat tyto {count} položky?} other{Smazat těchto {count} položek?}}", + "@deleteEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "moveUndatedConfirmationDialogMessage": "Uložit data položek, než budete pokračovat?", + "@moveUndatedConfirmationDialogMessage": {}, + "missingSystemFilePickerDialogMessage": "Systémový nástroj pro výběr souborů chybí nebo je zakázaný. Prosím povolte jej a zkuste to znovu.", + "@missingSystemFilePickerDialogMessage": {}, + "addShortcutDialogLabel": "Název zástupce", + "@addShortcutDialogLabel": {}, + "moveUndatedConfirmationDialogSetDate": "Uložit tada", + "@moveUndatedConfirmationDialogSetDate": {}, + "videoResumeDialogMessage": "Chcete obnovit přehrávání v čase {time}?", + "@videoResumeDialogMessage": { + "placeholders": { + "time": { + "type": "String", + "example": "13:37" + } + } + }, + "videoStartOverButtonLabel": "SPUSTIT ZNOVU", + "@videoStartOverButtonLabel": {}, + "videoResumeButtonLabel": "POKRAČOVAT", + "@videoResumeButtonLabel": {}, + "setCoverDialogLatest": "Poslední položka", + "@setCoverDialogLatest": {}, + "newAlbumDialogNameLabel": "Název alba", + "@newAlbumDialogNameLabel": {}, + "newAlbumDialogNameLabelAlreadyExistsHelper": "Adresář již existuje", + "@newAlbumDialogNameLabelAlreadyExistsHelper": {}, + "newAlbumDialogStorageLabel": "Úložiště:", + "@newAlbumDialogStorageLabel": {}, + "renameAlbumDialogLabel": "Nový název", + "@renameAlbumDialogLabel": {}, + "renameAlbumDialogLabelAlreadyExistsHelper": "Adresář již existuje", + "@renameAlbumDialogLabelAlreadyExistsHelper": {}, + "renameEntrySetPageTitle": "Přejmenovat", + "@renameEntrySetPageTitle": {}, + "renameEntrySetPagePatternFieldLabel": "Vzor názvu", + "@renameEntrySetPagePatternFieldLabel": {}, + "renameEntrySetPageInsertTooltip": "Vložit pole", + "@renameEntrySetPageInsertTooltip": {}, + "renameEntrySetPagePreviewSectionTitle": "Náhled", + "@renameEntrySetPagePreviewSectionTitle": {}, + "renameProcessorName": "Název", + "@renameProcessorName": {}, + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat toto album a tuto položku?} =2..4{Smazat toto album a tyto {count} položky?} other{Smazat toto album a těchto {count} položek?}}", + "@deleteSingleAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Smazat tato alba a jejich položku?} =2..4{Smazat tato alba a jejich {count} položky?} other{Smazat tato alba a jejich {count} položek?}}", + "@deleteMultiAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "exportEntryDialogFormat": "Formát:", + "@exportEntryDialogFormat": {}, + "exportEntryDialogWidth": "Šířka", + "@exportEntryDialogWidth": {}, + "exportEntryDialogHeight": "Výška", + "@exportEntryDialogHeight": {}, + "renameEntryDialogLabel": "Nový název", + "@renameEntryDialogLabel": {}, + "editEntryDialogCopyFromItem": "Kopírovat z jiné položky", + "@editEntryDialogCopyFromItem": {}, + "editEntryDialogTargetFieldsHeader": "Pole k úpravě", + "@editEntryDialogTargetFieldsHeader": {}, + "editEntryDateDialogCopyField": "Kopírovat z jiného data", + "@editEntryDateDialogCopyField": {}, + "editEntryDateDialogExtractFromTitle": "Odvodit z názvu", + "@editEntryDateDialogExtractFromTitle": {}, + "editEntryDateDialogShift": "Posun", + "@editEntryDateDialogShift": {}, + "editEntryDateDialogSourceFileModifiedDate": "Datum úpravy souboru", + "@editEntryDateDialogSourceFileModifiedDate": {}, + "durationDialogHours": "Hodiny", + "@durationDialogHours": {}, + "durationDialogMinutes": "Minuty", + "@durationDialogMinutes": {}, + "durationDialogSeconds": "Sekundy", + "@durationDialogSeconds": {}, + "renameProcessorCounter": "Počítadlo", + "@renameProcessorCounter": {}, + "editEntryLocationDialogTitle": "Poloha", + "@editEntryLocationDialogTitle": {}, + "editEntryLocationDialogSetCustom": "Nastavit vlastní polohu", + "@editEntryLocationDialogSetCustom": {}, + "editEntryLocationDialogChooseOnMap": "Vybrat na mapě", + "@editEntryLocationDialogChooseOnMap": {}, + "editEntryLocationDialogLatitude": "Zeměpisná šířka", + "@editEntryLocationDialogLatitude": {}, + "editEntryLocationDialogLongitude": "Zeměpisná délka", + "@editEntryLocationDialogLongitude": {}, + "locationPickerUseThisLocationButton": "Použít tuto polohu", + "@locationPickerUseThisLocationButton": {}, + "editEntryRatingDialogTitle": "Hodnocení", + "@editEntryRatingDialogTitle": {}, + "removeEntryMetadataDialogTitle": "Odstranění metadat", + "@removeEntryMetadataDialogTitle": {}, + "removeEntryMetadataDialogMore": "Zobrazit více", + "@removeEntryMetadataDialogMore": {}, + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "Pro přehrávání videa v pohyblivé fotografii je vyžadováno XMP.\n\nOpravdu jej chcete odstranit?", + "@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {}, + "videoSpeedDialogLabel": "Rychlost přehrávání", + "@videoSpeedDialogLabel": {}, + "videoStreamSelectionDialogVideo": "Video", + "@videoStreamSelectionDialogVideo": {}, + "videoStreamSelectionDialogAudio": "Audio", + "@videoStreamSelectionDialogAudio": {}, + "videoStreamSelectionDialogText": "Titulky", + "@videoStreamSelectionDialogText": {}, + "videoStreamSelectionDialogOff": "Vypnuto", + "@videoStreamSelectionDialogOff": {}, + "videoStreamSelectionDialogTrack": "Stopa", + "@videoStreamSelectionDialogTrack": {}, + "videoStreamSelectionDialogNoSelection": "Nejsou k dispozici žádné další stopy.", + "@videoStreamSelectionDialogNoSelection": {}, + "genericSuccessFeedback": "Hotovo!", + "@genericSuccessFeedback": {}, + "genericFailureFeedback": "Selhání", + "@genericFailureFeedback": {}, + "genericDangerWarningDialogMessage": "Jste si jisti?", + "@genericDangerWarningDialogMessage": {}, + "menuActionConfigureView": "Zobrazení", + "@menuActionConfigureView": {}, + "menuActionSelect": "Výběr", + "@menuActionSelect": {}, + "menuActionSelectAll": "Vybrat vše", + "@menuActionSelectAll": {}, + "menuActionSelectNone": "Zrušit výběr", + "@menuActionSelectNone": {}, + "menuActionMap": "Mapa", + "@menuActionMap": {}, + "menuActionSlideshow": "Slideshow", + "@menuActionSlideshow": {}, + "menuActionStats": "Statistiky", + "@menuActionStats": {}, + "viewDialogGroupSectionTitle": "Seskupení", + "@viewDialogGroupSectionTitle": {}, + "viewDialogLayoutSectionTitle": "Rozložení", + "@viewDialogLayoutSectionTitle": {}, + "viewDialogReverseSortOrder": "Obrátit řazení", + "@viewDialogReverseSortOrder": {}, + "tileLayoutGrid": "Mřížka", + "@tileLayoutGrid": {}, + "tileLayoutList": "Seznam", + "@tileLayoutList": {}, + "drawerCollectionVideos": "Videa", + "@drawerCollectionVideos": {}, + "drawerCollectionAnimated": "Animované", + "@drawerCollectionAnimated": {}, + "drawerCollectionMotionPhotos": "Pohyblivé fotografie", + "@drawerCollectionMotionPhotos": {}, + "sortByAlbumFileName": "Podle alba a názvu souboru", + "@sortByAlbumFileName": {}, + "sortByRating": "Podle hodnocení", + "@sortByRating": {}, + "settingsPageTitle": "Nastavení", + "@settingsPageTitle": {}, + "settingsSystemDefault": "Výchozí nastavení systému", + "@settingsSystemDefault": {}, + "settingsDefault": "Výchozí", + "@settingsDefault": {}, + "settingsDisabled": "Zakázáno", + "@settingsDisabled": {}, + "settingsSearchFieldLabel": "Prohledat nastavení", + "@settingsSearchFieldLabel": {}, + "settingsSearchEmpty": "Žádné odpovídající položky", + "@settingsSearchEmpty": {}, + "settingsActionExport": "Export", + "@settingsActionExport": {}, + "settingsActionExportDialogTitle": "Export", + "@settingsActionExportDialogTitle": {}, + "settingsActionImport": "Import", + "@settingsActionImport": {}, + "settingsActionImportDialogTitle": "Import", + "@settingsActionImportDialogTitle": {}, + "appExportCovers": "Přebaly", + "@appExportCovers": {}, + "appExportFavourites": "Oblíbené", + "@appExportFavourites": {}, + "appExportSettings": "Nastavení", + "@appExportSettings": {}, + "settingsHomeTile": "Domů", + "@settingsHomeTile": {}, + "settingsNavigationSectionTitle": "Navigace", + "@settingsNavigationSectionTitle": {}, + "settingsHomeDialogTitle": "Domů", + "@settingsHomeDialogTitle": {}, + "settingsShowBottomNavigationBar": "Zobrazit spodní navigační lištu", + "@settingsShowBottomNavigationBar": {}, + "settingsKeepScreenOnTile": "Ponechat obrazovku zapnutou", + "@settingsKeepScreenOnTile": {}, + "settingsKeepScreenOnDialogTitle": "Ponechat obrazovku zapnutou", + "@settingsKeepScreenOnDialogTitle": {}, + "albumGroupTier": "Podle úrovně", + "@albumGroupTier": {}, + "albumMimeTypeMixed": "Smíšené", + "@albumMimeTypeMixed": {}, + "settingsNavigationDrawerTabTypes": "Typy", + "@settingsNavigationDrawerTabTypes": {}, + "settingsNavigationDrawerTabAlbums": "Alba", + "@settingsNavigationDrawerTabAlbums": {}, + "settingsNavigationDrawerTabPages": "Stránky", + "@settingsNavigationDrawerTabPages": {}, + "settingsNavigationDrawerAddAlbum": "Přidat album", + "@settingsNavigationDrawerAddAlbum": {}, + "settingsThumbnailSectionTitle": "Náhledy", + "@settingsThumbnailSectionTitle": {}, + "settingsThumbnailShowFavouriteIcon": "Zobrazit ikonu oblíbených", + "@settingsThumbnailShowFavouriteIcon": {}, + "settingsThumbnailShowTagIcon": "Zobrazit ikonu štítků", + "@settingsThumbnailShowTagIcon": {}, + "settingsThumbnailShowLocationIcon": "Zobrazit ikonu polohy", + "@settingsThumbnailShowLocationIcon": {}, + "settingsCollectionQuickActionTabSelecting": "Výběr", + "@settingsCollectionQuickActionTabSelecting": {}, + "settingsCollectionBrowsingQuickActionEditorBanner": "Stiskněte a podržte pro přesun tlačítek a vyberte, které akce budou zobrazeny při procházení položek.", + "@settingsCollectionBrowsingQuickActionEditorBanner": {}, + "settingsCollectionSelectionQuickActionEditorBanner": "Stiskněte a podržte pro přesun tlačítek a vyberte, které akce budou zobrazeny při výběru položek.", + "@settingsCollectionSelectionQuickActionEditorBanner": {}, + "settingsViewerSectionTitle": "Prohlížeč", + "@settingsViewerSectionTitle": {}, + "settingsViewerGestureSideTapNext": "Stisknout v rozích obrazovky pro zobrazení předcházející/následující položky", + "@settingsViewerGestureSideTapNext": {}, + "settingsViewerUseCutout": "Použít oblast výřezu", + "@settingsViewerUseCutout": {}, + "settingsViewerMaximumBrightness": "Nejvyšší jas", + "@settingsViewerMaximumBrightness": {}, + "settingsMotionPhotoAutoPlay": "Automatické přehrávání pohyblivých fotografií", + "@settingsMotionPhotoAutoPlay": {}, + "settingsImageBackground": "Pozadí obrázku", + "@settingsImageBackground": {}, + "settingsViewerQuickActionsTile": "Rychlé akce", + "@settingsViewerQuickActionsTile": {}, + "settingsViewerQuickActionEditorPageTitle": "Rychlé akce", + "@settingsViewerQuickActionEditorPageTitle": {}, + "settingsViewerQuickActionEditorBanner": "Stiskněte a podržte pro přesun tlačítek a vyberte, které akce budou zobrazeny při prohlížení položek.", + "@settingsViewerQuickActionEditorBanner": {}, + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Zobrazená tlačítka", + "@settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": {}, + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Dostupná tlačítka", + "@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {}, + "settingsViewerQuickActionEmpty": "Žádná tlačítka", + "@settingsViewerQuickActionEmpty": {}, + "settingsViewerOverlayTile": "Souhrn", + "@settingsViewerOverlayTile": {}, + "settingsViewerOverlayPageTitle": "Souhrn", + "@settingsViewerOverlayPageTitle": {}, + "settingsThumbnailOverlayTile": "Zobrazené detaily", + "@settingsThumbnailOverlayTile": {}, + "settingsThumbnailOverlayPageTitle": "Zobrazené detaily", + "@settingsThumbnailOverlayPageTitle": {}, + "settingsViewerShowMinimap": "Zobrazit minimapu", + "@settingsViewerShowMinimap": {}, + "settingsViewerShowInformationSubtitle": "Zobrazit název, datum, polohu apod.", + "@settingsViewerShowInformationSubtitle": {}, + "settingsViewerShowShootingDetails": "Zobrazit podrobnosti o pořízené fogotrafii", + "@settingsViewerShowShootingDetails": {}, + "settingsViewerShowOverlayThumbnails": "Zobrazit náhledy", + "@settingsViewerShowOverlayThumbnails": {}, + "settingsViewerEnableOverlayBlurEffect": "Efekt rozostření", + "@settingsViewerEnableOverlayBlurEffect": {}, + "settingsViewerSlideshowTile": "Prezentace", + "@settingsViewerSlideshowTile": {}, + "settingsViewerSlideshowPageTitle": "Prezentace", + "@settingsViewerSlideshowPageTitle": {}, + "settingsSlideshowTransitionTile": "Přechod", + "@settingsSlideshowTransitionTile": {}, + "settingsSlideshowVideoPlaybackTile": "Přehrávání videa", + "@settingsSlideshowVideoPlaybackTile": {}, + "settingsSlideshowVideoPlaybackDialogTitle": "Přehrávání videa", + "@settingsSlideshowVideoPlaybackDialogTitle": {}, + "settingsVideoPageTitle": "Nastavení videa", + "@settingsVideoPageTitle": {}, + "settingsVideoSectionTitle": "Video", + "@settingsVideoSectionTitle": {}, + "settingsVideoShowVideos": "Zobrazovat videa", + "@settingsVideoShowVideos": {}, + "settingsVideoEnableHardwareAcceleration": "Hardwarová akcelerace", + "@settingsVideoEnableHardwareAcceleration": {}, + "settingsVideoAutoPlay": "Automatické přehrávání", + "@settingsVideoAutoPlay": {}, + "settingsVideoLoopModeTile": "Režim smyčky", + "@settingsVideoLoopModeTile": {}, + "settingsVideoLoopModeDialogTitle": "Režim smyčky", + "@settingsVideoLoopModeDialogTitle": {}, + "settingsSubtitleThemeTile": "Titulky", + "@settingsSubtitleThemeTile": {}, + "settingsSubtitleThemeTextAlignmentTile": "Zarovnání textu", + "@settingsSubtitleThemeTextAlignmentTile": {}, + "settingsSubtitleThemeTextAlignmentDialogTitle": "Zarovnání textu", + "@settingsSubtitleThemeTextAlignmentDialogTitle": {}, + "settingsSubtitleThemeShowOutline": "Zobrazovat obrys a stín", + "@settingsSubtitleThemeShowOutline": {}, + "settingsSubtitleThemeTextColor": "Barva textu", + "@settingsSubtitleThemeTextColor": {}, + "settingsSubtitleThemeTextOpacity": "Průhlednost textu", + "@settingsSubtitleThemeTextOpacity": {}, + "settingsSubtitleThemeBackgroundColor": "Barva pozadí", + "@settingsSubtitleThemeBackgroundColor": {}, + "settingsSubtitleThemeBackgroundOpacity": "Průhlednost pozadí", + "@settingsSubtitleThemeBackgroundOpacity": {}, + "settingsSubtitleThemeTextAlignmentLeft": "Doleva", + "@settingsSubtitleThemeTextAlignmentLeft": {}, + "settingsSubtitleThemeTextAlignmentCenter": "Na střed", + "@settingsSubtitleThemeTextAlignmentCenter": {}, + "settingsSubtitleThemeTextAlignmentRight": "Doprava", + "@settingsSubtitleThemeTextAlignmentRight": {}, + "settingsVideoControlsTile": "Ovládání", + "@settingsVideoControlsTile": {}, + "settingsVideoGestureDoubleTapTogglePlay": "Dvojité stisknutí pro spuštění / pauzu", + "@settingsVideoGestureDoubleTapTogglePlay": {}, + "settingsVideoGestureSideDoubleTapSeek": "Dvojití stisknutí v rozích obrazovky pro posun vzad / vpřed", + "@settingsVideoGestureSideDoubleTapSeek": {}, + "settingsAllowInstalledAppAccess": "Povolit přístup do aplikačního inventáře", + "@settingsAllowInstalledAppAccess": {}, + "settingsAllowInstalledAppAccessSubtitle": "Používá se pro zlepšení zobrazení alb", + "@settingsAllowInstalledAppAccessSubtitle": {}, + "settingsAllowErrorReporting": "Povolit anonymní chybová hlášení", + "@settingsAllowErrorReporting": {}, + "settingsSaveSearchHistory": "Uložit historii vyhledávání", + "@settingsSaveSearchHistory": {}, + "settingsEnableBin": "Používat koš", + "@settingsEnableBin": {}, + "settingsEnableBinSubtitle": "Ponechat v koši smazané položky po dobu 30 dní", + "@settingsEnableBinSubtitle": {}, + "settingsAllowMediaManagement": "Povolit správu médií", + "@settingsAllowMediaManagement": {}, + "settingsHiddenItemsTile": "Skryté položky", + "@settingsHiddenItemsTile": {}, + "settingsHiddenItemsPageTitle": "Skryté položky", + "@settingsHiddenItemsPageTitle": {}, + "settingsHiddenFiltersBanner": "Fotografie a videa odpovídající filtrům skrytých položek nebudou zobrazeny ve vaši sbírce.", + "@settingsHiddenFiltersBanner": {}, + "settingsHiddenItemsTabFilters": "Filtry položek", + "@settingsHiddenItemsTabFilters": {}, + "settingsHiddenFiltersEmpty": "Žádné filtry skrytých položek", + "@settingsHiddenFiltersEmpty": {}, + "settingsHiddenItemsTabPaths": "Filtry umístění", + "@settingsHiddenItemsTabPaths": {}, + "settingsHiddenPathsBanner": "Fotografie a videa v těchto adresářích a jejich podsložkách nebudou zobrazeny ve vaši sbírce.", + "@settingsHiddenPathsBanner": {}, + "addPathTooltip": "Přidat umístění", + "@addPathTooltip": {}, + "settingsStorageAccessTile": "Přístup k úložišti", + "@settingsStorageAccessTile": {}, + "settingsStorageAccessPageTitle": "Přístup k úložišti", + "@settingsStorageAccessPageTitle": {}, + "settingsStorageAccessEmpty": "Žádné povolené přístupy", + "@settingsStorageAccessEmpty": {}, + "settingsStorageAccessRevokeTooltip": "Odvolat", + "@settingsStorageAccessRevokeTooltip": {}, + "settingsAccessibilitySectionTitle": "Přístupnost", + "@settingsAccessibilitySectionTitle": {}, + "settingsRemoveAnimationsTile": "Zakázat animace", + "@settingsRemoveAnimationsTile": {}, + "settingsRemoveAnimationsDialogTitle": "Zakázat animace", + "@settingsRemoveAnimationsDialogTitle": {}, + "settingsTimeToTakeActionTile": "Čas k provedení akce", + "@settingsTimeToTakeActionTile": {}, + "settingsAccessibilityShowPinchGestureAlternatives": "Zobrazit alternativy vícedotykových gest", + "@settingsAccessibilityShowPinchGestureAlternatives": {}, + "settingsDisplaySectionTitle": "Zobrazení", + "@settingsDisplaySectionTitle": {}, + "settingsThemeBrightnessTile": "Vzhled", + "@settingsThemeBrightnessTile": {}, + "settingsThemeBrightnessDialogTitle": "Vzhled", + "@settingsThemeBrightnessDialogTitle": {}, + "settingsThemeColorHighlights": "Zvýraznění barev", + "@settingsThemeColorHighlights": {}, + "settingsThemeEnableDynamicColor": "Dynamické barvy", + "@settingsThemeEnableDynamicColor": {}, + "settingsDisplayRefreshRateModeTile": "Obnovovací frekvence displeje", + "@settingsDisplayRefreshRateModeTile": {}, + "settingsDisplayRefreshRateModeDialogTitle": "Obnovovací frekvence", + "@settingsDisplayRefreshRateModeDialogTitle": {}, + "settingsLanguageSectionTitle": "Jazyk a formáty", + "@settingsLanguageSectionTitle": {}, + "settingsLanguageTile": "Jazyk", + "@settingsLanguageTile": {}, + "settingsLanguagePageTitle": "Jazyk", + "@settingsLanguagePageTitle": {}, + "settingsCoordinateFormatTile": "Formát souřadnic", + "@settingsCoordinateFormatTile": {}, + "settingsCoordinateFormatDialogTitle": "Formát souřadnic", + "@settingsCoordinateFormatDialogTitle": {}, + "settingsUnitSystemDialogTitle": "Jednotky", + "@settingsUnitSystemDialogTitle": {}, + "settingsScreenSaverPageTitle": "Spořič displeje", + "@settingsScreenSaverPageTitle": {}, + "settingsWidgetPageTitle": "Rámeček fotografie", + "@settingsWidgetPageTitle": {}, + "settingsWidgetShowOutline": "Obrys", + "@settingsWidgetShowOutline": {}, + "settingsWidgetOpenPage": "Při stisknutí widgetu", + "@settingsWidgetOpenPage": {}, + "settingsWidgetDisplayedItem": "Zobrazená položka", + "@settingsWidgetDisplayedItem": {}, + "settingsCollectionTile": "Sbírka", + "@settingsCollectionTile": {}, + "statsPageTitle": "Statistiky", + "@statsPageTitle": {}, + "statsTopCountriesSectionTitle": "Nejčastější země", + "@statsTopCountriesSectionTitle": {}, + "statsTopPlacesSectionTitle": "Nejčastější místa", + "@statsTopPlacesSectionTitle": {}, + "statsTopTagsSectionTitle": "Nejčastější štítky", + "@statsTopTagsSectionTitle": {}, + "statsTopAlbumsSectionTitle": "Nejčastější alba", + "@statsTopAlbumsSectionTitle": {}, + "viewerOpenPanoramaButtonLabel": "OTEVŘÍT PANORAMA", + "@viewerOpenPanoramaButtonLabel": {}, + "viewerSetWallpaperButtonLabel": "NASTAVIT POZADÍ", + "@viewerSetWallpaperButtonLabel": {}, + "viewerErrorUnknown": "Jejda!", + "@viewerErrorUnknown": {}, + "viewerErrorDoesNotExist": "Soubor již neexistuje.", + "@viewerErrorDoesNotExist": {}, + "viewerInfoPageTitle": "Detaily", + "@viewerInfoPageTitle": {}, + "viewerInfoBackToViewerTooltip": "Zpět k prohlížení", + "@viewerInfoBackToViewerTooltip": {}, + "viewerInfoUnknown": "neznámý", + "@viewerInfoUnknown": {}, + "viewerInfoLabelDescription": "Popis", + "@viewerInfoLabelDescription": {}, + "viewerInfoLabelTitle": "Název", + "@viewerInfoLabelTitle": {}, + "viewerInfoLabelResolution": "Rozlišení", + "@viewerInfoLabelResolution": {}, + "viewerInfoLabelSize": "Velikost", + "@viewerInfoLabelSize": {}, + "viewerInfoLabelUri": "URI", + "@viewerInfoLabelUri": {}, + "viewerInfoLabelPath": "Umístění", + "@viewerInfoLabelPath": {}, + "viewerInfoLabelDuration": "Délka", + "@viewerInfoLabelDuration": {}, + "viewerInfoLabelCoordinates": "Souřadnice", + "@viewerInfoLabelCoordinates": {}, + "viewerInfoLabelAddress": "Adresa", + "@viewerInfoLabelAddress": {}, + "mapStyleDialogTitle": "Styl mapy", + "@mapStyleDialogTitle": {}, + "mapStyleTooltip": "Vyberte styl mapy", + "@mapStyleTooltip": {}, + "mapZoomInTooltip": "Přiblížit", + "@mapZoomInTooltip": {}, + "mapZoomOutTooltip": "Oddálit", + "@mapZoomOutTooltip": {}, + "mapPointNorthUpTooltip": "Sever nahoře", + "@mapPointNorthUpTooltip": {}, + "openMapPageTooltip": "Zobrazit na mapě", + "@openMapPageTooltip": {}, + "mapEmptyRegion": "Žádné obrázky v této oblasti", + "@mapEmptyRegion": {}, + "viewerInfoOpenEmbeddedFailureFeedback": "Nepodařilo se extrahovat vložená data", + "@viewerInfoOpenEmbeddedFailureFeedback": {}, + "viewerInfoOpenLinkText": "Otevřít", + "@viewerInfoOpenLinkText": {}, + "viewerInfoSearchFieldLabel": "Prohledat metadata", + "@viewerInfoSearchFieldLabel": {}, + "viewerInfoSearchEmpty": "Žádné odpovídající klíče", + "@viewerInfoSearchEmpty": {}, + "viewerInfoSearchSuggestionDate": "Datum a čas", + "@viewerInfoSearchSuggestionDate": {}, + "viewerInfoSearchSuggestionDescription": "Popis", + "@viewerInfoSearchSuggestionDescription": {}, + "viewerInfoSearchSuggestionResolution": "Rozlišení", + "@viewerInfoSearchSuggestionResolution": {}, + "wallpaperUseScrollEffect": "Použít rolovací efekt na domovské obrazovce", + "@wallpaperUseScrollEffect": {}, + "tagEditorPageTitle": "Upravit štítky", + "@tagEditorPageTitle": {}, + "mapAttributionOsmHot": "Mapová data © [OpenStreetMap](https://www.openstreetmap.org/copyright) přispěvatelé • Dlaždice z [HOT](https://www.hotosm.org/) • Hostováno na [OSM France](https://openstreetmap.fr/)", + "@mapAttributionOsmHot": {}, + "viewerInfoSearchSuggestionRights": "Práva", + "@viewerInfoSearchSuggestionRights": {}, + "tagEditorSectionPlaceholders": "Umístění", + "@tagEditorSectionPlaceholders": {}, + "tagPlaceholderCountry": "Země", + "@tagPlaceholderCountry": {}, + "tagPlaceholderPlace": "Místo", + "@tagPlaceholderPlace": {}, + "panoramaEnableSensorControl": "Povolit ovládání senzorem", + "@panoramaEnableSensorControl": {}, + "sourceViewerPageTitle": "Zdroj", + "@sourceViewerPageTitle": {}, + "filePickerShowHiddenFiles": "Zobrazit skryté soubory", + "@filePickerShowHiddenFiles": {}, + "filePickerDoNotShowHiddenFiles": "Nezobrazovat skryté soubory", + "@filePickerDoNotShowHiddenFiles": {}, + "filePickerOpenFrom": "Otevřít z", + "@filePickerOpenFrom": {}, + "filePickerNoItems": "Žádné položky", + "@filePickerNoItems": {}, + "filePickerUseThisFolder": "Použít tuto složku", + "@filePickerUseThisFolder": {}, + "pickTooltip": "Vybrat", + "@pickTooltip": {}, + "doubleBackExitMessage": "Pro ukončení klepněte znovu na „zpět“.", + "@doubleBackExitMessage": {}, + "mapStyleGoogleNormal": "Mapy Google", + "@mapStyleGoogleNormal": {}, + "mapStyleGoogleHybrid": "Mapy Google (satelitní)", + "@mapStyleGoogleHybrid": {}, + "displayRefreshRatePreferLowest": "Nejnižší", + "@displayRefreshRatePreferLowest": {}, + "nameConflictStrategySkip": "Přeskočit", + "@nameConflictStrategySkip": {}, + "keepScreenOnNever": "Nikdy", + "@keepScreenOnNever": {}, + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Přesunout tuto položku do koše?} =2..4{Přesunout tyto {count} položky do koše?} other{Přesunout těchto {count} položek do koše?}}", + "@binEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "setCoverDialogCustom": "Vlastní", + "@setCoverDialogCustom": {}, + "appPickDialogTitle": "Vybrat aplikaci", + "@appPickDialogTitle": {}, + "setCoverDialogAuto": "Automaticky", + "@setCoverDialogAuto": {}, + "hideFilterConfirmationDialogMessage": "Odpovídající fotografie a videa budou ve vaší sbírce schovány. Můžete je znovu zobrazit v nastavení \"Soukromí\".\n\nOpravdu je chcete skrýt?", + "@hideFilterConfirmationDialogMessage": {}, + "newAlbumDialogTitle": "Nové album", + "@newAlbumDialogTitle": {}, + "editEntryDateDialogTitle": "Datum a čas", + "@editEntryDateDialogTitle": {}, + "editEntryDateDialogSetCustom": "Nastavit vlastní datum", + "@editEntryDateDialogSetCustom": {}, + "aboutBugCopyInfoButton": "Kopírovat", + "@aboutBugCopyInfoButton": {}, + "viewDialogSortSectionTitle": "Řazení", + "@viewDialogSortSectionTitle": {}, + "coverDialogTabColor": "Barva", + "@coverDialogTabColor": {}, + "tileLayoutMosaic": "Mozaika", + "@tileLayoutMosaic": {}, + "coverDialogTabCover": "Přebal", + "@coverDialogTabCover": {}, + "coverDialogTabApp": "Aplikace", + "@coverDialogTabApp": {}, + "aboutPageTitle": "O aplikaci", + "@aboutPageTitle": {}, + "aboutBugSectionTitle": "Hlášení chyb", + "@aboutBugSectionTitle": {}, + "aboutLinkLicense": "Licence", + "@aboutLinkLicense": {}, + "appPickDialogNone": "Žádný", + "@appPickDialogNone": {}, + "aboutLinkPolicy": "Zásady ochrany osobních údajů", + "@aboutLinkPolicy": {}, + "aboutBugSaveLogInstruction": "Uložte aplikační logy do souboru", + "@aboutBugSaveLogInstruction": {}, + "aboutBugReportInstruction": "Nahlásit na GitHub s logy a informacemi o systému", + "@aboutBugReportInstruction": {}, + "aboutBugReportButton": "Nahlásit", + "@aboutBugReportButton": {}, + "aboutCreditsWorldAtlas2": "pod ISC licencí.", + "@aboutCreditsWorldAtlas2": {}, + "collectionEmptyFavourites": "Žádné oblíbené položky", + "@collectionEmptyFavourites": {}, + "albumScreenRecordings": "Nahrávky obrazovky", + "@albumScreenRecordings": {}, + "aboutCreditsSectionTitle": "Zásluhy", + "@aboutCreditsSectionTitle": {}, + "aboutBugCopyInfoInstruction": "Zkopírujte informace o systému", + "@aboutBugCopyInfoInstruction": {}, + "collectionMoveFailureFeedback": "{count, plural, =1{Chyba při přesouvání 1 položky} other{Chyba při přesouvání {count} položek}}", + "@collectionMoveFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionRenameFailureFeedback": "{count, plural, =1{Chyba přejmenování 1 položky} other{Chyba přejmenování {count} položek}}", + "@collectionRenameFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionExportFailureFeedback": "{count, plural, =1{Chyba při exportu 1 stránky} other{Chyba při exportu {count} stránek}}", + "@collectionExportFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "drawerCollectionRaws": "Fotografie Raw", + "@drawerCollectionRaws": {}, + "drawerTagPage": "Štítky", + "@drawerTagPage": {}, + "aboutCreditsWorldAtlas1": "Tato aplikace využívá soubor TopoJSON od", + "@aboutCreditsWorldAtlas1": {}, + "drawerCollectionSphericalVideos": "360° videa", + "@drawerCollectionSphericalVideos": {}, + "aboutLicensesBanner": "Tato aplikace využívá tyto open-source baličky a knihovny.", + "@aboutLicensesBanner": {}, + "collectionGroupNone": "Neseskupovat", + "@collectionGroupNone": {}, + "aboutLicensesSectionTitle": "Licence open-source", + "@aboutLicensesSectionTitle": {}, + "collectionActionHideTitleSearch": "Skrýt filtr dle názvu", + "@collectionActionHideTitleSearch": {}, + "aboutTranslatorsSectionTitle": "Překladatalé", + "@aboutTranslatorsSectionTitle": {}, + "aboutLicensesAndroidLibrariesSectionTitle": "Knihovny Androidu", + "@aboutLicensesAndroidLibrariesSectionTitle": {}, + "aboutLicensesFlutterPackagesSectionTitle": "Baličky Flutteru", + "@aboutLicensesFlutterPackagesSectionTitle": {}, + "policyPageTitle": "Zásady ochrany osobních údajů", + "@policyPageTitle": {}, + "collectionPageTitle": "Sbírky", + "@collectionPageTitle": {}, + "collectionPickPageTitle": "Výběr", + "@collectionPickPageTitle": {}, + "collectionActionShowTitleSearch": "Filtrovat dle názvu", + "@collectionActionShowTitleSearch": {}, + "collectionSelectPageTitle": "Vybrat položky", + "@collectionSelectPageTitle": {}, + "aboutLicensesFlutterPluginsSectionTitle": "Pluginy Flutteru", + "@aboutLicensesFlutterPluginsSectionTitle": {}, + "aboutLicensesDartPackagesSectionTitle": "Balíčky Dartu", + "@aboutLicensesDartPackagesSectionTitle": {}, + "aboutLicensesShowAllButtonLabel": "Zobrazit všechny licence", + "@aboutLicensesShowAllButtonLabel": {}, + "collectionActionEmptyBin": "Vysypat koš", + "@collectionActionEmptyBin": {}, + "collectionActionCopy": "Kopírovat do alba", + "@collectionActionCopy": {}, + "collectionActionMove": "Přesunout do alba", + "@collectionActionMove": {}, + "collectionActionEdit": "Upravit", + "@collectionActionEdit": {}, + "collectionCopyFailureFeedback": "{count, plural, =1{Chyba při kopírování 1 položky} other{Chyba při kopírování {count} položek}}", + "@collectionCopyFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionActionAddShortcut": "Vytvořit zástupce", + "@collectionActionAddShortcut": {}, + "collectionGroupDay": "Podle dne", + "@collectionGroupDay": {}, + "collectionSelectSectionTooltip": "Vybrat sekci", + "@collectionSelectSectionTooltip": {}, + "drawerAboutButton": "O aplikaci", + "@drawerAboutButton": {}, + "collectionActionRescan": "Znovu prohledat", + "@collectionActionRescan": {}, + "collectionSearchTitlesHintText": "Hledat názvy", + "@collectionSearchTitlesHintText": {}, + "collectionGroupAlbum": "Podle alba", + "@collectionGroupAlbum": {}, + "collectionGroupMonth": "Podle měsíce", + "@collectionGroupMonth": {}, + "sectionUnknown": "Neznámý", + "@sectionUnknown": {}, + "dateToday": "Dnes", + "@dateToday": {}, + "dateYesterday": "Včera", + "@dateYesterday": {}, + "dateThisMonth": "Tento měsíc", + "@dateThisMonth": {}, + "collectionDeleteFailureFeedback": "{count, plural, =1{Chyba při mazání 1 položky} other{Chyba při mazání {count} položek}}", + "@collectionDeleteFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "sortOrderLowestFirst": "Od nejnižšího", + "@sortOrderLowestFirst": {}, + "collectionEditFailureFeedback": "{count, plural, =1{Chyba při úpravě 1 položky} other{Chyba při úpravě {count} položek}}", + "@collectionEditFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionCopySuccessFeedback": "{count, plural, =1{Zkopírována 1 položka} =2..4{Zkopírovány {count} položky} other{Zkopírováno {count} položek}}", + "@collectionCopySuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveSuccessFeedback": "{count, plural, =1{Přesunuta 1 položka} =2..4{Přesunuty {count} položky} other{Přesunuto {count} položek}}", + "@collectionMoveSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionRenameSuccessFeedback": "{count, plural, =1{Přejmenována 1 položka} =2..4{Přejmenovány {count} položky} other{Přejmenováno {count} položek}}", + "@collectionRenameSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEditSuccessFeedback": "{count, plural, =1{Upravena 1 položka} =2..4{Upraveny {count} položky} other{Upraveno {count} položek}}", + "@collectionEditSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEmptyVideos": "Žádná videa", + "@collectionEmptyVideos": {}, + "collectionDeselectSectionTooltip": "Zrušit výběr sekce", + "@collectionDeselectSectionTooltip": {}, + "drawerSettingsButton": "Nastavení", + "@drawerSettingsButton": {}, + "drawerCollectionFavourites": "Oblíbené", + "@drawerCollectionFavourites": {}, + "drawerCollectionPanoramas": "Panoramata", + "@drawerCollectionPanoramas": {}, + "drawerCountryPage": "Země", + "@drawerCountryPage": {}, + "sortByDate": "Podle data", + "@sortByDate": {}, + "sortByName": "Abecedně", + "@sortByName": {}, + "sortByItemCount": "Podle počtu položek", + "@sortByItemCount": {}, + "sortBySize": "Podle velikosti", + "@sortBySize": {}, + "albumGroupVolume": "Podle úložiště", + "@albumGroupVolume": {}, + "drawerCollectionAll": "Celá sbírka", + "@drawerCollectionAll": {}, + "collectionEmptyImages": "Žádné obrázky", + "@collectionEmptyImages": {}, + "collectionEmptyGrantAccessButtonLabel": "Povolit přístup", + "@collectionEmptyGrantAccessButtonLabel": {}, + "drawerCollectionImages": "Obrázky", + "@drawerCollectionImages": {}, + "drawerAlbumPage": "Alba", + "@drawerAlbumPage": {}, + "sortOrderZtoA": "Od Z do A", + "@sortOrderZtoA": {}, + "sortOrderHighestFirst": "Od nejvyššího", + "@sortOrderHighestFirst": {}, + "albumGroupType": "Podle typu", + "@albumGroupType": {}, + "sortOrderNewestFirst": "Od nejnovějšího", + "@sortOrderNewestFirst": {}, + "sortOrderAtoZ": "Od A do Z", + "@sortOrderAtoZ": {}, + "sortOrderOldestFirst": "Od nejstaršího", + "@sortOrderOldestFirst": {}, + "albumPickPageTitleExport": "Exportovat do alba", + "@albumPickPageTitleExport": {}, + "albumCamera": "Fotoaparát", + "@albumCamera": {}, + "albumPageTitle": "Alba", + "@albumPageTitle": {}, + "albumEmpty": "Žádná alba", + "@albumEmpty": {}, + "createAlbumButtonLabel": "VYTVOŘIT", + "@createAlbumButtonLabel": {}, + "searchPlacesSectionTitle": "Místa", + "@searchPlacesSectionTitle": {}, + "sortOrderLargestFirst": "Od nejširšího", + "@sortOrderLargestFirst": {}, + "sortOrderSmallestFirst": "Od nejužšího", + "@sortOrderSmallestFirst": {}, + "albumGroupNone": "Neseskupovat", + "@albumGroupNone": {}, + "albumVideoCaptures": "Snímky videa", + "@albumVideoCaptures": {}, + "createAlbumTooltip": "Vytvořit album", + "@createAlbumTooltip": {}, + "countryPageTitle": "Země", + "@countryPageTitle": {}, + "searchCollectionFieldHint": "Prohledat sbírky", + "@searchCollectionFieldHint": {}, + "albumPickPageTitleCopy": "Kopírovat do alba", + "@albumPickPageTitleCopy": {}, + "tagPageTitle": "Štítky", + "@tagPageTitle": {}, + "albumPickPageTitleMove": "Přesunout do alba", + "@albumPickPageTitleMove": {}, + "albumPickPageTitlePick": "Vybrat album", + "@albumPickPageTitlePick": {}, + "albumDownload": "Stažené", + "@albumDownload": {}, + "albumScreenshots": "Snímky obrazovky", + "@albumScreenshots": {}, + "newFilterBanner": "nový", + "@newFilterBanner": {}, + "countryEmpty": "Žádné země", + "@countryEmpty": {}, + "tagEmpty": "Žádné štítky", + "@tagEmpty": {}, + "binPageTitle": "Koš", + "@binPageTitle": {}, + "searchMetadataSectionTitle": "Metadata", + "@searchMetadataSectionTitle": {}, + "searchRatingSectionTitle": "Hodnocení", + "@searchRatingSectionTitle": {}, + "settingsDoubleBackExit": "Klepnout dvakrát na „zpět“ pro ukončení", + "@settingsDoubleBackExit": {}, + "settingsConfirmationTile": "Potvrzovací dialogy", + "@settingsConfirmationTile": {}, + "settingsConfirmationBeforeDeleteItems": "Zeptat se před smazáním položek navždy", + "@settingsConfirmationBeforeDeleteItems": {}, + "settingsConfirmationAfterMoveToBinItems": "Zobrazit zprávu po přesunu položek do koše", + "@settingsConfirmationAfterMoveToBinItems": {}, + "searchRecentSectionTitle": "Nedávné", + "@searchRecentSectionTitle": {}, + "searchDateSectionTitle": "Datum", + "@searchDateSectionTitle": {}, + "searchAlbumsSectionTitle": "Alba", + "@searchAlbumsSectionTitle": {}, + "searchCountriesSectionTitle": "Země", + "@searchCountriesSectionTitle": {}, + "searchTagsSectionTitle": "Štítky", + "@searchTagsSectionTitle": {}, + "settingsNavigationDrawerTile": "Navigační menu", + "@settingsNavigationDrawerTile": {}, + "settingsNavigationDrawerEditorPageTitle": "Navigační menu", + "@settingsNavigationDrawerEditorPageTitle": {}, + "settingsCollectionQuickActionEditorPageTitle": "Rychlé akce", + "@settingsCollectionQuickActionEditorPageTitle": {}, + "settingsConfirmationDialogTitle": "Potvrzovací dialogy", + "@settingsConfirmationDialogTitle": {}, + "settingsConfirmationBeforeMoveUndatedItems": "Zeptat se před přesunem položek bez data", + "@settingsConfirmationBeforeMoveUndatedItems": {}, + "settingsConfirmationBeforeMoveToBinItems": "Zeptat se před přesunem položek do koše", + "@settingsConfirmationBeforeMoveToBinItems": {}, + "settingsNavigationDrawerBanner": "Stiskněte a podržte pro přesun položek a změnu jejich pořadí.", + "@settingsNavigationDrawerBanner": {}, + "settingsCollectionQuickActionTabBrowsing": "Procházení", + "@settingsCollectionQuickActionTabBrowsing": {}, + "settingsThumbnailShowMotionPhotoIcon": "Zobrazit ikonu pohyblivých fotografií", + "@settingsThumbnailShowMotionPhotoIcon": {}, + "settingsThumbnailShowRating": "Zobrazit hodnocení", + "@settingsThumbnailShowRating": {}, + "settingsThumbnailShowRawIcon": "Zobrazit ikonu RAW", + "@settingsThumbnailShowRawIcon": {}, + "settingsThumbnailShowVideoDuration": "Zobrazit délku videa", + "@settingsThumbnailShowVideoDuration": {}, + "settingsCollectionQuickActionsTile": "Rychlé akce", + "@settingsCollectionQuickActionsTile": {}, + "timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}", + "@timeMinutes": { + "placeholders": { + "minutes": {} + } + }, + "timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}", + "@timeSeconds": { + "placeholders": { + "seconds": {} + } + }, + "settingsViewerShowOverlayOnOpening": "Zobrazit při otevření", + "@settingsViewerShowOverlayOnOpening": {}, + "settingsViewerShowInformation": "Zobrazit informace", + "@settingsViewerShowInformation": {}, + "settingsViewerShowRatingTags": "Zobrazit hodnocení a štítky", + "@settingsViewerShowRatingTags": {}, + "settingsSlideshowRepeat": "Opakovat", + "@settingsSlideshowRepeat": {}, + "settingsSubtitleThemeTextPositionTile": "Pozice textu", + "@settingsSubtitleThemeTextPositionTile": {}, + "settingsSlideshowFillScreen": "Vyplnit obrazovku", + "@settingsSlideshowFillScreen": {}, + "settingsSlideshowShuffle": "Náhodně", + "@settingsSlideshowShuffle": {}, + "settingsSlideshowAnimatedZoomEffect": "Animovaný efekt přiblížení", + "@settingsSlideshowAnimatedZoomEffect": {}, + "settingsSlideshowIntervalTile": "Interval", + "@settingsSlideshowIntervalTile": {}, + "settingsSubtitleThemeSample": "Toto je příklad.", + "@settingsSubtitleThemeSample": {}, + "settingsSubtitleThemeTextSize": "Velikost textu", + "@settingsSubtitleThemeTextSize": {}, + "settingsSubtitleThemePageTitle": "Titulky", + "@settingsSubtitleThemePageTitle": {}, + "settingsSubtitleThemeTextPositionDialogTitle": "Pozice textu", + "@settingsSubtitleThemeTextPositionDialogTitle": {}, + "settingsVideoButtonsTile": "Tlačítka", + "@settingsVideoButtonsTile": {}, + "settingsVideoControlsPageTitle": "Ovládání", + "@settingsVideoControlsPageTitle": {}, + "settingsPrivacySectionTitle": "Soukromí", + "@settingsPrivacySectionTitle": {}, + "settingsStorageAccessBanner": "Některé adresáře vyžadují explicitní udělení přístupu k úpravě jejich souborů. Zde si můžete prohlédnout adresáře, ke kterým jste dříve udělili přístup.", + "@settingsStorageAccessBanner": {}, + "settingsUnitSystemTile": "Jednotky", + "@settingsUnitSystemTile": {}, + "statsWithGps": "{count, plural, =1{1 položka s polohou} =2..4{{count} položky s polohou} other{{count} položek s polohou}}", + "@statsWithGps": { + "placeholders": { + "count": {} + } + }, + "viewerInfoLabelDate": "Datum", + "@viewerInfoLabelDate": {}, + "viewerInfoLabelOwner": "Vlastník", + "@viewerInfoLabelOwner": {}, + "tagEditorPageAddTagTooltip": "Přidat štítek", + "@tagEditorPageAddTagTooltip": {}, + "viewerInfoViewXmlLinkText": "Zobrazit XML", + "@viewerInfoViewXmlLinkText": {}, + "viewerInfoSearchSuggestionDimensions": "Rozměry", + "@viewerInfoSearchSuggestionDimensions": {}, + "tagEditorPageNewTagFieldLabel": "Nový štítek", + "@tagEditorPageNewTagFieldLabel": {}, + "tagEditorSectionRecent": "Nedávné", + "@tagEditorSectionRecent": {}, + "mapAttributionStamen": "Mapová data © [OpenStreetMap](https://www.openstreetmap.org/copyright) přispěvatelé • Dlaždice z [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)", + "@mapAttributionStamen": {}, + "panoramaDisableSensorControl": "Zakázat ovládání senzorem", + "@panoramaDisableSensorControl": {} +} diff --git a/lib/l10n/app_el.arb b/lib/l10n/app_el.arb index 32692a0df..6214117be 100644 --- a/lib/l10n/app_el.arb +++ b/lib/l10n/app_el.arb @@ -1196,5 +1196,15 @@ "filterNoAddressLabel": "Χωρίς διεύθυνση", "@filterNoAddressLabel": {}, "settingsViewerShowRatingTags": "Εμφάνιση βαθμολογίας & ετικετών", - "@settingsViewerShowRatingTags": {} + "@settingsViewerShowRatingTags": {}, + "filterLocatedLabel": "Με τοποθεσία", + "@filterLocatedLabel": {}, + "filterTaggedLabel": "Με ετικέτα", + "@filterTaggedLabel": {}, + "settingsModificationWarningDialogMessage": "Άλλες ρυθμίσεις θα τροποποιηθούν.", + "@settingsModificationWarningDialogMessage": {}, + "settingsDisplayUseTvInterface": "Χρήση του Android TV περιβάλλον", + "@settingsDisplayUseTvInterface": {}, + "settingsViewerShowDescription": "Εμφάνιση περιγραφής", + "@settingsViewerShowDescription": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8f1c08b35..52c9ab346 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 95fc9ebc5..3cf489059 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -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": {} } diff --git a/lib/l10n/app_fa.arb b/lib/l10n/app_fa.arb index 242e48083..9c1c2e9a3 100644 --- a/lib/l10n/app_fa.arb +++ b/lib/l10n/app_fa.arb @@ -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": {} } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 1a08c095a..90c5e9903 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -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": {} } diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index da533b164..9b1209d1e 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -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": {} } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index eb05a73f9..a9577f7bc 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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": {} } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 708959f76..5a62c46eb 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -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": {} } diff --git a/lib/l10n/app_nn.arb b/lib/l10n/app_nn.arb index b838058b6..d4bd4a9a3 100644 --- a/lib/l10n/app_nn.arb +++ b/lib/l10n/app_nn.arb @@ -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": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index b8d2e39c1..d8bc6fbe4 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -13,7 +13,7 @@ "@resetTooltip": {}, "pickTooltip": "Wybierz", "@pickTooltip": {}, - "doubleBackExitMessage": "Tapnij ponownie \"wstecz\" aby wyjść.", + "doubleBackExitMessage": "Tapnij ponownie „wstecz” aby wyjść.", "@doubleBackExitMessage": {}, "saveTooltip": "Zapisz", "@saveTooltip": {}, @@ -184,5 +184,1185 @@ "entryActionInfo": "Informacje", "@entryActionInfo": {}, "entryActionRestore": "Przywróć", - "@entryActionRestore": {} + "@entryActionRestore": {}, + "subtitlePositionTop": "Na górze", + "@subtitlePositionTop": {}, + "wallpaperTargetHome": "Ekran główny", + "@wallpaperTargetHome": {}, + "nameConflictStrategySkip": "Pomiń", + "@nameConflictStrategySkip": {}, + "videoLoopModeAlways": "Zawsze", + "@videoLoopModeAlways": {}, + "filterLocatedLabel": "Usytuowany", + "@filterLocatedLabel": {}, + "filterTaggedLabel": "Oznaczony", + "@filterTaggedLabel": {}, + "nameConflictStrategyReplace": "Zastąp", + "@nameConflictStrategyReplace": {}, + "subtitlePositionBottom": "Na dole", + "@subtitlePositionBottom": {}, + "wallpaperTargetLock": "Ekran blokady", + "@wallpaperTargetLock": {}, + "videoPlaybackSkip": "Pomiń", + "@videoPlaybackSkip": {}, + "viewerTransitionNone": "Brak", + "@viewerTransitionNone": {}, + "filterAspectRatioPortraitLabel": "Portret", + "@filterAspectRatioPortraitLabel": {}, + "filterNoAddressLabel": "Brak adresu", + "@filterNoAddressLabel": {}, + "videoControlsPlaySeek": "Odtwórz i szukaj do przodu/do tyłu", + "@videoControlsPlaySeek": {}, + "videoControlsPlayOutside": "Otwórz w innym odtwarzaczu", + "@videoControlsPlayOutside": {}, + "mapStyleGoogleNormal": "Mapy Google", + "@mapStyleGoogleNormal": {}, + "videoLoopModeShortOnly": "Tylko krótkie wideo", + "@videoLoopModeShortOnly": {}, + "mapStyleHuaweiTerrain": "Mapy Petal (teren)", + "@mapStyleHuaweiTerrain": {}, + "mapStyleStamenToner": "Stamen Toner (czarno-białe)", + "@mapStyleStamenToner": {}, + "mapStyleStamenWatercolor": "Stamen Watercolor (kolory wody)", + "@mapStyleStamenWatercolor": {}, + "nameConflictStrategyRename": "Zmień nazwę", + "@nameConflictStrategyRename": {}, + "mapStyleOsmHot": "Humanitarny OSM", + "@mapStyleOsmHot": {}, + "keepScreenOnVideoPlayback": "Podczas odtwarzania wideo", + "@keepScreenOnVideoPlayback": {}, + "displayRefreshRatePreferLowest": "Najniższa", + "@displayRefreshRatePreferLowest": {}, + "videoPlaybackMuted": "Odtwarzaj bez dźwięku", + "@videoPlaybackMuted": {}, + "itemCount": "{count, plural, =1{1 element} =2..4{{count} elementy} other{{count} elelmentów}}", + "@itemCount": { + "placeholders": { + "count": {} + } + }, + "columnCount": "{count, plural, =1{1 rząd} =2..4{{count} rzędy} other{{count} rzędów}}", + "@columnCount": { + "placeholders": { + "count": {} + } + }, + "timeSeconds": "{seconds, plural, =1{1 sekunda} =2..4{{seconds} sekundy} other{{seconds} sekund}}", + "@timeSeconds": { + "placeholders": { + "seconds": {} + } + }, + "timeMinutes": "{minutes, plural, =1{1 minuta} =2..4{{minutes} minuty} other{{minutes} minut}}", + "@timeMinutes": { + "placeholders": { + "minutes": {} + } + }, + "timeDays": "{days, plural, =1{1 dzień} =2..4{{days} dni} other{{days} dni}}", + "@timeDays": { + "placeholders": { + "days": {} + } + }, + "focalLength": "{length} mm", + "@focalLength": { + "placeholders": { + "length": { + "type": "String", + "example": "5.4" + } + } + }, + "filterTypeRawLabel": "Raw", + "@filterTypeRawLabel": {}, + "filterTypeSphericalVideoLabel": "Wideo 360°", + "@filterTypeSphericalVideoLabel": {}, + "coordinateFormatDecimal": "Stopnie z miejscami dziesiętnymi", + "@coordinateFormatDecimal": {}, + "coordinateDmsSouth": "Pd", + "@coordinateDmsSouth": {}, + "coordinateDms": "{coordinate} {direction}", + "@coordinateDms": { + "placeholders": { + "coordinate": { + "type": "String", + "example": "38° 41′ 47.72″" + }, + "direction": { + "type": "String", + "example": "S" + } + } + }, + "coordinateDmsNorth": "Pn", + "@coordinateDmsNorth": {}, + "mapStyleGoogleHybrid": "Mapy Google (hybrydowe)", + "@mapStyleGoogleHybrid": {}, + "mapStyleGoogleTerrain": "Mapy Google (teren)", + "@mapStyleGoogleTerrain": {}, + "mapStyleHuaweiNormal": "Mapy Petal", + "@mapStyleHuaweiNormal": {}, + "keepScreenOnAlways": "Zawsze", + "@keepScreenOnAlways": {}, + "themeBrightnessLight": "Jasny", + "@themeBrightnessLight": {}, + "themeBrightnessDark": "Ciemny", + "@themeBrightnessDark": {}, + "themeBrightnessBlack": "Czarny", + "@themeBrightnessBlack": {}, + "viewerTransitionSlide": "Slajd", + "@viewerTransitionSlide": {}, + "viewerTransitionParallax": "Paralaksa", + "@viewerTransitionParallax": {}, + "entryActionShareImageOnly": "Udostępnij tylko zdjęcia", + "@entryActionShareImageOnly": {}, + "entryInfoActionRemoveLocation": "Usuń lokalizację", + "@entryInfoActionRemoveLocation": {}, + "entryActionShareVideoOnly": "Udostępnij tylko wideo", + "@entryActionShareVideoOnly": {}, + "filterAspectRatioLandscapeLabel": "Pejzaż", + "@filterAspectRatioLandscapeLabel": {}, + "filterTypeGeotiffLabel": "GeoTIFF", + "@filterTypeGeotiffLabel": {}, + "filterMimeImageLabel": "Obraz", + "@filterMimeImageLabel": {}, + "unitSystemImperial": "Imperialny", + "@unitSystemImperial": {}, + "videoLoopModeNever": "Nigdy", + "@videoLoopModeNever": {}, + "videoControlsNone": "Nic", + "@videoControlsNone": {}, + "accessibilityAnimationsRemove": "Zapobiegaj efektom ekranu", + "@accessibilityAnimationsRemove": {}, + "accessibilityAnimationsKeep": "Zachowaj efekty ekranu", + "@accessibilityAnimationsKeep": {}, + "displayRefreshRatePreferHighest": "Najwyższa", + "@displayRefreshRatePreferHighest": {}, + "keepScreenOnNever": "Nigdy", + "@keepScreenOnNever": {}, + "keepScreenOnViewerOnly": "Tylko na stronie przeglądarki", + "@keepScreenOnViewerOnly": {}, + "videoPlaybackWithSound": "Odtwarzaj z dźwiękiem", + "@videoPlaybackWithSound": {}, + "viewerTransitionFade": "Zanikanie", + "@viewerTransitionFade": {}, + "viewerTransitionZoomIn": "Powiększanie", + "@viewerTransitionZoomIn": {}, + "entryInfoActionExportMetadata": "Wyeksportuj metadane", + "@entryInfoActionExportMetadata": {}, + "filterMimeVideoLabel": "Wideo", + "@filterMimeVideoLabel": {}, + "coordinateFormatDms": "Stopnie, Minuty, Sekundy", + "@coordinateFormatDms": {}, + "coordinateDmsEast": "Ws", + "@coordinateDmsEast": {}, + "coordinateDmsWest": "Z", + "@coordinateDmsWest": {}, + "unitSystemMetric": "Metryczny", + "@unitSystemMetric": {}, + "videoControlsPlay": "Odtwórz", + "@videoControlsPlay": {}, + "albumTierPinned": "Przypięty", + "@albumTierPinned": {}, + "videoResumeButtonLabel": "WZNÓW", + "@videoResumeButtonLabel": {}, + "widgetDisplayedItemRandom": "Losowo", + "@widgetDisplayedItemRandom": {}, + "widgetOpenPageHome": "Otwórz stronę główną", + "@widgetOpenPageHome": {}, + "widgetOpenPageViewer": "Otwórz przeglądarkę", + "@widgetOpenPageViewer": {}, + "albumTierNew": "Nowy", + "@albumTierNew": {}, + "albumTierSpecial": "Wspólny", + "@albumTierSpecial": {}, + "albumTierApps": "Aplikacje", + "@albumTierApps": {}, + "storageVolumeDescriptionFallbackNonPrimary": "Karta SD", + "@storageVolumeDescriptionFallbackNonPrimary": {}, + "rootDirectoryDescription": "katalog główny", + "@rootDirectoryDescription": {}, + "storageAccessDialogMessage": "Wybierz {directory} lub „{volume}” na następnym ekranie, aby dać tej aplikacji dostęp do niego.", + "@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" + } + } + }, + "unsupportedTypeDialogMessage": "{count, plural, =1{Ta operacja nie jest obsługiwana dla elementów następującego typu: {types}.} other{Ta operacja nie jest obsługiwana dla elementów następujących typów: {types}.}}", + "@unsupportedTypeDialogMessage": { + "placeholders": { + "count": {}, + "types": { + "type": "String", + "example": "GIF, TIFF, MP4", + "description": "a list of unsupported types" + } + } + }, + "hideFilterConfirmationDialogMessage": "Pasujące zdjęcia i filmy zostaną ukryte w kolekcji. Możesz je ponownie wyświetlić w ustawieniach, w sekcji „Prywatność”.\n\nCzy na pewno chcesz je ukryć?", + "@hideFilterConfirmationDialogMessage": {}, + "wallpaperTargetHomeLock": "Ekran główny i blokady", + "@wallpaperTargetHomeLock": {}, + "widgetOpenPageCollection": "Otwórz kolekcję", + "@widgetOpenPageCollection": {}, + "albumTierRegular": "Inne", + "@albumTierRegular": {}, + "setCoverDialogLatest": "Ostatni element", + "@setCoverDialogLatest": {}, + "newAlbumDialogNameLabel": "Nazwa albumu", + "@newAlbumDialogNameLabel": {}, + "nameConflictDialogMultipleSourceMessage": "Niektóre pliki mają tę samą nazwę.", + "@nameConflictDialogMultipleSourceMessage": {}, + "videoResumeDialogMessage": "Czy chcesz wznowić odtwarzanie od {time}?", + "@videoResumeDialogMessage": { + "placeholders": { + "time": { + "type": "String", + "example": "13:37" + } + } + }, + "nameConflictDialogSingleSourceMessage": "Niektóre pliki w katalogu docelowym mają taką samą nazwę.", + "@nameConflictDialogSingleSourceMessage": {}, + "otherDirectoryDescription": "katalog „{name}”", + "@otherDirectoryDescription": { + "placeholders": { + "name": { + "type": "String", + "example": "Pictures", + "description": "the name of a specific directory" + } + } + }, + "addShortcutButtonLabel": "DODAJ", + "@addShortcutButtonLabel": {}, + "widgetDisplayedItemMostRecent": "Najnowszy", + "@widgetDisplayedItemMostRecent": {}, + "restrictedAccessDialogMessage": "Ta aplikacja nie może modyfikować plików w {directory} „{volume}”.\n\nUżyj wstępnie zainstalowanego menedżera plików lub aplikacji galerii, aby przenieść elementy do innego katalogu.", + "@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" + } + } + }, + "storageVolumeDescriptionFallbackPrimary": "Pamięć wewnętrzna", + "@storageVolumeDescriptionFallbackPrimary": {}, + "notEnoughSpaceDialogMessage": "Ta operacja wymaga {neededSize} wolnego miejsca na „{volume}”, aby ją ukończyć, ale pozostało tylko {freeSize}.", + "@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" + } + } + }, + "noMatchingAppDialogMessage": "Nie ma aplikacji, które mogłyby sobie z tym poradzić.", + "@noMatchingAppDialogMessage": {}, + "addShortcutDialogLabel": "Etykieta skrótu", + "@addShortcutDialogLabel": {}, + "binEntriesConfirmationDialogMessage": "{count, plural, =1{Przenieść ten element do Kosza?} other{Przenieść te elementy {count} do Kosza?}}", + "@binEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{Usunąć ten element?} other{Usunąć te elementy {count}?}}", + "@deleteEntriesConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "moveUndatedConfirmationDialogMessage": "Zapisać daty elementów przed kontynuacją?", + "@moveUndatedConfirmationDialogMessage": {}, + "newAlbumDialogTitle": "Nowy album", + "@newAlbumDialogTitle": {}, + "newAlbumDialogNameLabelAlreadyExistsHelper": "Katalog już istnieje", + "@newAlbumDialogNameLabelAlreadyExistsHelper": {}, + "missingSystemFilePickerDialogMessage": "Brak selektora plików systemowych lub jest on wyłączony. Włącz go i spróbuj ponownie.", + "@missingSystemFilePickerDialogMessage": {}, + "setCoverDialogAuto": "Automatycznie", + "@setCoverDialogAuto": {}, + "moveUndatedConfirmationDialogSetDate": "Zapisz daty", + "@moveUndatedConfirmationDialogSetDate": {}, + "videoStartOverButtonLabel": "ZACZNIJ OD NOWA", + "@videoStartOverButtonLabel": {}, + "setCoverDialogCustom": "Własny", + "@setCoverDialogCustom": {}, + "collectionActionCopy": "Kopiuj do albumu", + "@collectionActionCopy": {}, + "albumGroupNone": "Nie grupuj", + "@albumGroupNone": {}, + "sortOrderOldestFirst": "Najpierw najstarsze", + "@sortOrderOldestFirst": {}, + "searchDateSectionTitle": "Data", + "@searchDateSectionTitle": {}, + "albumPageTitle": "Albumy", + "@albumPageTitle": {}, + "countryPageTitle": "Kraje", + "@countryPageTitle": {}, + "sortOrderZtoA": "Od Z do A", + "@sortOrderZtoA": {}, + "dateYesterday": "Wczoraj", + "@dateYesterday": {}, + "videoStreamSelectionDialogAudio": "Audio", + "@videoStreamSelectionDialogAudio": {}, + "dateThisMonth": "W tym miesiącu", + "@dateThisMonth": {}, + "videoStreamSelectionDialogTrack": "Ścieżka", + "@videoStreamSelectionDialogTrack": {}, + "appPickDialogTitle": "Wybierz aplikację", + "@appPickDialogTitle": {}, + "newAlbumDialogStorageLabel": "Pamięć:", + "@newAlbumDialogStorageLabel": {}, + "renameAlbumDialogLabel": "Nowa nazwa", + "@renameAlbumDialogLabel": {}, + "renameEntrySetPagePatternFieldLabel": "Wzorzec nazewnictwa", + "@renameEntrySetPagePatternFieldLabel": {}, + "renameProcessorCounter": "Licznik", + "@renameProcessorCounter": {}, + "renameProcessorName": "Nazwa", + "@renameProcessorName": {}, + "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć ten album i jego element?} =2..4{Usunąć ten album i jego {count} elementy?} other{Usunąć ten album i jego {count} elementów?}}", + "@deleteSingleAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "editEntryDateDialogShift": "Zmiana", + "@editEntryDateDialogShift": {}, + "editEntryRatingDialogTitle": "Ocena", + "@editEntryRatingDialogTitle": {}, + "videoStreamSelectionDialogOff": "Wyłącz", + "@videoStreamSelectionDialogOff": {}, + "videoStreamSelectionDialogNoSelection": "Nie ma innych ścieżek.", + "@videoStreamSelectionDialogNoSelection": {}, + "genericSuccessFeedback": "Gotowe!", + "@genericSuccessFeedback": {}, + "genericFailureFeedback": "Niepowodzenie", + "@genericFailureFeedback": {}, + "genericDangerWarningDialogMessage": "Czy na pewno?", + "@genericDangerWarningDialogMessage": {}, + "tileLayoutMosaic": "Mozaika", + "@tileLayoutMosaic": {}, + "appPickDialogNone": "Brak", + "@appPickDialogNone": {}, + "aboutBugReportInstruction": "Zgłoś w usłudze GitHub z dziennikami i informacją o systemie", + "@aboutBugReportInstruction": {}, + "aboutCreditsSectionTitle": "Zasługi", + "@aboutCreditsSectionTitle": {}, + "aboutLicensesFlutterPackagesSectionTitle": "Pakiety Fluttera", + "@aboutLicensesFlutterPackagesSectionTitle": {}, + "aboutLicensesDartPackagesSectionTitle": "Pakiety Dart", + "@aboutLicensesDartPackagesSectionTitle": {}, + "collectionPageTitle": "Kolekcja", + "@collectionPageTitle": {}, + "collectionSelectPageTitle": "Wybierz elementy", + "@collectionSelectPageTitle": {}, + "collectionActionShowTitleSearch": "Pokaż filtr tytułu", + "@collectionActionShowTitleSearch": {}, + "collectionActionHideTitleSearch": "Ukryj filtr tytułu", + "@collectionActionHideTitleSearch": {}, + "collectionActionMove": "Przenieś do albumu", + "@collectionActionMove": {}, + "collectionActionRescan": "Przeskanuj", + "@collectionActionRescan": {}, + "collectionActionEdit": "Edytuj", + "@collectionActionEdit": {}, + "collectionSearchTitlesHintText": "Szukaj tytułów", + "@collectionSearchTitlesHintText": {}, + "collectionGroupMonth": "Według miesiąca", + "@collectionGroupMonth": {}, + "collectionGroupNone": "Nie grupuj", + "@collectionGroupNone": {}, + "sectionUnknown": "Nieznany", + "@sectionUnknown": {}, + "dateToday": "Dzisiaj", + "@dateToday": {}, + "collectionCopySuccessFeedback": "{count, plural, =1{Skopiowano 1 element} =2..4{Skopiowano {count} elementy} other{Skopiowano {count} elementów}}", + "@collectionCopySuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEditSuccessFeedback": "{count, plural, =1{Wyedytowano 1 element} =2..4{Wyedytowano {count} elementy} other{Wyedytowano {count} elementów}}", + "@collectionEditSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEmptyFavourites": "Brak ulubionych", + "@collectionEmptyFavourites": {}, + "collectionEmptyVideos": "Brak filmów", + "@collectionEmptyVideos": {}, + "sortByDate": "Według daty", + "@sortByDate": {}, + "sortByRating": "Według oceny", + "@sortByRating": {}, + "sortOrderNewestFirst": "Najpierw najnowsze", + "@sortOrderNewestFirst": {}, + "sortOrderHighestFirst": "Najpierw najwyższe", + "@sortOrderHighestFirst": {}, + "searchRatingSectionTitle": "Oceny", + "@searchRatingSectionTitle": {}, + "sortOrderLowestFirst": "Najpierw najniższe", + "@sortOrderLowestFirst": {}, + "sortOrderLargestFirst": "Najpierw największe", + "@sortOrderLargestFirst": {}, + "albumGroupVolume": "Według pojemności magazynu", + "@albumGroupVolume": {}, + "createAlbumButtonLabel": "UTWÓRZ", + "@createAlbumButtonLabel": {}, + "newFilterBanner": "nowy", + "@newFilterBanner": {}, + "countryEmpty": "Brak krajów", + "@countryEmpty": {}, + "tagPageTitle": "Znaczniki", + "@tagPageTitle": {}, + "searchMetadataSectionTitle": "Metadane", + "@searchMetadataSectionTitle": {}, + "settingsPageTitle": "Ustawienia", + "@settingsPageTitle": {}, + "exportEntryDialogHeight": "Wysokość", + "@exportEntryDialogHeight": {}, + "renameEntryDialogLabel": "Nowa nazwa", + "@renameEntryDialogLabel": {}, + "durationDialogMinutes": "Minuty", + "@durationDialogMinutes": {}, + "editEntryLocationDialogLongitude": "Długość geograficzna", + "@editEntryLocationDialogLongitude": {}, + "locationPickerUseThisLocationButton": "Użyj tej pozycji", + "@locationPickerUseThisLocationButton": {}, + "removeEntryMetadataDialogTitle": "Usuwanie metadanych", + "@removeEntryMetadataDialogTitle": {}, + "menuActionMap": "Mapa", + "@menuActionMap": {}, + "menuActionStats": "Statystyki", + "@menuActionStats": {}, + "viewDialogGroupSectionTitle": "Grupuj", + "@viewDialogGroupSectionTitle": {}, + "viewDialogLayoutSectionTitle": "Układ", + "@viewDialogLayoutSectionTitle": {}, + "viewDialogReverseSortOrder": "Odwróć porządek sortowania", + "@viewDialogReverseSortOrder": {}, + "coverDialogTabColor": "Kolor", + "@coverDialogTabColor": {}, + "aboutBugCopyInfoInstruction": "Skopiuj informację o systemie", + "@aboutBugCopyInfoInstruction": {}, + "aboutBugReportButton": "Zgłoś", + "@aboutBugReportButton": {}, + "aboutCreditsWorldAtlas2": "na licencji ISC.", + "@aboutCreditsWorldAtlas2": {}, + "aboutTranslatorsSectionTitle": "Tłumacze", + "@aboutTranslatorsSectionTitle": {}, + "aboutLicensesSectionTitle": "Licencje o otwartym kodzie", + "@aboutLicensesSectionTitle": {}, + "aboutLicensesBanner": "Ta aplikacja używa następujących pakietów i bibliotek z otwartym kodem.", + "@aboutLicensesBanner": {}, + "aboutLicensesAndroidLibrariesSectionTitle": "Biblioteki systemu Android", + "@aboutLicensesAndroidLibrariesSectionTitle": {}, + "aboutLicensesFlutterPluginsSectionTitle": "Wtyczki Fluttera", + "@aboutLicensesFlutterPluginsSectionTitle": {}, + "collectionActionAddShortcut": "Dodaj skrót", + "@collectionActionAddShortcut": {}, + "collectionActionEmptyBin": "Opróżnij kosz", + "@collectionActionEmptyBin": {}, + "collectionRenameSuccessFeedback": "{count, plural, =1{Zmieniono nazwę 1 elementowi} other{Zmieniono nazwę {count} elementom}}", + "@collectionRenameSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionEmptyImages": "Brak obrazów", + "@collectionEmptyImages": {}, + "collectionEmptyGrantAccessButtonLabel": "Przyznaj dostęp", + "@collectionEmptyGrantAccessButtonLabel": {}, + "sortByItemCount": "Według liczby elementów", + "@sortByItemCount": {}, + "sortBySize": "Według rozmiaru", + "@sortBySize": {}, + "createAlbumTooltip": "Utwórz album", + "@createAlbumTooltip": {}, + "albumEmpty": "Brak albumów", + "@albumEmpty": {}, + "renameEntrySetPageTitle": "Zmień nazwę", + "@renameEntrySetPageTitle": {}, + "durationDialogHours": "Godziny", + "@durationDialogHours": {}, + "editEntryLocationDialogLatitude": "Szerokość geograficzna", + "@editEntryLocationDialogLatitude": {}, + "removeEntryMetadataDialogMore": "Więcej", + "@removeEntryMetadataDialogMore": {}, + "viewDialogSortSectionTitle": "Sortuj", + "@viewDialogSortSectionTitle": {}, + "aboutCreditsWorldAtlas1": "Ta aplikacja używa pliku TopoJSON z", + "@aboutCreditsWorldAtlas1": {}, + "policyPageTitle": "Polityka prywatności", + "@policyPageTitle": {}, + "drawerCollectionImages": "Obrazy", + "@drawerCollectionImages": {}, + "drawerCollectionVideos": "Wideo", + "@drawerCollectionVideos": {}, + "drawerCountryPage": "Kraje", + "@drawerCountryPage": {}, + "drawerTagPage": "Znaczniki", + "@drawerTagPage": {}, + "albumGroupTier": "Według poziomu", + "@albumGroupTier": {}, + "sortOrderSmallestFirst": "Najpierw najmniejsze", + "@sortOrderSmallestFirst": {}, + "albumPickPageTitleMove": "Przenieś do albumu", + "@albumPickPageTitleMove": {}, + "albumPickPageTitlePick": "Wybierz album", + "@albumPickPageTitlePick": {}, + "albumScreenshots": "Zrzuty ekranu", + "@albumScreenshots": {}, + "albumCamera": "Kamera", + "@albumCamera": {}, + "albumDownload": "Pobrane", + "@albumDownload": {}, + "settingsDisabled": "Wyłączono", + "@settingsDisabled": {}, + "editEntryLocationDialogTitle": "Pozycja", + "@editEntryLocationDialogTitle": {}, + "menuActionSlideshow": "Pokaz slajdów", + "@menuActionSlideshow": {}, + "collectionEditFailureFeedback": "{count, plural, =1{Nie udało się wyedytować 1 elementu} other{Nie udało się wyedytować {count} elementów}}", + "@collectionEditFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "drawerCollectionAnimated": "Animacje", + "@drawerCollectionAnimated": {}, + "drawerCollectionMotionPhotos": "Animowane zdjęcia", + "@drawerCollectionMotionPhotos": {}, + "collectionSelectSectionTooltip": "Zaznacz sekcję", + "@collectionSelectSectionTooltip": {}, + "drawerCollectionPanoramas": "Zdjęcia panoramiczne", + "@drawerCollectionPanoramas": {}, + "drawerCollectionRaws": "Nieprzetworzone zdjęcia", + "@drawerCollectionRaws": {}, + "sortByAlbumFileName": "Według albumu i nazwy pliku", + "@sortByAlbumFileName": {}, + "albumMimeTypeMixed": "Mieszane", + "@albumMimeTypeMixed": {}, + "albumPickPageTitleCopy": "Skopiuj do albumu", + "@albumPickPageTitleCopy": {}, + "albumPickPageTitleExport": "Wyeksportuj do albumu", + "@albumPickPageTitleExport": {}, + "tagEmpty": "Bez znaczników", + "@tagEmpty": {}, + "searchCountriesSectionTitle": "Kraje", + "@searchCountriesSectionTitle": {}, + "searchPlacesSectionTitle": "Miejsca", + "@searchPlacesSectionTitle": {}, + "searchTagsSectionTitle": "Znaczniki", + "@searchTagsSectionTitle": {}, + "exportEntryDialogWidth": "Szerokość", + "@exportEntryDialogWidth": {}, + "aboutBugSectionTitle": "Raport o błędach", + "@aboutBugSectionTitle": {}, + "aboutBugSaveLogInstruction": "Zapisz dzienniki aplikacji w pliku", + "@aboutBugSaveLogInstruction": {}, + "albumVideoCaptures": "Przechwycone wideo", + "@albumVideoCaptures": {}, + "searchCollectionFieldHint": "Wyszukaj kolekcję", + "@searchCollectionFieldHint": {}, + "binPageTitle": "Kosz", + "@binPageTitle": {}, + "editEntryDialogCopyFromItem": "Kopiuj z innego elementu", + "@editEntryDialogCopyFromItem": {}, + "editEntryDialogTargetFieldsHeader": "Pola do modyfikacji", + "@editEntryDialogTargetFieldsHeader": {}, + "editEntryDateDialogTitle": "Data i godzina", + "@editEntryDateDialogTitle": {}, + "editEntryDateDialogExtractFromTitle": "Wydobądź z tytułu", + "@editEntryDateDialogExtractFromTitle": {}, + "menuActionSelectAll": "Zaznacz wszystkie", + "@menuActionSelectAll": {}, + "removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP jest wymagane do odtworzenia wideo wewnątrz zdjęcia.\n\nCzy na pewno chcesz go usunąć?", + "@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {}, + "tileLayoutGrid": "Siatka", + "@tileLayoutGrid": {}, + "aboutBugCopyInfoButton": "Kopiuj", + "@aboutBugCopyInfoButton": {}, + "albumGroupType": "Według typu", + "@albumGroupType": {}, + "renameEntrySetPagePreviewSectionTitle": "Podgląd", + "@renameEntrySetPagePreviewSectionTitle": {}, + "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Usunąć te albumy i ich element?} =2..4{Usunąć te albumy i ich {count} elementy?} other{Usunąć te albumy i ich {count} elementów?}}", + "@deleteMultiAlbumConfirmationDialogMessage": { + "placeholders": { + "count": {} + } + }, + "renameAlbumDialogLabelAlreadyExistsHelper": "Katalog już istnieje", + "@renameAlbumDialogLabelAlreadyExistsHelper": {}, + "renameEntrySetPageInsertTooltip": "Wstaw pole", + "@renameEntrySetPageInsertTooltip": {}, + "exportEntryDialogFormat": "Format:", + "@exportEntryDialogFormat": {}, + "editEntryDateDialogSetCustom": "Ustaw własną datę", + "@editEntryDateDialogSetCustom": {}, + "editEntryDateDialogCopyField": "Skopiuj z innej daty", + "@editEntryDateDialogCopyField": {}, + "durationDialogSeconds": "Sekundy", + "@durationDialogSeconds": {}, + "editEntryLocationDialogChooseOnMap": "Wybierz z mapy", + "@editEntryLocationDialogChooseOnMap": {}, + "collectionGroupDay": "Według dnia", + "@collectionGroupDay": {}, + "editEntryDateDialogSourceFileModifiedDate": "Data modyfikacji pliku", + "@editEntryDateDialogSourceFileModifiedDate": {}, + "editEntryLocationDialogSetCustom": "Ustaw własną pozycję", + "@editEntryLocationDialogSetCustom": {}, + "videoStreamSelectionDialogVideo": "Wideo", + "@videoStreamSelectionDialogVideo": {}, + "videoStreamSelectionDialogText": "Napisy", + "@videoStreamSelectionDialogText": {}, + "menuActionSelect": "Zaznacz", + "@menuActionSelect": {}, + "videoSpeedDialogLabel": "Szybkość odtwarzania", + "@videoSpeedDialogLabel": {}, + "aboutPageTitle": "O aplikacji", + "@aboutPageTitle": {}, + "menuActionConfigureView": "Widok", + "@menuActionConfigureView": {}, + "menuActionSelectNone": "Odznacz", + "@menuActionSelectNone": {}, + "tileLayoutList": "Lista", + "@tileLayoutList": {}, + "coverDialogTabCover": "Okładka", + "@coverDialogTabCover": {}, + "coverDialogTabApp": "Aplikacja", + "@coverDialogTabApp": {}, + "aboutLinkLicense": "Licencje", + "@aboutLinkLicense": {}, + "aboutLinkPolicy": "Polityka prywatności", + "@aboutLinkPolicy": {}, + "aboutLicensesShowAllButtonLabel": "Pokaż wszystkie licencje", + "@aboutLicensesShowAllButtonLabel": {}, + "collectionPickPageTitle": "Wybierz", + "@collectionPickPageTitle": {}, + "collectionGroupAlbum": "Według albumu", + "@collectionGroupAlbum": {}, + "collectionDeleteFailureFeedback": "{count, plural, =1{Nie udało się usunąć 1 elementu} other{Nie udało się usunąć {count} elementów}}", + "@collectionDeleteFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionCopyFailureFeedback": "{count, plural, =1{Nie udało się skopiować 1 elementu} other{Nie udało się skopiować {count} elementów}}", + "@collectionCopyFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveFailureFeedback": "{count, plural, =1{Nie udało się przenieść 1 elementu} other{Nie udało się przenieść {count} elementów}}", + "@collectionMoveFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionRenameFailureFeedback": "{count, plural, =1{Nie udało się zmienić nazwy 1 elementowi} other{Nie udało się zmienić nazwy {count} elementom}}", + "@collectionRenameFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "albumScreenRecordings": "Nagrania ekranu", + "@albumScreenRecordings": {}, + "collectionExportFailureFeedback": "{count, plural, =1{Nie udało się wyeksportować 1 strony} other{Nie udało się wyeksportować{count} stron}}", + "@collectionExportFailureFeedback": { + "placeholders": { + "count": {} + } + }, + "collectionMoveSuccessFeedback": "{count, plural, =1{Przeniesiono 1 element} =2..4{Przeniesiono {count} elementy} other{Przeniesiono {count} elementów}}", + "@collectionMoveSuccessFeedback": { + "placeholders": { + "count": {} + } + }, + "drawerCollectionAll": "Wszystkie kolekcje", + "@drawerCollectionAll": {}, + "drawerSettingsButton": "Ustawienia", + "@drawerSettingsButton": {}, + "drawerCollectionFavourites": "Ulubione", + "@drawerCollectionFavourites": {}, + "sortByName": "Według nazwy", + "@sortByName": {}, + "searchAlbumsSectionTitle": "Albumy", + "@searchAlbumsSectionTitle": {}, + "settingsSystemDefault": "Ustawienie systemu", + "@settingsSystemDefault": {}, + "settingsModificationWarningDialogMessage": "Pozostałe ustawienia zostaną zmodyfikowane.", + "@settingsModificationWarningDialogMessage": {}, + "settingsDefault": "Domyślne", + "@settingsDefault": {}, + "collectionDeselectSectionTooltip": "Odznacz sekcję", + "@collectionDeselectSectionTooltip": {}, + "drawerAboutButton": "O programie", + "@drawerAboutButton": {}, + "drawerCollectionSphericalVideos": "Wideo 360°", + "@drawerCollectionSphericalVideos": {}, + "drawerAlbumPage": "Albumy", + "@drawerAlbumPage": {}, + "sortOrderAtoZ": "Od A do Z", + "@sortOrderAtoZ": {}, + "searchRecentSectionTitle": "Ostatni", + "@searchRecentSectionTitle": {}, + "settingsSlideshowShuffle": "Mieszaj", + "@settingsSlideshowShuffle": {}, + "settingsWidgetDisplayedItem": "Wyświetlany element", + "@settingsWidgetDisplayedItem": {}, + "statsPageTitle": "Statystyki", + "@statsPageTitle": {}, + "settingsViewerShowOverlayThumbnails": "Pokaż miniaturki", + "@settingsViewerShowOverlayThumbnails": {}, + "settingsSearchFieldLabel": "Wyszukaj ustawienia", + "@settingsSearchFieldLabel": {}, + "settingsActionImport": "Zaimportuj", + "@settingsActionImport": {}, + "appExportFavourites": "Ulubione", + "@appExportFavourites": {}, + "appExportSettings": "Ustawienia", + "@appExportSettings": {}, + "settingsHomeDialogTitle": "Strona główna", + "@settingsHomeDialogTitle": {}, + "settingsShowBottomNavigationBar": "Pokaż dolny pasek nawigacyjny", + "@settingsShowBottomNavigationBar": {}, + "settingsDoubleBackExit": "Dotknij dwa razy „wstecz”, aby wyjść", + "@settingsDoubleBackExit": {}, + "settingsNavigationDrawerTabTypes": "Kategorie", + "@settingsNavigationDrawerTabTypes": {}, + "settingsNavigationDrawerAddAlbum": "Dodaj album", + "@settingsNavigationDrawerAddAlbum": {}, + "settingsConfirmationTile": "Działania do potwierdzenia", + "@settingsConfirmationTile": {}, + "settingsThumbnailShowTagIcon": "Pokaż ikonę znacznika", + "@settingsThumbnailShowTagIcon": {}, + "settingsThumbnailShowLocationIcon": "Pokaż ikonę położenia", + "@settingsThumbnailShowLocationIcon": {}, + "settingsThumbnailShowRawIcon": "Pokaż ikonę RAW", + "@settingsThumbnailShowRawIcon": {}, + "settingsCollectionSelectionQuickActionEditorBanner": "Dotknij i przytrzymaj, aby przesuwać przyciski i wybrać akcje, które mają być wyświetlane podczas wybierania elementów.", + "@settingsCollectionSelectionQuickActionEditorBanner": {}, + "settingsViewerGestureSideTapNext": "Dotknij krawędzi ekranu, aby wyświetlić poprzedni / następny element", + "@settingsViewerGestureSideTapNext": {}, + "settingsViewerQuickActionEditorPageTitle": "Szybkie działania", + "@settingsViewerQuickActionEditorPageTitle": {}, + "settingsSlideshowVideoPlaybackDialogTitle": "Odtwarzanie wideo", + "@settingsSlideshowVideoPlaybackDialogTitle": {}, + "settingsVideoPageTitle": "Ustawienia wideo", + "@settingsVideoPageTitle": {}, + "settingsVideoLoopModeTile": "Tryb pętli", + "@settingsVideoLoopModeTile": {}, + "settingsVideoLoopModeDialogTitle": "Tryb pętli", + "@settingsVideoLoopModeDialogTitle": {}, + "settingsSubtitleThemeTextAlignmentTile": "Dopasowanie tekstu", + "@settingsSubtitleThemeTextAlignmentTile": {}, + "settingsSubtitleThemeTextPositionDialogTitle": "Pozycja tekstu", + "@settingsSubtitleThemeTextPositionDialogTitle": {}, + "settingsSubtitleThemeTextSize": "Rozmiar tekstu", + "@settingsSubtitleThemeTextSize": {}, + "settingsSubtitleThemeBackgroundColor": "Kolor tła", + "@settingsSubtitleThemeBackgroundColor": {}, + "settingsSubtitleThemeTextAlignmentLeft": "Lewo", + "@settingsSubtitleThemeTextAlignmentLeft": {}, + "settingsVideoControlsTile": "Sterowanie", + "@settingsVideoControlsTile": {}, + "settingsVideoGestureDoubleTapTogglePlay": "Dotknij dwukrotnie, aby odtworzyć/wstrzymać", + "@settingsVideoGestureDoubleTapTogglePlay": {}, + "settingsPrivacySectionTitle": "Prywatność", + "@settingsPrivacySectionTitle": {}, + "settingsAllowInstalledAppAccess": "Zezwól na dostęp do spisu aplikacji", + "@settingsAllowInstalledAppAccess": {}, + "settingsAllowErrorReporting": "Pozwól na anonimowe zgłaszanie błędów", + "@settingsAllowErrorReporting": {}, + "settingsSaveSearchHistory": "Zapisz historię wyszukiwania", + "@settingsSaveSearchHistory": {}, + "settingsHiddenItemsTabPaths": "Ukryte ścieżki", + "@settingsHiddenItemsTabPaths": {}, + "settingsStorageAccessTile": "Dostęp do pamięci masowej", + "@settingsStorageAccessTile": {}, + "settingsRemoveAnimationsTile": "Usuń animacje", + "@settingsRemoveAnimationsTile": {}, + "settingsDisplayUseTvInterface": "Interfejs Android TV", + "@settingsDisplayUseTvInterface": {}, + "settingsLanguagePageTitle": "Język", + "@settingsLanguagePageTitle": {}, + "settingsScreenSaverPageTitle": "Wygaszacz ekranu", + "@settingsScreenSaverPageTitle": {}, + "settingsWidgetPageTitle": "Ramka zdjęcia", + "@settingsWidgetPageTitle": {}, + "settingsWidgetOpenPage": "Po dotknięciu widżetu", + "@settingsWidgetOpenPage": {}, + "statsTopAlbumsSectionTitle": "Najlepsze albumy", + "@statsTopAlbumsSectionTitle": {}, + "viewerOpenPanoramaButtonLabel": "OTWÓRZ PANORAMĘ", + "@viewerOpenPanoramaButtonLabel": {}, + "viewerErrorUnknown": "Ups!", + "@viewerErrorUnknown": {}, + "viewerErrorDoesNotExist": "Plik już nie istnieje.", + "@viewerErrorDoesNotExist": {}, + "viewerInfoLabelResolution": "Rozdzielczość", + "@viewerInfoLabelResolution": {}, + "viewerInfoLabelSize": "Rozmiar", + "@viewerInfoLabelSize": {}, + "viewerInfoLabelPath": "Ścieżka", + "@viewerInfoLabelPath": {}, + "mapZoomInTooltip": "Powiększ", + "@mapZoomInTooltip": {}, + "mapAttributionStamen": "Dane mapy © [OpenStreetMap](https://www.openstreetmap.org/copyright) współtwórcy • Kafelki od [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)", + "@mapAttributionStamen": {}, + "filePickerUseThisFolder": "Użyj tego katalogu", + "@filePickerUseThisFolder": {}, + "mapEmptyRegion": "Brak obrazów w tym regionie", + "@mapEmptyRegion": {}, + "settingsKeepScreenOnTile": "Pozostaw ekran załączony", + "@settingsKeepScreenOnTile": {}, + "filePickerOpenFrom": "Otwórz z", + "@filePickerOpenFrom": {}, + "filePickerNoItems": "Brak elementów", + "@filePickerNoItems": {}, + "viewerInfoSearchSuggestionRights": "Prawa", + "@viewerInfoSearchSuggestionRights": {}, + "filePickerDoNotShowHiddenFiles": "Nie pokazuj ukrytych plików", + "@filePickerDoNotShowHiddenFiles": {}, + "settingsActionImportDialogTitle": "Zaimportuj", + "@settingsActionImportDialogTitle": {}, + "settingsKeepScreenOnDialogTitle": "Pozostaw ekran załączony", + "@settingsKeepScreenOnDialogTitle": {}, + "settingsNavigationDrawerTile": "Menu nawigacyjne", + "@settingsNavigationDrawerTile": {}, + "settingsThumbnailSectionTitle": "Miniatury", + "@settingsThumbnailSectionTitle": {}, + "settingsCollectionQuickActionTabSelecting": "Wybieranie", + "@settingsCollectionQuickActionTabSelecting": {}, + "settingsThumbnailShowVideoDuration": "Pokaż czas trwania wideo", + "@settingsThumbnailShowVideoDuration": {}, + "settingsViewerUseCutout": "Użyj obszaru wycięcia", + "@settingsViewerUseCutout": {}, + "settingsViewerMaximumBrightness": "Jasność maksymalna", + "@settingsViewerMaximumBrightness": {}, + "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "Wyświetlane przyciski", + "@settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": {}, + "settingsViewerSlideshowTile": "Pokaz slajdów", + "@settingsViewerSlideshowTile": {}, + "settingsSlideshowFillScreen": "Wypełnij ekran", + "@settingsSlideshowFillScreen": {}, + "settingsSlideshowIntervalTile": "Interwał", + "@settingsSlideshowIntervalTile": {}, + "settingsVideoEnableHardwareAcceleration": "Przyspieszenie sprzętowe", + "@settingsVideoEnableHardwareAcceleration": {}, + "settingsVideoAutoPlay": "Odtwarzaj automatycznie", + "@settingsVideoAutoPlay": {}, + "settingsSubtitleThemeSample": "To jest próbka.", + "@settingsSubtitleThemeSample": {}, + "settingsSubtitleThemeTextAlignmentDialogTitle": "Dopasowanie tekstu", + "@settingsSubtitleThemeTextAlignmentDialogTitle": {}, + "settingsSubtitleThemeTextPositionTile": "Pozycja tekstu", + "@settingsSubtitleThemeTextPositionTile": {}, + "settingsSubtitleThemeBackgroundOpacity": "Krycie tła", + "@settingsSubtitleThemeBackgroundOpacity": {}, + "settingsSubtitleThemeTextAlignmentCenter": "Środek", + "@settingsSubtitleThemeTextAlignmentCenter": {}, + "settingsSubtitleThemeTextAlignmentRight": "Prawo", + "@settingsSubtitleThemeTextAlignmentRight": {}, + "settingsEnableBinSubtitle": "Zachowaj usunięte elementy przez 30 dni", + "@settingsEnableBinSubtitle": {}, + "settingsAllowMediaManagement": "Zezwalaj na zarządzanie multimediami", + "@settingsAllowMediaManagement": {}, + "settingsHiddenFiltersBanner": "Zdjęcia i wideo pasujące do ukrytych filtrów nie pojawią się w twojej kolekcji.", + "@settingsHiddenFiltersBanner": {}, + "settingsHiddenFiltersEmpty": "Brak ukrytych filtrów", + "@settingsHiddenFiltersEmpty": {}, + "settingsStorageAccessEmpty": "Brak przyznanych uprawnień", + "@settingsStorageAccessEmpty": {}, + "settingsAccessibilityShowPinchGestureAlternatives": "Pokaż alternatywy gestów wielodotykowych", + "@settingsAccessibilityShowPinchGestureAlternatives": {}, + "settingsThemeBrightnessDialogTitle": "Motyw", + "@settingsThemeBrightnessDialogTitle": {}, + "settingsThemeColorHighlights": "Podkreślenia kolorów", + "@settingsThemeColorHighlights": {}, + "settingsThemeEnableDynamicColor": "Kolor dynamiczny", + "@settingsThemeEnableDynamicColor": {}, + "settingsDisplayRefreshRateModeTile": "Częstotliwość odświeżania ekranu", + "@settingsDisplayRefreshRateModeTile": {}, + "settingsDisplayRefreshRateModeDialogTitle": "Częstotliwość odświeżania", + "@settingsDisplayRefreshRateModeDialogTitle": {}, + "settingsLanguageSectionTitle": "Język i formaty", + "@settingsLanguageSectionTitle": {}, + "settingsCoordinateFormatTile": "Format współrzędnych", + "@settingsCoordinateFormatTile": {}, + "settingsUnitSystemDialogTitle": "Jednostki", + "@settingsUnitSystemDialogTitle": {}, + "viewerSetWallpaperButtonLabel": "USTAW TAPETĘ", + "@viewerSetWallpaperButtonLabel": {}, + "viewerInfoLabelAddress": "Adres", + "@viewerInfoLabelAddress": {}, + "mapStyleTooltip": "Wybierz styl mapy", + "@mapStyleTooltip": {}, + "mapStyleDialogTitle": "Styl mapy", + "@mapStyleDialogTitle": {}, + "wallpaperUseScrollEffect": "Użyj efektu przewijania na ekranie głównym", + "@wallpaperUseScrollEffect": {}, + "tagEditorSectionRecent": "Ostatnie", + "@tagEditorSectionRecent": {}, + "sourceViewerPageTitle": "Źródło", + "@sourceViewerPageTitle": {}, + "viewerInfoSearchEmpty": "Brak pasujących kluczy", + "@viewerInfoSearchEmpty": {}, + "settingsStorageAccessRevokeTooltip": "Odwołaj", + "@settingsStorageAccessRevokeTooltip": {}, + "settingsAccessibilitySectionTitle": "Dostępność", + "@settingsAccessibilitySectionTitle": {}, + "viewerInfoOpenLinkText": "Otwórz", + "@viewerInfoOpenLinkText": {}, + "tagEditorSectionPlaceholders": "Symbole zastępcze", + "@tagEditorSectionPlaceholders": {}, + "viewerInfoPageTitle": "Informacje", + "@viewerInfoPageTitle": {}, + "viewerInfoViewXmlLinkText": "Zobacz XML", + "@viewerInfoViewXmlLinkText": {}, + "viewerInfoOpenEmbeddedFailureFeedback": "Nie udało się wyodrębnić osadzonych danych", + "@viewerInfoOpenEmbeddedFailureFeedback": {}, + "settingsConfirmationBeforeMoveToBinItems": "Zapytaj, zanim przeniesiesz elementy do kosza", + "@settingsConfirmationBeforeMoveToBinItems": {}, + "settingsConfirmationBeforeMoveUndatedItems": "Zapytaj, zanim przeniesiesz niedatowane elementy", + "@settingsConfirmationBeforeMoveUndatedItems": {}, + "settingsConfirmationAfterMoveToBinItems": "Pokaż komunikat po przeniesieniu elementów do Kosza", + "@settingsConfirmationAfterMoveToBinItems": {}, + "settingsNavigationDrawerEditorPageTitle": "Menu nawigacyjne", + "@settingsNavigationDrawerEditorPageTitle": {}, + "settingsNavigationDrawerBanner": "Dotknij i przytrzymaj, aby przenieść i zmienić kolejność pozycji menu.", + "@settingsNavigationDrawerBanner": {}, + "settingsThumbnailShowFavouriteIcon": "Pokaż ikonę ulubionych", + "@settingsThumbnailShowFavouriteIcon": {}, + "settingsThumbnailShowMotionPhotoIcon": "Pokaż ikonę zdjęcia ruchomego", + "@settingsThumbnailShowMotionPhotoIcon": {}, + "settingsThumbnailOverlayTile": "Nakładka", + "@settingsThumbnailOverlayTile": {}, + "settingsThumbnailShowRating": "Pokaż ocenę", + "@settingsThumbnailShowRating": {}, + "settingsCollectionQuickActionsTile": "Szybkie działania", + "@settingsCollectionQuickActionsTile": {}, + "settingsCollectionBrowsingQuickActionEditorBanner": "Dotknij i przytrzymaj, aby przenieść przyciski i wybrać akcje wyświetlane podczas przeglądania elementów.", + "@settingsCollectionBrowsingQuickActionEditorBanner": {}, + "appExportCovers": "Okładki", + "@appExportCovers": {}, + "settingsHomeTile": "Strona główna", + "@settingsHomeTile": {}, + "settingsThumbnailOverlayPageTitle": "Nakładka", + "@settingsThumbnailOverlayPageTitle": {}, + "settingsLanguageTile": "Język", + "@settingsLanguageTile": {}, + "settingsCollectionTile": "Kolekcja", + "@settingsCollectionTile": {}, + "statsTopPlacesSectionTitle": "Najlepsze miejsca", + "@statsTopPlacesSectionTitle": {}, + "statsTopTagsSectionTitle": "Najlepsze znaczniki", + "@statsTopTagsSectionTitle": {}, + "viewerInfoBackToViewerTooltip": "Wróć do przeglądarki", + "@viewerInfoBackToViewerTooltip": {}, + "mapZoomOutTooltip": "Pomniejsz", + "@mapZoomOutTooltip": {}, + "viewerInfoSearchSuggestionDescription": "Opis", + "@viewerInfoSearchSuggestionDescription": {}, + "viewerInfoSearchSuggestionDimensions": "Wymiary", + "@viewerInfoSearchSuggestionDimensions": {}, + "viewerInfoSearchSuggestionDate": "Data i godzina", + "@viewerInfoSearchSuggestionDate": {}, + "viewerInfoSearchSuggestionResolution": "Rozdzielczość", + "@viewerInfoSearchSuggestionResolution": {}, + "tagEditorPageTitle": "Edytuj znaczniki", + "@tagEditorPageTitle": {}, + "tagEditorPageNewTagFieldLabel": "Nowy znacznik", + "@tagEditorPageNewTagFieldLabel": {}, + "tagEditorPageAddTagTooltip": "Dodaj znacznik", + "@tagEditorPageAddTagTooltip": {}, + "settingsNavigationSectionTitle": "Nawigowanie", + "@settingsNavigationSectionTitle": {}, + "viewerInfoLabelDate": "Data", + "@viewerInfoLabelDate": {}, + "settingsTimeToTakeActionTile": "Czas na podjęcie działania", + "@settingsTimeToTakeActionTile": {}, + "settingsDisplaySectionTitle": "Wyświetlanie", + "@settingsDisplaySectionTitle": {}, + "settingsNavigationDrawerTabAlbums": "Albumy", + "@settingsNavigationDrawerTabAlbums": {}, + "settingsNavigationDrawerTabPages": "Strony", + "@settingsNavigationDrawerTabPages": {}, + "settingsVideoSectionTitle": "Wideo", + "@settingsVideoSectionTitle": {}, + "settingsSubtitleThemeShowOutline": "Pokaż kontur i cień", + "@settingsSubtitleThemeShowOutline": {}, + "settingsSubtitleThemeTextColor": "Kolor tekstu", + "@settingsSubtitleThemeTextColor": {}, + "settingsSubtitleThemeTextOpacity": "Krycie tekstu", + "@settingsSubtitleThemeTextOpacity": {}, + "settingsUnitSystemTile": "Jednostki", + "@settingsUnitSystemTile": {}, + "addPathTooltip": "Dodaj ścieżkę", + "@addPathTooltip": {}, + "settingsHiddenPathsBanner": "Zdjęcia i wideo w tych folderach ani w żadnym z ich podfolderów nie pojawią się w kolekcji.", + "@settingsHiddenPathsBanner": {}, + "viewerInfoLabelOwner": "Właściciel", + "@viewerInfoLabelOwner": {}, + "viewerInfoSearchFieldLabel": "Wyszukaj metadane", + "@viewerInfoSearchFieldLabel": {}, + "panoramaEnableSensorControl": "Włącz sterowanie czujnikiem", + "@panoramaEnableSensorControl": {}, + "settingsSearchEmpty": "Brak pasującego ustawienia", + "@settingsSearchEmpty": {}, + "settingsActionExport": "Wyeksportuj", + "@settingsActionExport": {}, + "settingsActionExportDialogTitle": "Wyeksportuj", + "@settingsActionExportDialogTitle": {}, + "settingsCollectionQuickActionEditorPageTitle": "Szybkie działania", + "@settingsCollectionQuickActionEditorPageTitle": {}, + "settingsViewerSectionTitle": "Przeglądarka", + "@settingsViewerSectionTitle": {}, + "settingsImageBackground": "Tło obrazu", + "@settingsImageBackground": {}, + "settingsViewerShowRatingTags": "Pokaż ocenę i znaczniki", + "@settingsViewerShowRatingTags": {}, + "settingsViewerShowShootingDetails": "Pokaż szczegóły fotografowania", + "@settingsViewerShowShootingDetails": {}, + "settingsViewerShowInformation": "Pokaż informacje", + "@settingsViewerShowInformation": {}, + "settingsViewerEnableOverlayBlurEffect": "Efekt rozmycia", + "@settingsViewerEnableOverlayBlurEffect": {}, + "settingsSubtitleThemePageTitle": "Napisy", + "@settingsSubtitleThemePageTitle": {}, + "settingsVideoShowVideos": "Pokaż wideo", + "@settingsVideoShowVideos": {}, + "settingsCoordinateFormatDialogTitle": "Format współrzędnych", + "@settingsCoordinateFormatDialogTitle": {}, + "statsTopCountriesSectionTitle": "Najlepsze kraje", + "@statsTopCountriesSectionTitle": {}, + "viewerInfoLabelUri": "URI", + "@viewerInfoLabelUri": {}, + "panoramaDisableSensorControl": "Wyłącz sterowanie czujnikiem", + "@panoramaDisableSensorControl": {}, + "tagPlaceholderCountry": "Kraj", + "@tagPlaceholderCountry": {}, + "settingsConfirmationBeforeDeleteItems": "Zapytaj, zanim usuniesz elementy na zawsze", + "@settingsConfirmationBeforeDeleteItems": {}, + "settingsConfirmationDialogTitle": "Działania do potwierdzenia", + "@settingsConfirmationDialogTitle": {}, + "settingsMotionPhotoAutoPlay": "Automatyczne odtwarzanie ruchomych zdjęć", + "@settingsMotionPhotoAutoPlay": {}, + "settingsViewerQuickActionsTile": "Szybkie działania", + "@settingsViewerQuickActionsTile": {}, + "settingsViewerQuickActionEditorBanner": "Dotknij i przytrzymaj, aby przesunąć przyciski i wybrać czynności, które mają być wyświetlane w przeglądarce.", + "@settingsViewerQuickActionEditorBanner": {}, + "settingsViewerQuickActionEmpty": "Brak przycisków", + "@settingsViewerQuickActionEmpty": {}, + "settingsSlideshowAnimatedZoomEffect": "Animowany efekt powiększenia", + "@settingsSlideshowAnimatedZoomEffect": {}, + "settingsViewerShowInformationSubtitle": "Pokaż tytuł, datę, położenie itp.", + "@settingsViewerShowInformationSubtitle": {}, + "filePickerShowHiddenFiles": "Pokaż ukryte pliki", + "@filePickerShowHiddenFiles": {}, + "settingsHiddenItemsTabFilters": "Ukryte filtry", + "@settingsHiddenItemsTabFilters": {}, + "settingsThemeBrightnessTile": "Motyw", + "@settingsThemeBrightnessTile": {}, + "tagPlaceholderPlace": "Miejsce", + "@tagPlaceholderPlace": {}, + "settingsHiddenItemsPageTitle": "Ukryte elementy", + "@settingsHiddenItemsPageTitle": {}, + "settingsRemoveAnimationsDialogTitle": "Usuń animacje", + "@settingsRemoveAnimationsDialogTitle": {}, + "settingsVideoControlsPageTitle": "Sterowanie", + "@settingsVideoControlsPageTitle": {}, + "settingsVideoButtonsTile": "Przyciski", + "@settingsVideoButtonsTile": {}, + "settingsAllowInstalledAppAccessSubtitle": "Używane do poprawy wyświetlania albumu", + "@settingsAllowInstalledAppAccessSubtitle": {}, + "settingsEnableBin": "Użyj kosza", + "@settingsEnableBin": {}, + "settingsWidgetShowOutline": "Zarys", + "@settingsWidgetShowOutline": {}, + "settingsHiddenItemsTile": "Ukryte elementy", + "@settingsHiddenItemsTile": {}, + "viewerInfoLabelDuration": "Czas trwania", + "@viewerInfoLabelDuration": {}, + "settingsStorageAccessPageTitle": "Dostęp do pamięci masowej", + "@settingsStorageAccessPageTitle": {}, + "settingsStorageAccessBanner": "Niektóre katalogi wymagają jawnego udzielenia dostępu, aby modyfikować znajdujące się w nich pliki. Możesz przejrzeć tutaj katalogi, do których wcześniej udzielono dostępu.", + "@settingsStorageAccessBanner": {}, + "statsWithGps": "{count, plural, =1{1 element z położeniem} =2..4{{count} elementy z położeniem} other{{count} elementów z położeniem}}", + "@statsWithGps": { + "placeholders": { + "count": {} + } + }, + "viewerInfoLabelDescription": "Opis", + "@viewerInfoLabelDescription": {}, + "viewerInfoLabelTitle": "Tytuł", + "@viewerInfoLabelTitle": {}, + "viewerInfoLabelCoordinates": "Współrzędne", + "@viewerInfoLabelCoordinates": {}, + "viewerInfoUnknown": "Nieznany", + "@viewerInfoUnknown": {}, + "mapAttributionOsmHot": "Dane mapy © [OpenStreetMap](https://www.openstreetmap.org/copyright) współtwórcy • Kafelki od [HOT](https://www.hotosm.org/) • Obsługiwany przez: [OSM France](https://openstreetmap.fr/)", + "@mapAttributionOsmHot": {}, + "mapPointNorthUpTooltip": "Północ u góry", + "@mapPointNorthUpTooltip": {}, + "settingsCollectionQuickActionTabBrowsing": "Przeglądanie", + "@settingsCollectionQuickActionTabBrowsing": {}, + "settingsViewerQuickActionEditorAvailableButtonsSectionTitle": "Dostępne przyciski", + "@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {}, + "settingsViewerOverlayTile": "Nakładka", + "@settingsViewerOverlayTile": {}, + "settingsViewerOverlayPageTitle": "Nakładka", + "@settingsViewerOverlayPageTitle": {}, + "settingsViewerShowOverlayOnOpening": "Pokaz w momencie otwarcia", + "@settingsViewerShowOverlayOnOpening": {}, + "settingsViewerShowMinimap": "Pokaż minimapę", + "@settingsViewerShowMinimap": {}, + "settingsVideoGestureSideDoubleTapSeek": "Dotknij dwukrotnie krawędzi ekranu, aby przeszukać do tyłu / do przodu", + "@settingsVideoGestureSideDoubleTapSeek": {}, + "settingsViewerShowDescription": "Pokaż opis", + "@settingsViewerShowDescription": {}, + "settingsViewerSlideshowPageTitle": "Pokaz slajdów", + "@settingsViewerSlideshowPageTitle": {}, + "settingsSlideshowRepeat": "Powtarzaj", + "@settingsSlideshowRepeat": {}, + "settingsSlideshowTransitionTile": "Przejście", + "@settingsSlideshowTransitionTile": {}, + "settingsSlideshowVideoPlaybackTile": "Odtwarzanie wideo", + "@settingsSlideshowVideoPlaybackTile": {}, + "settingsSubtitleThemeTile": "Napisy", + "@settingsSubtitleThemeTile": {}, + "openMapPageTooltip": "Wyświetl na mapie", + "@openMapPageTooltip": {} } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 3ef91434c..075bb96f4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.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": {} } diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb index 7bd5ebd81..d0ffb46ed 100644 --- a/lib/l10n/app_ro.arb +++ b/lib/l10n/app_ro.arb @@ -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": {} } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 8f4d75410..3cd48cd05 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -1196,5 +1196,7 @@ "entryActionShareImageOnly": "Поделиться только изображением", "@entryActionShareImageOnly": {}, "entryActionShareVideoOnly": "Поделиться только видео", - "@entryActionShareVideoOnly": {} + "@entryActionShareVideoOnly": {}, + "settingsViewerShowDescription": "Показать описание", + "@settingsViewerShowDescription": {} } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 6637d66e6..84018f06c 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -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": {} } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index e106e51cf..37a0c5579 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -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": {} } diff --git a/lib/model/actions/map_actions.dart b/lib/model/actions/map_actions.dart new file mode 100644 index 000000000..07cc9189a --- /dev/null +++ b/lib/model/actions/map_actions.dart @@ -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; + } + } +} diff --git a/lib/model/device.dart b/lib/model/device.dart index deefa5e6d..b69195128 100644 --- a/lib/model/device.dart +++ b/lib/model/device.dart @@ -27,8 +27,6 @@ class Device { bool get isDynamicColorAvailable => _isDynamicColorAvailable; - bool get isReadOnly => _isTelevision; - bool get isTelevision => _isTelevision; bool get showPinShortcutFeedback => _showPinShortcutFeedback; diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 86e1fcdce..6a9dc3625 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -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); diff --git a/lib/model/entry_metadata_edition.dart b/lib/model/entry_metadata_edition.dart index 21ba5d001..7aa91fae6 100644 --- a/lib/model/entry_metadata_edition.dart +++ b/lib/model/entry_metadata_edition.dart @@ -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) { diff --git a/lib/model/metadata/fields.dart b/lib/model/metadata/fields.dart index 552909243..17a344aa6 100644 --- a/lib/model/metadata/fields.dart +++ b/lib/model/metadata/fields.dart @@ -37,6 +37,7 @@ enum MetadataField { exifGpsTrackRef, exifGpsVersionId, exifImageDescription, + exifUserComment, mp4GpsCoordinates, mp4RotationDegrees, mp4Xmp, @@ -119,6 +120,7 @@ extension ExtraMetadataField on MetadataField { case MetadataField.exifGpsTrackRef: case MetadataField.exifGpsVersionId: case MetadataField.exifImageDescription: + case MetadataField.exifUserComment: return MetadataType.exif; case MetadataField.mp4GpsCoordinates: case MetadataField.mp4RotationDegrees: @@ -220,6 +222,8 @@ extension ExtraMetadataField on MetadataField { return 'GPSVersionID'; case MetadataField.exifImageDescription: return 'ImageDescription'; + case MetadataField.exifUserComment: + return 'UserComment'; default: return null; } diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index db0c59c8d..1dbcc1a18 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -24,6 +24,7 @@ class SettingsDefaults { static const themeColorMode = AvesThemeColorMode.polychrome; static const enableDynamicColor = false; static const enableBlurEffect = true; // `enableBlurEffect` has a contextual default value + static const forceTvLayout = false; // navigation static const mustBackTwiceToExit = true; @@ -56,8 +57,8 @@ class SettingsDefaults { EntrySetAction.delete, ]; static const showThumbnailFavourite = true; - static const showThumbnailTag = false; - static const showThumbnailLocation = true; + static const thumbnailLocationIcon = ThumbnailOverlayLocationIcon.none; + static const thumbnailTagIcon = ThumbnailOverlayTagIcon.none; static const showThumbnailMotionPhoto = true; static const showThumbnailRating = true; static const showThumbnailRaw = true; @@ -79,6 +80,7 @@ class SettingsDefaults { static const showOverlayOnOpening = true; static const showOverlayMinimap = false; static const showOverlayInfo = true; + static const showOverlayDescription = false; static const showOverlayRatingTags = false; static const showOverlayShootingDetails = false; static const showOverlayThumbnailPreview = false; diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index ab1f42f9c..96f7fd39d 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -22,6 +22,10 @@ enum SlideshowVideoPlayback { skip, playMuted, playWithSound } enum SubtitlePosition { top, bottom } +enum ThumbnailOverlayLocationIcon { located, unlocated, none } + +enum ThumbnailOverlayTagIcon { tagged, untagged, none } + enum UnitSystem { metric, imperial } enum VideoControls { play, playSeek, playOutside, none } diff --git a/lib/model/settings/enums/thumbnail_overlay_location_icon.dart b/lib/model/settings/enums/thumbnail_overlay_location_icon.dart new file mode 100644 index 000000000..a0f76c54e --- /dev/null +++ b/lib/model/settings/enums/thumbnail_overlay_location_icon.dart @@ -0,0 +1,29 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; + +import 'enums.dart'; + +extension ExtraThumbnailOverlayLocationIcon on ThumbnailOverlayLocationIcon { + String getName(BuildContext context) { + switch (this) { + case ThumbnailOverlayLocationIcon.located: + return context.l10n.filterLocatedLabel; + case ThumbnailOverlayLocationIcon.unlocated: + return context.l10n.filterNoLocationLabel; + case ThumbnailOverlayLocationIcon.none: + return context.l10n.settingsDisabled; + } + } + + IconData getIcon(BuildContext context) { + switch (this) { + case ThumbnailOverlayLocationIcon.located: + return AIcons.location; + case ThumbnailOverlayLocationIcon.unlocated: + return AIcons.locationUnlocated; + case ThumbnailOverlayLocationIcon.none: + return AIcons.location; + } + } +} diff --git a/lib/model/settings/enums/thumbnail_overlay_tag_icon.dart b/lib/model/settings/enums/thumbnail_overlay_tag_icon.dart new file mode 100644 index 000000000..00f006ec9 --- /dev/null +++ b/lib/model/settings/enums/thumbnail_overlay_tag_icon.dart @@ -0,0 +1,29 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/material.dart'; + +import 'enums.dart'; + +extension ExtraThumbnailOverlayTagIcon on ThumbnailOverlayTagIcon { + String getName(BuildContext context) { + switch (this) { + case ThumbnailOverlayTagIcon.tagged: + return context.l10n.filterTaggedLabel; + case ThumbnailOverlayTagIcon.untagged: + return context.l10n.filterNoTagLabel; + case ThumbnailOverlayTagIcon.none: + return context.l10n.settingsDisabled; + } + } + + IconData getIcon(BuildContext context) { + switch (this) { + case ThumbnailOverlayTagIcon.tagged: + return AIcons.tag; + case ThumbnailOverlayTagIcon.untagged: + return AIcons.tagUntagged; + case ThumbnailOverlayTagIcon.none: + return AIcons.tag; + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index aa747b127..27c944d82 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -13,6 +13,7 @@ import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/source/enums/enums.dart'; +import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/aves_app.dart'; @@ -70,6 +71,7 @@ class Settings extends ChangeNotifier { static const themeColorModeKey = 'theme_color_mode'; static const enableDynamicColorKey = 'dynamic_color'; static const enableBlurEffectKey = 'enable_overlay_blur_effect'; + static const forceTvLayoutKey = 'force_tv_layout'; // navigation static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; @@ -92,8 +94,8 @@ class Settings extends ChangeNotifier { static const collectionBrowsingQuickActionsKey = 'collection_browsing_quick_actions'; static const collectionSelectionQuickActionsKey = 'collection_selection_quick_actions'; static const showThumbnailFavouriteKey = 'show_thumbnail_favourite'; - static const showThumbnailTagKey = 'show_thumbnail_tag'; - static const showThumbnailLocationKey = 'show_thumbnail_location'; + static const thumbnailLocationIconKey = 'thumbnail_location_icon'; + static const thumbnailTagIconKey = 'thumbnail_tag_icon'; static const showThumbnailMotionPhotoKey = 'show_thumbnail_motion_photo'; static const showThumbnailRatingKey = 'show_thumbnail_rating'; static const showThumbnailRawKey = 'show_thumbnail_raw'; @@ -115,6 +117,7 @@ class Settings extends ChangeNotifier { static const showOverlayOnOpeningKey = 'show_overlay_on_opening'; static const showOverlayMinimapKey = 'show_overlay_minimap'; static const showOverlayInfoKey = 'show_overlay_info'; + static const showOverlayDescriptionKey = 'show_overlay_description'; static const showOverlayRatingTagsKey = 'show_overlay_rating_tags'; static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details'; static const showOverlayThumbnailPreviewKey = 'show_overlay_thumbnail_preview'; @@ -239,7 +242,11 @@ class Settings extends ChangeNotifier { } } - if (device.isTelevision) { + applyTvSettings(); + } + + void applyTvSettings() { + if (settings.useTvLayout) { themeBrightness = AvesThemeBrightness.dark; mustBackTwiceToExit = false; // address `TV-BU` / `TV-BY` requirements from https://developer.android.com/docs/quality-guidelines/tv-app-quality @@ -270,23 +277,32 @@ class Settings extends ChangeNotifier { } } + Future sanitize() async { + if (timeToTakeAction == AccessibilityTimeout.system && !(await AccessibilityService.hasRecommendedTimeouts())) { + _set(timeToTakeActionKey, null); + } + if (viewerUseCutout != SettingsDefaults.viewerUseCutout && !(await windowService.isCutoutAware())) { + _set(viewerUseCutoutKey, null); + } + } + // app bool get hasAcceptedTerms => getBool(hasAcceptedTermsKey) ?? SettingsDefaults.hasAcceptedTerms; - set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue); + set hasAcceptedTerms(bool newValue) => _set(hasAcceptedTermsKey, newValue); bool get canUseAnalysisService => getBool(canUseAnalysisServiceKey) ?? SettingsDefaults.canUseAnalysisService; - set canUseAnalysisService(bool newValue) => setAndNotify(canUseAnalysisServiceKey, newValue); + set canUseAnalysisService(bool newValue) => _set(canUseAnalysisServiceKey, newValue); bool get isInstalledAppAccessAllowed => getBool(isInstalledAppAccessAllowedKey) ?? SettingsDefaults.isInstalledAppAccessAllowed; - set isInstalledAppAccessAllowed(bool newValue) => setAndNotify(isInstalledAppAccessAllowedKey, newValue); + set isInstalledAppAccessAllowed(bool newValue) => _set(isInstalledAppAccessAllowedKey, newValue); bool get isErrorReportingAllowed => getBool(isErrorReportingAllowedKey) ?? SettingsDefaults.isErrorReportingAllowed; - set isErrorReportingAllowed(bool newValue) => setAndNotify(isErrorReportingAllowedKey, newValue); + set isErrorReportingAllowed(bool newValue) => _set(isErrorReportingAllowedKey, newValue); static const localeSeparator = '-'; @@ -313,7 +329,7 @@ class Settings extends ChangeNotifier { newValue.countryCode ?? '', ].join(localeSeparator); } - setAndNotify(localeKey, tag); + _set(localeKey, tag); _appliedLocale = null; } @@ -343,91 +359,99 @@ class Settings extends ChangeNotifier { String get catalogTimeZone => getString(catalogTimeZoneKey) ?? ''; - set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); + set catalogTimeZone(String newValue) => _set(catalogTimeZoneKey, newValue); double getTileExtent(String routeName) => getDouble(tileExtentPrefixKey + routeName) ?? 0; - void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue); + void setTileExtent(String routeName, double newValue) => _set(tileExtentPrefixKey + routeName, newValue); TileLayout getTileLayout(String routeName) => getEnumOrDefault(tileLayoutPrefixKey + routeName, SettingsDefaults.tileLayout, TileLayout.values); - void setTileLayout(String routeName, TileLayout newValue) => setAndNotify(tileLayoutPrefixKey + routeName, newValue.toString()); + void setTileLayout(String routeName, TileLayout newValue) => _set(tileLayoutPrefixKey + routeName, newValue.toString()); String get entryRenamingPattern => getString(entryRenamingPatternKey) ?? SettingsDefaults.entryRenamingPattern; - set entryRenamingPattern(String newValue) => setAndNotify(entryRenamingPatternKey, newValue); + set entryRenamingPattern(String newValue) => _set(entryRenamingPatternKey, newValue); List? get topEntryIds => getStringList(topEntryIdsKey)?.map(int.tryParse).whereNotNull().toList(); - set topEntryIds(List? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList()); + set topEntryIds(List? newValue) => _set(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList()); List get recentDestinationAlbums => getStringList(recentDestinationAlbumsKey) ?? []; - set recentDestinationAlbums(List newValue) => setAndNotify(recentDestinationAlbumsKey, newValue.take(_recentFilterHistoryMax).toList()); + set recentDestinationAlbums(List newValue) => _set(recentDestinationAlbumsKey, newValue.take(_recentFilterHistoryMax).toList()); List get recentTags => (getStringList(recentTagsKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList(); - set recentTags(List newValue) => setAndNotify(recentTagsKey, newValue.take(_recentFilterHistoryMax).map((filter) => filter.toJson()).toList()); + set recentTags(List newValue) => _set(recentTagsKey, newValue.take(_recentFilterHistoryMax).map((filter) => filter.toJson()).toList()); // display DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values); - set displayRefreshRateMode(DisplayRefreshRateMode newValue) => setAndNotify(displayRefreshRateModeKey, newValue.toString()); + set displayRefreshRateMode(DisplayRefreshRateMode newValue) => _set(displayRefreshRateModeKey, newValue.toString()); AvesThemeBrightness get themeBrightness => getEnumOrDefault(themeBrightnessKey, SettingsDefaults.themeBrightness, AvesThemeBrightness.values); - set themeBrightness(AvesThemeBrightness newValue) => setAndNotify(themeBrightnessKey, newValue.toString()); + set themeBrightness(AvesThemeBrightness newValue) => _set(themeBrightnessKey, newValue.toString()); AvesThemeColorMode get themeColorMode => getEnumOrDefault(themeColorModeKey, SettingsDefaults.themeColorMode, AvesThemeColorMode.values); - set themeColorMode(AvesThemeColorMode newValue) => setAndNotify(themeColorModeKey, newValue.toString()); + set themeColorMode(AvesThemeColorMode newValue) => _set(themeColorModeKey, newValue.toString()); bool get enableDynamicColor => getBool(enableDynamicColorKey) ?? SettingsDefaults.enableDynamicColor; - set enableDynamicColor(bool newValue) => setAndNotify(enableDynamicColorKey, newValue); + set enableDynamicColor(bool newValue) => _set(enableDynamicColorKey, newValue); bool get enableBlurEffect => getBool(enableBlurEffectKey) ?? SettingsDefaults.enableBlurEffect; - set enableBlurEffect(bool newValue) => setAndNotify(enableBlurEffectKey, newValue); + set enableBlurEffect(bool newValue) => _set(enableBlurEffectKey, newValue); + + bool get forceTvLayout => getBool(forceTvLayoutKey) ?? SettingsDefaults.forceTvLayout; + + set forceTvLayout(bool newValue) => _set(forceTvLayoutKey, newValue); + + bool get useTvLayout => device.isTelevision || forceTvLayout; + + bool get isReadOnly => useTvLayout; // navigation bool get mustBackTwiceToExit => getBool(mustBackTwiceToExitKey) ?? SettingsDefaults.mustBackTwiceToExit; - set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue); + set mustBackTwiceToExit(bool newValue) => _set(mustBackTwiceToExitKey, newValue); KeepScreenOn get keepScreenOn => getEnumOrDefault(keepScreenOnKey, SettingsDefaults.keepScreenOn, KeepScreenOn.values); - set keepScreenOn(KeepScreenOn newValue) => setAndNotify(keepScreenOnKey, newValue.toString()); + set keepScreenOn(KeepScreenOn newValue) => _set(keepScreenOnKey, newValue.toString()); HomePageSetting get homePage => getEnumOrDefault(homePageKey, SettingsDefaults.homePage, HomePageSetting.values); - set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); + set homePage(HomePageSetting newValue) => _set(homePageKey, newValue.toString()); bool get enableBottomNavigationBar => getBool(enableBottomNavigationBarKey) ?? SettingsDefaults.enableBottomNavigationBar; - set enableBottomNavigationBar(bool newValue) => setAndNotify(enableBottomNavigationBarKey, newValue); + set enableBottomNavigationBar(bool newValue) => _set(enableBottomNavigationBarKey, newValue); bool get confirmDeleteForever => getBool(confirmDeleteForeverKey) ?? SettingsDefaults.confirmDeleteForever; - set confirmDeleteForever(bool newValue) => setAndNotify(confirmDeleteForeverKey, newValue); + set confirmDeleteForever(bool newValue) => _set(confirmDeleteForeverKey, newValue); bool get confirmMoveToBin => getBool(confirmMoveToBinKey) ?? SettingsDefaults.confirmMoveToBin; - set confirmMoveToBin(bool newValue) => setAndNotify(confirmMoveToBinKey, newValue); + set confirmMoveToBin(bool newValue) => _set(confirmMoveToBinKey, newValue); bool get confirmMoveUndatedItems => getBool(confirmMoveUndatedItemsKey) ?? SettingsDefaults.confirmMoveUndatedItems; - set confirmMoveUndatedItems(bool newValue) => setAndNotify(confirmMoveUndatedItemsKey, newValue); + set confirmMoveUndatedItems(bool newValue) => _set(confirmMoveUndatedItemsKey, newValue); bool get confirmAfterMoveToBin => getBool(confirmAfterMoveToBinKey) ?? SettingsDefaults.confirmAfterMoveToBin; - set confirmAfterMoveToBin(bool newValue) => setAndNotify(confirmAfterMoveToBinKey, newValue); + set confirmAfterMoveToBin(bool newValue) => _set(confirmAfterMoveToBinKey, newValue); bool get setMetadataDateBeforeFileOp => getBool(setMetadataDateBeforeFileOpKey) ?? SettingsDefaults.setMetadataDateBeforeFileOp; - set setMetadataDateBeforeFileOp(bool newValue) => setAndNotify(setMetadataDateBeforeFileOpKey, newValue); + set setMetadataDateBeforeFileOp(bool newValue) => _set(setMetadataDateBeforeFileOpKey, newValue); List get drawerTypeBookmarks => (getStringList(drawerTypeBookmarksKey))?.map((v) { @@ -436,103 +460,103 @@ class Settings extends ChangeNotifier { }).toList() ?? SettingsDefaults.drawerTypeBookmarks; - set drawerTypeBookmarks(List newValue) => setAndNotify(drawerTypeBookmarksKey, newValue.map((filter) => filter?.toJson() ?? '').toList()); + set drawerTypeBookmarks(List newValue) => _set(drawerTypeBookmarksKey, newValue.map((filter) => filter?.toJson() ?? '').toList()); List? get drawerAlbumBookmarks => getStringList(drawerAlbumBookmarksKey); - set drawerAlbumBookmarks(List? newValue) => setAndNotify(drawerAlbumBookmarksKey, newValue); + set drawerAlbumBookmarks(List? newValue) => _set(drawerAlbumBookmarksKey, newValue); List get drawerPageBookmarks => getStringList(drawerPageBookmarksKey) ?? SettingsDefaults.drawerPageBookmarks; - set drawerPageBookmarks(List newValue) => setAndNotify(drawerPageBookmarksKey, newValue); + set drawerPageBookmarks(List newValue) => _set(drawerPageBookmarksKey, newValue); // collection EntryGroupFactor get collectionSectionFactor => getEnumOrDefault(collectionGroupFactorKey, SettingsDefaults.collectionSectionFactor, EntryGroupFactor.values); - set collectionSectionFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString()); + set collectionSectionFactor(EntryGroupFactor newValue) => _set(collectionGroupFactorKey, newValue.toString()); EntrySortFactor get collectionSortFactor => getEnumOrDefault(collectionSortFactorKey, SettingsDefaults.collectionSortFactor, EntrySortFactor.values); - set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); + set collectionSortFactor(EntrySortFactor newValue) => _set(collectionSortFactorKey, newValue.toString()); bool get collectionSortReverse => getBool(collectionSortReverseKey) ?? false; - set collectionSortReverse(bool newValue) => setAndNotify(collectionSortReverseKey, newValue); + set collectionSortReverse(bool newValue) => _set(collectionSortReverseKey, newValue); List get collectionBrowsingQuickActions => getEnumListOrDefault(collectionBrowsingQuickActionsKey, SettingsDefaults.collectionBrowsingQuickActions, EntrySetAction.values); - set collectionBrowsingQuickActions(List newValue) => setAndNotify(collectionBrowsingQuickActionsKey, newValue.map((v) => v.toString()).toList()); + set collectionBrowsingQuickActions(List newValue) => _set(collectionBrowsingQuickActionsKey, newValue.map((v) => v.toString()).toList()); List get collectionSelectionQuickActions => getEnumListOrDefault(collectionSelectionQuickActionsKey, SettingsDefaults.collectionSelectionQuickActions, EntrySetAction.values); - set collectionSelectionQuickActions(List newValue) => setAndNotify(collectionSelectionQuickActionsKey, newValue.map((v) => v.toString()).toList()); + set collectionSelectionQuickActions(List newValue) => _set(collectionSelectionQuickActionsKey, newValue.map((v) => v.toString()).toList()); bool get showThumbnailFavourite => getBool(showThumbnailFavouriteKey) ?? SettingsDefaults.showThumbnailFavourite; - set showThumbnailFavourite(bool newValue) => setAndNotify(showThumbnailFavouriteKey, newValue); + set showThumbnailFavourite(bool newValue) => _set(showThumbnailFavouriteKey, newValue); - bool get showThumbnailTag => getBool(showThumbnailTagKey) ?? SettingsDefaults.showThumbnailTag; + ThumbnailOverlayLocationIcon get thumbnailLocationIcon => getEnumOrDefault(thumbnailLocationIconKey, SettingsDefaults.thumbnailLocationIcon, ThumbnailOverlayLocationIcon.values); - set showThumbnailTag(bool newValue) => setAndNotify(showThumbnailTagKey, newValue); + set thumbnailLocationIcon(ThumbnailOverlayLocationIcon newValue) => _set(thumbnailLocationIconKey, newValue.toString()); - bool get showThumbnailLocation => getBool(showThumbnailLocationKey) ?? SettingsDefaults.showThumbnailLocation; + ThumbnailOverlayTagIcon get thumbnailTagIcon => getEnumOrDefault(thumbnailTagIconKey, SettingsDefaults.thumbnailTagIcon, ThumbnailOverlayTagIcon.values); - set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue); + set thumbnailTagIcon(ThumbnailOverlayTagIcon newValue) => _set(thumbnailTagIconKey, newValue.toString()); bool get showThumbnailMotionPhoto => getBool(showThumbnailMotionPhotoKey) ?? SettingsDefaults.showThumbnailMotionPhoto; - set showThumbnailMotionPhoto(bool newValue) => setAndNotify(showThumbnailMotionPhotoKey, newValue); + set showThumbnailMotionPhoto(bool newValue) => _set(showThumbnailMotionPhotoKey, newValue); bool get showThumbnailRating => getBool(showThumbnailRatingKey) ?? SettingsDefaults.showThumbnailRating; - set showThumbnailRating(bool newValue) => setAndNotify(showThumbnailRatingKey, newValue); + set showThumbnailRating(bool newValue) => _set(showThumbnailRatingKey, newValue); bool get showThumbnailRaw => getBool(showThumbnailRawKey) ?? SettingsDefaults.showThumbnailRaw; - set showThumbnailRaw(bool newValue) => setAndNotify(showThumbnailRawKey, newValue); + set showThumbnailRaw(bool newValue) => _set(showThumbnailRawKey, newValue); bool get showThumbnailVideoDuration => getBool(showThumbnailVideoDurationKey) ?? SettingsDefaults.showThumbnailVideoDuration; - set showThumbnailVideoDuration(bool newValue) => setAndNotify(showThumbnailVideoDurationKey, newValue); + set showThumbnailVideoDuration(bool newValue) => _set(showThumbnailVideoDurationKey, newValue); // filter grids AlbumChipGroupFactor get albumGroupFactor => getEnumOrDefault(albumGroupFactorKey, SettingsDefaults.albumGroupFactor, AlbumChipGroupFactor.values); - set albumGroupFactor(AlbumChipGroupFactor newValue) => setAndNotify(albumGroupFactorKey, newValue.toString()); + set albumGroupFactor(AlbumChipGroupFactor newValue) => _set(albumGroupFactorKey, newValue.toString()); ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, SettingsDefaults.albumSortFactor, ChipSortFactor.values); - set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString()); + set albumSortFactor(ChipSortFactor newValue) => _set(albumSortFactorKey, newValue.toString()); ChipSortFactor get countrySortFactor => getEnumOrDefault(countrySortFactorKey, SettingsDefaults.countrySortFactor, ChipSortFactor.values); - set countrySortFactor(ChipSortFactor newValue) => setAndNotify(countrySortFactorKey, newValue.toString()); + set countrySortFactor(ChipSortFactor newValue) => _set(countrySortFactorKey, newValue.toString()); ChipSortFactor get tagSortFactor => getEnumOrDefault(tagSortFactorKey, SettingsDefaults.tagSortFactor, ChipSortFactor.values); - set tagSortFactor(ChipSortFactor newValue) => setAndNotify(tagSortFactorKey, newValue.toString()); + set tagSortFactor(ChipSortFactor newValue) => _set(tagSortFactorKey, newValue.toString()); bool get albumSortReverse => getBool(albumSortReverseKey) ?? false; - set albumSortReverse(bool newValue) => setAndNotify(albumSortReverseKey, newValue); + set albumSortReverse(bool newValue) => _set(albumSortReverseKey, newValue); bool get countrySortReverse => getBool(countrySortReverseKey) ?? false; - set countrySortReverse(bool newValue) => setAndNotify(countrySortReverseKey, newValue); + set countrySortReverse(bool newValue) => _set(countrySortReverseKey, newValue); bool get tagSortReverse => getBool(tagSortReverseKey) ?? false; - set tagSortReverse(bool newValue) => setAndNotify(tagSortReverseKey, newValue); + set tagSortReverse(bool newValue) => _set(tagSortReverseKey, newValue); Set get pinnedFilters => (getStringList(pinnedFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); - set pinnedFilters(Set newValue) => setAndNotify(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); + set pinnedFilters(Set newValue) => _set(pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList()); Set get hiddenFilters => (getStringList(hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); - set hiddenFilters(Set newValue) => setAndNotify(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList()); + set hiddenFilters(Set newValue) => _set(hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList()); void changeFilterVisibility(Set filters, bool visible) { final _hiddenFilters = hiddenFilters; @@ -549,127 +573,131 @@ class Settings extends ChangeNotifier { List get viewerQuickActions => getEnumListOrDefault(viewerQuickActionsKey, SettingsDefaults.viewerQuickActions, EntryAction.values); - set viewerQuickActions(List newValue) => setAndNotify(viewerQuickActionsKey, newValue.map((v) => v.toString()).toList()); + set viewerQuickActions(List newValue) => _set(viewerQuickActionsKey, newValue.map((v) => v.toString()).toList()); bool get showOverlayOnOpening => getBool(showOverlayOnOpeningKey) ?? SettingsDefaults.showOverlayOnOpening; - set showOverlayOnOpening(bool newValue) => setAndNotify(showOverlayOnOpeningKey, newValue); + set showOverlayOnOpening(bool newValue) => _set(showOverlayOnOpeningKey, newValue); bool get showOverlayMinimap => getBool(showOverlayMinimapKey) ?? SettingsDefaults.showOverlayMinimap; - set showOverlayMinimap(bool newValue) => setAndNotify(showOverlayMinimapKey, newValue); + set showOverlayMinimap(bool newValue) => _set(showOverlayMinimapKey, newValue); bool get showOverlayInfo => getBool(showOverlayInfoKey) ?? SettingsDefaults.showOverlayInfo; - set showOverlayInfo(bool newValue) => setAndNotify(showOverlayInfoKey, newValue); + set showOverlayInfo(bool newValue) => _set(showOverlayInfoKey, newValue); + + bool get showOverlayDescription => getBool(showOverlayDescriptionKey) ?? SettingsDefaults.showOverlayDescription; + + set showOverlayDescription(bool newValue) => _set(showOverlayDescriptionKey, newValue); bool get showOverlayRatingTags => getBool(showOverlayRatingTagsKey) ?? SettingsDefaults.showOverlayRatingTags; - set showOverlayRatingTags(bool newValue) => setAndNotify(showOverlayRatingTagsKey, newValue); + set showOverlayRatingTags(bool newValue) => _set(showOverlayRatingTagsKey, newValue); bool get showOverlayShootingDetails => getBool(showOverlayShootingDetailsKey) ?? SettingsDefaults.showOverlayShootingDetails; - set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue); + set showOverlayShootingDetails(bool newValue) => _set(showOverlayShootingDetailsKey, newValue); bool get showOverlayThumbnailPreview => getBool(showOverlayThumbnailPreviewKey) ?? SettingsDefaults.showOverlayThumbnailPreview; - set showOverlayThumbnailPreview(bool newValue) => setAndNotify(showOverlayThumbnailPreviewKey, newValue); + set showOverlayThumbnailPreview(bool newValue) => _set(showOverlayThumbnailPreviewKey, newValue); bool get viewerGestureSideTapNext => getBool(viewerGestureSideTapNextKey) ?? SettingsDefaults.viewerGestureSideTapNext; - set viewerGestureSideTapNext(bool newValue) => setAndNotify(viewerGestureSideTapNextKey, newValue); + set viewerGestureSideTapNext(bool newValue) => _set(viewerGestureSideTapNextKey, newValue); bool get viewerUseCutout => getBool(viewerUseCutoutKey) ?? SettingsDefaults.viewerUseCutout; - set viewerUseCutout(bool newValue) => setAndNotify(viewerUseCutoutKey, newValue); + set viewerUseCutout(bool newValue) => _set(viewerUseCutoutKey, newValue); bool get viewerMaxBrightness => getBool(viewerMaxBrightnessKey) ?? SettingsDefaults.viewerMaxBrightness; - set viewerMaxBrightness(bool newValue) => setAndNotify(viewerMaxBrightnessKey, newValue); + set viewerMaxBrightness(bool newValue) => _set(viewerMaxBrightnessKey, newValue); bool get enableMotionPhotoAutoPlay => getBool(enableMotionPhotoAutoPlayKey) ?? SettingsDefaults.enableMotionPhotoAutoPlay; - set enableMotionPhotoAutoPlay(bool newValue) => setAndNotify(enableMotionPhotoAutoPlayKey, newValue); + set enableMotionPhotoAutoPlay(bool newValue) => _set(enableMotionPhotoAutoPlayKey, newValue); EntryBackground get imageBackground => getEnumOrDefault(imageBackgroundKey, SettingsDefaults.imageBackground, EntryBackground.values); - set imageBackground(EntryBackground newValue) => setAndNotify(imageBackgroundKey, newValue.toString()); + set imageBackground(EntryBackground newValue) => _set(imageBackgroundKey, newValue.toString()); // video bool get enableVideoHardwareAcceleration => getBool(enableVideoHardwareAccelerationKey) ?? SettingsDefaults.enableVideoHardwareAcceleration; - set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue); + set enableVideoHardwareAcceleration(bool newValue) => _set(enableVideoHardwareAccelerationKey, newValue); VideoAutoPlayMode get videoAutoPlayMode => getEnumOrDefault(videoAutoPlayModeKey, SettingsDefaults.videoAutoPlayMode, VideoAutoPlayMode.values); - set videoAutoPlayMode(VideoAutoPlayMode newValue) => setAndNotify(videoAutoPlayModeKey, newValue.toString()); + set videoAutoPlayMode(VideoAutoPlayMode newValue) => _set(videoAutoPlayModeKey, newValue.toString()); VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values); - set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString()); + set videoLoopMode(VideoLoopMode newValue) => _set(videoLoopModeKey, newValue.toString()); bool get videoShowRawTimedText => getBool(videoShowRawTimedTextKey) ?? SettingsDefaults.videoShowRawTimedText; - set videoShowRawTimedText(bool newValue) => setAndNotify(videoShowRawTimedTextKey, newValue); + set videoShowRawTimedText(bool newValue) => _set(videoShowRawTimedTextKey, newValue); VideoControls get videoControls => getEnumOrDefault(videoControlsKey, SettingsDefaults.videoControls, VideoControls.values); - set videoControls(VideoControls newValue) => setAndNotify(videoControlsKey, newValue.toString()); + set videoControls(VideoControls newValue) => _set(videoControlsKey, newValue.toString()); bool get videoGestureDoubleTapTogglePlay => getBool(videoGestureDoubleTapTogglePlayKey) ?? SettingsDefaults.videoGestureDoubleTapTogglePlay; - set videoGestureDoubleTapTogglePlay(bool newValue) => setAndNotify(videoGestureDoubleTapTogglePlayKey, newValue); + set videoGestureDoubleTapTogglePlay(bool newValue) => _set(videoGestureDoubleTapTogglePlayKey, newValue); bool get videoGestureSideDoubleTapSeek => getBool(videoGestureSideDoubleTapSeekKey) ?? SettingsDefaults.videoGestureSideDoubleTapSeek; - set videoGestureSideDoubleTapSeek(bool newValue) => setAndNotify(videoGestureSideDoubleTapSeekKey, newValue); + set videoGestureSideDoubleTapSeek(bool newValue) => _set(videoGestureSideDoubleTapSeekKey, newValue); // subtitles double get subtitleFontSize => getDouble(subtitleFontSizeKey) ?? SettingsDefaults.subtitleFontSize; - set subtitleFontSize(double newValue) => setAndNotify(subtitleFontSizeKey, newValue); + set subtitleFontSize(double newValue) => _set(subtitleFontSizeKey, newValue); TextAlign get subtitleTextAlignment => getEnumOrDefault(subtitleTextAlignmentKey, SettingsDefaults.subtitleTextAlignment, TextAlign.values); - set subtitleTextAlignment(TextAlign newValue) => setAndNotify(subtitleTextAlignmentKey, newValue.toString()); + set subtitleTextAlignment(TextAlign newValue) => _set(subtitleTextAlignmentKey, newValue.toString()); SubtitlePosition get subtitleTextPosition => getEnumOrDefault(subtitleTextPositionKey, SettingsDefaults.subtitleTextPosition, SubtitlePosition.values); - set subtitleTextPosition(SubtitlePosition newValue) => setAndNotify(subtitleTextPositionKey, newValue.toString()); + set subtitleTextPosition(SubtitlePosition newValue) => _set(subtitleTextPositionKey, newValue.toString()); bool get subtitleShowOutline => getBool(subtitleShowOutlineKey) ?? SettingsDefaults.subtitleShowOutline; - set subtitleShowOutline(bool newValue) => setAndNotify(subtitleShowOutlineKey, newValue); + set subtitleShowOutline(bool newValue) => _set(subtitleShowOutlineKey, newValue); Color get subtitleTextColor => Color(getInt(subtitleTextColorKey) ?? SettingsDefaults.subtitleTextColor.value); - set subtitleTextColor(Color newValue) => setAndNotify(subtitleTextColorKey, newValue.value); + set subtitleTextColor(Color newValue) => _set(subtitleTextColorKey, newValue.value); Color get subtitleBackgroundColor => Color(getInt(subtitleBackgroundColorKey) ?? SettingsDefaults.subtitleBackgroundColor.value); - set subtitleBackgroundColor(Color newValue) => setAndNotify(subtitleBackgroundColorKey, newValue.value); + set subtitleBackgroundColor(Color newValue) => _set(subtitleBackgroundColorKey, newValue.value); // info double get infoMapZoom => getDouble(infoMapZoomKey) ?? SettingsDefaults.infoMapZoom; - set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); + set infoMapZoom(double newValue) => _set(infoMapZoomKey, newValue); CoordinateFormat get coordinateFormat => getEnumOrDefault(coordinateFormatKey, SettingsDefaults.coordinateFormat, CoordinateFormat.values); - set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString()); + set coordinateFormat(CoordinateFormat newValue) => _set(coordinateFormatKey, newValue.toString()); UnitSystem get unitSystem => getEnumOrDefault(unitSystemKey, SettingsDefaults.unitSystem, UnitSystem.values); - set unitSystem(UnitSystem newValue) => setAndNotify(unitSystemKey, newValue.toString()); + set unitSystem(UnitSystem newValue) => _set(unitSystemKey, newValue.toString()); // tag editor bool get tagEditorCurrentFilterSectionExpanded => getBool(tagEditorCurrentFilterSectionExpandedKey) ?? SettingsDefaults.tagEditorCurrentFilterSectionExpanded; - set tagEditorCurrentFilterSectionExpanded(bool newValue) => setAndNotify(tagEditorCurrentFilterSectionExpandedKey, newValue); + set tagEditorCurrentFilterSectionExpanded(bool newValue) => _set(tagEditorCurrentFilterSectionExpandedKey, newValue); // map @@ -681,106 +709,106 @@ class Settings extends ChangeNotifier { return available.contains(preferred) ? preferred : available.first; } - set mapStyle(EntryMapStyle? newValue) => setAndNotify(mapStyleKey, newValue?.toString()); + set mapStyle(EntryMapStyle? newValue) => _set(mapStyleKey, newValue?.toString()); LatLng? get mapDefaultCenter { final json = getString(mapDefaultCenterKey); return json != null ? LatLng.fromJson(jsonDecode(json)) : null; } - set mapDefaultCenter(LatLng? newValue) => setAndNotify(mapDefaultCenterKey, newValue != null ? jsonEncode(newValue.toJson()) : null); + set mapDefaultCenter(LatLng? newValue) => _set(mapDefaultCenterKey, newValue != null ? jsonEncode(newValue.toJson()) : null); // search bool get saveSearchHistory => getBool(saveSearchHistoryKey) ?? SettingsDefaults.saveSearchHistory; - set saveSearchHistory(bool newValue) => setAndNotify(saveSearchHistoryKey, newValue); + set saveSearchHistory(bool newValue) => _set(saveSearchHistoryKey, newValue); List get searchHistory => (getStringList(searchHistoryKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toList(); - set searchHistory(List newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); + set searchHistory(List newValue) => _set(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); // bin bool get enableBin => getBool(enableBinKey) ?? SettingsDefaults.enableBin; - set enableBin(bool newValue) => setAndNotify(enableBinKey, newValue); + set enableBin(bool newValue) => _set(enableBinKey, newValue); // accessibility bool get showPinchGestureAlternatives => getBool(showPinchGestureAlternativesKey) ?? SettingsDefaults.showPinchGestureAlternatives; - set showPinchGestureAlternatives(bool newValue) => setAndNotify(showPinchGestureAlternativesKey, newValue); + set showPinchGestureAlternatives(bool newValue) => _set(showPinchGestureAlternativesKey, newValue); AccessibilityAnimations get accessibilityAnimations => getEnumOrDefault(accessibilityAnimationsKey, SettingsDefaults.accessibilityAnimations, AccessibilityAnimations.values); - set accessibilityAnimations(AccessibilityAnimations newValue) => setAndNotify(accessibilityAnimationsKey, newValue.toString()); + set accessibilityAnimations(AccessibilityAnimations newValue) => _set(accessibilityAnimationsKey, newValue.toString()); AccessibilityTimeout get timeToTakeAction => getEnumOrDefault(timeToTakeActionKey, SettingsDefaults.timeToTakeAction, AccessibilityTimeout.values); - set timeToTakeAction(AccessibilityTimeout newValue) => setAndNotify(timeToTakeActionKey, newValue.toString()); + set timeToTakeAction(AccessibilityTimeout newValue) => _set(timeToTakeActionKey, newValue.toString()); // file picker bool get filePickerShowHiddenFiles => getBool(filePickerShowHiddenFilesKey) ?? SettingsDefaults.filePickerShowHiddenFiles; - set filePickerShowHiddenFiles(bool newValue) => setAndNotify(filePickerShowHiddenFilesKey, newValue); + set filePickerShowHiddenFiles(bool newValue) => _set(filePickerShowHiddenFilesKey, newValue); // screen saver bool get screenSaverFillScreen => getBool(screenSaverFillScreenKey) ?? SettingsDefaults.slideshowFillScreen; - set screenSaverFillScreen(bool newValue) => setAndNotify(screenSaverFillScreenKey, newValue); + set screenSaverFillScreen(bool newValue) => _set(screenSaverFillScreenKey, newValue); bool get screenSaverAnimatedZoomEffect => getBool(screenSaverAnimatedZoomEffectKey) ?? SettingsDefaults.slideshowAnimatedZoomEffect; - set screenSaverAnimatedZoomEffect(bool newValue) => setAndNotify(screenSaverAnimatedZoomEffectKey, newValue); + set screenSaverAnimatedZoomEffect(bool newValue) => _set(screenSaverAnimatedZoomEffectKey, newValue); ViewerTransition get screenSaverTransition => getEnumOrDefault(screenSaverTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); - set screenSaverTransition(ViewerTransition newValue) => setAndNotify(screenSaverTransitionKey, newValue.toString()); + set screenSaverTransition(ViewerTransition newValue) => _set(screenSaverTransitionKey, newValue.toString()); SlideshowVideoPlayback get screenSaverVideoPlayback => getEnumOrDefault(screenSaverVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values); - set screenSaverVideoPlayback(SlideshowVideoPlayback newValue) => setAndNotify(screenSaverVideoPlaybackKey, newValue.toString()); + set screenSaverVideoPlayback(SlideshowVideoPlayback newValue) => _set(screenSaverVideoPlaybackKey, newValue.toString()); int get screenSaverInterval => getInt(screenSaverIntervalKey) ?? SettingsDefaults.slideshowInterval; - set screenSaverInterval(int newValue) => setAndNotify(screenSaverIntervalKey, newValue); + set screenSaverInterval(int newValue) => _set(screenSaverIntervalKey, newValue); Set get screenSaverCollectionFilters => (getStringList(screenSaverCollectionFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); - set screenSaverCollectionFilters(Set newValue) => setAndNotify(screenSaverCollectionFiltersKey, newValue.map((filter) => filter.toJson()).toList()); + set screenSaverCollectionFilters(Set newValue) => _set(screenSaverCollectionFiltersKey, newValue.map((filter) => filter.toJson()).toList()); // slideshow bool get slideshowRepeat => getBool(slideshowRepeatKey) ?? SettingsDefaults.slideshowRepeat; - set slideshowRepeat(bool newValue) => setAndNotify(slideshowRepeatKey, newValue); + set slideshowRepeat(bool newValue) => _set(slideshowRepeatKey, newValue); bool get slideshowShuffle => getBool(slideshowShuffleKey) ?? SettingsDefaults.slideshowShuffle; - set slideshowShuffle(bool newValue) => setAndNotify(slideshowShuffleKey, newValue); + set slideshowShuffle(bool newValue) => _set(slideshowShuffleKey, newValue); bool get slideshowFillScreen => getBool(slideshowFillScreenKey) ?? SettingsDefaults.slideshowFillScreen; - set slideshowFillScreen(bool newValue) => setAndNotify(slideshowFillScreenKey, newValue); + set slideshowFillScreen(bool newValue) => _set(slideshowFillScreenKey, newValue); bool get slideshowAnimatedZoomEffect => getBool(slideshowAnimatedZoomEffectKey) ?? SettingsDefaults.slideshowAnimatedZoomEffect; - set slideshowAnimatedZoomEffect(bool newValue) => setAndNotify(slideshowAnimatedZoomEffectKey, newValue); + set slideshowAnimatedZoomEffect(bool newValue) => _set(slideshowAnimatedZoomEffectKey, newValue); ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values); - set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString()); + set slideshowTransition(ViewerTransition newValue) => _set(slideshowTransitionKey, newValue.toString()); SlideshowVideoPlayback get slideshowVideoPlayback => getEnumOrDefault(slideshowVideoPlaybackKey, SettingsDefaults.slideshowVideoPlayback, SlideshowVideoPlayback.values); - set slideshowVideoPlayback(SlideshowVideoPlayback newValue) => setAndNotify(slideshowVideoPlaybackKey, newValue.toString()); + set slideshowVideoPlayback(SlideshowVideoPlayback newValue) => _set(slideshowVideoPlaybackKey, newValue.toString()); int get slideshowInterval => getInt(slideshowIntervalKey) ?? SettingsDefaults.slideshowInterval; - set slideshowInterval(int newValue) => setAndNotify(slideshowIntervalKey, newValue); + set slideshowInterval(int newValue) => _set(slideshowIntervalKey, newValue); // widget @@ -789,27 +817,27 @@ class Settings extends ChangeNotifier { return value != null ? Color(value) : null; } - void setWidgetOutline(int widgetId, Color? newValue) => setAndNotify('$widgetOutlinePrefixKey$widgetId', newValue?.value); + void setWidgetOutline(int widgetId, Color? newValue) => _set('$widgetOutlinePrefixKey$widgetId', newValue?.value); WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('$widgetShapePrefixKey$widgetId', SettingsDefaults.widgetShape, WidgetShape.values); - void setWidgetShape(int widgetId, WidgetShape newValue) => setAndNotify('$widgetShapePrefixKey$widgetId', newValue.toString()); + void setWidgetShape(int widgetId, WidgetShape newValue) => _set('$widgetShapePrefixKey$widgetId', newValue.toString()); Set getWidgetCollectionFilters(int widgetId) => (getStringList('$widgetCollectionFiltersPrefixKey$widgetId') ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet(); - void setWidgetCollectionFilters(int widgetId, Set newValue) => setAndNotify('$widgetCollectionFiltersPrefixKey$widgetId', newValue.map((filter) => filter.toJson()).toList()); + void setWidgetCollectionFilters(int widgetId, Set newValue) => _set('$widgetCollectionFiltersPrefixKey$widgetId', newValue.map((filter) => filter.toJson()).toList()); WidgetOpenPage getWidgetOpenPage(int widgetId) => getEnumOrDefault('$widgetOpenPagePrefixKey$widgetId', SettingsDefaults.widgetOpenPage, WidgetOpenPage.values); - void setWidgetOpenPage(int widgetId, WidgetOpenPage newValue) => setAndNotify('$widgetOpenPagePrefixKey$widgetId', newValue.toString()); + void setWidgetOpenPage(int widgetId, WidgetOpenPage newValue) => _set('$widgetOpenPagePrefixKey$widgetId', newValue.toString()); WidgetDisplayedItem getWidgetDisplayedItem(int widgetId) => getEnumOrDefault('$widgetDisplayedItemPrefixKey$widgetId', SettingsDefaults.widgetDisplayedItem, WidgetDisplayedItem.values); - void setWidgetDisplayedItem(int widgetId, WidgetDisplayedItem newValue) => setAndNotify('$widgetDisplayedItemPrefixKey$widgetId', newValue.toString()); + void setWidgetDisplayedItem(int widgetId, WidgetDisplayedItem newValue) => _set('$widgetDisplayedItemPrefixKey$widgetId', newValue.toString()); String? getWidgetUri(int widgetId) => getString('$widgetUriPrefixKey$widgetId'); - void setWidgetUri(int widgetId, String? newValue) => setAndNotify('$widgetUriPrefixKey$widgetId', newValue); + void setWidgetUri(int widgetId, String? newValue) => _set('$widgetUriPrefixKey$widgetId', newValue); // convenience methods @@ -876,7 +904,7 @@ class Settings extends ChangeNotifier { return settingsStore.getStringList(key)?.map((s) => values.firstWhereOrNull((v) => v.toString() == s)).whereNotNull().toList() ?? defaultValue; } - void setAndNotify(String key, dynamic newValue) { + void _set(String key, dynamic newValue) { var oldValue = settingsStore.get(key); if (newValue == null) { settingsStore.remove(key); @@ -922,11 +950,11 @@ class Settings extends ChangeNotifier { bool get isRotationLocked => getBool(platformAccelerometerRotationKey) ?? SettingsDefaults.isRotationLocked; - set isRotationLocked(bool newValue) => setAndNotify(platformAccelerometerRotationKey, newValue); + set isRotationLocked(bool newValue) => _set(platformAccelerometerRotationKey, newValue); bool get areAnimationsRemoved => getBool(platformTransitionAnimationScaleKey) ?? SettingsDefaults.areAnimationsRemoved; - set areAnimationsRemoved(bool newValue) => setAndNotify(platformTransitionAnimationScaleKey, newValue); + set areAnimationsRemoved(bool newValue) => _set(platformTransitionAnimationScaleKey, newValue); // import/export @@ -990,8 +1018,6 @@ class Settings extends ChangeNotifier { case setMetadataDateBeforeFileOpKey: case collectionSortReverseKey: case showThumbnailFavouriteKey: - case showThumbnailTagKey: - case showThumbnailLocationKey: case showThumbnailMotionPhotoKey: case showThumbnailRatingKey: case showThumbnailRawKey: @@ -1002,6 +1028,7 @@ class Settings extends ChangeNotifier { case showOverlayOnOpeningKey: case showOverlayMinimapKey: case showOverlayInfoKey: + case showOverlayDescriptionKey: case showOverlayRatingTagsKey: case showOverlayShootingDetailsKey: case showOverlayThumbnailPreviewKey: @@ -1037,6 +1064,8 @@ class Settings extends ChangeNotifier { case homePageKey: case collectionGroupFactorKey: case collectionSortFactorKey: + case thumbnailLocationIconKey: + case thumbnailTagIconKey: case albumGroupFactorKey: case albumSortFactorKey: case countrySortFactorKey: @@ -1084,6 +1113,7 @@ class Settings extends ChangeNotifier { _updateStreamController.add(SettingsChangedEvent(key, oldValue, newValue)); } }); + await sanitize(); notifyListeners(); } } diff --git a/lib/model/video/metadata.dart b/lib/model/video/metadata.dart index c6853c6ee..7245ca79b 100644 --- a/lib/model/video/metadata.dart +++ b/lib/model/video/metadata.dart @@ -22,7 +22,7 @@ import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/foundation.dart'; class VideoMetadataFormatter { - static final _dateY4M2D2H2m2s2Pattern = RegExp(r'(\d{4})[-./](\d{1,2})[-./](\d{1,2})([ T](\d{1,2}):(\d{1,2}):(\d{1,2})( ([ap]\.? ?m\.?))?)?'); + static final _dateY4M2D2H2m2s2Pattern = RegExp(r'(\d{4})[-./:](\d{1,2})[-./:](\d{1,2})([ T](\d{1,2}):(\d{1,2}):(\d{1,2})( ([ap]\.? ?m\.?))?)?'); static final _ambiguousDatePatterns = { RegExp(r'^\d{2}[-/]\d{2}[-/]\d{4}$'), }; @@ -127,6 +127,7 @@ class VideoMetadataFormatter { // - `2022-01-28T5:07:46 p. m.Z` // - `2012-1-1T12:00:00Z` // - `2020.10.14` + // - `2016:11:16 18:00:00` // - `2021` (not enough to build a date) var match = _dateY4M2D2H2m2s2Pattern.firstMatch(dateString); diff --git a/lib/ref/mime_types.dart b/lib/ref/mime_types.dart index 9df697fc2..744fc9712 100644 --- a/lib/ref/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -60,6 +60,7 @@ class MimeTypes { static const mkv = 'video/mkv'; static const mkvX = 'video/x-matroska'; static const mov = 'video/quicktime'; + static const movX = 'video/x-quicktime'; static const mp2p = 'video/mp2p'; static const mp2t = 'video/mp2t'; // .m2ts, .ts static const mp2ts = 'video/mp2ts'; // .ts (prefer `mp2t` when possible) @@ -89,7 +90,7 @@ class MimeTypes { static const Set _knownOpaqueImages = {jpeg}; - static const Set _knownVideos = {v3gpp, asf, avi, aviMSVideo, aviVnd, aviXMSVideo, flv, flvX, mkv, mkvX, mov, mp2p, mp2t, mp2ts, mp4, mpeg, ogv, realVideo, webm, wmv}; + static const Set _knownVideos = {v3gpp, asf, avi, aviMSVideo, aviVnd, aviXMSVideo, flv, flvX, mkv, mkvX, mov, movX, mp2p, mp2t, mp2ts, mp4, mpeg, ogv, realVideo, webm, wmv}; static final Set knownMediaTypes = { anyImage, @@ -107,30 +108,35 @@ class MimeTypes { static bool isVisual(String mimeType) => isImage(mimeType) || isVideo(mimeType); - static bool refersToSameType(String a, b) { - switch (a) { + static String _collapsedType(String mimeType) { + switch (mimeType) { case avi: case aviMSVideo: case aviVnd: case aviXMSVideo: - return [avi, aviVnd].contains(b); + return avi; case bmp: case bmpX: - return [bmp, bmpX].contains(b); + return bmp; case flv: case flvX: - return [flv, flvX].contains(b); + return flv; case heic: case heif: - return [heic, heif].contains(b); + return heic; + case mov: + case movX: + return mov; case psdVnd: case psdX: - return [psdVnd, psdX].contains(b); + return psdVnd; default: - return a == b; + return mimeType; } } + static bool refersToSameType(String a, b) => _collapsedType(a) == _collapsedType(b); + static String? forExtension(String extension) { switch (extension) { case '.jpg': diff --git a/lib/services/accessibility_service.dart b/lib/services/accessibility_service.dart index 61ac1bb34..42a9f81f0 100644 --- a/lib/services/accessibility_service.dart +++ b/lib/services/accessibility_service.dart @@ -1,4 +1,5 @@ import 'package:aves/services/common/services.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; class AccessibilityService { @@ -24,14 +25,17 @@ class AccessibilityService { return false; } + static bool? _hasRecommendedTimeouts; + static Future hasRecommendedTimeouts() async { + if (_hasRecommendedTimeouts != null) return SynchronousFuture(_hasRecommendedTimeouts!); try { final result = await _platform.invokeMethod('hasRecommendedTimeouts'); - if (result != null) return result as bool; + _hasRecommendedTimeouts = result as bool?; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } - return false; + return _hasRecommendedTimeouts ?? false; } static Future getRecommendedTimeToRead(Duration originalTimeoutDuration) async { diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index 5152c9ad1..1db5487ad 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -148,32 +148,34 @@ class PlatformAndroidAppService implements AndroidAppService { } @override - Future shareEntries(Iterable entries) async { - // loosen MIME type to a generic one, so we can share with badly defined apps - // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats - final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); + Future shareEntries(Iterable entries) { + return _share(groupBy( + entries, + // loosen MIME type to a generic one, so we can share with badly defined apps + // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats + (e) => e.mimeTypeAnySubtype, + ).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()))); + } + + @override + Future shareSingle(String uri, String mimeType) { + return _share({ + mimeType: [uri] + }); + } + + Future _share(Map> urisByMimeType) async { try { final result = await _platform.invokeMethod('share', { 'urisByMimeType': urisByMimeType, }); if (result != null) return result as bool; } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); - } - return false; - } - - @override - Future shareSingle(String uri, String mimeType) async { - try { - final result = await _platform.invokeMethod('share', { - 'urisByMimeType': { - mimeType: [uri] - }, - }); - if (result != null) return result as bool; - } on PlatformException catch (e, stack) { - await reportService.recordError(e, stack); + if (e.code == 'share-large') { + throw TooManyItemsException(); + } else { + await reportService.recordError(e, stack); + } } return false; } @@ -207,3 +209,5 @@ class PlatformAndroidAppService implements AndroidAppService { } } } + +class TooManyItemsException implements Exception {} diff --git a/lib/services/media/embedded_data_service.dart b/lib/services/media/embedded_data_service.dart index 23b47849c..33f024ba2 100644 --- a/lib/services/media/embedded_data_service.dart +++ b/lib/services/media/embedded_data_service.dart @@ -6,6 +6,8 @@ import 'package:flutter/services.dart'; abstract class EmbeddedDataService { Future> getExifThumbnails(AvesEntry entry); + Future extractGoogleDeviceItem(AvesEntry entry, String dataUri); + Future extractMotionPhotoImage(AvesEntry entry); Future extractMotionPhotoVideo(AvesEntry entry); @@ -33,6 +35,23 @@ class PlatformEmbeddedDataService implements EmbeddedDataService { return []; } + @override + Future extractGoogleDeviceItem(AvesEntry entry, String dataUri) async { + try { + final result = await _platform.invokeMethod('extractGoogleDeviceItem', { + 'mimeType': entry.mimeType, + 'uri': entry.uri, + 'sizeBytes': entry.sizeBytes, + 'displayName': ['${entry.bestTitle}', dataUri].join(Constants.separator), + 'dataUri': dataUri, + }); + if (result != null) return result as Map; + } on PlatformException catch (e, stack) { + await reportService.recordError(e, stack); + } + return {}; + } + @override Future extractMotionPhotoImage(AvesEntry entry) async { try { diff --git a/lib/services/media/media_session_service.dart b/lib/services/media/media_session_service.dart index 4914e9237..c6cb94b8a 100644 --- a/lib/services/media/media_session_service.dart +++ b/lib/services/media/media_session_service.dart @@ -1,18 +1,33 @@ import 'dart:async'; +import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; abstract class MediaSessionService { + Stream get mediaCommands; + Future update(AvesVideoController controller); - Future release(String uri); + Future release(); } class PlatformMediaSessionService implements MediaSessionService { static const _platformObject = MethodChannel('deckers.thibault/aves/media_session'); + final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command'); + final StreamController _streamController = StreamController.broadcast(); + + PlatformMediaSessionService() { + _mediaCommandChannel.receiveBroadcastStream().listen((event) => _onMediaCommand(event as Map?)); + } + + @override + Stream get mediaCommands => _streamController.stream.where((event) => event is MediaCommandEvent).cast(); + @override Future update(AvesVideoController controller) async { final entry = controller.entry; @@ -31,11 +46,9 @@ class PlatformMediaSessionService implements MediaSessionService { } @override - Future release(String uri) async { + Future release() async { try { - await _platformObject.invokeMethod('release', { - 'uri': uri, - }); + await _platformObject.invokeMethod('release'); } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } @@ -54,4 +67,52 @@ class PlatformMediaSessionService implements MediaSessionService { return 'stopped'; } } + + void _onMediaCommand(Map? fields) { + if (fields == null) return; + final command = fields['command'] as String?; + MediaCommandEvent? event; + switch (command) { + case 'play': + event = const MediaCommandEvent(MediaCommand.play); + break; + case 'pause': + event = const MediaCommandEvent(MediaCommand.pause); + break; + case 'stop': + event = const MediaCommandEvent(MediaCommand.stop); + break; + case 'seek': + final position = fields['position'] as int?; + if (position != null) { + event = MediaSeekCommandEvent(MediaCommand.stop, position: position); + } + break; + } + if (event != null) { + _streamController.add(event); + } + } +} + +enum MediaCommand { play, pause, stop, seek } + +@immutable +class MediaCommandEvent extends Equatable { + final MediaCommand command; + + @override + List get props => [command]; + + const MediaCommandEvent(this.command); +} + +@immutable +class MediaSeekCommandEvent extends MediaCommandEvent { + final int position; + + @override + List get props => [...super.props, position]; + + const MediaSeekCommandEvent(super.command, {required this.position}); } diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart index 1c99b1161..576b832b8 100644 --- a/lib/services/window_service.dart +++ b/lib/services/window_service.dart @@ -1,4 +1,5 @@ import 'package:aves/services/common/services.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -11,9 +12,9 @@ abstract class WindowService { Future requestOrientation([Orientation? orientation]); - Future canSetCutoutMode(); + Future isCutoutAware(); - Future setCutoutMode(bool use); + Future getCutoutInsets(); } class PlatformWindowService implements WindowService { @@ -79,25 +80,35 @@ class PlatformWindowService implements WindowService { } } + bool? _isCutoutAware; + @override - Future canSetCutoutMode() async { + Future isCutoutAware() async { + if (_isCutoutAware != null) return SynchronousFuture(_isCutoutAware!); try { - final result = await _platform.invokeMethod('canSetCutoutMode'); - if (result != null) return result as bool; + final result = await _platform.invokeMethod('isCutoutAware'); + _isCutoutAware = result as bool?; } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } - return false; + return _isCutoutAware ?? false; } @override - Future setCutoutMode(bool use) async { + Future getCutoutInsets() async { try { - await _platform.invokeMethod('setCutoutMode', { - 'use': use, - }); + final result = await _platform.invokeMethod('getCutoutInsets'); + if (result != null) { + return EdgeInsets.only( + left: result['left']?.toDouble() ?? 0, + top: result['top']?.toDouble() ?? 0, + right: result['right']?.toDouble() ?? 0, + bottom: result['bottom']?.toDouble() ?? 0, + ); + } } on PlatformException catch (e, stack) { await reportService.recordError(e, stack); } + return EdgeInsets.zero; } } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index aa618c6c7..859ae55f6 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -1,6 +1,5 @@ import 'dart:ui'; -import 'package:aves/app_flavor.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; @@ -69,367 +68,4 @@ class Constants { static const int infoGroupMaxValueLength = 140; static const String avesGithub = 'https://github.com/deckerst/aves'; - - static const String apache2 = 'Apache License 2.0'; - static const String bsd2 = 'BSD 2-Clause "Simplified" License'; - static const String bsd3 = 'BSD 3-Clause "Revised" License'; - static const String eclipse1 = 'Eclipse Public License 1.0'; - static const String mit = 'MIT License'; - - static const List androidDependencies = [ - Dependency( - name: 'AndroidSVG', - license: apache2, - sourceUrl: 'https://github.com/BigBadaboom/androidsvg', - ), - Dependency( - name: 'AndroidX (Core Kotlin, Exifinterface, Lifecycle Process, Multidex)', - license: apache2, - licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt', - sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/core/core-ktx', - ), - Dependency( - name: 'CWAC-Document', - license: apache2, - sourceUrl: 'https://github.com/commonsguy/cwac-document', - ), - Dependency( - name: 'Glide', - license: '$apache2, $bsd2', - sourceUrl: 'https://github.com/bumptech/glide', - ), - Dependency( - name: 'Metadata Extractor', - license: apache2, - sourceUrl: 'https://github.com/drewnoakes/metadata-extractor', - ), - Dependency( - name: 'MP4 Parser (Aves fork)', - license: apache2, - sourceUrl: 'https://github.com/deckerst/mp4parser', - ), - Dependency( - name: 'PixyMeta Android (Aves fork)', - license: eclipse1, - sourceUrl: 'https://github.com/deckerst/pixymeta-android', - ), - Dependency( - name: 'Tiff Bitmap Factory (Aves fork)', - license: mit, - licenseUrl: 'https://github.com/deckerst/Android-TiffBitmapFactory/blob/master/license.txt', - sourceUrl: 'https://github.com/deckerst/Android-TiffBitmapFactory', - ), - ]; - - static const List _flutterPluginsCommon = [ - Dependency( - name: 'Connectivity Plus', - license: bsd3, - licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/connectivity_plus/connectivity_plus/LICENSE', - sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus', - ), - Dependency( - name: 'Device Info Plus', - license: bsd3, - licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/device_info_plus/device_info_plus/LICENSE', - sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus', - ), - Dependency( - name: 'Dynamic Color', - license: bsd3, - sourceUrl: 'https://github.com/material-foundation/material-dynamic-color-flutter', - ), - Dependency( - name: 'fijkplayer (Aves fork)', - license: mit, - sourceUrl: 'https://github.com/deckerst/fijkplayer', - ), - Dependency( - name: 'Flutter Display Mode', - license: mit, - sourceUrl: 'https://github.com/ajinasokan/flutter_displaymode', - ), - Dependency( - name: 'Package Info Plus', - license: bsd3, - licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/package_info_plus/package_info_plus/LICENSE', - sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus', - ), - Dependency( - name: 'Permission Handler', - license: mit, - sourceUrl: 'https://github.com/Baseflow/flutter-permission-handler', - ), - Dependency( - name: 'Printing', - license: apache2, - sourceUrl: 'https://github.com/DavBfr/dart_pdf', - ), - Dependency( - name: 'Screen Brightness', - license: mit, - sourceUrl: 'https://github.com/aaassseee/screen_brightness', - ), - Dependency( - name: 'Shared Preferences', - license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences', - ), - Dependency( - name: 'sqflite', - license: bsd2, - sourceUrl: 'https://github.com/tekartik/sqflite', - ), - Dependency( - name: 'Streams Channel (Aves fork)', - license: apache2, - sourceUrl: 'https://github.com/deckerst/aves_streams_channel', - ), - Dependency( - name: 'URL Launcher', - license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher', - ), - ]; - - static const List _googleMobileServices = [ - Dependency( - name: 'Google API Availability', - license: mit, - sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability', - ), - Dependency( - name: 'Google Maps for Flutter', - license: bsd3, - licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', - sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter', - ), - ]; - - static const List _huaweiMobileServices = [ - Dependency( - name: 'Huawei Mobile Services (Availability, Map)', - license: apache2, - licenseUrl: 'https://github.com/HMS-Core/hms-flutter-plugin/blob/master/LICENCE', - sourceUrl: 'https://github.com/HMS-Core/hms-flutter-plugin', - ), - ]; - - static const List _flutterPluginsHuaweiOnly = [ - ..._huaweiMobileServices, - ]; - - static const List _flutterPluginsIzzyOnly = [ - ..._googleMobileServices, - ]; - - static const List _flutterPluginsLibreOnly = []; - - static const List _flutterPluginsPlayOnly = [ - ..._googleMobileServices, - Dependency( - name: 'FlutterFire (Core, Crashlytics)', - license: bsd3, - sourceUrl: 'https://github.com/FirebaseExtended/flutterfire', - ), - ]; - - static List flutterPlugins(AppFlavor flavor) => [ - ..._flutterPluginsCommon, - if (flavor == AppFlavor.huawei) ..._flutterPluginsHuaweiOnly, - if (flavor == AppFlavor.izzy) ..._flutterPluginsIzzyOnly, - if (flavor == AppFlavor.libre) ..._flutterPluginsLibreOnly, - if (flavor == AppFlavor.play) ..._flutterPluginsPlayOnly, - ]; - - static const List flutterPackages = [ - Dependency( - name: 'Charts', - license: apache2, - sourceUrl: 'https://github.com/google/charts', - ), - Dependency( - name: 'Custom rounded rectangle border', - license: mit, - sourceUrl: 'https://github.com/lekanbar/custom_rounded_rectangle_border', - ), - Dependency( - name: 'Decorated Icon', - license: mit, - sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', - ), - Dependency( - name: 'Expansion Tile Card (Aves fork)', - license: bsd3, - sourceUrl: 'https://github.com/deckerst/expansion_tile_card', - ), - Dependency( - name: 'FlexColorPicker', - license: bsd3, - sourceUrl: 'https://github.com/rydmike/flex_color_picker', - ), - Dependency( - name: 'Flutter Highlight', - license: mit, - sourceUrl: 'https://github.com/git-touch/highlight', - ), - Dependency( - name: 'Flutter Map', - license: bsd3, - sourceUrl: 'https://github.com/fleaflet/flutter_map', - ), - Dependency( - name: 'Flutter Markdown', - license: bsd3, - licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/flutter_markdown/LICENSE', - sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/flutter_markdown', - ), - Dependency( - name: 'Flutter Staggered Animations', - license: mit, - sourceUrl: 'https://github.com/mobiten/flutter_staggered_animations', - ), - Dependency( - name: 'Material Design Icons Flutter', - license: mit, - sourceUrl: 'https://github.com/ziofat/material_design_icons_flutter', - ), - Dependency( - name: 'Overlay Support', - license: apache2, - sourceUrl: 'https://github.com/boyan01/overlay_support', - ), - Dependency( - name: 'Palette Generator', - license: bsd3, - licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/palette_generator/LICENSE', - sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/palette_generator', - ), - Dependency( - name: 'Panorama (Aves fork)', - license: apache2, - sourceUrl: 'https://github.com/zesage/panorama', - ), - Dependency( - name: 'Percent Indicator', - license: bsd2, - sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator', - ), - Dependency( - name: 'Provider', - license: mit, - sourceUrl: 'https://github.com/rrousselGit/provider', - ), - Dependency( - name: 'Smooth Page Indicator', - license: mit, - sourceUrl: 'https://github.com/Milad-Akarie/smooth_page_indicator', - ), - ]; - - static const List dartPackages = [ - Dependency( - name: 'Collection', - license: bsd3, - sourceUrl: 'https://github.com/dart-lang/collection', - ), - Dependency( - name: 'Country Code', - license: mit, - sourceUrl: 'https://github.com/denixport/dart.country', - ), - Dependency( - name: 'Equatable', - license: mit, - sourceUrl: 'https://github.com/felangel/equatable', - ), - Dependency( - name: 'Event Bus', - license: mit, - sourceUrl: 'https://github.com/marcojakob/dart-event-bus', - ), - Dependency( - name: 'Fluster', - license: mit, - sourceUrl: 'https://github.com/alfonsocejudo/fluster', - ), - Dependency( - name: 'Flutter Lints', - license: bsd3, - licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/flutter_lints/LICENSE', - sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/flutter_lints', - ), - Dependency( - name: 'Get It', - license: mit, - sourceUrl: 'https://github.com/fluttercommunity/get_it', - ), - Dependency( - name: 'Intl', - license: bsd3, - sourceUrl: 'https://github.com/dart-lang/intl', - ), - Dependency( - name: 'LatLong2', - license: apache2, - sourceUrl: 'https://github.com/jifalops/dart-latlong', - ), - Dependency( - name: 'Material Color Utilities', - license: apache2, - licenseUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart/LICENSE', - sourceUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', - ), - Dependency( - name: 'Path', - license: bsd3, - sourceUrl: 'https://github.com/dart-lang/path', - ), - Dependency( - name: 'PDF for Dart and Flutter', - license: apache2, - sourceUrl: 'https://github.com/DavBfr/dart_pdf', - ), - Dependency( - name: 'Proj4dart', - license: mit, - sourceUrl: 'https://github.com/maRci002/proj4dart', - ), - Dependency( - name: 'Stack Trace', - license: bsd3, - sourceUrl: 'https://github.com/dart-lang/stack_trace', - ), - Dependency( - name: 'Transparent Image', - license: mit, - sourceUrl: 'https://github.com/brianegan/transparent_image', - ), - Dependency( - name: 'Tuple', - license: bsd2, - sourceUrl: 'https://github.com/google/tuple.dart', - ), - Dependency( - name: 'XML', - license: mit, - sourceUrl: 'https://github.com/renggli/dart-xml', - ), - ]; -} - -class Dependency { - final String name; - final String license; - final String sourceUrl; - final String licenseUrl; - - const Dependency({ - required this.name, - required this.license, - String? licenseUrl, - required this.sourceUrl, - }) : licenseUrl = licenseUrl ?? '$sourceUrl/blob/master/LICENSE'; } diff --git a/lib/utils/dependencies.dart b/lib/utils/dependencies.dart new file mode 100644 index 000000000..9bb4babd0 --- /dev/null +++ b/lib/utils/dependencies.dart @@ -0,0 +1,366 @@ +import 'package:aves/app_flavor.dart'; + +class Dependencies { + static const String apache2 = 'Apache License 2.0'; + static const String bsd2 = 'BSD 2-Clause "Simplified" License'; + static const String bsd3 = 'BSD 3-Clause "Revised" License'; + static const String eclipse1 = 'Eclipse Public License 1.0'; + static const String mit = 'MIT License'; + + static const List androidDependencies = [ + Dependency( + name: 'AndroidSVG', + license: apache2, + sourceUrl: 'https://github.com/BigBadaboom/androidsvg', + ), + Dependency( + name: 'AndroidX (Core Kotlin, Exifinterface, Lifecycle Process, Multidex)', + license: apache2, + licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt', + sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-main/core/core-ktx', + ), + Dependency( + name: 'CWAC-Document', + license: apache2, + sourceUrl: 'https://github.com/commonsguy/cwac-document', + ), + Dependency( + name: 'Glide', + license: '$apache2, $bsd2', + sourceUrl: 'https://github.com/bumptech/glide', + ), + Dependency( + name: 'Metadata Extractor', + license: apache2, + sourceUrl: 'https://github.com/drewnoakes/metadata-extractor', + ), + Dependency( + name: 'MP4 Parser (Aves fork)', + license: apache2, + sourceUrl: 'https://github.com/deckerst/mp4parser', + ), + Dependency( + name: 'PixyMeta Android (Aves fork)', + license: eclipse1, + sourceUrl: 'https://github.com/deckerst/pixymeta-android', + ), + Dependency( + name: 'Tiff Bitmap Factory (Aves fork)', + license: mit, + licenseUrl: 'https://github.com/deckerst/Android-TiffBitmapFactory/blob/master/license.txt', + sourceUrl: 'https://github.com/deckerst/Android-TiffBitmapFactory', + ), + ]; + + static const List _flutterPluginsCommon = [ + Dependency( + name: 'Connectivity Plus', + license: bsd3, + licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/connectivity_plus/connectivity_plus/LICENSE', + sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/connectivity_plus', + ), + Dependency( + name: 'Device Info Plus', + license: bsd3, + licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/device_info_plus/device_info_plus/LICENSE', + sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/device_info_plus', + ), + Dependency( + name: 'Dynamic Color', + license: bsd3, + sourceUrl: 'https://github.com/material-foundation/material-dynamic-color-flutter', + ), + Dependency( + name: 'fijkplayer (Aves fork)', + license: mit, + sourceUrl: 'https://github.com/deckerst/fijkplayer', + ), + Dependency( + name: 'Flutter Display Mode', + license: mit, + sourceUrl: 'https://github.com/ajinasokan/flutter_displaymode', + ), + Dependency( + name: 'Package Info Plus', + license: bsd3, + licenseUrl: 'https://github.com/fluttercommunity/plus_plugins/blob/main/packages/package_info_plus/package_info_plus/LICENSE', + sourceUrl: 'https://github.com/fluttercommunity/plus_plugins/tree/main/packages/package_info_plus', + ), + Dependency( + name: 'Permission Handler', + license: mit, + sourceUrl: 'https://github.com/Baseflow/flutter-permission-handler', + ), + Dependency( + name: 'Printing', + license: apache2, + sourceUrl: 'https://github.com/DavBfr/dart_pdf', + ), + Dependency( + name: 'Screen Brightness', + license: mit, + sourceUrl: 'https://github.com/aaassseee/screen_brightness', + ), + Dependency( + name: 'Shared Preferences', + license: bsd3, + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/shared_preferences/shared_preferences/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences', + ), + Dependency( + name: 'sqflite', + license: bsd2, + sourceUrl: 'https://github.com/tekartik/sqflite', + ), + Dependency( + name: 'Streams Channel (Aves fork)', + license: apache2, + sourceUrl: 'https://github.com/deckerst/aves_streams_channel', + ), + Dependency( + name: 'URL Launcher', + license: bsd3, + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher', + ), + ]; + + static const List _googleMobileServices = [ + Dependency( + name: 'Google API Availability', + license: mit, + sourceUrl: 'https://github.com/Baseflow/flutter-google-api-availability', + ), + Dependency( + name: 'Google Maps for Flutter', + license: bsd3, + licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/google_maps_flutter/google_maps_flutter/LICENSE', + sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter', + ), + ]; + + static const List _huaweiMobileServices = [ + Dependency( + name: 'Huawei Mobile Services (Availability, Map)', + license: apache2, + licenseUrl: 'https://github.com/HMS-Core/hms-flutter-plugin/blob/master/LICENCE', + sourceUrl: 'https://github.com/HMS-Core/hms-flutter-plugin', + ), + ]; + + static const List _flutterPluginsHuaweiOnly = [ + ..._huaweiMobileServices, + ]; + + static const List _flutterPluginsIzzyOnly = [ + ..._googleMobileServices, + ]; + + static const List _flutterPluginsLibreOnly = []; + + static const List _flutterPluginsPlayOnly = [ + ..._googleMobileServices, + Dependency( + name: 'FlutterFire (Core, Crashlytics)', + license: bsd3, + sourceUrl: 'https://github.com/FirebaseExtended/flutterfire', + ), + ]; + + static List flutterPlugins(AppFlavor flavor) => [ + ..._flutterPluginsCommon, + if (flavor == AppFlavor.huawei) ..._flutterPluginsHuaweiOnly, + if (flavor == AppFlavor.izzy) ..._flutterPluginsIzzyOnly, + if (flavor == AppFlavor.libre) ..._flutterPluginsLibreOnly, + if (flavor == AppFlavor.play) ..._flutterPluginsPlayOnly, + ]; + + static const List flutterPackages = [ + Dependency( + name: 'Charts (fzyzcjy fork)', + license: apache2, + sourceUrl: 'https://github.com/fzyzcjy/charts', + ), + Dependency( + name: 'Custom rounded rectangle border', + license: mit, + sourceUrl: 'https://github.com/lekanbar/custom_rounded_rectangle_border', + ), + Dependency( + name: 'Decorated Icon', + license: mit, + sourceUrl: 'https://github.com/benPesso/flutter_decorated_icon', + ), + Dependency( + name: 'Expansion Tile Card (Aves fork)', + license: bsd3, + sourceUrl: 'https://github.com/deckerst/expansion_tile_card', + ), + Dependency( + name: 'FlexColorPicker', + license: bsd3, + sourceUrl: 'https://github.com/rydmike/flex_color_picker', + ), + Dependency( + name: 'Flutter Highlight', + license: mit, + sourceUrl: 'https://github.com/git-touch/highlight', + ), + Dependency( + name: 'Flutter Map', + license: bsd3, + sourceUrl: 'https://github.com/fleaflet/flutter_map', + ), + Dependency( + name: 'Flutter Markdown', + license: bsd3, + licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/flutter_markdown/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/flutter_markdown', + ), + Dependency( + name: 'Flutter Staggered Animations', + license: mit, + sourceUrl: 'https://github.com/mobiten/flutter_staggered_animations', + ), + Dependency( + name: 'Material Design Icons Flutter', + license: mit, + sourceUrl: 'https://github.com/ziofat/material_design_icons_flutter', + ), + Dependency( + name: 'Overlay Support', + license: apache2, + sourceUrl: 'https://github.com/boyan01/overlay_support', + ), + Dependency( + name: 'Palette Generator', + license: bsd3, + licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/palette_generator/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/palette_generator', + ), + Dependency( + name: 'Panorama (Aves fork)', + license: apache2, + sourceUrl: 'https://github.com/zesage/panorama', + ), + Dependency( + name: 'Percent Indicator', + license: bsd2, + sourceUrl: 'https://github.com/diegoveloper/flutter_percent_indicator', + ), + Dependency( + name: 'Provider', + license: mit, + sourceUrl: 'https://github.com/rrousselGit/provider', + ), + Dependency( + name: 'Smooth Page Indicator', + license: mit, + sourceUrl: 'https://github.com/Milad-Akarie/smooth_page_indicator', + ), + ]; + + static const List dartPackages = [ + Dependency( + name: 'Collection', + license: bsd3, + sourceUrl: 'https://github.com/dart-lang/collection', + ), + Dependency( + name: 'Country Code', + license: mit, + sourceUrl: 'https://github.com/denixport/dart.country', + ), + Dependency( + name: 'Equatable', + license: mit, + sourceUrl: 'https://github.com/felangel/equatable', + ), + Dependency( + name: 'Event Bus', + license: mit, + sourceUrl: 'https://github.com/marcojakob/dart-event-bus', + ), + Dependency( + name: 'Fluster', + license: mit, + sourceUrl: 'https://github.com/alfonsocejudo/fluster', + ), + Dependency( + name: 'Flutter Lints', + license: bsd3, + licenseUrl: 'https://github.com/flutter/packages/blob/master/packages/flutter_lints/LICENSE', + sourceUrl: 'https://github.com/flutter/packages/tree/master/packages/flutter_lints', + ), + Dependency( + name: 'Get It', + license: mit, + sourceUrl: 'https://github.com/fluttercommunity/get_it', + ), + Dependency( + name: 'Intl', + license: bsd3, + sourceUrl: 'https://github.com/dart-lang/intl', + ), + Dependency( + name: 'LatLong2', + license: apache2, + sourceUrl: 'https://github.com/jifalops/dart-latlong', + ), + Dependency( + name: 'Material Color Utilities', + license: apache2, + licenseUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart/LICENSE', + sourceUrl: 'https://github.com/material-foundation/material-color-utilities/tree/main/dart', + ), + Dependency( + name: 'Path', + license: bsd3, + sourceUrl: 'https://github.com/dart-lang/path', + ), + Dependency( + name: 'PDF for Dart and Flutter', + license: apache2, + sourceUrl: 'https://github.com/DavBfr/dart_pdf', + ), + Dependency( + name: 'Proj4dart', + license: mit, + sourceUrl: 'https://github.com/maRci002/proj4dart', + ), + Dependency( + name: 'Stack Trace', + license: bsd3, + sourceUrl: 'https://github.com/dart-lang/stack_trace', + ), + Dependency( + name: 'Transparent Image', + license: mit, + sourceUrl: 'https://github.com/brianegan/transparent_image', + ), + Dependency( + name: 'Tuple', + license: bsd2, + sourceUrl: 'https://github.com/google/tuple.dart', + ), + Dependency( + name: 'XML', + license: mit, + sourceUrl: 'https://github.com/renggli/dart-xml', + ), + ]; +} + +class Dependency { + final String name; + final String license; + final String sourceUrl; + final String licenseUrl; + + const Dependency({ + required this.name, + required this.license, + String? licenseUrl, + required this.sourceUrl, + }) : licenseUrl = licenseUrl ?? '$sourceUrl/blob/master/LICENSE'; +} diff --git a/lib/utils/mime_utils.dart b/lib/utils/mime_utils.dart index 46afcf635..15a0ababa 100644 --- a/lib/utils/mime_utils.dart +++ b/lib/utils/mime_utils.dart @@ -8,6 +8,7 @@ class MimeUtils { case MimeTypes.ico: return 'ICO'; case MimeTypes.mov: + case MimeTypes.movX: return 'MOV'; case MimeTypes.psdVnd: case MimeTypes.psdX: diff --git a/lib/utils/xmp_utils.dart b/lib/utils/xmp_utils.dart index f81fa46d8..72f2b3515 100644 --- a/lib/utils/xmp_utils.dart +++ b/lib/utils/xmp_utils.dart @@ -41,6 +41,7 @@ class Namespaces { static const iptc4xmpExt = 'http://iptc.org/std/Iptc4xmpExt/2008-02-29/'; static const lr = 'http://ns.adobe.com/lightroom/1.0/'; static const mediapro = 'http://ns.iview-multimedia.com/mediapro/1.0/'; + static const miCamera = 'http://ns.xiaomi.com/photos/1.0/camera/'; // also seen in the wild for prefix `MicrosoftPhoto`: 'http://ns.microsoft.com/photo/1.0' static const microsoftPhoto = 'http://ns.microsoft.com/photo/1.0/'; @@ -111,6 +112,7 @@ class Namespaces { iptc4xmpExt: 'IPTC Extension', lr: 'Lightroom', mediapro: 'MediaPro', + miCamera: 'Mi Camera', microsoftPhoto: 'Microsoft Photo 1.0', mp1: 'Microsoft Photo 1.1', mp: 'Microsoft Photo 1.2', diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index a4d1c18ef..c5c1e654b 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/about/app_ref.dart'; import 'package:aves/widgets/about/bug_report.dart'; import 'package:aves/widgets/about/credits.dart'; @@ -20,6 +20,7 @@ class AboutPage extends StatelessWidget { @override Widget build(BuildContext context) { final appBarTitle = Text(context.l10n.aboutPageTitle); + final useTvLayout = settings.useTvLayout; final body = CustomScrollView( slivers: [ SliverPadding( @@ -27,8 +28,8 @@ class AboutPage extends StatelessWidget { sliver: SliverList( delegate: SliverChildListDelegate( [ - const AppReference(), - if (!device.isTelevision) ...[ + AppReference(showLogo: !useTvLayout), + if (!settings.useTvLayout) ...[ const Divider(), const BugReport(), ], @@ -46,7 +47,7 @@ class AboutPage extends StatelessWidget { ], ); - if (device.isTelevision) { + if (useTvLayout) { return Scaffold( body: AvesPopScope( handlers: const [TvNavigationPopHandler.pop], @@ -55,7 +56,12 @@ class AboutPage extends StatelessWidget { TvRail( controller: context.read(), ), - Expanded(child: body), + Expanded( + child: DirectionalSafeArea( + start: false, + child: body, + ), + ), ], ), ), diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index f9c05ac66..ce4a94ba4 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -10,7 +10,12 @@ import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; class AppReference extends StatefulWidget { - const AppReference({super.key}); + final bool showLogo; + + const AppReference({ + super.key, + required this.showLogo, + }); @override State createState() => _AppReferenceState(); @@ -52,10 +57,12 @@ class _AppReferenceState extends State { return Row( mainAxisSize: MainAxisSize.min, children: [ - AvesLogo( - size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, - ), - const SizedBox(width: 8), + if (widget.showLogo) ...[ + AvesLogo( + size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, + ), + const SizedBox(width: 8), + ], Text( '${context.l10n.appName} ${snapshot.data?.version}', style: style, diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 08088bfdd..813776474 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -2,6 +2,7 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/dependencies.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -24,10 +25,10 @@ class _LicensesState extends State { @override void initState() { super.initState(); - _platform = List.from(Constants.androidDependencies); - _flutterPlugins = List.from(Constants.flutterPlugins(context.read())); - _flutterPackages = List.from(Constants.flutterPackages); - _dartPackages = List.from(Constants.dartPackages); + _platform = List.from(Dependencies.androidDependencies); + _flutterPlugins = List.from(Dependencies.flutterPlugins(context.read())); + _flutterPackages = List.from(Dependencies.flutterPackages); + _dartPackages = List.from(Dependencies.dartPackages); _sortPackages(); } diff --git a/lib/widgets/about/translators.dart b/lib/widgets/about/translators.dart index 66e673776..d83094991 100644 --- a/lib/widgets/about/translators.dart +++ b/lib/widgets/about/translators.dart @@ -38,10 +38,14 @@ class AboutTranslators extends StatelessWidget { Contributor('Allan Nordhøy', 'epost@anotheragency.no'), Contributor('pemibe', 'pemibe4634@dmonies.com'), Contributor('Linerly', 'linerly@protonmail.com'), - Contributor('Olexandr Mazur', 'rozihrash.ya6w7@simplelogin.com'), + Contributor('Skrripy', 'rozihrash.ya6w7@simplelogin.com'), + Contributor('vesp', 'vesp@post.cz'), + Contributor('Dan', 'denqwerta@gmail.com'), + Contributor('Tijolinho', 'pedrohenrique29.alfenas@gmail.com'), + Contributor('Piotr K', '1337.kelt@gmail.com'), + Contributor('rehork', 'cooky@e.email'), // Contributor('SAMIRAH AIL', 'samiratalzahrani@gmail.com'), // Arabic // Contributor('Salih Ail', 'rrrfff444@gmail.com'), // Arabic - // Contributor('Piotr K', '1337.kelt@gmail.com'), // Polish // Contributor('امیر جهانگرد', 'ijahangard.a@gmail.com'), // Persian // Contributor('slasb37', 'p84haghi@gmail.com'), // Persian // Contributor('tryvseu', 'tryvseu@tuta.io'), // Nynorsk diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index a7ff2eae7..445fae01c 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math'; import 'dart:ui'; import 'package:aves/app_flavor.dart'; @@ -53,8 +54,9 @@ class AvesApp extends StatefulWidget { final AppFlavor flavor; // temporary exclude locales not ready yet for prime time - static final _unsupportedLocales = {'ar', 'fa', 'gl', 'nn', 'pl', 'th'}.map(Locale.new).toSet(); + static final _unsupportedLocales = {'ar', 'fa', 'gl', 'nn', 'th'}.map(Locale.new).toSet(); static final List supportedLocales = AppLocalizations.supportedLocales.where((v) => !_unsupportedLocales.contains(v)).toList(); + static final ValueNotifier cutoutInsetsNotifier = ValueNotifier(EdgeInsets.zero); static final GlobalKey navigatorKey = GlobalKey(debugLabel: 'app-navigator'); // do not monitor all `ModalRoute`s, which would include popup menus, @@ -135,11 +137,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { final Set _changedUris = {}; Size? _screenSize; - // Flutter has various page transition implementations for Android: - // - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below - // - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 - // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0) - final ValueNotifier _pageTransitionsBuilderNotifier = ValueNotifier(const FadeUpwardsPageTransitionsBuilder()); + final ValueNotifier _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder); final ValueNotifier _tvMediaQueryModifierNotifier = ValueNotifier(null); final ValueNotifier _appModeNotifier = ValueNotifier(AppMode.main); @@ -151,6 +149,12 @@ class _AvesAppState extends State with WidgetsBindingObserver { final EventChannel _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events'); final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error'); + // Flutter has various page transition implementations for Android: + // - `FadeUpwardsPageTransitionsBuilder` on Oreo / API 27 and below + // - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28 + // - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.0.0) + static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder(); + @override void initState() { super.initState(); @@ -164,6 +168,7 @@ class _AvesAppState extends State with WidgetsBindingObserver { _subscriptions.add(_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?))); _subscriptions.add(_analysisCompletionChannel.receiveBroadcastStream().listen((event) => _onAnalysisCompletion())); _subscriptions.add(_errorChannel.receiveBroadcastStream().listen((event) => _onError(event as String?))); + _updateCutoutInsets(); WidgetsBinding.instance.addObserver(this); } @@ -375,6 +380,15 @@ class _AvesAppState extends State with WidgetsBindingObserver { } } + @override + void didChangeMetrics() => _updateCutoutInsets(); + + Future _updateCutoutInsets() async { + if (await windowService.isCutoutAware()) { + AvesApp.cutoutInsetsNotifier.value = await windowService.getCutoutInsets(); + } + } + Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage(); Size? _getScreenSize() { @@ -407,17 +421,11 @@ class _AvesAppState extends State with WidgetsBindingObserver { final stopwatch = Stopwatch()..start(); await device.init(); - if (device.isTelevision) { - _pageTransitionsBuilderNotifier.value = const TvPageTransitionsBuilder(); - _tvMediaQueryModifierNotifier.value = (mq) => mq.copyWith( - textScaleFactor: 1.1, - navigationMode: NavigationMode.directional, - ); - } await mobileServices.init(); await settings.init(monitorPlatformSettings: true); settings.isRotationLocked = await windowService.isRotationLocked(); settings.areAnimationsRemoved = await AccessibilityService.areAnimationsRemoved(); + await _onTvLayoutChanged(); _monitorSettings(); FijkLog.setLevel(FijkLogLevel.Warn); @@ -426,6 +434,51 @@ class _AvesAppState extends State with WidgetsBindingObserver { debugPrint('App setup in ${stopwatch.elapsed.inMilliseconds}ms'); } + Future _onTvLayoutChanged() async { + if (settings.useTvLayout) { + settings.applyTvSettings(); + + _pageTransitionsBuilderNotifier.value = const TvPageTransitionsBuilder(); + _tvMediaQueryModifierNotifier.value = (mq) { + // cf https://developer.android.com/training/tv/start/layouts.html#overscan + final screenSize = mq.size; + const overscanFactor = .05; + final overscanInsets = EdgeInsets.symmetric( + vertical: screenSize.shortestSide * overscanFactor, + horizontal: screenSize.longestSide * overscanFactor, + ); + final oldViewPadding = mq.viewPadding; + final newViewPadding = EdgeInsets.only( + top: max(oldViewPadding.top, overscanInsets.top), + right: max(oldViewPadding.right, overscanInsets.right), + bottom: max(oldViewPadding.bottom, overscanInsets.bottom), + left: max(oldViewPadding.left, overscanInsets.left), + ); + var newPadding = newViewPadding - mq.viewInsets; + newPadding = EdgeInsets.only( + top: max(0.0, newPadding.top), + right: max(0.0, newPadding.right), + bottom: max(0.0, newPadding.bottom), + left: max(0.0, newPadding.left), + ); + + return mq.copyWith( + textScaleFactor: 1.1, + padding: newPadding, + viewPadding: newViewPadding, + navigationMode: NavigationMode.directional, + ); + }; + if (settings.forceTvLayout) { + await windowService.requestOrientation(Orientation.landscape); + } + } else { + _pageTransitionsBuilderNotifier.value = defaultPageTransitionsBuilder; + _tvMediaQueryModifierNotifier.value = null; + await windowService.requestOrientation(null); + } + } + void _monitorSettings() { void applyIsInstalledAppAccessAllowed() { if (settings.isInstalledAppAccessAllowed) { @@ -439,15 +492,32 @@ class _AvesAppState extends State with WidgetsBindingObserver { void applyKeepScreenOn() => settings.keepScreenOn.apply(); void applyIsRotationLocked() { - if (!settings.isRotationLocked) { + if (!settings.isRotationLocked && !settings.useTvLayout) { windowService.requestOrientation(); } } - settings.updateStream.where((event) => event.key == Settings.isInstalledAppAccessAllowedKey).listen((_) => applyIsInstalledAppAccessAllowed()); - settings.updateStream.where((event) => event.key == Settings.displayRefreshRateModeKey).listen((_) => applyDisplayRefreshRateMode()); - settings.updateStream.where((event) => event.key == Settings.keepScreenOnKey).listen((_) => applyKeepScreenOn()); - settings.updateStream.where((event) => event.key == Settings.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked()); + void applyForceTvLayout() { + _onTvLayoutChanged(); + unawaited(AvesApp.navigatorKey.currentState!.pushAndRemoveUntil( + MaterialPageRoute( + settings: const RouteSettings(name: HomePage.routeName), + builder: (_) => _getFirstPage(), + ), + (route) => false, + )); + } + + final settingStream = settings.updateStream; + // app + settingStream.where((event) => event.key == Settings.isInstalledAppAccessAllowedKey).listen((_) => applyIsInstalledAppAccessAllowed()); + // display + settingStream.where((event) => event.key == Settings.displayRefreshRateModeKey).listen((_) => applyDisplayRefreshRateMode()); + settingStream.where((event) => event.key == Settings.forceTvLayoutKey).listen((_) => applyForceTvLayout()); + // navigation + settingStream.where((event) => event.key == Settings.keepScreenOnKey).listen((_) => applyKeepScreenOn()); + // platform settings + settingStream.where((event) => event.key == Settings.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked()); applyDisplayRefreshRateMode(); applyKeepScreenOn(); diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index a76e63f95..3c697f6eb 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -1,9 +1,7 @@ import 'dart:async'; -import 'dart:ui'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_set_actions.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/query.dart'; @@ -143,9 +141,7 @@ class _CollectionAppBarState extends State with SingleTickerPr } @override - void didChangeMetrics() { - _updateStatusBarHeight(); - } + void didChangeMetrics() => _updateStatusBarHeight(); @override Widget build(BuildContext context) { @@ -153,69 +149,76 @@ class _CollectionAppBarState extends State with SingleTickerPr final selection = context.watch>(); final isSelecting = selection.isSelecting; _isSelectingNotifier.value = isSelecting; - return AnimatedBuilder( - animation: collection.filterChangeNotifier, - builder: (context, child) { - final removableFilters = appMode != AppMode.pickMediaInternal; - return Selector( - selector: (context, query) => query.enabled, - builder: (context, queryEnabled, child) { - return Selector>( - selector: (context, s) => s.collectionBrowsingQuickActions, - builder: (context, _, child) { - final isTelevision = device.isTelevision; - final actions = _buildActions(context, selection); - return AvesAppBar( - contentHeight: appBarContentHeight, - leading: _buildAppBarLeading( - hasDrawer: appMode.canNavigate, - isSelecting: isSelecting, - ), - title: _buildAppBarTitle(isSelecting), - actions: isTelevision ? [] : actions, - bottom: Column( - children: [ - if (isTelevision) - SizedBox( - height: CaptionedButton.getTelevisionButtonHeight(context), - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 8), - scrollDirection: Axis.horizontal, - children: actions, + return NotificationListener( + // cancel notification bubbling so that the draggable scroll bar + // does not misinterpret filter bar scrolling for collection scrolling + onNotification: (notification) => true, + child: AnimatedBuilder( + animation: collection.filterChangeNotifier, + builder: (context, child) { + final removableFilters = appMode != AppMode.pickMediaInternal; + return Selector( + selector: (context, query) => query.enabled, + builder: (context, queryEnabled, child) { + return Selector>( + selector: (context, s) => s.collectionBrowsingQuickActions, + builder: (context, _, child) { + final useTvLayout = settings.useTvLayout; + final actions = _buildActions(context, selection); + final onFilterTap = removableFilters ? collection.removeFilter : null; + return AvesAppBar( + contentHeight: appBarContentHeight, + pinned: context.select, bool>((selection) => selection.isSelecting), + leading: _buildAppBarLeading( + hasDrawer: appMode.canNavigate, + isSelecting: isSelecting, + ), + title: _buildAppBarTitle(isSelecting), + actions: useTvLayout ? [] : actions, + bottom: Column( + children: [ + if (useTvLayout) + SizedBox( + height: CaptionedButton.getTelevisionButtonHeight(context), + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + children: actions, + ), ), - ), - if (showFilterBar) - NotificationListener( - onNotification: (notification) { - collection.addFilter(notification.reversedFilter); - return true; - }, - child: FilterBar( - filters: visibleFilters, - removable: removableFilters, - onTap: removableFilters ? collection.removeFilter : null, + if (showFilterBar) + NotificationListener( + onNotification: (notification) { + collection.addFilter(notification.reversedFilter); + return true; + }, + child: FilterBar( + filters: visibleFilters, + onTap: onFilterTap, + onRemove: onFilterTap, + ), ), - ), - if (queryEnabled) - EntryQueryBar( - queryNotifier: context.select>((query) => query.queryNotifier), - focusNode: _queryBarFocusNode, - ), - ], - ), - transitionKey: isSelecting, - ); - }, - ); - }, - ); - }, + if (queryEnabled) + EntryQueryBar( + queryNotifier: context.select>((query) => query.queryNotifier), + focusNode: _queryBarFocusNode, + ), + ], + ), + transitionKey: isSelecting, + ); + }, + ); + }, + ); + }, + ), ); } double get appBarContentHeight { double height = kToolbarHeight; - if (device.isTelevision) { + if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); } if (showFilterBar) { @@ -228,7 +231,7 @@ class _CollectionAppBarState extends State with SingleTickerPr } Widget? _buildAppBarLeading({required bool hasDrawer, required bool isSelecting}) { - if (device.isTelevision) return null; + if (settings.useTvLayout) return null; if (!hasDrawer) { return const CloseButton(); @@ -310,7 +313,7 @@ class _CollectionAppBarState extends State with SingleTickerPr selectedItemCount: selectedItemCount, ); - return device.isTelevision + return settings.useTvLayout ? _buildTelevisionActions( context: context, appMode: appMode, @@ -342,7 +345,13 @@ class _CollectionAppBarState extends State with SingleTickerPr ].where(isVisible).map((action) { final enabled = canApply(action); return CaptionedButton( - iconButton: _buildButtonIcon(context, action, enabled: enabled, selection: selection), + iconButtonBuilder: (context, focusNode) => _buildButtonIcon( + context, + action, + enabled: enabled, + selection: selection, + focusNode: focusNode, + ), captionText: _buildButtonCaption(context, action, enabled: enabled), onPressed: enabled ? () => _onActionSelected(action) : null, ); @@ -383,7 +392,7 @@ class _CollectionAppBarState extends State with SingleTickerPr ...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( (action) => _toMenuItem(action, enabled: canApply(action), selection: selection), ), - if (isSelecting && !device.isReadOnly && appMode == AppMode.main && !isTrash) + if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash) PopupMenuItem( enabled: hasSelection, padding: EdgeInsets.zero, @@ -429,6 +438,7 @@ class _CollectionAppBarState extends State with SingleTickerPr BuildContext context, EntrySetAction action, { required bool enabled, + FocusNode? focusNode, required Selection selection, }) { final onPressed = enabled ? () => _onActionSelected(action) : null; @@ -441,12 +451,14 @@ class _CollectionAppBarState extends State with SingleTickerPr return TitleSearchToggler( queryEnabled: queryEnabled, onPressed: onPressed, + focusNode: focusNode, ); }, ); case EntrySetAction.toggleFavourite: return FavouriteToggler( entries: _getExpandedSelectedItems(selection), + focusNode: focusNode, onPressed: onPressed, ); default: @@ -454,6 +466,7 @@ class _CollectionAppBarState extends State with SingleTickerPr key: _getActionKey(action), icon: action.getIcon(), onPressed: onPressed, + focusNode: focusNode, tooltip: action.getText(context), ); } @@ -577,7 +590,7 @@ class _CollectionAppBarState extends State with SingleTickerPr void _onQueryFocusRequest() => _queryBarFocusNode.requestFocus(); void _updateStatusBarHeight() { - _statusBarHeight = EdgeInsets.fromWindowPadding(window.padding, window.devicePixelRatio).top; + _statusBarHeight = context.read().padding.top; _updateAppBarHeight(); } diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index 051a3935b..a063ed908 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/favourite.dart'; @@ -57,7 +56,7 @@ class CollectionGrid extends StatefulWidget { static const double fixedExtentLayoutSpacing = 2; static const double mosaicLayoutSpacing = 4; - static int get columnCountDefault => device.isTelevision ? 6 : 4; + static int get columnCountDefault => settings.useTvLayout ? 6 : 4; const CollectionGrid({ super.key, @@ -176,7 +175,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> { tileLayout: tileLayout, isScrollingNotifier: _isScrollingNotifier, ); - if (!device.isTelevision) return tile; + if (!settings.useTvLayout) return tile; return Focus( onFocusChange: (focused) { @@ -281,7 +280,7 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent child: scrollView, ); - final selector = GridSelectionGestureDetector( + final selector = GridSelectionGestureDetector( scrollableKey: _scrollableKey, selectable: widget.selectable, items: collection.sortedEntries, @@ -580,9 +579,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge }); } - void _stopScrollMonitoringTimer() { - _scrollMonitoringTimer?.cancel(); - } + void _stopScrollMonitoringTimer() => _scrollMonitoringTimer?.cancel(); Map _getCrumbs(List sectionLayouts) { final crumbs = {}; diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index ece27a063..39b2ea928 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/query.dart'; @@ -83,6 +82,7 @@ class _CollectionPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return SelectionProvider( child: Selector, bool>( @@ -105,11 +105,12 @@ class _CollectionPageState extends State { TvNavigationPopHandler.pop, _doubleBackPopHandler.pop, ], - child: const GestureAreaProtectorStack( - child: SafeArea( + child: GestureAreaProtectorStack( + child: DirectionalSafeArea( + start: !useTvLayout, top: false, bottom: false, - child: CollectionGrid( + child: const CollectionGrid( // key is expected by test driver key: Key('collection-grid'), settingsRouteKey: CollectionPage.routeName, @@ -122,7 +123,7 @@ class _CollectionPageState extends State { ); Widget page; - if (device.isTelevision) { + if (useTvLayout) { page = Scaffold( body: Row( children: [ diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 93b35158f..3198fd9f1 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -17,6 +17,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/analysis_controller.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; @@ -55,7 +56,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware required int selectedItemCount, required bool isTrash, }) { - final canWrite = !device.isReadOnly; + final canWrite = !settings.isReadOnly; final isMain = appMode == AppMode.main; switch (action) { // general @@ -69,7 +70,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware return isSelecting && selectedItemCount == itemCount; // browsing case EntrySetAction.searchCollection: - return !device.isTelevision && appMode.canNavigate && !isSelecting; + return !settings.useTvLayout && appMode.canNavigate && !isSelecting; case EntrySetAction.toggleTitleSearch: return !isSelecting; case EntrySetAction.addShortcut: @@ -82,7 +83,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware case EntrySetAction.stats: return isMain; case EntrySetAction.rescan: - return !device.isTelevision && isMain && !isTrash; + return !settings.useTvLayout && isMain && !isTrash; // selecting case EntrySetAction.share: case EntrySetAction.toggleFavourite: @@ -252,11 +253,21 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware return groupedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet(); } - void _share(BuildContext context) { + Future _share(BuildContext context) async { final entries = _getTargetItems(context); - androidAppService.shareEntries(entries).then((success) { - if (!success) showNoMatchingAppDialog(context); - }); + try { + if (!await androidAppService.shareEntries(entries)) { + await showNoMatchingAppDialog(context); + } + } on TooManyItemsException catch (_) { + await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text(context.l10n.tooManyItemsErrorDialogMessage), + actions: const [OkButton()], + ), + ); + } } void _rescan(BuildContext context) { @@ -447,25 +458,20 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware if (unsupported.isEmpty) return supported; final unsupportedTypes = unsupported.map((entry) => entry.mimeType).toSet().map(MimeUtils.displayType).toList()..sort(); + final l10n = context.l10n; final confirmed = await showDialog( context: context, - builder: (context) { - final l10n = context.l10n; - return AvesDialog( - content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))), - actions: [ + builder: (context) => AvesDialog( + content: Text(l10n.unsupportedTypeDialogMessage(unsupportedTypes.length, unsupportedTypes.join(', '))), + actions: [ + const CancelButton(), + if (supported.isNotEmpty) TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + onPressed: () => Navigator.pop(context, true), + child: Text(l10n.continueButtonLabel), ), - if (supported.isNotEmpty) - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(l10n.continueButtonLabel), - ), - ], - ); - }, + ], + ), ); if (confirmed == null || !confirmed) return null; @@ -536,21 +542,16 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware Future removeLocation(BuildContext context, Set entries) async { final confirmed = await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.genericDangerWarningDialogMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.applyButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(context.l10n.genericDangerWarningDialogMessage), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.applyButtonLabel), + ), + ], + ), ); if (confirmed == null || !confirmed) return; diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index dfbb3b873..20f1a47ed 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -11,14 +11,13 @@ class FilterBar extends StatefulWidget { static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding; final List filters; - final bool removable; - final FilterCallback? onTap; + final FilterCallback? onTap, onRemove; FilterBar({ super.key, required Set filters, - this.removable = false, this.onTap, + this.onRemove, }) : filters = List.from(filters)..sort(); @override @@ -31,8 +30,6 @@ class _FilterBarState extends State { List get filters => widget.filters; - FilterCallback? get onTap => widget.onTap; - @override void didUpdateWidget(covariant FilterBar oldWidget) { super.didUpdateWidget(oldWidget); @@ -85,49 +82,51 @@ class _FilterBarState extends State { // chip border clipping when the floating app bar is fading color: Colors.transparent, height: FilterBar.preferredHeight, - child: NotificationListener( - // cancel notification bubbling so that the draggable scroll bar - // does not misinterpret filter bar scrolling for collection scrolling - onNotification: (notification) => true, - child: AnimatedList( - key: _animatedListKey, - initialItemCount: filters.length, - scrollDirection: Axis.horizontal, - padding: FilterBar.rowPadding, - itemBuilder: (context, index, animation) { - if (index >= filters.length) return const SizedBox(); - return _buildChip(filters.toList()[index]); - }, - ), + child: AnimatedList( + key: _animatedListKey, + initialItemCount: filters.length, + scrollDirection: Axis.horizontal, + padding: FilterBar.rowPadding, + itemBuilder: (context, index, animation) { + if (index >= filters.length) return const SizedBox(); + return _buildChip(filters.toList()[index]); + }, ), ); } Widget _buildChip(CollectionFilter filter) { + final onTap = widget.onTap != null + ? (filter) { + _userTappedFilter = filter; + widget.onTap?.call(filter); + } + : null; + final onRemove = widget.onRemove != null + ? (filter) { + _userTappedFilter = filter; + widget.onRemove?.call(filter); + } + : null; return _Chip( filter: filter, - removable: widget.removable, single: filters.length == 1, - onTap: onTap != null - ? (filter) { - _userTappedFilter = filter; - onTap!(filter); - } - : null, + onTap: onTap, + onRemove: onRemove, ); } } class _Chip extends StatelessWidget { final CollectionFilter filter; - final bool removable, single; - final FilterCallback? onTap; + final bool single; + final FilterCallback? onTap, onRemove; const _Chip({ required this.filter, - required this.removable, required this.single, required this.onTap, + required this.onRemove, }); @override @@ -138,7 +137,6 @@ class _Chip extends StatelessWidget { child: AvesFilterChip( key: ValueKey(filter), filter: filter, - removable: removable, maxWidth: single ? AvesFilterChip.computeMaxWidth( context, @@ -149,6 +147,7 @@ class _Chip extends StatelessWidget { : null, heroType: HeroType.always, onTap: onTap, + onRemove: onRemove, ), ), ); diff --git a/lib/widgets/common/action_controls/quick_choosers/common/button.dart b/lib/widgets/common/action_controls/quick_choosers/common/button.dart index d83f728ad..6b6636b0d 100644 --- a/lib/widgets/common/action_controls/quick_choosers/common/button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/common/button.dart @@ -8,12 +8,14 @@ import 'package:provider/provider.dart'; abstract class ChooserQuickButton extends StatefulWidget { final bool blurred; final ValueSetter? onChooserValue; + final FocusNode? focusNode; final VoidCallback? onPressed; const ChooserQuickButton({ super.key, required this.blurred, this.onChooserValue, + this.focusNode, required this.onPressed, }); } @@ -71,6 +73,7 @@ abstract class ChooserQuickButtonState, U> exten child: IconButton( icon: icon, onPressed: widget.onPressed, + focusNode: widget.focusNode, tooltip: _hasChooser ? null : tooltip, ), ); diff --git a/lib/widgets/common/action_controls/quick_choosers/rate_button.dart b/lib/widgets/common/action_controls/quick_choosers/rate_button.dart index cb3882d75..e14e1cb8e 100644 --- a/lib/widgets/common/action_controls/quick_choosers/rate_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/rate_button.dart @@ -8,6 +8,7 @@ class RateButton extends ChooserQuickButton { super.key, required super.blurred, super.onChooserValue, + super.focusNode, required super.onPressed, }); diff --git a/lib/widgets/common/action_controls/quick_choosers/share_button.dart b/lib/widgets/common/action_controls/quick_choosers/share_button.dart index d4b234071..379c6b9a2 100644 --- a/lib/widgets/common/action_controls/quick_choosers/share_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/share_button.dart @@ -13,6 +13,7 @@ class ShareButton extends ChooserQuickButton { required super.blurred, required this.entries, super.onChooserValue, + super.focusNode, required super.onPressed, }); diff --git a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart index cb048e3e6..6735d644a 100644 --- a/lib/widgets/common/action_controls/quick_choosers/tag_button.dart +++ b/lib/widgets/common/action_controls/quick_choosers/tag_button.dart @@ -17,6 +17,7 @@ class TagButton extends ChooserQuickButton { super.key, required super.blurred, super.onChooserValue, + super.focusNode, required super.onPressed, }); diff --git a/lib/widgets/common/action_controls/togglers/favourite.dart b/lib/widgets/common/action_controls/togglers/favourite.dart index cfa4d9b4f..c19c646d1 100644 --- a/lib/widgets/common/action_controls/togglers/favourite.dart +++ b/lib/widgets/common/action_controls/togglers/favourite.dart @@ -12,12 +12,14 @@ import 'package:provider/provider.dart'; class FavouriteToggler extends StatefulWidget { final Set entries; final bool isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const FavouriteToggler({ super.key, required this.entries, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -76,6 +78,7 @@ class _FavouriteTogglerState extends State { IconButton( icon: Icon(isFavourite ? isFavouriteIcon : isNotFavouriteIcon), onPressed: widget.onPressed, + focusNode: widget.focusNode, tooltip: isFavourite ? context.l10n.entryActionRemoveFavourite : context.l10n.entryActionAddFavourite, ), Sweeper( diff --git a/lib/widgets/common/action_controls/togglers/mute.dart b/lib/widgets/common/action_controls/togglers/mute.dart index 94c691f8c..e42bcab80 100644 --- a/lib/widgets/common/action_controls/togglers/mute.dart +++ b/lib/widgets/common/action_controls/togglers/mute.dart @@ -10,12 +10,14 @@ import 'package:flutter/material.dart'; class MuteToggler extends StatelessWidget { final AvesVideoController? controller; final bool isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const MuteToggler({ super.key, required this.controller, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -40,6 +42,7 @@ class MuteToggler extends StatelessWidget { : IconButton( icon: icon, onPressed: canDo ? onPressed : null, + focusNode: focusNode, tooltip: text, ); }, diff --git a/lib/widgets/common/action_controls/togglers/play.dart b/lib/widgets/common/action_controls/togglers/play.dart index 814958b2b..354da0ddf 100644 --- a/lib/widgets/common/action_controls/togglers/play.dart +++ b/lib/widgets/common/action_controls/togglers/play.dart @@ -12,12 +12,14 @@ import 'package:provider/provider.dart'; class PlayToggler extends StatefulWidget { final AvesVideoController? controller; final bool isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const PlayToggler({ super.key, required this.controller, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -86,6 +88,7 @@ class _PlayTogglerState extends State with SingleTickerProviderStat progress: _playPauseAnimation, ), onPressed: widget.onPressed, + focusNode: widget.focusNode, tooltip: text, ); } diff --git a/lib/widgets/common/action_controls/togglers/title_search.dart b/lib/widgets/common/action_controls/togglers/title_search.dart index 3b3d9ae55..dfe577994 100644 --- a/lib/widgets/common/action_controls/togglers/title_search.dart +++ b/lib/widgets/common/action_controls/togglers/title_search.dart @@ -8,12 +8,14 @@ import 'package:provider/provider.dart'; class TitleSearchToggler extends StatelessWidget { final bool queryEnabled, isMenuItem; + final FocusNode? focusNode; final VoidCallback? onPressed; const TitleSearchToggler({ super.key, required this.queryEnabled, this.isMenuItem = false, + this.focusNode, this.onPressed, }); @@ -29,6 +31,7 @@ class TitleSearchToggler extends StatelessWidget { : IconButton( icon: icon, onPressed: onPressed, + focusNode: focusNode, tooltip: text, ); } diff --git a/lib/widgets/common/action_mixins/entry_editor.dart b/lib/widgets/common/action_mixins/entry_editor.dart index 1f7efb28b..49c411271 100644 --- a/lib/widgets/common/action_mixins/entry_editor.dart +++ b/lib/widgets/common/action_mixins/entry_editor.dart @@ -123,21 +123,16 @@ mixin EntryEditorMixin { if (entries.any((entry) => entry.isMotionPhoto) && types.contains(MetadataType.xmp)) { final confirmed = await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.applyButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(context.l10n.removeEntryMetadataMotionPhotoXmpWarningDialogMessage), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.applyButtonLabel), + ), + ], + ), ); if (confirmed == null || !confirmed) return null; } diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index 2ccd754c4..06352fa9d 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -136,21 +136,20 @@ mixin FeedbackMixin { int? itemCount, VoidCallback? onCancel, void Function(Set processed)? onDone, - }) { - return showDialog( - context: context, - barrierDismissible: false, - builder: (context) => ReportOverlay( - opStream: opStream, - itemCount: itemCount, - onCancel: onCancel, - onDone: (processed) { - Navigator.pop(context); - onDone?.call(processed); - }, - ), - ); - } + }) => + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => ReportOverlay( + opStream: opStream, + itemCount: itemCount, + onCancel: onCancel, + onDone: (processed) { + Navigator.pop(context); + onDone?.call(processed); + }, + ), + ); } class ReportOverlay extends StatefulWidget { diff --git a/lib/widgets/common/action_mixins/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart index 61a8a918b..b54f2411b 100644 --- a/lib/widgets/common/action_mixins/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -54,10 +54,7 @@ mixin PermissionAwareMixin { return AvesDialog( content: Text(l10n.storageAccessDialogMessage(directory, volume)), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: () => Navigator.pop(context, true), child: Text(MaterialLocalizations.of(context).okButtonLabel), @@ -72,17 +69,10 @@ mixin PermissionAwareMixin { if (!await deviceService.isSystemFilePickerEnabled()) { await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.missingSystemFilePickerDialogMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).okButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(context.l10n.missingSystemFilePickerDialogMessage), + actions: const [OkButton()], + ), ); return false; } @@ -103,12 +93,7 @@ mixin PermissionAwareMixin { final volume = dir.getVolumeDescription(context); return AvesDialog( content: Text(context.l10n.restrictedAccessDialogMessage(directory, volume)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).okButtonLabel), - ), - ], + actions: const [OkButton()], ); }, ); diff --git a/lib/widgets/common/action_mixins/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart index c6fb9c59a..5e57bb021 100644 --- a/lib/widgets/common/action_mixins/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -85,12 +85,7 @@ mixin SizeAwareMixin { final volume = destinationVolume.getDescription(context); return AvesDialog( content: Text(l10n.notEnoughSpaceDialogMessage(neededSize, freeSize, volume)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).okButtonLabel), - ), - ], + actions: const [OkButton()], ); }, ); diff --git a/lib/widgets/common/basic/color_list_tile.dart b/lib/widgets/common/basic/color_list_tile.dart index babf354e9..ae44a47fb 100644 --- a/lib/widgets/common/basic/color_list_tile.dart +++ b/lib/widgets/common/basic/color_list_tile.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/borders.dart'; @@ -72,13 +72,13 @@ class _ColorPickerDialogState extends State { @override Widget build(BuildContext context) { - final isTelevision = device.isTelevision; + final useTvLayout = settings.useTvLayout; return AvesDialog( scrollableContent: [ ColorPicker( color: color, onColorChanged: (v) => color = v, - pickersEnabled: isTelevision + pickersEnabled: useTvLayout ? const { ColorPickerType.primary: true, ColorPickerType.accent: false, @@ -90,14 +90,11 @@ class _ColorPickerDialogState extends State { }, hasBorder: true, borderRadius: 20, - subheading: isTelevision ? const SizedBox(height: 16) : null, + subheading: useTvLayout ? const SizedBox(height: 16) : null, ) ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: () => Navigator.pop(context, color), child: Text(context.l10n.applyButtonLabel), diff --git a/lib/widgets/common/basic/insets.dart b/lib/widgets/common/basic/insets.dart index 33e91eb0b..fa1a73d46 100644 --- a/lib/widgets/common/basic/insets.dart +++ b/lib/widgets/common/basic/insets.dart @@ -1,5 +1,10 @@ -import 'package:aves/model/device.dart'; +import 'dart:math'; + +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/tile_extent_controller.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -123,8 +128,96 @@ class TvTileGridBottomPaddingSliver extends StatelessWidget { Widget build(BuildContext context) { return SliverToBoxAdapter( child: SizedBox( - height: device.isTelevision ? context.select((controller) => controller.spacing) : 0, + height: settings.useTvLayout ? context.select((controller) => controller.spacing) : 0, ), ); } } + +// `MediaQuery.padding` matches cutout areas but also includes other system UI like the status bar +// so we cannot use `SafeArea` along `MediaQuery.removePadding()` to remove cutout areas +class SafeCutoutArea extends StatelessWidget { + final Animation? animation; + final Widget child; + + const SafeCutoutArea({ + super.key, + this.animation, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: AvesApp.cutoutInsetsNotifier, + builder: (context, cutoutInsets, child) { + return ValueListenableBuilder( + valueListenable: animation ?? ValueNotifier(1), + builder: (context, factor, child) { + final effectiveInsets = cutoutInsets * factor; + return Padding( + padding: effectiveInsets, + child: MediaQueryDataProvider( + value: MediaQuery.of(context).removeCutoutInsets(effectiveInsets), + child: child!, + ), + ); + }, + child: child, + ); + }, + child: child, + ); + } +} + +extension ExtraMediaQueryData on MediaQueryData { + MediaQueryData removeCutoutInsets(EdgeInsets cutoutInsets) { + return copyWith( + padding: EdgeInsets.only( + left: max(0.0, padding.left - cutoutInsets.left), + top: max(0.0, padding.top - cutoutInsets.top), + right: max(0.0, padding.right - cutoutInsets.right), + bottom: max(0.0, padding.bottom - cutoutInsets.bottom), + ), + viewPadding: EdgeInsets.only( + left: max(0.0, viewPadding.left - cutoutInsets.left), + top: max(0.0, viewPadding.top - cutoutInsets.top), + right: max(0.0, viewPadding.right - cutoutInsets.right), + bottom: max(0.0, viewPadding.bottom - cutoutInsets.bottom), + ), + ); + } +} + +class DirectionalSafeArea extends StatelessWidget { + final bool start, top, end, bottom; + final EdgeInsets minimum; + final bool maintainBottomViewPadding; + final Widget child; + + const DirectionalSafeArea({ + super.key, + this.start = true, + this.top = true, + this.end = true, + this.bottom = true, + this.minimum = EdgeInsets.zero, + this.maintainBottomViewPadding = false, + required this.child, + }); + + @override + Widget build(BuildContext context) { + final isRtl = context.isRtl; + return SafeArea( + left: isRtl ? end : start, + top: top, + right: isRtl ? start : end, + bottom: bottom, + minimum: minimum, + maintainBottomViewPadding: maintainBottomViewPadding, + child: child, + ); + } +} diff --git a/lib/widgets/common/behaviour/pop/tv_navigation.dart b/lib/widgets/common/behaviour/pop/tv_navigation.dart index d996416a4..97480a9af 100644 --- a/lib/widgets/common/behaviour/pop/tv_navigation.dart +++ b/lib/widgets/common/behaviour/pop/tv_navigation.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/settings.dart'; @@ -13,7 +12,7 @@ import 'package:provider/provider.dart'; // address `TV-DB` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality class TvNavigationPopHandler { static bool pop(BuildContext context) { - if (!device.isTelevision || _isHome(context)) { + if (!settings.useTvLayout || _isHome(context)) { return true; } diff --git a/lib/widgets/common/expandable_filter_row.dart b/lib/widgets/common/expandable_filter_row.dart index cafcf96b2..54d0ebf63 100644 --- a/lib/widgets/common/expandable_filter_row.dart +++ b/lib/widgets/common/expandable_filter_row.dart @@ -67,10 +67,11 @@ class TitledExpandableFilterRow extends StatelessWidget { class ExpandableFilterRow extends StatelessWidget { final List filters; final bool isExpanded; - final bool removable, showGenericIcon; + final bool showGenericIcon; final Widget? Function(CollectionFilter)? leadingBuilder; final HeroType Function(CollectionFilter filter)? heroTypeBuilder; final FilterCallback onTap; + final FilterCallback? onRemove; final OffsetFilterCallback? onLongPress; static const double horizontalPadding = 8; @@ -80,11 +81,11 @@ class ExpandableFilterRow extends StatelessWidget { super.key, required this.filters, required this.isExpanded, - this.removable = false, this.showGenericIcon = true, this.leadingBuilder, this.heroTypeBuilder, required this.onTap, + this.onRemove, required this.onLongPress, }); @@ -143,11 +144,11 @@ class ExpandableFilterRow extends StatelessWidget { // key `album-{path}` is expected by test driver key: Key(filter.key), filter: filter, - removable: removable, showGenericIcon: showGenericIcon, leadingOverride: leadingBuilder?.call(filter), heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap, onTap: onTap, + onRemove: onRemove, onLongPress: onLongPress, ); } diff --git a/lib/widgets/common/fx/borders.dart b/lib/widgets/common/fx/borders.dart index 2e2fe275e..b9dc02a0d 100644 --- a/lib/widgets/common/fx/borders.dart +++ b/lib/widgets/common/fx/borders.dart @@ -13,15 +13,15 @@ class AvesBorder { // 1 device pixel for curves is too thin static double get curvedBorderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0; - static BorderSide straightSide(BuildContext context) => BorderSide( + static BorderSide straightSide(BuildContext context, {double? width}) => BorderSide( color: _borderColor(context), - width: straightBorderWidth, + width: width ?? straightBorderWidth, ); - static BorderSide curvedSide(BuildContext context) => BorderSide( + static BorderSide curvedSide(BuildContext context, {double? width}) => BorderSide( color: _borderColor(context), - width: curvedBorderWidth, + width: width ?? curvedBorderWidth, ); - static Border border(BuildContext context) => Border.fromBorderSide(curvedSide(context)); + static Border border(BuildContext context, {double? width}) => Border.fromBorderSide(curvedSide(context, width: width)); } diff --git a/lib/widgets/common/grid/header.dart b/lib/widgets/common/grid/header.dart index 341f969e5..bc941d63b 100644 --- a/lib/widgets/common/grid/header.dart +++ b/lib/widgets/common/grid/header.dart @@ -1,5 +1,5 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/section_keys.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -33,18 +33,17 @@ class SectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { Widget child = _buildContent(context); - if (device.isTelevision) { - final colors = Theme.of(context).colorScheme; + if (settings.useTvLayout) { + final primaryColor = Theme.of(context).colorScheme.primary; child = Material( type: MaterialType.transparency, child: InkResponse( onTap: _onTap(context), - onHover: (_) {}, + containedInkWell: true, highlightShape: BoxShape.rectangle, borderRadius: const BorderRadius.all(Radius.circular(123)), - containedInkWell: true, - splashColor: colors.primary.withOpacity(0.12), - hoverColor: colors.primary.withOpacity(0.04), + hoverColor: primaryColor.withOpacity(0.04), + splashColor: primaryColor.withOpacity(0.12), child: child, ), ); diff --git a/lib/widgets/common/grid/selector.dart b/lib/widgets/common/grid/selector.dart index 20c0c98f7..e4643e362 100644 --- a/lib/widgets/common/grid/selector.dart +++ b/lib/widgets/common/grid/selector.dart @@ -6,6 +6,7 @@ import 'package:aves/utils/math_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/media_query.dart'; import 'package:aves/widgets/common/grid/sections/list_layout.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -37,7 +38,9 @@ class _GridSelectionGestureDetectorState extends State get items => widget.items; @@ -56,12 +59,42 @@ class _GridSelectionGestureDetectorState extends State oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + _stopScrollMonitoringTimer(); + super.dispose(); + } + + void _registerWidget(GridSelectionGestureDetector widget) { + widget.scrollController.addListener(_onScrollChanged); + } + + void _unregisterWidget(GridSelectionGestureDetector widget) { + widget.scrollController.removeListener(_onScrollChanged); + } + @override Widget build(BuildContext context) { final selectable = widget.selectable; return GestureDetector( onLongPressStart: selectable ? (details) { + if (_isScrolling) return; + final fromItem = _getItemAt(details.localPosition); if (fromItem == null) return; @@ -105,6 +138,16 @@ class _GridSelectionGestureDetectorState extends State _scrollMonitoringTimer?.cancel(); + void _onLongPressUpdate() { final dy = _localPosition.dy; @@ -128,7 +171,7 @@ class _GridSelectionGestureDetectorState extends State extends State _onLongPressUpdate()); + _selectionUpdateTimer = Timer.periodic(scrollUpdateInterval, (_) => _onLongPressUpdate()); } } diff --git a/lib/widgets/common/grid/theme.dart b/lib/widgets/common/grid/theme.dart index 4ab591721..4076bd32e 100644 --- a/lib/widgets/common/grid/theme.dart +++ b/lib/widgets/common/grid/theme.dart @@ -1,5 +1,7 @@ import 'dart:math'; +import 'package:aves/model/entry.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; @@ -7,13 +9,14 @@ import 'package:provider/provider.dart'; class GridTheme extends StatelessWidget { final double extent; - final bool? showLocation, showTrash; + final bool showLocation; + final bool? showTrash; final Widget child; const GridTheme({ super.key, required this.extent, - this.showLocation, + this.showLocation = true, this.showTrash, required this.child, }); @@ -32,11 +35,11 @@ class GridTheme extends StatelessWidget { fontSize: fontSize, highlightBorderWidth: highlightBorderWidth, showFavourite: settings.showThumbnailFavourite, - showLocation: showLocation ?? settings.showThumbnailLocation, + locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none, + tagIcon: settings.thumbnailTagIcon, showMotionPhoto: settings.showThumbnailMotionPhoto, showRating: settings.showThumbnailRating, showRaw: settings.showThumbnailRaw, - showTag: settings.showThumbnailTag, showTrash: showTrash ?? true, showVideoDuration: settings.showThumbnailVideoDuration, ); @@ -46,21 +49,55 @@ class GridTheme extends StatelessWidget { } } +typedef GridThemeIconBuilder = List Function(BuildContext context, AvesEntry entry); + class GridThemeData { final double iconSize, fontSize, highlightBorderWidth; - final bool showFavourite, showLocation, showMotionPhoto, showRating, showRaw, showTag, showTrash, showVideoDuration; + final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration; + final bool showLocated, showUnlocated, showTagged, showUntagged; + late final GridThemeIconBuilder iconBuilder; - const GridThemeData({ + GridThemeData({ required this.iconSize, required this.fontSize, required this.highlightBorderWidth, required this.showFavourite, - required this.showLocation, + required ThumbnailOverlayLocationIcon locationIcon, + required ThumbnailOverlayTagIcon tagIcon, required this.showMotionPhoto, required this.showRating, required this.showRaw, - required this.showTag, required this.showTrash, required this.showVideoDuration, - }); + }) : showLocated = locationIcon == ThumbnailOverlayLocationIcon.located, + showUnlocated = locationIcon == ThumbnailOverlayLocationIcon.unlocated, + showTagged = tagIcon == ThumbnailOverlayTagIcon.tagged, + showUntagged = tagIcon == ThumbnailOverlayTagIcon.untagged { + iconBuilder = (context, entry) { + final located = entry.hasGps; + final tagged = entry.tags.isNotEmpty; + return [ + if (entry.isFavourite && showFavourite) const FavouriteIcon(), + if (tagged && showTagged) TagIcon.tagged(), + if (!tagged && showUntagged) TagIcon.untagged(), + if (located && showLocated) LocationIcon.located(), + if (!located && showUnlocated) LocationIcon.unlocated(), + if (entry.rating != 0 && showRating) RatingIcon(entry: entry), + if (entry.isVideo) + VideoIcon(entry: entry) + else if (entry.isAnimated) + const AnimatedImageIcon() + else ...[ + if (entry.isRaw && showRaw) const RawIcon(), + if (entry.is360) const PanoramaIcon(), + ], + if (entry.isMultiPage) ...[ + if (entry.isMotionPhoto && showMotionPhoto) const MotionPhotoIcon(), + if (!entry.isMotionPhoto) MultiPageIcon(entry: entry), + ], + if (entry.isGeotiff) const GeoTiffIcon(), + if (entry.trashed && showTrash) TrashIcon(trashDaysLeft: entry.trashDaysLeft), + ]; + }; + } } diff --git a/lib/widgets/common/identity/aves_app_bar.dart b/lib/widgets/common/identity/aves_app_bar.dart index 9234d781f..7ad74c6f4 100644 --- a/lib/widgets/common/identity/aves_app_bar.dart +++ b/lib/widgets/common/identity/aves_app_bar.dart @@ -1,13 +1,14 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/aves_app.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class AvesAppBar extends StatelessWidget { final double contentHeight; + final bool pinned; final Widget? leading; final Widget title; final List actions; @@ -20,6 +21,7 @@ class AvesAppBar extends StatelessWidget { const AvesAppBar({ super.key, required this.contentHeight, + required this.pinned, required this.leading, required this.title, required this.actions, @@ -29,69 +31,72 @@ class AvesAppBar extends StatelessWidget { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return Selector( selector: (context, mq) => mq.padding.top, builder: (context, mqPaddingTop, child) { return SliverPersistentHeader( - floating: !device.isTelevision, - pinned: false, + floating: !useTvLayout, + pinned: pinned, delegate: _SliverAppBarDelegate( height: mqPaddingTop + appBarHeightForContentHeight(contentHeight), - child: SafeArea( - bottom: false, - child: AvesFloatingBar( - builder: (context, backgroundColor, child) => Material( - color: backgroundColor, - child: child, - ), - child: Column( - children: [ - SizedBox( - height: kToolbarHeight, - child: Row( - children: [ - leading != null - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Hero( - tag: leadingHeroTag, - flightShuttleBuilder: _flightShuttleBuilder, - transitionOnUserGestures: true, - child: leading!, - ), - ) - : const SizedBox(width: 16), - Expanded( - child: DefaultTextStyle( - style: Theme.of(context).appBarTheme.titleTextStyle!, - child: Hero( - tag: titleHeroTag, - flightShuttleBuilder: _flightShuttleBuilder, - transitionOnUserGestures: true, - child: AnimatedSwitcher( - duration: context.read().iconAnimation, - child: Row( - key: ValueKey(transitionKey), - children: [ - Expanded(child: title), - ...actions, - ], - ), - ), - ), - ), - ), - ], - ), - ), - if (bottom != null) bottom!, - ], - ), - ), - ), + child: child!, ), ); }, + child: DirectionalSafeArea( + start: !useTvLayout, + bottom: false, + child: AvesFloatingBar( + builder: (context, backgroundColor, child) => Material( + color: backgroundColor, + child: child, + ), + child: Column( + children: [ + SizedBox( + height: kToolbarHeight, + child: Row( + children: [ + leading != null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Hero( + tag: leadingHeroTag, + flightShuttleBuilder: _flightShuttleBuilder, + transitionOnUserGestures: true, + child: leading!, + ), + ) + : const SizedBox(width: 16), + Expanded( + child: DefaultTextStyle( + style: Theme.of(context).appBarTheme.titleTextStyle!, + child: Hero( + tag: titleHeroTag, + flightShuttleBuilder: _flightShuttleBuilder, + transitionOnUserGestures: true, + child: AnimatedSwitcher( + duration: context.read().iconAnimation, + child: Row( + key: ValueKey(transitionKey), + children: [ + Expanded(child: title), + ...actions, + ], + ), + ), + ), + ), + ), + ], + ), + ), + if (bottom != null) bottom!, + ], + ), + ), + ), ); } diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 78a7122ca..a58f796b8 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -44,14 +44,14 @@ class AvesFilterDecoration { class AvesFilterChip extends StatefulWidget { final CollectionFilter filter; - final bool removable, showText, showGenericIcon, useFilterColor; + final bool showText, showGenericIcon, useFilterColor; final AvesFilterDecoration? decoration; final String? banner; final Widget? leadingOverride, details; final double padding; final double? maxWidth; final HeroType heroType; - final FilterCallback? onTap; + final FilterCallback? onTap, onRemove; final OffsetFilterCallback? onLongPress; static const double defaultRadius = 32; @@ -65,7 +65,6 @@ class AvesFilterChip extends StatefulWidget { const AvesFilterChip({ super.key, required this.filter, - this.removable = false, this.showText = true, this.showGenericIcon = true, this.useFilterColor = true, @@ -77,6 +76,7 @@ class AvesFilterChip extends StatefulWidget { this.maxWidth, this.heroType = HeroType.onTap, this.onTap, + this.onRemove, this.onLongPress = showDefaultLongPressMenu, }); @@ -154,10 +154,6 @@ class _AvesFilterChipState extends State { double get padding => widget.padding; - FilterCallback? get onTap => widget.onTap; - - OffsetFilterCallback? get onLongPress => widget.onLongPress; - @override void initState() { super.initState(); @@ -219,12 +215,40 @@ class _AvesFilterChipState extends State { final decoration = widget.decoration; final chipBackground = Theme.of(context).scaffoldBackgroundColor; + final onTap = widget.onTap != null + ? () { + WidgetsBinding.instance.addPostFrameCallback((_) => widget.onTap?.call(filter)); + setState(() => _tapped = true); + } + : null; + final onRemove = widget.onRemove != null + ? () { + WidgetsBinding.instance.addPostFrameCallback((_) => widget.onRemove?.call(filter)); + setState(() => _tapped = true); + } + : null; + final onLongPress = widget.onLongPress != null + ? () { + if (_tapPosition != null) { + widget.onLongPress?.call(context, filter, _tapPosition!); + } + } + : null; + Widget? content; if (widget.showText) { final textScaleFactor = MediaQuery.textScaleFactorOf(context); final iconSize = AvesFilterChip.iconSize * textScaleFactor; final leading = widget.leadingOverride ?? filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon); - final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null; + final trailing = onRemove != null + ? IconButton( + icon: Icon(AIcons.clear, size: iconSize), + padding: EdgeInsets.zero, + splashRadius: IconTheme.of(context).size, + constraints: const BoxConstraints(), + onPressed: onRemove, + ) + : null; content = Row( mainAxisSize: decoration != null ? MainAxisSize.max : MainAxisSize.min, @@ -317,13 +341,8 @@ class _AvesFilterChipState extends State { // as of Flutter v2.8.0, `InkWell` does not have `onLongPressStart` like `GestureDetector`, // so we get the long press details from the tap instead onTapDown: onLongPress != null ? (details) => _tapPosition = details.globalPosition : null, - onTap: onTap != null - ? () { - WidgetsBinding.instance.addPostFrameCallback((_) => onTap!(filter)); - setState(() => _tapped = true); - } - : null, - onLongPress: onLongPress != null ? () => onLongPress!(context, filter, _tapPosition!) : null, + onTap: onTap, + onLongPress: onLongPress, borderRadius: borderRadius, child: FutureBuilder( future: _colorFuture, diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index 56f89de0f..7bb9c9be3 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -91,27 +91,39 @@ class FavouriteIcon extends StatelessWidget { } class TagIcon extends StatelessWidget { - const TagIcon({super.key}); + final IconData icon; + + const TagIcon._private({required this.icon}); + + factory TagIcon.tagged() => const TagIcon._private(icon: AIcons.tag); + + factory TagIcon.untagged() => const TagIcon._private(icon: AIcons.tagUntagged); static const scale = .9; @override Widget build(BuildContext context) { - return const OverlayIcon( - icon: AIcons.tag, + return OverlayIcon( + icon: icon, iconScale: scale, - relativeOffset: Offset(.05, .05), + relativeOffset: const Offset(.05, .05), ); } } -class GpsIcon extends StatelessWidget { - const GpsIcon({super.key}); +class LocationIcon extends StatelessWidget { + final IconData icon; + + const LocationIcon._private({required this.icon}); + + factory LocationIcon.located() => const LocationIcon._private(icon: AIcons.location); + + factory LocationIcon.unlocated() => const LocationIcon._private(icon: AIcons.locationUnlocated); @override Widget build(BuildContext context) { - return const OverlayIcon( - icon: AIcons.location, + return OverlayIcon( + icon: icon, ); } } @@ -181,10 +193,9 @@ class RatingIcon extends StatelessWidget { @override Widget build(BuildContext context) { - final gridTheme = context.watch(); return DefaultTextStyle( style: TextStyle( - fontSize: gridTheme.fontSize, + fontSize: context.select((t) => t.fontSize), ), child: OverlayIcon( icon: AIcons.rating, diff --git a/lib/widgets/common/identity/buttons/captioned_button.dart b/lib/widgets/common/identity/buttons/captioned_button.dart index 6c8e01ada..dda3e7787 100644 --- a/lib/widgets/common/identity/buttons/captioned_button.dart +++ b/lib/widgets/common/identity/buttons/captioned_button.dart @@ -2,58 +2,39 @@ import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -class CaptionedButton extends StatelessWidget { +typedef CaptionedIconButtonBuilder = Widget Function(BuildContext context, FocusNode focusNode); + +class CaptionedButton extends StatefulWidget { final Animation scale; final Widget captionText; - final Widget iconButton; + final CaptionedIconButtonBuilder iconButtonBuilder; final bool showCaption; final VoidCallback? onPressed; + static const EdgeInsets padding = EdgeInsets.symmetric(horizontal: 8); + static const double iconTextPadding = 8; + CaptionedButton({ super.key, this.scale = kAlwaysCompleteAnimation, Widget? icon, - Widget? iconButton, + CaptionedIconButtonBuilder? iconButtonBuilder, String? caption, Widget? captionText, this.showCaption = true, required this.onPressed, - }) : assert(icon != null || iconButton != null), + }) : assert(icon != null || iconButtonBuilder != null), assert(caption != null || captionText != null), - iconButton = iconButton ?? IconButton(icon: icon!, onPressed: onPressed), + iconButtonBuilder = iconButtonBuilder ?? ((_, focusNode) => IconButton(icon: icon!, onPressed: onPressed, focusNode: focusNode)), captionText = captionText ?? CaptionedButtonText(text: caption!, enabled: onPressed != null); - static const double padding = 8; - @override - Widget build(BuildContext context) { - return SizedBox( - width: _width(context), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: padding), - OverlayButton( - scale: scale, - child: iconButton, - ), - if (showCaption) ...[ - const SizedBox(height: padding), - ScaleTransition( - scale: scale, - child: captionText, - ), - ], - const SizedBox(height: padding), - ], - ), - ); - } + State createState() => _CaptionedButtonState(); - static double _width(BuildContext context) => OverlayButton.getSize(context) + padding * 2; + static double getWidth(BuildContext context) => OverlayButton.getSize(context) + padding.horizontal; static Size getSize(BuildContext context, String text, {required bool showCaption}) { - final width = _width(context); + final width = getWidth(context); var height = width; if (showCaption) { final para = RenderParagraph( @@ -62,7 +43,7 @@ class CaptionedButton extends StatelessWidget { textScaleFactor: MediaQuery.textScaleFactorOf(context), maxLines: CaptionedButtonText.maxLines, )..layout(const BoxConstraints(), parentUsesSize: true); - height += para.getMaxIntrinsicHeight(width) + padding; + height += para.getMaxIntrinsicHeight(width) + padding.vertical; } return Size(width, height); } @@ -73,6 +54,81 @@ class CaptionedButton extends StatelessWidget { } } +class _CaptionedButtonState extends State { + final FocusNode _focusNode = FocusNode(); + final ValueNotifier _focusedNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + _updateTraversal(); + _focusNode.addListener(_onFocusChanged); + } + + @override + void didUpdateWidget(covariant CaptionedButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.onPressed != widget.onPressed) { + _updateTraversal(); + } + } + + @override + void dispose() { + _focusNode.dispose(); + _focusedNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: CaptionedButton.getWidth(context), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox(height: CaptionedButton.padding.top), + OverlayButton( + scale: widget.scale, + focusNode: _focusNode, + child: widget.iconButtonBuilder(context, _focusNode), + ), + if (widget.showCaption) ...[ + const SizedBox(height: CaptionedButton.iconTextPadding), + ScaleTransition( + scale: widget.scale, + child: ValueListenableBuilder( + valueListenable: _focusedNotifier, + builder: (context, focused, child) { + final style = CaptionedButtonText.textStyle(context); + return AnimatedDefaultTextStyle( + style: focused + ? style.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ) + : style, + duration: const Duration(milliseconds: 200), + child: widget.captionText, + ); + }, + ), + ), + ], + SizedBox(height: CaptionedButton.padding.bottom), + ], + ), + ); + } + + void _onFocusChanged() => _focusedNotifier.value = _focusNode.hasFocus; + + void _updateTraversal() { + final enabled = widget.onPressed != null; + _focusNode.skipTraversal = !enabled; + _focusNode.canRequestFocus = enabled; + } +} + class CaptionedButtonText extends StatelessWidget { final String text; final bool enabled; @@ -87,7 +143,7 @@ class CaptionedButtonText extends StatelessWidget { @override Widget build(BuildContext context) { - var style = textStyle(context); + var style = DefaultTextStyle.of(context).style; if (!enabled) { style = style.copyWith(color: style.color!.withOpacity(.2)); } diff --git a/lib/widgets/common/identity/buttons/overlay_button.dart b/lib/widgets/common/identity/buttons/overlay_button.dart index 1feee178a..d4b085433 100644 --- a/lib/widgets/common/identity/buttons/overlay_button.dart +++ b/lib/widgets/common/identity/buttons/overlay_button.dart @@ -4,60 +4,116 @@ import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; -class OverlayButton extends StatelessWidget { +class OverlayButton extends StatefulWidget { final Animation scale; final BorderRadius? borderRadius; + final FocusNode? focusNode; final Widget child; const OverlayButton({ super.key, this.scale = kAlwaysCompleteAnimation, this.borderRadius, + this.focusNode, required this.child, }); + @override + State createState() => _OverlayButtonState(); + + // icon (24) + icon padding (8) + button padding (16) + static double getSize(BuildContext context) => 48; +} + +class _OverlayButtonState extends State { + final ValueNotifier _focusedNotifier = ValueNotifier(false); + + @override + void initState() { + super.initState(); + _registerWidget(widget); + } + + @override + void didUpdateWidget(covariant OverlayButton oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + + @override + void dispose() { + _unregisterWidget(widget); + _focusedNotifier.dispose(); + super.dispose(); + } + + void _registerWidget(OverlayButton widget) { + widget.focusNode?.addListener(_onFocusChanged); + } + + void _unregisterWidget(OverlayButton widget) { + widget.focusNode?.removeListener(_onFocusChanged); + } + @override Widget build(BuildContext context) { - final brightness = Theme.of(context).brightness; + final borderRadius = widget.borderRadius; + final blurred = settings.enableBlurEffect; + final overlayBackground = Themes.overlayBackgroundColor( + brightness: Theme.of(context).brightness, + blurred: blurred, + ); + return ScaleTransition( - scale: scale, - child: borderRadius != null - ? BlurredRRect( - enabled: blurred, - borderRadius: borderRadius, - child: Material( - type: MaterialType.button, - borderRadius: borderRadius, - color: Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred), - child: Ink( - decoration: BoxDecoration( - border: AvesBorder.border(context), + scale: widget.scale, + child: ValueListenableBuilder( + valueListenable: _focusedNotifier, + builder: (context, focused, child) { + final border = AvesBorder.border( + context, + width: AvesBorder.curvedBorderWidth * (focused ? 3 : 1), + ); + return borderRadius != null + ? BlurredRRect( + enabled: blurred, + borderRadius: borderRadius, + child: Material( + type: MaterialType.button, borderRadius: borderRadius, + color: overlayBackground, + child: AnimatedContainer( + foregroundDecoration: BoxDecoration( + border: border, + borderRadius: borderRadius, + ), + duration: const Duration(milliseconds: 200), + child: widget.child, + ), ), - child: child, - ), - ), - ) - : BlurredOval( - enabled: blurred, - child: Material( - type: MaterialType.circle, - color: Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred), - child: Ink( - decoration: BoxDecoration( - border: AvesBorder.border(context), - shape: BoxShape.circle, + ) + : BlurredOval( + enabled: blurred, + child: Material( + type: MaterialType.circle, + color: overlayBackground, + child: AnimatedContainer( + foregroundDecoration: BoxDecoration( + border: border, + shape: BoxShape.circle, + ), + duration: const Duration(milliseconds: 200), + child: widget.child, + ), ), - child: child, - ), - ), - ), + ); + }, + ), ); } - // icon (24) + icon padding (8) + button padding (16) + border (1 or 2) - static double getSize(BuildContext context) => 48.0 + AvesBorder.curvedBorderWidth * 2; + void _onFocusChanged() => _focusedNotifier.value = widget.focusNode?.hasFocus ?? false; } class ScalingOverlayTextButton extends StatelessWidget { diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index fb1b8b0e7..e7205b1b5 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -1,29 +1,27 @@ -import 'package:aves/model/device.dart'; -import 'package:aves/model/settings/enums/map_style.dart'; +import 'package:aves/model/actions/map_actions.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/map/buttons/button.dart'; import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; import 'package:aves/widgets/common/map/compass.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/common/map/map_action_delegate.dart'; import 'package:aves_map/aves_map.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; class MapButtonPanel extends StatelessWidget { + final AvesMapController? controller; final ValueNotifier boundsNotifier; - final Future Function(double amount)? zoomBy; final void Function(BuildContext context)? openMapPage; final VoidCallback? resetRotation; const MapButtonPanel({ super.key, + required this.controller, required this.boundsNotifier, - this.zoomBy, this.openMapPage, this.resetRotation, }); @@ -36,7 +34,7 @@ class MapButtonPanel extends StatelessWidget { Widget? navigationButton; switch (context.select((v) => v.navigationButton)) { case MapNavigationButton.back: - if (!device.isTelevision) { + if (!settings.useTvLayout) { navigationButton = MapOverlayButton( icon: const BackButtonIcon(), onPressed: () => Navigator.pop(context), @@ -124,21 +122,8 @@ class MapButtonPanel extends StatelessWidget { : const Spacer(), Padding( padding: EdgeInsets.only(top: padding), - child: MapOverlayButton( - // key is expected by test driver - buttonKey: const Key('map-menu-layers'), - icon: const Icon(AIcons.layers), - onPressed: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.mapStyle, - options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.mapStyleDialogTitle, - ), - onSelection: (v) => settings.mapStyle = v, - ), - tooltip: context.l10n.mapStyleTooltip, - ), + // key is expected by test driver + child: _buildButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')), ), ], ), @@ -149,17 +134,9 @@ class MapButtonPanel extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - MapOverlayButton( - icon: const Icon(AIcons.zoomIn), - onPressed: zoomBy != null ? () => zoomBy?.call(1) : null, - tooltip: context.l10n.mapZoomInTooltip, - ), + _buildButton(context, MapAction.zoomIn), SizedBox(height: padding), - MapOverlayButton( - icon: const Icon(AIcons.zoomOut), - onPressed: zoomBy != null ? () => zoomBy?.call(-1) : null, - tooltip: context.l10n.mapZoomOutTooltip, - ), + _buildButton(context, MapAction.zoomOut), ], ), ), @@ -169,4 +146,11 @@ class MapButtonPanel extends StatelessWidget { ), ); } + + Widget _buildButton(BuildContext context, MapAction action, {Key? buttonKey}) => MapOverlayButton( + buttonKey: buttonKey, + icon: action.getIcon(), + onPressed: () => MapActionDelegate(controller).onActionSelected(context, action), + tooltip: action.getText(context), + ); } diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index 796b30179..0643b2ce4 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -144,6 +144,7 @@ class _GeoMapState extends State { ); bool _isMarkerImageReady(MarkerKey key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent); + final controller = widget.controller; Widget child = const SizedBox(); if (mapStyle != null) { switch (mapStyle) { @@ -153,7 +154,7 @@ class _GeoMapState extends State { case EntryMapStyle.hmsNormal: case EntryMapStyle.hmsTerrain: child = mobileServices.buildMap( - controller: widget.controller, + controller: controller, clusterListenable: _clusterChangeNotifier, boundsNotifier: _boundsNotifier, style: mapStyle, @@ -175,7 +176,7 @@ class _GeoMapState extends State { case EntryMapStyle.stamenToner: case EntryMapStyle.stamenWatercolor: child = EntryLeafletMap( - controller: widget.controller, + controller: controller, clusterListenable: _clusterChangeNotifier, boundsNotifier: _boundsNotifier, minZoom: 2, @@ -260,6 +261,7 @@ class _GeoMapState extends State { children: [ const MapDecorator(), MapButtonPanel( + controller: controller, boundsNotifier: _boundsNotifier, openMapPage: widget.openMapPage, ), @@ -485,14 +487,13 @@ class _GeoMapState extends State { Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child); - Widget _buildButtonPanel( - Future Function(double amount) zoomBy, - VoidCallback resetRotation, - ) => - MapButtonPanel( - boundsNotifier: _boundsNotifier, - zoomBy: zoomBy, - openMapPage: widget.openMapPage, - resetRotation: resetRotation, - ); + Widget _buildButtonPanel(VoidCallback resetRotation) { + if (settings.useTvLayout) return const SizedBox(); + return MapButtonPanel( + controller: widget.controller, + boundsNotifier: _boundsNotifier, + openMapPage: widget.openMapPage, + resetRotation: resetRotation, + ); + } } diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index 358972f3f..d0c60933f 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -95,6 +95,7 @@ class _EntryLeafletMapState extends State> with TickerProv final avesMapController = widget.controller; if (avesMapController != null) { _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng))); + _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta))); } _subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion())); widget.clusterListenable.addListener(_updateMarkers); @@ -114,7 +115,7 @@ class _EntryLeafletMapState extends State> with TickerProv return Stack( children: [ widget.decoratorBuilder(context, _buildMap()), - widget.buttonPanelBuilder(_zoomBy, _resetRotation), + widget.buttonPanelBuilder(_resetRotation), ], ); } diff --git a/lib/widgets/common/map/map_action_delegate.dart b/lib/widgets/common/map/map_action_delegate.dart new file mode 100644 index 000000000..06a984005 --- /dev/null +++ b/lib/widgets/common/map/map_action_delegate.dart @@ -0,0 +1,37 @@ +import 'package:aves/model/actions/map_actions.dart'; +import 'package:aves/model/settings/enums/map_style.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves_map/aves_map.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class MapActionDelegate { + final AvesMapController? controller; + + const MapActionDelegate(this.controller); + + void onActionSelected(BuildContext context, MapAction action) { + switch (action) { + case MapAction.selectStyle: + showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.mapStyle, + options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.mapStyleDialogTitle, + ), + onSelection: (v) => settings.mapStyle = v, + ); + break; + case MapAction.zoomIn: + controller?.zoomBy(1); + break; + case MapAction.zoomOut: + controller?.zoomBy(-1); + break; + } + } +} diff --git a/lib/widgets/common/providers/media_query_data_provider.dart b/lib/widgets/common/providers/media_query_data_provider.dart index 71c45c4a4..742c775fc 100644 --- a/lib/widgets/common/providers/media_query_data_provider.dart +++ b/lib/widgets/common/providers/media_query_data_provider.dart @@ -2,17 +2,19 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; class MediaQueryDataProvider extends StatelessWidget { + final MediaQueryData? value; final Widget child; const MediaQueryDataProvider({ super.key, + this.value, required this.child, }); @override Widget build(BuildContext context) { return Provider.value( - value: MediaQuery.of(context), + value: value ?? MediaQuery.of(context), child: child, ); } diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart index 54e9d3859..282c38aa1 100644 --- a/lib/widgets/common/search/delegate.dart +++ b/lib/widgets/common/search/delegate.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/search/route.dart'; @@ -20,7 +20,7 @@ abstract class AvesSearchDelegate extends SearchDelegate { @override Widget? buildLeading(BuildContext context) { - if (device.isTelevision) { + if (settings.useTvLayout) { return const Icon(AIcons.search); } diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index ae89a1577..9d7dd5c55 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -9,6 +9,7 @@ import 'package:aves/model/settings/enums/entry_background.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/fx/checkered_decoration.dart'; import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -271,13 +272,20 @@ class _ThumbnailImageState extends State { image = Hero( tag: widget.heroTag!, flightShuttleBuilder: (flight, animation, direction, fromHero, toHero) { - return TransitionImage( + Widget child = TransitionImage( image: entry.bestCachedThumbnail, animation: animation, thumbnailFit: isMosaic ? BoxFit.contain : BoxFit.cover, viewerFit: BoxFit.contain, background: backgroundColor, ); + if (!settings.viewerUseCutout) { + child = SafeCutoutArea( + animation: animation, + child: child, + ); + } + return child; }, transitionOnUserGestures: true, child: image, diff --git a/lib/widgets/common/thumbnail/overlay.dart b/lib/widgets/common/thumbnail/overlay.dart index e9ca7f249..aa3b639d6 100644 --- a/lib/widgets/common/thumbnail/overlay.dart +++ b/lib/widgets/common/thumbnail/overlay.dart @@ -4,7 +4,6 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/highlight.dart'; import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/grid/theme.dart'; -import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -18,26 +17,8 @@ class ThumbnailEntryOverlay extends StatelessWidget { @override Widget build(BuildContext context) { - final children = [ - if (entry.isFavourite && context.select((t) => t.showFavourite)) const FavouriteIcon(), - if (entry.tags.isNotEmpty && context.select((t) => t.showTag)) const TagIcon(), - if (entry.hasGps && context.select((t) => t.showLocation)) const GpsIcon(), - if (entry.rating != 0 && context.select((t) => t.showRating)) RatingIcon(entry: entry), - if (entry.isVideo) - VideoIcon(entry: entry) - else if (entry.isAnimated) - const AnimatedImageIcon() - else ...[ - if (entry.isRaw && context.select((t) => t.showRaw)) const RawIcon(), - if (entry.is360) const PanoramaIcon(), - ], - if (entry.isMultiPage) ...[ - if (entry.isMotionPhoto && context.select((t) => t.showMotionPhoto)) const MotionPhotoIcon(), - if (!entry.isMotionPhoto) MultiPageIcon(entry: entry), - ], - if (entry.isGeotiff) const GeoTiffIcon(), - if (entry.trashed && context.select((t) => t.showTrash)) TrashIcon(trashDaysLeft: entry.trashDaysLeft), - ]; + final iconBuilder = context.select((t) => t.iconBuilder); + final children = iconBuilder(context, entry); if (children.isEmpty) return const SizedBox(); return Align( alignment: AlignmentDirectional.bottomStart, diff --git a/lib/widgets/common/thumbnail/scroller.dart b/lib/widgets/common/thumbnail/scroller.dart index 259621305..25cf3d955 100644 --- a/lib/widgets/common/thumbnail/scroller.dart +++ b/lib/widgets/common/thumbnail/scroller.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; -import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/behaviour/known_extent_scroll_physics.dart'; import 'package:aves/widgets/common/grid/theme.dart'; @@ -95,7 +94,7 @@ class _ThumbnailScrollerState extends State { return GridTheme( extent: thumbnailExtent, - showLocation: widget.showLocation && settings.showThumbnailLocation, + showLocation: widget.showLocation, showTrash: false, child: SizedBox( width: scrollable ? null : widthFor(entryCount), diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index 7cd2a0c69..1d0e8d081 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -87,10 +87,7 @@ class _AddShortcutDialogState extends State { ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { diff --git a/lib/widgets/dialogs/aves_confirmation_dialog.dart b/lib/widgets/dialogs/aves_confirmation_dialog.dart index 8fcedc0b6..621627172 100644 --- a/lib/widgets/dialogs/aves_confirmation_dialog.dart +++ b/lib/widgets/dialogs/aves_confirmation_dialog.dart @@ -110,10 +110,7 @@ class _AvesConfirmationDialogState extends State<_AvesConfirmationDialog> { ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: () { if (_skip.value) { diff --git a/lib/widgets/dialogs/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart index 411c700a0..2cf3c01e9 100644 --- a/lib/widgets/dialogs/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -152,19 +152,34 @@ class DialogTitle extends StatelessWidget { } } -void showNoMatchingAppDialog(BuildContext context) { - showDialog( - context: context, - builder: (context) { - return AvesDialog( +Future showNoMatchingAppDialog(BuildContext context) => showDialog( + context: context, + builder: (context) => AvesDialog( content: Text(context.l10n.noMatchingAppDialogMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).okButtonLabel), - ), - ], - ); - }, - ); + actions: const [OkButton()], + ), + ); + +class CancelButton extends StatelessWidget { + const CancelButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).cancelButtonLabel), + ); + } +} + +class OkButton extends StatelessWidget { + const OkButton({super.key}); + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: () => Navigator.pop(context), + child: Text(MaterialLocalizations.of(context).okButtonLabel), + ); + } } diff --git a/lib/widgets/dialogs/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart index 15dce1fe0..95352649d 100644 --- a/lib/widgets/dialogs/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -86,10 +86,7 @@ class _AvesSelectionDialogState extends State> { if (verticalPadding != 0) SizedBox(height: verticalPadding), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), if (needConfirmation) TextButton( onPressed: () => Navigator.pop(context, _selectedValue), diff --git a/lib/widgets/dialogs/duration_dialog.dart b/lib/widgets/dialogs/duration_dialog.dart index 48ae0ad4c..91236ce0a 100644 --- a/lib/widgets/dialogs/duration_dialog.dart +++ b/lib/widgets/dialogs/duration_dialog.dart @@ -88,10 +88,7 @@ class _DurationDialogState extends State { ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), AnimatedBuilder( animation: Listenable.merge([_minutes, _seconds]), builder: (context, child) { diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index 21bb629df..f8c784d10 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -15,8 +15,8 @@ import 'package:aves/widgets/common/basic/wheel.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; -import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart'; import 'package:aves/widgets/dialogs/item_picker.dart'; +import 'package:aves/widgets/dialogs/pick_dialogs/item_pick_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -43,6 +43,7 @@ class _EditEntryDateDialogState extends State { late ValueNotifier _shiftSign; bool _showOptions = false; final Set _fields = {...DateModifier.writableFields}; + final ValueNotifier _isValidNotifier = ValueNotifier(false); DateTime get copyItemDate => _copyItemSource.bestDate ?? DateTime.now(); @@ -52,6 +53,13 @@ class _EditEntryDateDialogState extends State { _initCustom(); _initCopyItem(); _initShift(minutesInHour); + _validate(); + } + + @override + void dispose() { + _isValidNotifier.dispose(); + super.dispose(); } void _initCustom() { @@ -88,7 +96,11 @@ class _EditEntryDateDialogState extends State { values: DateEditAction.values, valueText: (v) => v.getText(context), value: _action, - onChanged: (v) => setState(() => _action = v!), + onChanged: (v) { + _action = v!; + _validate(); + setState(() {}); + }, isExpanded: true, dropdownColor: Themes.thirdLayerColor(context), ), @@ -112,13 +124,15 @@ class _EditEntryDateDialogState extends State { ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => _submit(context), - child: Text(l10n.applyButtonLabel), + const CancelButton(), + ValueListenableBuilder( + valueListenable: _isValidNotifier, + builder: (context, isValid, child) { + return TextButton( + onPressed: isValid ? () => _submit(context) : null, + child: Text(l10n.applyButtonLabel), + ); + }, ), ], ); @@ -268,7 +282,11 @@ class _EditEntryDateDialogState extends State { children: DateModifier.writableFields .map((field) => SwitchListTile( value: _fields.contains(field), - onChanged: (selected) => setState(() => selected ? _fields.add(field) : _fields.remove(field)), + onChanged: (selected) { + selected ? _fields.add(field) : _fields.remove(field); + _validate(); + setState(() {}); + }, title: Text(field.title), )) .toList(), @@ -349,5 +367,24 @@ class _EditEntryDateDialogState extends State { } } - void _submit(BuildContext context) => Navigator.pop(context, _getModifier()); + void _validate() { + switch (_action) { + case DateEditAction.setCustom: + case DateEditAction.copyField: + case DateEditAction.copyItem: + case DateEditAction.extractFromTitle: + _isValidNotifier.value = true; + break; + case DateEditAction.shift: + case DateEditAction.remove: + _isValidNotifier.value = _fields.isNotEmpty; + break; + } + } + + void _submit(BuildContext context) { + if (_isValidNotifier.value) { + Navigator.pop(context, _getModifier()); + } + } } diff --git a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart index 7e9c67473..a080cc870 100644 --- a/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_description_dialog.dart @@ -44,10 +44,7 @@ class _EditEntryTitleDescriptionDialogState extends State Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: fields.isEmpty ? null : () => _submit(context), child: Text(context.l10n.applyButtonLabel), diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart index 12c80eba5..31223fd29 100644 --- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart @@ -126,10 +126,7 @@ class _EditEntryLocationDialogState extends State { const SizedBox(height: 8), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { diff --git a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart index 2e07e0751..d7b8399a3 100644 --- a/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_rating_dialog.dart @@ -99,10 +99,7 @@ class _EditEntryRatingDialogState extends State { ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: isValid ? () => _submit(context) : null, child: Text(l10n.applyButtonLabel), diff --git a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart index 18c2a1e83..a2f212008 100644 --- a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart @@ -80,10 +80,7 @@ class _RemoveEntryMetadataDialogState extends State { ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { diff --git a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart index b68aa54d2..e99be396d 100644 --- a/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/rename_entry_dialog.dart @@ -58,10 +58,7 @@ class _RenameEntryDialogState extends State { onSubmitted: (_) => _submit(context), ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { diff --git a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart index 96d961f02..9596b7a41 100644 --- a/lib/widgets/dialogs/entry_editors/tag_editor_page.dart +++ b/lib/widgets/dialogs/entry_editors/tag_editor_page.dart @@ -150,14 +150,23 @@ class _TagEditorPageState extends State { secondChild: ExpandableFilterRow( filters: sortedTags.map((kv) => kv.key).toList(), isExpanded: context.select((v) => v.tagEditorCurrentFilterSectionExpanded), - removable: true, showGenericIcon: false, leadingBuilder: showCount ? (filter) => _TagCount( count: sortedTags.firstWhere((kv) => kv.key == filter).value, ) : null, - onTap: _removeTag, + onTap: (filter) { + if (tagsByEntry.keys.length > 1) { + // for multiple entries, set tag for all of them + tagsByEntry.forEach((entry, filters) => filters.add(filter)); + setState(() {}); + } else { + // for single entry, remove tag (like pressing on the remove icon) + _removeTag(filter); + } + }, + onRemove: _removeTag, onLongPress: null, ), crossFadeState: sortedTags.isEmpty ? CrossFadeState.showFirst : CrossFadeState.showSecond, diff --git a/lib/widgets/dialogs/export_entry_dialog.dart b/lib/widgets/dialogs/export_entry_dialog.dart index 5b59c90d2..e0e51f3c5 100644 --- a/lib/widgets/dialogs/export_entry_dialog.dart +++ b/lib/widgets/dialogs/export_entry_dialog.dart @@ -114,10 +114,7 @@ class _ExportEntryDialogState extends State { const SizedBox(height: 16), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { diff --git a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart index d2bd5b693..3bcf76bc7 100644 --- a/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/cover_selection_dialog.dart @@ -155,10 +155,7 @@ class _CoverSelectionDialogState extends State { spacing: AvesDialog.buttonPadding.horizontal / 2, overflowAlignment: OverflowBarAlignment.end, children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: () { final entry = _isCustomEntry ? _customEntry : null; diff --git a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart index 5bf025ba2..f0839e8fe 100644 --- a/lib/widgets/dialogs/filter_editors/create_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/create_album_dialog.dart @@ -90,10 +90,7 @@ class _CreateAlbumDialogState extends State { ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { diff --git a/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart b/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart index f270af325..e18abe61e 100644 --- a/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart +++ b/lib/widgets/dialogs/filter_editors/rename_album_dialog.dart @@ -36,6 +36,8 @@ class _RenameAlbumDialogState extends State { @override void dispose() { _nameController.dispose(); + _existsNotifier.dispose(); + _isValidNotifier.dispose(); super.dispose(); } @@ -57,10 +59,7 @@ class _RenameAlbumDialogState extends State { ); }), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), ValueListenableBuilder( valueListenable: _isValidNotifier, builder: (context, isValid, child) { @@ -69,7 +68,7 @@ class _RenameAlbumDialogState extends State { child: Text(context.l10n.applyButtonLabel), ); }, - ) + ), ], ); } diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 9e0386225..7f5272c0c 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -145,10 +145,7 @@ class _TileViewDialogState extends State> with ), ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( key: const Key('button-apply'), onPressed: () { diff --git a/lib/widgets/dialogs/video_speed_dialog.dart b/lib/widgets/dialogs/video_speed_dialog.dart index 4b44b8cb9..f835e1026 100644 --- a/lib/widgets/dialogs/video_speed_dialog.dart +++ b/lib/widgets/dialogs/video_speed_dialog.dart @@ -57,10 +57,7 @@ class _VideoSpeedDialogState extends State { ], ), actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: () => _submit(context), child: Text(context.l10n.applyButtonLabel), diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index b89950b49..eef7d88d9 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.dart @@ -80,10 +80,7 @@ class _VideoStreamSelectionDialogState extends State ] : null, actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), if (canSelect) TextButton( onPressed: () => _submit(context), diff --git a/lib/widgets/dialogs/wallpaper_settings_dialog.dart b/lib/widgets/dialogs/wallpaper_settings_dialog.dart index ba8d6ce73..5dc85114d 100644 --- a/lib/widgets/dialogs/wallpaper_settings_dialog.dart +++ b/lib/widgets/dialogs/wallpaper_settings_dialog.dart @@ -39,10 +39,7 @@ class _WallpaperSettingsDialogState extends State { ) ], actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: () => Navigator.pop(context, Tuple2(_selectedTarget, _useScrollEffect)), child: Text(context.l10n.applyButtonLabel), diff --git a/lib/widgets/filter_grids/common/action_delegates/album_set.dart b/lib/widgets/filter_grids/common/action_delegates/album_set.dart index 25cc2a85d..843a5eda3 100644 --- a/lib/widgets/filter_grids/common/action_delegates/album_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/album_set.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/move_type.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/highlight.dart'; @@ -76,10 +75,10 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with }) { switch (action) { case ChipSetAction.createAlbum: - return !device.isReadOnly && appMode == AppMode.main && !isSelecting; + return !settings.isReadOnly && appMode == AppMode.main && !isSelecting; case ChipSetAction.delete: case ChipSetAction.rename: - return !device.isReadOnly && appMode == AppMode.main && isSelecting; + return !settings.isReadOnly && appMode == AppMode.main && isSelecting; default: return super.isVisible( action, @@ -235,21 +234,16 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate with final confirmed = await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(l10n.deleteButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(filters.length == 1 ? l10n.deleteSingleAlbumConfirmationDialogMessage(todoCount) : l10n.deleteMultiAlbumConfirmationDialogMessage(todoCount)), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(l10n.deleteButtonLabel), + ), + ], + ), ); if (confirmed == null || !confirmed) return; diff --git a/lib/widgets/filter_grids/common/action_delegates/chip.dart b/lib/widgets/filter_grids/common/action_delegates/chip.dart index 1d75b73cd..988e30792 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip.dart @@ -53,21 +53,16 @@ class ChipActionDelegate { Future _hide(BuildContext context, CollectionFilter filter) async { final confirmed = await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.hideFilterConfirmationDialogMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.hideButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(context.l10n.hideFilterConfirmationDialogMessage), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.hideButtonLabel), + ), + ], + ), ); if (confirmed == null || !confirmed) return; diff --git a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart index e095ccb7f..310d2b382 100644 --- a/lib/widgets/filter_grids/common/action_delegates/chip_set.dart +++ b/lib/widgets/filter_grids/common/action_delegates/chip_set.dart @@ -1,7 +1,6 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/covers.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; @@ -83,7 +82,7 @@ abstract class ChipSetActionDelegate with FeedbackMi return isSelecting && selectedItemCount == itemCount; // browsing case ChipSetAction.search: - return !device.isTelevision && appMode.canNavigate && !isSelecting; + return !settings.useTvLayout && appMode.canNavigate && !isSelecting; case ChipSetAction.toggleTitleSearch: return !isSelecting; case ChipSetAction.createAlbum: @@ -305,21 +304,16 @@ abstract class ChipSetActionDelegate with FeedbackMi Future _hide(BuildContext context, Set filters) async { final confirmed = await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.hideFilterConfirmationDialogMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.hideButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(context.l10n.hideFilterConfirmationDialogMessage), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.hideButtonLabel), + ), + ], + ), ); if (confirmed == null || !confirmed) return; diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart index e95b110c9..e425690f4 100644 --- a/lib/widgets/filter_grids/common/app_bar.dart +++ b/lib/widgets/filter_grids/common/app_bar.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/chip_set_actions.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/query.dart'; import 'package:aves/model/selection.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/action_controls/togglers/title_search.dart'; @@ -125,47 +125,53 @@ class _FilterGridAppBarState>>(); final isSelecting = selection.isSelecting; _isSelectingNotifier.value = isSelecting; - return Selector( - selector: (context, query) => query.enabled, - builder: (context, queryEnabled, child) { - ActionsBuilder actionsBuilder = widget.actionsBuilder ?? _buildActions; - final isTelevision = device.isTelevision; - final actions = actionsBuilder(context, appMode, selection, widget.actionDelegate); - return AvesAppBar( - contentHeight: appBarContentHeight, - leading: _buildAppBarLeading( - hasDrawer: appMode.canNavigate, - isSelecting: isSelecting, - ), - title: _buildAppBarTitle(isSelecting), - actions: isTelevision ? [] : actions, - bottom: Column( - children: [ - if (isTelevision) - SizedBox( - height: CaptionedButton.getTelevisionButtonHeight(context), - child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 8), - scrollDirection: Axis.horizontal, - children: actions, + return NotificationListener( + // cancel notification bubbling so that the draggable scroll bar + // does not misinterpret filter bar scrolling for collection scrolling + onNotification: (notification) => true, + child: Selector( + selector: (context, query) => query.enabled, + builder: (context, queryEnabled, child) { + ActionsBuilder actionsBuilder = widget.actionsBuilder ?? _buildActions; + final useTvLayout = settings.useTvLayout; + final actions = actionsBuilder(context, appMode, selection, widget.actionDelegate); + return AvesAppBar( + contentHeight: appBarContentHeight, + pinned: context.select>, bool>((selection) => selection.isSelecting), + leading: _buildAppBarLeading( + hasDrawer: appMode.canNavigate, + isSelecting: isSelecting, + ), + title: _buildAppBarTitle(isSelecting), + actions: useTvLayout ? [] : actions, + bottom: Column( + children: [ + if (useTvLayout) + SizedBox( + height: CaptionedButton.getTelevisionButtonHeight(context), + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8), + scrollDirection: Axis.horizontal, + children: actions, + ), ), - ), - if (queryEnabled) - FilterQueryBar( - queryNotifier: context.select>((query) => query.queryNotifier), - focusNode: _queryBarFocusNode, - ), - ], - ), - transitionKey: isSelecting, - ); - }, + if (queryEnabled) + FilterQueryBar( + queryNotifier: context.select>((query) => query.queryNotifier), + focusNode: _queryBarFocusNode, + ), + ], + ), + transitionKey: isSelecting, + ); + }, + ), ); } double get appBarContentHeight { double height = kToolbarHeight; - if (device.isTelevision) { + if (settings.useTvLayout) { height += CaptionedButton.getTelevisionButtonHeight(context); } if (context.read().enabled) { @@ -175,7 +181,7 @@ class _FilterGridAppBarState _buildButtonIcon( + context, + actionDelegate, + action, + enabled: enabled, + focusNode: focusNode, + ), captionText: _buildButtonCaption(context, action, enabled: enabled), onPressed: enabled ? () => _onActionSelected(context, action, actionDelegate) : null, ); @@ -345,6 +357,7 @@ class _FilterGridAppBarState _onActionSelected(context, action, actionDelegate) : null; switch (action) { @@ -355,6 +368,7 @@ class _FilterGridAppBarState extends StatelessWidget { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; final body = QueryProvider( initialQuery: null, child: GestureAreaProtectorStack( - child: SafeArea( + child: DirectionalSafeArea( + start: !useTvLayout, top: false, bottom: false, child: Selector( @@ -113,7 +114,7 @@ class FilterGridPage extends StatelessWidget { ), ); - if (device.isTelevision) { + if (useTvLayout) { return Scaffold( body: Row( children: [ @@ -202,7 +203,7 @@ class _FilterGridState extends State<_FilterGrid> Widget build(BuildContext context) { _tileExtentController ??= TileExtentController( settingsRouteKey: widget.settingsRouteKey ?? context.currentRouteName!, - columnCountDefault: device.isTelevision ? 4 : 3, + columnCountDefault: settings.useTvLayout ? 4 : 3, extentMin: 60, extentMax: 300, spacing: 8, @@ -356,7 +357,7 @@ class _FilterGridContentState extends State<_FilterG banner: _getFilterBanner(context, gridItem.filter), heroType: widget.heroType, ); - if (!device.isTelevision) return tile; + if (!settings.useTvLayout) return tile; return Focus( onFocusChange: (focused) { diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 5d59205a6..25a94f89e 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/app_mode.dart'; +import 'package:aves/model/actions/map_actions.dart'; import 'package:aves/model/actions/map_cluster_actions.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/coordinate.dart'; @@ -16,11 +17,14 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; +import 'package:aves/widgets/common/map/map_action_delegate.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; @@ -231,7 +235,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin } Widget _buildMap() { - return MapTheme( + Widget child = MapTheme( interactive: true, showCoordinateFilter: true, navigationButton: MapNavigationButton.back, @@ -259,6 +263,37 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin onMarkerLongPress: _onMarkerLongPress, ), ); + if (settings.useTvLayout) { + child = DirectionalSafeArea( + top: false, + end: false, + bottom: false, + child: Row( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + MapAction.selectStyle, + MapAction.zoomIn, + MapAction.zoomOut, + ] + .map((action) => Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: CaptionedButton( + icon: action.getIcon(), + caption: action.getText(context), + onPressed: () => MapActionDelegate(_mapController).onActionSelected(context, action), + ), + )) + .toList(), + ), + const SizedBox(width: 16), + Expanded(child: child), + ], + ), + ); + } + return child; } Widget _buildOverlayController() { diff --git a/lib/widgets/navigation/tv_rail.dart b/lib/widgets/navigation/tv_rail.dart index b7eb63e2e..cac68e950 100644 --- a/lib/widgets/navigation/tv_rail.dart +++ b/lib/widgets/navigation/tv_rail.dart @@ -9,6 +9,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; @@ -32,6 +33,8 @@ class TvRail extends StatefulWidget { final CollectionLens? currentCollection; final TvRailController controller; + static const double minExtendedWidth = 256; + const TvRail({ super.key, required this.controller, @@ -44,6 +47,7 @@ class TvRail extends StatefulWidget { class _TvRailState extends State { late final ScrollController _scrollController; + final ValueNotifier _extendedNotifier = ValueNotifier(true); final FocusNode _focusNode = FocusNode(); TvRailController get controller => widget.controller; @@ -53,82 +57,111 @@ class _TvRailState extends State { @override void initState() { super.initState(); - _scrollController = ScrollController(initialScrollOffset: controller.offset); _scrollController.addListener(_onScrollChanged); + _registerWidget(widget); WidgetsBinding.instance.addPostFrameCallback((_) => _initFocus()); } + @override + void didUpdateWidget(covariant TvRail oldWidget) { + super.didUpdateWidget(oldWidget); + _unregisterWidget(oldWidget); + _registerWidget(widget); + } + @override void dispose() { + _unregisterWidget(widget); _scrollController.removeListener(_onScrollChanged); _scrollController.dispose(); + _extendedNotifier.dispose(); _focusNode.dispose(); super.dispose(); } + void _registerWidget(TvRail widget) { + widget.currentCollection?.filterChangeNotifier.addListener(_onCollectionFilterChanged); + } + + void _unregisterWidget(TvRail widget) { + widget.currentCollection?.filterChangeNotifier.removeListener(_onCollectionFilterChanged); + } + @override Widget build(BuildContext context) { - final header = Row( - children: [ - const AvesLogo(size: 48), - const SizedBox(width: 16), - Text( - context.l10n.appName, - style: const TextStyle( - color: Colors.white, - fontSize: 32, - fontWeight: FontWeight.w300, - letterSpacing: 1.0, - fontFeatures: [FontFeature.enable('smcp')], - ), - ), - ], - ); - final navEntries = _getNavEntries(context); + return DirectionalSafeArea( + end: false, + child: ValueListenableBuilder( + valueListenable: _extendedNotifier, + builder: (context, extended, child) { + const logo = AvesLogo(size: 48); + final header = extended + ? Row( + children: [ + logo, + const SizedBox(width: 16), + Text( + context.l10n.appName, + style: const TextStyle( + color: Colors.white, + fontSize: 32, + fontWeight: FontWeight.w300, + letterSpacing: 1.0, + fontFeatures: [FontFeature.enable('smcp')], + ), + ), + ], + ) + : logo; - final rail = Focus( - focusNode: _focusNode, - skipTraversal: true, - child: NavigationRail( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - extended: true, - destinations: navEntries - .map((v) => NavigationRailDestination( - icon: v.icon, - label: v.label, - )) - .toList(), - selectedIndex: max(0, navEntries.indexWhere(((v) => v.isSelected))), - onDestinationSelected: (index) { - controller.focusedIndex = index; - navEntries[index].onSelection(); + final rail = Focus( + focusNode: _focusNode, + skipTraversal: true, + child: NavigationRail( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + extended: extended, + destinations: navEntries + .map((v) => NavigationRailDestination( + icon: v.icon, + label: v.label, + )) + .toList(), + selectedIndex: max(0, navEntries.indexWhere(((v) => v.isSelected))), + onDestinationSelected: (index) { + controller.focusedIndex = index; + navEntries[index].onSelection(); + }, + minExtendedWidth: TvRail.minExtendedWidth, + ), + ); + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8), + header, + const SizedBox(height: 4), + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + controller: _scrollController, + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight(child: rail), + ), + ); + }, + ), + ), + ], + ); }, ), ); - - return Column( - children: [ - const SizedBox(height: 8), - header, - const SizedBox(height: 4), - Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - controller: _scrollController, - child: ConstrainedBox( - constraints: BoxConstraints(minHeight: constraints.maxHeight), - child: IntrinsicHeight(child: rail), - ), - ); - }, - ), - ), - ], - ); } void _initFocus() { @@ -238,6 +271,8 @@ class _TvRailState extends State { } void _onScrollChanged() => controller.offset = _scrollController.offset; + + void _onCollectionFilterChanged() => setState(() {}); } @immutable diff --git a/lib/widgets/settings/accessibility/accessibility.dart b/lib/widgets/settings/accessibility/accessibility.dart index 6f90eb815..484414f3a 100644 --- a/lib/widgets/settings/accessibility/accessibility.dart +++ b/lib/widgets/settings/accessibility/accessibility.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; @@ -29,7 +28,7 @@ class AccessibilitySection extends SettingsSection { @override FutureOr> tiles(BuildContext context) => [ - if (!device.isTelevision) SettingsTileAccessibilityShowPinchGestureAlternatives(), + if (!settings.useTvLayout) SettingsTileAccessibilityShowPinchGestureAlternatives(), SettingsTileAccessibilityAnimations(), SettingsTileAccessibilityTimeToTakeAction(), ]; diff --git a/lib/widgets/settings/app_export/selection_dialog.dart b/lib/widgets/settings/app_export/selection_dialog.dart index 986b83603..167dfbca7 100644 --- a/lib/widgets/settings/app_export/selection_dialog.dart +++ b/lib/widgets/settings/app_export/selection_dialog.dart @@ -54,10 +54,7 @@ class _AppExportItemSelectionDialogState extends State Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), + const CancelButton(), TextButton( onPressed: _selectedItems.isEmpty ? null : () => Navigator.pop(context, _selectedItems), child: Text(context.l10n.applyButtonLabel), diff --git a/lib/widgets/settings/common/quick_actions/available_actions.dart b/lib/widgets/settings/common/quick_actions/available_actions.dart index 9feb52162..66a724e34 100644 --- a/lib/widgets/settings/common/quick_actions/available_actions.dart +++ b/lib/widgets/settings/common/quick_actions/available_actions.dart @@ -17,7 +17,8 @@ class AvailableActionPanel extends StatelessWidget { final String Function(BuildContext context, T action) actionText; static const double spacing = 8; - static const padding = EdgeInsets.all(spacing); + static const double runSpacing = 20; + static const padding = EdgeInsets.symmetric(vertical: 16, horizontal: 8); const AvailableActionPanel({ super.key, @@ -56,7 +57,7 @@ class AvailableActionPanel extends StatelessWidget { child: Wrap( alignment: WrapAlignment.spaceEvenly, spacing: spacing, - runSpacing: spacing, + runSpacing: runSpacing, children: allActions.map((action) { final dragged = action == draggedAvailableAction.value; final enabled = dragged || !quickActions.contains(action); @@ -124,11 +125,10 @@ class AvailableActionPanel extends StatelessWidget { final buttonSizes = captions.map((v) => CaptionedButton.getSize(context, v, showCaption: true)); final actionsPerRun = (width - padding.horizontal + spacing) ~/ (buttonSizes.first.width + spacing); final runCount = (captions.length / actionsPerRun).ceil(); - var height = .0; + var height = runSpacing * (runCount - 1) + padding.vertical / 2; for (var i = 0; i < runCount; i++) { height += buttonSizes.skip(i * actionsPerRun).take(actionsPerRun).map((v) => v.height).max; } - height += spacing * (runCount - 1) + padding.vertical; return height; } } diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart index 320b56f0e..8af0698ae 100644 --- a/lib/widgets/settings/common/tiles.dart +++ b/lib/widgets/settings/common/tiles.dart @@ -43,6 +43,8 @@ class SettingsSwitchListTile extends StatelessWidget { final String? subtitle; final Widget? trailing; + static const disabledOpacity = .2; + const SettingsSwitchListTile({ super.key, required this.selector, @@ -63,7 +65,7 @@ class SettingsSwitchListTile extends StatelessWidget { children: [ Expanded(child: titleWidget), AnimatedOpacity( - opacity: current ? 1 : .2, + opacity: current ? 1 : disabledOpacity, duration: Durations.toggleableTransitionAnimation, child: trailing, ), @@ -87,6 +89,7 @@ class SettingsSelectionListTile extends StatelessWidget { final T Function(BuildContext, Settings) selector; final ValueChanged onSelection; final String tileTitle; + final WidgetBuilder? trailingBuilder; final String? dialogTitle; final TextBuilder? optionSubtitleBuilder; @@ -97,6 +100,7 @@ class SettingsSelectionListTile extends StatelessWidget { required this.selector, required this.onSelection, required this.tileTitle, + this.trailingBuilder, this.dialogTitle, this.optionSubtitleBuilder, }); @@ -105,20 +109,31 @@ class SettingsSelectionListTile extends StatelessWidget { Widget build(BuildContext context) { return Selector( selector: selector, - builder: (context, current, child) => ListTile( - title: Text(tileTitle), - subtitle: AvesCaption(getName(context, current)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: current, - options: Map.fromEntries(values.map((v) => MapEntry(v, getName(context, v)))), - optionSubtitleBuilder: optionSubtitleBuilder, - title: dialogTitle, + builder: (context, current, child) { + Widget titleWidget = Text(tileTitle); + if (trailingBuilder != null) { + titleWidget = Row( + children: [ + Expanded(child: titleWidget), + trailingBuilder!(context), + ], + ); + } + return ListTile( + title: titleWidget, + subtitle: AvesCaption(getName(context, current)), + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: current, + options: Map.fromEntries(values.map((v) => MapEntry(v, getName(context, v)))), + optionSubtitleBuilder: optionSubtitleBuilder, + title: dialogTitle, + ), + onSelection: onSelection, ), - onSelection: onSelection, - ), - ), + ); + }, ); } } diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart index 48ec43eb3..222319665 100644 --- a/lib/widgets/settings/display/display.dart +++ b/lib/widgets/settings/display/display.dart @@ -8,6 +8,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; @@ -29,11 +30,12 @@ class DisplaySection extends SettingsSection { @override FutureOr> tiles(BuildContext context) => [ - if (!device.isTelevision) SettingsTileDisplayThemeBrightness(), + if (!settings.useTvLayout) SettingsTileDisplayThemeBrightness(), SettingsTileDisplayThemeColorMode(), if (device.isDynamicColorAvailable) SettingsTileDisplayEnableDynamicColor(), SettingsTileDisplayEnableBlurEffect(), - if (!device.isTelevision) SettingsTileDisplayDisplayRefreshRateMode(), + if (!settings.useTvLayout) SettingsTileDisplayRefreshRateMode(), + if (!device.isTelevision) SettingsTileDisplayForceTvLayout(), ]; } @@ -88,7 +90,7 @@ class SettingsTileDisplayEnableBlurEffect extends SettingsTile { ); } -class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { +class SettingsTileDisplayRefreshRateMode extends SettingsTile { @override String title(BuildContext context) => context.l10n.settingsDisplayRefreshRateModeTile; @@ -102,3 +104,38 @@ class SettingsTileDisplayDisplayRefreshRateMode extends SettingsTile { dialogTitle: context.l10n.settingsDisplayRefreshRateModeDialogTitle, ); } + +class SettingsTileDisplayForceTvLayout extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsDisplayUseTvInterface; + + @override + Widget build(BuildContext context) => SettingsSwitchListTile( + selector: (context, s) => s.forceTvLayout, + onChanged: (v) async { + if (v) { + final l10n = context.l10n; + final confirmed = await showDialog( + context: context, + builder: (context) => AvesDialog( + content: Text([ + l10n.settingsModificationWarningDialogMessage, + l10n.genericDangerWarningDialogMessage, + ].join('\n\n')), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(l10n.applyButtonLabel), + ), + ], + ), + ); + if (confirmed == null || !confirmed) return; + } + + settings.forceTvLayout = v; + }, + title: title(context), + ); +} diff --git a/lib/widgets/settings/language/locale_selection_page.dart b/lib/widgets/settings/language/locale_selection_page.dart index b799bea65..7a8621a7b 100644 --- a/lib/widgets/settings/language/locale_selection_page.dart +++ b/lib/widgets/settings/language/locale_selection_page.dart @@ -30,8 +30,10 @@ class _LocaleSelectionPageState extends State { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.settingsLanguagePageTitle), ), body: SafeArea( @@ -41,10 +43,11 @@ class _LocaleSelectionPageState extends State { final upQuery = query.toUpperCase().trim(); return ListView( children: [ - QueryBar( - queryNotifier: _queryNotifier, - leadingPadding: const EdgeInsetsDirectional.only(start: 24, end: 8), - ), + if (!useTvLayout) + QueryBar( + queryNotifier: _queryNotifier, + leadingPadding: const EdgeInsetsDirectional.only(start: 24, end: 8), + ), ..._getLocaleOptions(context).entries.where((kv) { if (upQuery.isEmpty) return true; final title = kv.value; diff --git a/lib/widgets/settings/language/locales.dart b/lib/widgets/settings/language/locales.dart index d2bc3bab2..0fcc896de 100644 --- a/lib/widgets/settings/language/locales.dart +++ b/lib/widgets/settings/language/locales.dart @@ -3,6 +3,7 @@ class SupportedLocales { static const languagesByLanguageCode = { + 'cs': 'Čeština', 'de': 'Deutsch', 'el': 'Ελληνικά', 'en': 'English', @@ -15,6 +16,7 @@ class SupportedLocales { 'lt': 'Lietuvių', 'nb': 'Norsk (Bokmål)', 'nl': 'Nederlands', + 'pl': 'Polski', 'pt': 'Português (Brasil)', 'ro': 'Română', 'ru': 'Русский', diff --git a/lib/widgets/settings/navigation/drawer.dart b/lib/widgets/settings/navigation/drawer.dart index e0a65ef2d..e0df890b5 100644 --- a/lib/widgets/settings/navigation/drawer.dart +++ b/lib/widgets/settings/navigation/drawer.dart @@ -93,6 +93,7 @@ class _NavigationDrawerEditorPageState extends State length: tabs.length, child: Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsNavigationDrawerEditorPageTitle), bottom: TabBar( tabs: tabs.map((t) => t.item1).toList(), diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 4b756171f..2c738b136 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/home_page.dart'; import 'package:aves/model/settings/enums/screen_on.dart'; @@ -32,11 +31,11 @@ class NavigationSection extends SettingsSection { @override FutureOr> tiles(BuildContext context) => [ SettingsTileNavigationHomePage(), - if (!device.isTelevision) SettingsTileNavigationKeepScreenOn(), - if (!device.isTelevision) SettingsTileShowBottomNavigationBar(), - if (!device.isTelevision) SettingsTileNavigationDoubleBackExit(), + if (!settings.useTvLayout) SettingsTileNavigationKeepScreenOn(), + if (!settings.useTvLayout) SettingsTileShowBottomNavigationBar(), + if (!settings.useTvLayout) SettingsTileNavigationDoubleBackExit(), SettingsTileNavigationDrawer(), - if (!device.isTelevision) SettingsTileNavigationConfirmationDialog(), + if (!settings.useTvLayout) SettingsTileNavigationConfirmationDialog(), ]; } diff --git a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart index c8dc37ccb..c0786f6e3 100644 --- a/lib/widgets/settings/privacy/file_picker/file_picker_page.dart +++ b/lib/widgets/settings/privacy/file_picker/file_picker_page.dart @@ -150,11 +150,13 @@ class _FilePickerPageState extends State { return Drawer( child: ListView( children: [ - Padding( - padding: const EdgeInsets.all(16), - child: Text( - context.l10n.filePickerOpenFrom, - style: Theme.of(context).textTheme.headlineSmall, + SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + context.l10n.filePickerOpenFrom, + style: Theme.of(context).textTheme.headlineSmall, + ), ), ), ...volumes.map((v) { diff --git a/lib/widgets/settings/privacy/hidden_items_page.dart b/lib/widgets/settings/privacy/hidden_items_page.dart index bbc2ae0f2..9a58f892e 100644 --- a/lib/widgets/settings/privacy/hidden_items_page.dart +++ b/lib/widgets/settings/privacy/hidden_items_page.dart @@ -36,6 +36,7 @@ class HiddenItemsPage extends StatelessWidget { length: tabs.length, child: Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsHiddenItemsPageTitle), bottom: TabBar( tabs: tabs.map((t) => t.item1).toList(), @@ -88,14 +89,15 @@ class _HiddenFilters extends StatelessWidget { child: Wrap( spacing: 8, runSpacing: 8, - children: filterList - .map((filter) => AvesFilterChip( - filter: filter, - removable: true, - onTap: (filter) => settings.changeFilterVisibility({filter}, true), - onLongPress: null, - )) - .toList(), + children: filterList.map((filter) { + void onRemove(CollectionFilter filter) => settings.changeFilterVisibility({filter}, true); + return AvesFilterChip( + filter: filter, + onTap: onRemove, + onRemove: onRemove, + onLongPress: null, + ); + }).toList(), ), ), ], diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index 4cbe168fe..31f002afe 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -34,11 +34,11 @@ class PrivacySection extends SettingsSection { return [ SettingsTilePrivacyAllowInstalledAppAccess(), if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(), - if (!device.isTelevision && device.canRequestManageMedia) SettingsTilePrivacyManageMedia(), + if (!settings.useTvLayout && device.canRequestManageMedia) SettingsTilePrivacyManageMedia(), SettingsTilePrivacySaveSearchHistory(), - if (!device.isTelevision) SettingsTilePrivacyEnableBin(), + if (!settings.useTvLayout) SettingsTilePrivacyEnableBin(), SettingsTilePrivacyHiddenItems(), - if (!device.isTelevision && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(), + if (!settings.useTvLayout && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(), ]; } } diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index cf8a6c445..21f480fb4 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:aves/model/actions/settings_actions.dart'; -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; @@ -73,7 +73,7 @@ class _SettingsPageState extends State with FeedbackMixin { Widget build(BuildContext context) { final appBarTitle = Text(context.l10n.settingsPageTitle); - if (device.isTelevision) { + if (settings.useTvLayout) { return Scaffold( body: AvesPopScope( handlers: const [TvNavigationPopHandler.pop], @@ -86,10 +86,15 @@ class _SettingsPageState extends State with FeedbackMixin { child: Column( children: [ const SizedBox(height: 8), - AppBar( - automaticallyImplyLeading: false, - title: appBarTitle, - elevation: 0, + DirectionalSafeArea( + start: false, + bottom: false, + child: AppBar( + automaticallyImplyLeading: false, + title: appBarTitle, + elevation: 0, + primary: false, + ), ), Expanded( child: ValueListenableBuilder( @@ -106,6 +111,7 @@ class _SettingsPageState extends State with FeedbackMixin { .toList(), selectedIndex: selectedIndex, onDestinationSelected: (index) => _tvSelectedIndexNotifier.value = index, + minExtendedWidth: TvRail.minExtendedWidth, ); return LayoutBuilder( builder: (context, constraints) { @@ -118,8 +124,13 @@ class _SettingsPageState extends State with FeedbackMixin { ), ), Expanded( - child: _SettingsSectionBody( - loader: Future.value(sections[selectedIndex].tiles(context)), + child: MediaQuery.removePadding( + context: context, + removeLeft: !context.isRtl, + removeRight: context.isRtl, + child: _SettingsSectionBody( + loader: Future.value(sections[selectedIndex].tiles(context)), + ), ), ), ], diff --git a/lib/widgets/settings/thumbnails/overlay.dart b/lib/widgets/settings/thumbnails/overlay.dart index 928a72638..56ed0d055 100644 --- a/lib/widgets/settings/thumbnails/overlay.dart +++ b/lib/widgets/settings/thumbnails/overlay.dart @@ -1,9 +1,14 @@ +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/thumbnail_overlay_location_icon.dart'; +import 'package:aves/model/settings/enums/thumbnail_overlay_tag_icon.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -14,11 +19,12 @@ class ThumbnailOverlayPage extends StatelessWidget { @override Widget build(BuildContext context) { - final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); - final iconColor = context.select((v) => v.neutral); + final iconSize = _getIconSize(context); + final iconColor = _getIconColor(context); return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsThumbnailOverlayPageTitle), ), body: SafeArea( @@ -37,29 +43,6 @@ class ThumbnailOverlayPage extends StatelessWidget { ), ), ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailTag, - onChanged: (v) => settings.showThumbnailTag = v, - title: context.l10n.settingsThumbnailShowTagIcon, - trailing: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - TagIcon.scale) / 2), - child: Icon( - AIcons.tag, - size: iconSize * TagIcon.scale, - color: iconColor, - ), - ), - ), - SettingsSwitchListTile( - selector: (context, s) => s.showThumbnailLocation, - onChanged: (v) => settings.showThumbnailLocation = v, - title: context.l10n.settingsThumbnailShowLocationIcon, - trailing: Icon( - AIcons.location, - size: iconSize, - color: iconColor, - ), - ), SettingsSwitchListTile( selector: (context, s) => s.showThumbnailMotionPhoto, onChanged: (v) => settings.showThumbnailMotionPhoto = v, @@ -98,9 +81,86 @@ class ThumbnailOverlayPage extends StatelessWidget { onChanged: (v) => settings.showThumbnailVideoDuration = v, title: context.l10n.settingsThumbnailShowVideoDuration, ), + SettingsTileThumbnailLocationIcon().build(context), + SettingsTileThumbnailTagIcon().build(context), ], ), ), ); } + + static double _getIconSize(BuildContext context) => IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); + + static Color _getIconColor(BuildContext context) => context.select((v) => v.neutral); + + static Widget buildTrailingIcon({ + required BuildContext context, + required Object key, + required IconData icon, + required bool disabled, + }) { + return Padding( + // Switch width (`_kSwitchWidth`) + tile content padding + padding: const EdgeInsetsDirectional.only(end: 59 + 16), + child: AnimatedSwitcher( + duration: context.read().iconAnimation, + child: Icon( + icon, + key: ValueKey(key), + size: _getIconSize(context), + color: _getIconColor(context).withOpacity(disabled ? SettingsSwitchListTile.disabledOpacity : 1.0), + ), + ), + ); + } +} + +class SettingsTileThumbnailLocationIcon extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThumbnailShowLocationIcon; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: ThumbnailOverlayLocationIcon.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.thumbnailLocationIcon, + onSelection: (v) => settings.thumbnailLocationIcon = v, + tileTitle: title(context), + trailingBuilder: _buildTrailing, + ); + + Widget _buildTrailing(BuildContext context) { + final iconType = context.select((s) => s.thumbnailLocationIcon); + return ThumbnailOverlayPage.buildTrailingIcon( + context: context, + key: iconType, + icon: iconType.getIcon(context), + disabled: iconType == ThumbnailOverlayLocationIcon.none, + ); + } +} + +class SettingsTileThumbnailTagIcon extends SettingsTile { + @override + String title(BuildContext context) => context.l10n.settingsThumbnailShowTagIcon; + + @override + Widget build(BuildContext context) => SettingsSelectionListTile( + values: ThumbnailOverlayTagIcon.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.thumbnailTagIcon, + onSelection: (v) => settings.thumbnailTagIcon = v, + tileTitle: title(context), + trailingBuilder: _buildTrailing, + ); + + Widget _buildTrailing(BuildContext context) { + final iconType = context.select((s) => s.thumbnailTagIcon); + return ThumbnailOverlayPage.buildTrailingIcon( + context: context, + key: iconType, + icon: iconType.getIcon(context), + disabled: iconType == ThumbnailOverlayTagIcon.none, + ); + } } diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index 351a78052..2bf00f4e2 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -1,4 +1,4 @@ -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -25,7 +25,7 @@ class ThumbnailsSection extends SettingsSection { @override List tiles(BuildContext context) => [ - if (!device.isTelevision) SettingsTileCollectionQuickActions(), + if (!settings.useTvLayout) SettingsTileCollectionQuickActions(), SettingsTileThumbnailOverlay(), ]; } diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index 66c597032..14fb00feb 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -18,6 +18,7 @@ class SubtitleThemePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsSubtitleThemePageTitle), ), body: SafeArea( diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index cc6387cf1..d7c3b978e 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/video_auto_play_mode.dart'; @@ -43,7 +42,7 @@ class VideoSection extends SettingsSection { SettingsTileVideoEnableHardwareAcceleration(), SettingsTileVideoEnableAutoPlay(), SettingsTileVideoLoopMode(), - if (!device.isTelevision) SettingsTileVideoControls(), + if (!settings.useTvLayout) SettingsTileVideoControls(), SettingsTileVideoSubtitleTheme(), ]; } diff --git a/lib/widgets/settings/video/video_settings_page.dart b/lib/widgets/settings/video/video_settings_page.dart index 21187d01b..f01b7bf85 100644 --- a/lib/widgets/settings/video/video_settings_page.dart +++ b/lib/widgets/settings/video/video_settings_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/settings_definition.dart'; import 'package:aves/widgets/settings/video/video.dart'; @@ -20,6 +21,7 @@ class _VideoSettingsPageState extends State { final theme = Theme.of(context); return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(context.l10n.settingsVideoPageTitle), ), body: Theme( diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index dab35532b..acfd88678 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -1,4 +1,3 @@ -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/settings/common/tiles.dart'; @@ -13,14 +12,16 @@ class ViewerOverlayPage extends StatelessWidget { @override Widget build(BuildContext context) { + final useTvLayout = settings.useTvLayout; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !useTvLayout, title: Text(context.l10n.settingsViewerOverlayPageTitle), ), body: SafeArea( child: ListView( children: [ - if (!device.isTelevision) + if (!useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayOnOpening, onChanged: (v) => settings.showOverlayOnOpening = v, @@ -56,13 +57,25 @@ class ViewerOverlayPage extends StatelessWidget { ); }, ), - if (!device.isTelevision) + Selector>( + selector: (context, s) => Tuple2(s.showOverlayInfo, s.showOverlayDescription), + builder: (context, s, child) { + final showInfo = s.item1; + final current = s.item2; + return SwitchListTile( + value: current, + onChanged: showInfo ? (v) => settings.showOverlayDescription = v : null, + title: Text(context.l10n.settingsViewerShowDescription), + ); + }, + ), + if (!useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayMinimap, onChanged: (v) => settings.showOverlayMinimap = v, title: context.l10n.settingsViewerShowMinimap, ), - if (!device.isTelevision) + if (!useTvLayout) SettingsSwitchListTile( selector: (context, s) => s.showOverlayThumbnailPreview, onChanged: (v) => settings.showOverlayThumbnailPreview = v, diff --git a/lib/widgets/settings/viewer/slideshow.dart b/lib/widgets/settings/viewer/slideshow.dart index 4849bf88e..208bca58d 100644 --- a/lib/widgets/settings/viewer/slideshow.dart +++ b/lib/widgets/settings/viewer/slideshow.dart @@ -16,6 +16,7 @@ class ViewerSlideshowPage extends StatelessWidget { final l10n = context.l10n; return Scaffold( appBar: AppBar( + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.settingsViewerSlideshowPageTitle), ), body: SafeArea( diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 74bd92888..1f9707568 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; @@ -32,14 +31,14 @@ class ViewerSection extends SettingsSection { @override FutureOr> tiles(BuildContext context) async { - final canSetCutoutMode = await windowService.canSetCutoutMode(); + final isCutoutAware = await windowService.isCutoutAware(); return [ - if (!device.isTelevision) SettingsTileViewerQuickActions(), + if (!settings.useTvLayout) SettingsTileViewerQuickActions(), SettingsTileViewerOverlay(), SettingsTileViewerSlideshow(), - if (!device.isTelevision) SettingsTileViewerGestureSideTapNext(), - if (!device.isTelevision && canSetCutoutMode) SettingsTileViewerCutoutMode(), - if (!device.isTelevision) SettingsTileViewerMaxBrightness(), + if (!settings.useTvLayout) SettingsTileViewerGestureSideTapNext(), + if (!settings.useTvLayout && isCutoutAware) SettingsTileViewerUseCutout(), + if (!settings.useTvLayout) SettingsTileViewerMaxBrightness(), SettingsTileViewerMotionPhotoAutoPlay(), SettingsTileViewerImageBackground(), ]; @@ -94,7 +93,7 @@ class SettingsTileViewerGestureSideTapNext extends SettingsTile { ); } -class SettingsTileViewerCutoutMode extends SettingsTile { +class SettingsTileViewerUseCutout extends SettingsTile { @override String title(BuildContext context) => context.l10n.settingsViewerUseCutout; diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index d94020345..145d54c79 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -2,9 +2,9 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/stats/percent_text.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; @@ -36,7 +36,6 @@ class FilterTable extends StatelessWidget { Widget build(BuildContext context) { final locale = context.l10n.localeName; final numberFormat = NumberFormat.decimalPattern(locale); - final percentFormat = NumberFormat.percentPattern(); final animate = context.select((v) => v.accessibilityAnimations.animate); final sortedEntries = entryCountMap.entries.toList(); @@ -91,12 +90,7 @@ class FilterTable extends StatelessWidget { animation: animate, isRTL: isRtl, barRadius: barRadius, - center: Text( - percentFormat.format(percent), - style: TextStyle( - shadows: theme.brightness == Brightness.dark ? Constants.embossShadows : null, - ), - ), + center: LinearPercentIndicatorText(percent: percent), padding: EdgeInsets.symmetric(horizontal: lineHeight), ); }, diff --git a/lib/widgets/stats/mime_donut.dart b/lib/widgets/stats/mime_donut.dart index 76c633256..f7739612e 100644 --- a/lib/widgets/stats/mime_donut.dart +++ b/lib/widgets/stats/mime_donut.dart @@ -111,35 +111,45 @@ class _MimeDonutState extends State with AutomaticKeepAliveClientMixi ], ), ); + final primaryColor = Theme.of(context).colorScheme.primary; final legend = SizedBox( width: dim, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: seriesData - .map((d) => GestureDetector( - onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(AIcons.disc, color: d.color), - const SizedBox(width: 8), - Flexible( - child: Text( - d.displayText, - overflow: TextOverflow.fade, - softWrap: false, - maxLines: 1, + .map((d) => Material( + type: MaterialType.transparency, + child: InkResponse( + onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), + containedInkWell: true, + highlightShape: BoxShape.rectangle, + borderRadius: const BorderRadius.all(Radius.circular(123)), + hoverColor: primaryColor.withOpacity(0.04), + splashColor: primaryColor.withOpacity(0.12), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(AIcons.disc, color: d.color), + const SizedBox(width: 8), + Flexible( + child: Text( + d.displayText, + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1, + ), ), - ), - const SizedBox(width: 8), - Text( - numberFormat.format(d.entryCount), - style: TextStyle( - color: Theme.of(context).textTheme.bodySmall!.color, + const SizedBox(width: 8), + Text( + numberFormat.format(d.entryCount), + style: TextStyle( + color: Theme.of(context).textTheme.bodySmall!.color, + ), ), - ), - ], + const SizedBox(width: 4), + ], + ), ), )) .toList(), diff --git a/lib/widgets/stats/percent_text.dart b/lib/widgets/stats/percent_text.dart new file mode 100644 index 000000000..44681e02e --- /dev/null +++ b/lib/widgets/stats/percent_text.dart @@ -0,0 +1,30 @@ +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/basic/text/outlined.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +class LinearPercentIndicatorText extends StatelessWidget { + final double percent; + final percentFormat = NumberFormat.percentPattern(); + + LinearPercentIndicatorText({ + super.key, + required this.percent, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return OutlinedText( + textSpans: [ + TextSpan( + text: percentFormat.format(percent), + style: TextStyle( + shadows: theme.brightness == Brightness.dark ? Constants.embossShadows : null, + ), + ) + ], + outlineColor: theme.scaffoldBackgroundColor, + ); + } +} diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index f22187cee..1edaad08a 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; @@ -24,11 +23,11 @@ import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/stats/date/histogram.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:aves/widgets/stats/mime_donut.dart'; +import 'package:aves/widgets/stats/percent_text.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:intl/intl.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; import 'package:provider/provider.dart'; @@ -106,7 +105,6 @@ class _StatsPageState extends State { if (!animating) { final durations = context.watch(); - final percentFormat = NumberFormat.percentPattern(); if (entries.isEmpty) { child = EmptyContent( @@ -115,7 +113,6 @@ class _StatsPageState extends State { ); } else { final theme = Theme.of(context); - final isDark = theme.brightness == Brightness.dark; final chartAnimationDuration = context.read().chartTransition; final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); @@ -164,12 +161,7 @@ class _StatsPageState extends State { animation: context.select((v) => v.accessibilityAnimations.animate), isRTL: context.isRtl, barRadius: barRadius, - center: Text( - percentFormat.format(withGpsPercent), - style: TextStyle( - shadows: isDark ? Constants.embossShadows : null, - ), - ), + center: LinearPercentIndicatorText(percent: withGpsPercent), padding: EdgeInsets.symmetric(horizontal: lineHeight), ), ), @@ -226,7 +218,7 @@ class _StatsPageState extends State { return Scaffold( appBar: AppBar( - automaticallyImplyLeading: !device.isTelevision, + automaticallyImplyLeading: !settings.useTvLayout, title: Text(l10n.statsPageTitle), ), body: GestureAreaProtectorStack( @@ -257,43 +249,70 @@ class _StatsPageState extends State { final totalEntryCount = entries.length; final hasMore = maxRowCount != null && entryCountMap.length > maxRowCount; - return [ - Padding( + final onHeaderPressed = hasMore + ? () => Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: StatsTopPage.routeName), + builder: (context) => StatsTopPage( + title: title, + tableBuilder: (context) => FilterTable( + totalEntryCount: totalEntryCount, + entryCountMap: entryCountMap, + filterBuilder: filterBuilder, + sortByCount: sortByCount, + maxRowCount: null, + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + onFilterSelection: (filter) => _onFilterSelection(context, filter), + ), + ), + ) + : null; + Widget header = Text( + title, + style: Constants.knownTitleTextStyle, + ); + if (settings.useTvLayout) { + final primaryColor = Theme.of(context).colorScheme.primary; + header = Container( + padding: const EdgeInsets.symmetric(vertical: 12), + alignment: AlignmentDirectional.centerStart, + child: Material( + type: MaterialType.transparency, + child: InkResponse( + onTap: onHeaderPressed, + containedInkWell: true, + highlightShape: BoxShape.rectangle, + borderRadius: const BorderRadius.all(Radius.circular(123)), + hoverColor: primaryColor.withOpacity(0.04), + splashColor: primaryColor.withOpacity(0.12), + child: Padding( + padding: const EdgeInsets.all(16), + child: header, + ), + ), + ), + ); + } else { + header = Padding( padding: const EdgeInsets.all(16), child: Row( children: [ - Text( - title, - style: Constants.knownTitleTextStyle, - ), + header, const Spacer(), IconButton( icon: const Icon(AIcons.next), - onPressed: hasMore - ? () => Navigator.push( - context, - MaterialPageRoute( - settings: const RouteSettings(name: StatsTopPage.routeName), - builder: (context) => StatsTopPage( - title: title, - tableBuilder: (context) => FilterTable( - totalEntryCount: totalEntryCount, - entryCountMap: entryCountMap, - filterBuilder: filterBuilder, - sortByCount: sortByCount, - maxRowCount: null, - onFilterSelection: (filter) => _onFilterSelection(context, filter), - ), - onFilterSelection: (filter) => _onFilterSelection(context, filter), - ), - ), - ) - : null, + onPressed: onHeaderPressed, tooltip: MaterialLocalizations.of(context).moreButtonTooltip, ), ], ), - ), + ); + } + + return [ + header, FilterTable( totalEntryCount: totalEntryCount, entryCountMap: entryCountMap, @@ -356,7 +375,7 @@ class StatsTopPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - automaticallyImplyLeading: !device.isTelevision, + automaticallyImplyLeading: !settings.useTvLayout, title: Text(title), ), body: GestureAreaProtectorStack( diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 9f05f2790..61577aa3b 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -67,7 +67,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix } } else { final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; - final canWrite = !device.isReadOnly; + final canWrite = !settings.isReadOnly; switch (action) { case EntryAction.toggleFavourite: return collection != null; @@ -87,13 +87,13 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.print: return device.canPrint && !targetEntry.isVideo; case EntryAction.openMap: - return targetEntry.hasGps; + return !settings.useTvLayout && targetEntry.hasGps; case EntryAction.viewSource: return targetEntry.isSvg; case EntryAction.videoCaptureFrame: return canWrite && targetEntry.isVideo; case EntryAction.videoToggleMute: - return !device.isTelevision && targetEntry.isVideo; + return !settings.useTvLayout && targetEntry.isVideo; case EntryAction.videoSelectStreams: case EntryAction.videoSetSpeed: case EntryAction.videoSettings: @@ -103,15 +103,15 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.openVideo: return targetEntry.isVideo; case EntryAction.rotateScreen: - return !device.isTelevision && settings.isRotationLocked; + return !settings.useTvLayout && settings.isRotationLocked; case EntryAction.addShortcut: return device.canPinShortcut; case EntryAction.edit: return canWrite; case EntryAction.copyToClipboard: - return !device.isTelevision; - case EntryAction.info: case EntryAction.open: + return !settings.useTvLayout; + case EntryAction.info: case EntryAction.setAs: case EntryAction.share: return true; diff --git a/lib/widgets/viewer/action/entry_info_action_delegate.dart b/lib/widgets/viewer/action/entry_info_action_delegate.dart index 7abc986a9..48ed09938 100644 --- a/lib/widgets/viewer/action/entry_info_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_info_action_delegate.dart @@ -3,12 +3,12 @@ import 'dart:convert'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/events.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_info.dart'; import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/geotiff.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; @@ -30,7 +30,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi Stream> get eventStream => _eventStreamController.stream; bool isVisible(AvesEntry targetEntry, EntryAction action) { - final canWrite = !device.isReadOnly; + final canWrite = !settings.isReadOnly; switch (action) { // general case EntryAction.editDate: @@ -229,21 +229,16 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi Future _convertMotionPhotoToStillImage(BuildContext context, AvesEntry targetEntry) async { final confirmed = await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.genericDangerWarningDialogMessage), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).cancelButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.applyButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(context.l10n.genericDangerWarningDialogMessage), + actions: [ + const CancelButton(), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.applyButtonLabel), + ), + ], + ), ); if (confirmed == null || !confirmed) return; diff --git a/lib/widgets/viewer/embedded/embedded_data_opener.dart b/lib/widgets/viewer/embedded/embedded_data_opener.dart index 288a9c256..a95a21a1a 100644 --- a/lib/widgets/viewer/embedded/embedded_data_opener.dart +++ b/lib/widgets/viewer/embedded/embedded_data_opener.dart @@ -35,6 +35,9 @@ class EmbeddedDataOpener extends StatelessWidget with FeedbackMixin { Future _openEmbeddedData(BuildContext context, OpenEmbeddedDataNotification notification) async { late Map fields; switch (notification.source) { + case EmbeddedDataSource.googleDevice: + fields = await embeddedDataService.extractGoogleDeviceItem(entry, notification.dataUri!); + break; case EmbeddedDataSource.motionPhotoVideo: fields = await embeddedDataService.extractMotionPhotoVideo(entry); break; diff --git a/lib/widgets/viewer/embedded/notifications.dart b/lib/widgets/viewer/embedded/notifications.dart index cc1581d35..a857b9aa3 100644 --- a/lib/widgets/viewer/embedded/notifications.dart +++ b/lib/widgets/viewer/embedded/notifications.dart @@ -1,20 +1,29 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -enum EmbeddedDataSource { motionPhotoVideo, videoCover, xmp } +enum EmbeddedDataSource { googleDevice, motionPhotoVideo, videoCover, xmp } @immutable class OpenEmbeddedDataNotification extends Notification { final EmbeddedDataSource source; final List? props; - final String? mimeType; + final String? mimeType, dataUri; const OpenEmbeddedDataNotification._private({ required this.source, this.props, this.mimeType, + this.dataUri, }); + factory OpenEmbeddedDataNotification.googleDevice({ + required String dataUri, + }) => + OpenEmbeddedDataNotification._private( + source: EmbeddedDataSource.googleDevice, + dataUri: dataUri, + ); + factory OpenEmbeddedDataNotification.motionPhotoVideo() => const OpenEmbeddedDataNotification._private( source: EmbeddedDataSource.motionPhotoVideo, ); @@ -34,5 +43,5 @@ class OpenEmbeddedDataNotification extends Notification { ); @override - String toString() => '$runtimeType#${shortHash(this)}{source=$source, props=$props, mimeType=$mimeType}'; + String toString() => '$runtimeType#${shortHash(this)}{source=$source, props=$props, mimeType=$mimeType, dataUri=$dataUri}'; } diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 206d3750b..76c11f35a 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -4,7 +4,6 @@ import 'dart:ui'; import 'package:aves/app_mode.dart'; import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -181,12 +180,12 @@ class _ViewerVerticalPageViewState extends State { } Widget _buildImagePage() { - final isTelevision = device.isTelevision; + final useTvLayout = settings.useTvLayout; Widget? child; Map? shortcuts = { - const SingleActivator(LogicalKeyboardKey.arrowUp): isTelevision ? const TvShowLessInfoIntent() : const _LeaveIntent(), - const SingleActivator(LogicalKeyboardKey.arrowDown): isTelevision ? const _TvShowMoreInfoIntent() : const _ShowInfoIntent(), + const SingleActivator(LogicalKeyboardKey.arrowUp): useTvLayout ? const TvShowLessInfoIntent() : const _LeaveIntent(), + const SingleActivator(LogicalKeyboardKey.arrowDown): useTvLayout ? const _TvShowMoreInfoIntent() : const _ShowInfoIntent(), const SingleActivator(LogicalKeyboardKey.mediaPause): const _PlayPauseIntent.pause(), const SingleActivator(LogicalKeyboardKey.mediaPlay): const _PlayPauseIntent.play(), const SingleActivator(LogicalKeyboardKey.mediaPlayPause): const _PlayPauseIntent.toggle(), @@ -211,7 +210,7 @@ class _ViewerVerticalPageViewState extends State { ); } if (child != null) { - if (device.isTelevision) { + if (settings.useTvLayout) { child = ValueListenableBuilder( valueListenable: _isImageFocusedNotifier, builder: (context, isImageFocused, child) { @@ -238,7 +237,7 @@ class _ViewerVerticalPageViewState extends State { _TvShowMoreInfoIntent: CallbackAction(onInvoke: (intent) => TvShowMoreInfoNotification().dispatch(context)), _PlayPauseIntent: CallbackAction<_PlayPauseIntent>(onInvoke: (intent) => _onPlayPauseIntent(intent, entry)), ActivateIntent: CallbackAction(onInvoke: (intent) { - if (isTelevision) { + if (useTvLayout) { final _entry = entry; if (_entry != null && _entry.isVideo) { // address `TV-PC` requirement from https://developer.android.com/docs/quality-guidelines/tv-app-quality diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 1a1eea9b9..a58dc6315 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -1,4 +1,3 @@ -import 'package:aves/app_mode.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/viewer/controller.dart'; @@ -96,9 +95,7 @@ class VideoConductorProvider extends StatelessWidget { @override Widget build(BuildContext context) { return Provider( - create: (context) => VideoConductor( - persistPlayback: context.read>().value == AppMode.main, - ), + create: (context) => VideoConductor(), dispose: (context, value) => value.dispose(), child: child, ); diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index b74579cdf..5dc2c3d74 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -95,9 +95,6 @@ class _EntryViewerStackState extends State with EntryViewContr @override void initState() { super.initState(); - if (!settings.viewerUseCutout) { - windowService.setCutoutMode(false); - } if (settings.viewerMaxBrightness) { ScreenBrightness().setScreenBrightness(1); } @@ -205,88 +202,35 @@ class _EntryViewerStackState extends State with EntryViewContr child: ValueListenableProvider.value( value: _heroInfoNotifier, child: NotificationListener( - onNotification: (dynamic notification) { - if (notification is FilterSelectedNotification) { - _goToCollection(notification.filter); - } else if (notification is EntryDeletedNotification) { - _onEntryRemoved(context, notification.entries); - } else if (notification is EntryMovedNotification) { - // only add or remove entries following user actions, - // instead of applying all collection source changes - final isBin = collection?.filters.contains(TrashFilter.instance) ?? false; - final entries = notification.entries; - switch (notification.moveType) { - case MoveType.move: - _onEntryRemoved(context, entries); - break; - case MoveType.toBin: - if (!isBin) { - _onEntryRemoved(context, entries); - } - break; - case MoveType.fromBin: - if (isBin) { - _onEntryRemoved(context, entries); - } else { - _onEntryRestored(entries); - } - break; - case MoveType.copy: - case MoveType.export: - break; - } - } else if (notification is ToggleOverlayNotification) { - _overlayVisible.value = notification.visible ?? !_overlayVisible.value; - } else if (notification is TvShowLessInfoNotification) { - if (_overlayVisible.value) { - _overlayVisible.value = false; - } else { - _onWillPop(); - } - } else if (notification is TvShowMoreInfoNotification) { - if (!_overlayVisible.value) { - _overlayVisible.value = true; - } - } else if (notification is ShowInfoPageNotification) { - _goToVerticalPage(infoPage); - } else if (notification is JumpToPreviousEntryNotification) { - _jumpToHorizontalPageByDelta(-1); - } else if (notification is JumpToNextEntryNotification) { - _jumpToHorizontalPageByDelta(1); - } else if (notification is JumpToEntryNotification) { - _jumpToHorizontalPageByIndex(notification.index); - } else if (notification is VideoActionNotification) { - final controller = notification.controller; - final action = notification.action; - _onVideoAction(context, controller, action); - } else { - return false; - } - return true; - }, - child: Stack( - children: [ - ViewerVerticalPageView( - collection: collection, - entryNotifier: entryNotifier, - viewerController: viewerController, - overlayOpacity: _overlayInitialized - ? _overlayOpacity - : settings.showOverlayOnOpening - ? kAlwaysCompleteAnimation - : kAlwaysDismissedAnimation, - verticalPager: _verticalPager, - horizontalPager: _horizontalPager, - onVerticalPageChanged: _onVerticalPageChanged, - onHorizontalPageChanged: _onHorizontalPageChanged, - onImagePageRequested: () => _goToVerticalPage(imagePage), - onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), - ), - ..._buildOverlays().map(_decorateOverlay), - const TopGestureAreaProtector(), - const SideGestureAreaProtector(), - const BottomGestureAreaProtector(), - ], + onNotification: _handleNotification, + child: LayoutBuilder( + builder: (context, constraints) { + final availableSize = Size(constraints.maxWidth, constraints.maxHeight); + return Stack( + children: [ + ViewerVerticalPageView( + collection: collection, + entryNotifier: entryNotifier, + viewerController: viewerController, + overlayOpacity: _overlayInitialized + ? _overlayOpacity + : settings.showOverlayOnOpening + ? kAlwaysCompleteAnimation + : kAlwaysDismissedAnimation, + verticalPager: _verticalPager, + horizontalPager: _horizontalPager, + onVerticalPageChanged: _onVerticalPageChanged, + onHorizontalPageChanged: _onHorizontalPageChanged, + onImagePageRequested: () => _goToVerticalPage(imagePage), + onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry), + ), + ..._buildOverlays(availableSize).map(_decorateOverlay), + const TopGestureAreaProtector(), + const SideGestureAreaProtector(), + const BottomGestureAreaProtector(), + ], + ); + }, ), ), ), @@ -306,46 +250,41 @@ class _EntryViewerStackState extends State with EntryViewContr ); } - List _buildOverlays() { + List _buildOverlays(Size availableSize) { final appMode = context.read>().value; switch (appMode) { case AppMode.screenSaver: return []; case AppMode.slideshow: return [ - _buildSlideshowBottomOverlay(), + _buildSlideshowBottomOverlay(availableSize), ]; default: return [ - _buildViewerTopOverlay(), - _buildViewerBottomOverlay(), + _buildViewerTopOverlay(availableSize), + _buildViewerBottomOverlay(availableSize), ]; } } - Widget _buildSlideshowBottomOverlay() { - return Selector( - selector: (context, mq) => mq.size, - builder: (context, mqSize, child) { - return SizedBox.fromSize( - size: mqSize, - child: Align( - alignment: AlignmentDirectional.bottomEnd, - child: TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: SlideshowButtons( - animationController: _overlayAnimationController, - ), - ), + Widget _buildSlideshowBottomOverlay(Size availableSize) { + return SizedBox.fromSize( + size: availableSize, + child: Align( + alignment: AlignmentDirectional.bottomEnd, + child: TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, ), - ); - }, + child: SlideshowButtons( + animationController: _overlayAnimationController, + ), + ), + ), ); } - Widget _buildViewerTopOverlay() { + Widget _buildViewerTopOverlay(Size availableSize) { Widget child = ValueListenableBuilder( valueListenable: entryNotifier, builder: (context, mainEntry, child) { @@ -359,6 +298,7 @@ class _EntryViewerStackState extends State with EntryViewContr hasCollection: hasCollection, mainEntry: mainEntry, scale: _overlayButtonScale, + availableSize: availableSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, ), @@ -380,7 +320,7 @@ class _EntryViewerStackState extends State with EntryViewContr return child; } - Widget _buildViewerBottomOverlay() { + Widget _buildViewerBottomOverlay(Size availableSize) { Widget child = ValueListenableBuilder( valueListenable: entryNotifier, builder: (context, mainEntry, child) { @@ -447,6 +387,7 @@ class _EntryViewerStackState extends State with EntryViewContr index: _currentEntryIndex, collection: collection, animationController: _overlayAnimationController, + availableSize: availableSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, multiPageController: multiPageController, @@ -466,7 +407,7 @@ class _EntryViewerStackState extends State with EntryViewContr return AnimatedBuilder( animation: _verticalScrollNotifier, builder: (context, child) => Positioned( - bottom: (_verticalPager.hasClients && _verticalPager.position.hasPixels ? _verticalPager.offset : 0) - mqHeight, + bottom: (_verticalPager.hasClients && _verticalPager.position.hasPixels ? _verticalPager.offset : 0) - availableSize.height, child: child!, ), child: child, @@ -478,6 +419,66 @@ class _EntryViewerStackState extends State with EntryViewContr return child; } + bool _handleNotification(dynamic notification) { + if (notification is FilterSelectedNotification) { + _goToCollection(notification.filter); + } else if (notification is EntryDeletedNotification) { + _onEntryRemoved(context, notification.entries); + } else if (notification is EntryMovedNotification) { + // only add or remove entries following user actions, + // instead of applying all collection source changes + final isBin = collection?.filters.contains(TrashFilter.instance) ?? false; + final entries = notification.entries; + switch (notification.moveType) { + case MoveType.move: + _onEntryRemoved(context, entries); + break; + case MoveType.toBin: + if (!isBin) { + _onEntryRemoved(context, entries); + } + break; + case MoveType.fromBin: + if (isBin) { + _onEntryRemoved(context, entries); + } else { + _onEntryRestored(entries); + } + break; + case MoveType.copy: + case MoveType.export: + break; + } + } else if (notification is ToggleOverlayNotification) { + _overlayVisible.value = notification.visible ?? !_overlayVisible.value; + } else if (notification is TvShowLessInfoNotification) { + if (_overlayVisible.value) { + _overlayVisible.value = false; + } else { + _onWillPop(); + } + } else if (notification is TvShowMoreInfoNotification) { + if (!_overlayVisible.value) { + _overlayVisible.value = true; + } + } else if (notification is ShowInfoPageNotification) { + _goToVerticalPage(infoPage); + } else if (notification is JumpToPreviousEntryNotification) { + _jumpToHorizontalPageByDelta(-1); + } else if (notification is JumpToNextEntryNotification) { + _jumpToHorizontalPageByDelta(1); + } else if (notification is JumpToEntryNotification) { + _jumpToHorizontalPageByIndex(notification.index); + } else if (notification is VideoActionNotification) { + final controller = notification.controller; + final action = notification.action; + _onVideoAction(context, controller, action); + } else { + return false; + } + return true; + } + Future _onVideoAction(BuildContext context, AvesVideoController controller, EntryAction action) async { await _videoActionDelegate.onActionSelected(context, controller, action); if (action == EntryAction.videoToggleMute) { @@ -603,8 +604,7 @@ class _EntryViewerStackState extends State with EntryViewContr if (Navigator.canPop(context)) { Navigator.pop(context); } else { - // leave viewer - SystemNavigator.pop(); + _leaveViewer(); } } @@ -653,11 +653,18 @@ class _EntryViewerStackState extends State with EntryViewContr pop(); } } else { - // exit app when trying to pop a viewer page for a single entry - SystemNavigator.pop(); + // exit app when trying to pop a viewer page + _leaveViewer(); } } + Future _leaveViewer() async { + // widgets do not get disposed normally when popping the `SystemNavigator` + // so we manually clean video controllers and save playback state + await context.read().dispose(); + await SystemNavigator.pop(); + } + // track item when returning to collection, // if they are not fully visible already void _trackEntry() { @@ -673,19 +680,18 @@ class _EntryViewerStackState extends State with EntryViewContr } Future _onLeave() async { - if (!settings.viewerUseCutout) { - await windowService.setCutoutMode(true); - } if (settings.viewerMaxBrightness) { await ScreenBrightness().resetScreenBrightness(); } if (settings.keepScreenOn == KeepScreenOn.viewerOnly) { await windowService.keepScreenOn(false); } - + await mediaSessionService.release(); await AvesApp.showSystemUI(); AvesApp.setSystemUIStyle(context); - await windowService.requestOrientation(); + if (!settings.useTvLayout) { + await windowService.requestOrientation(); + } } // overlay diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index 578a3439d..8f73ced5e 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -91,7 +91,7 @@ class _InfoRowGroupState extends State { @override Widget build(BuildContext context) { - if (keyValues.isEmpty) return const SizedBox.shrink(); + if (keyValues.isEmpty) return const SizedBox(); final _keyStyle = InfoRowGroup.keyStyle(context); diff --git a/lib/widgets/viewer/info/info_app_bar.dart b/lib/widgets/viewer/info/info_app_bar.dart index ac1be650e..03ccc2f30 100644 --- a/lib/widgets/viewer/info/info_app_bar.dart +++ b/lib/widgets/viewer/info/info_app_bar.dart @@ -1,6 +1,6 @@ import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -35,9 +35,9 @@ class InfoAppBar extends StatelessWidget { Widget build(BuildContext context) { final commonActions = EntryActions.commonMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v)); - + final useTvLayout = settings.useTvLayout; return SliverAppBar( - leading: device.isTelevision + leading: useTvLayout ? null : IconButton( // key is expected by test driver @@ -53,34 +53,36 @@ class InfoAppBar extends StatelessWidget { child: Text(context.l10n.viewerInfoPageTitle), ), ), - actions: [ - IconButton( - icon: const Icon(AIcons.search), - onPressed: () => _goToSearch(context), - tooltip: MaterialLocalizations.of(context).searchFieldLabel, - ), - if (entry.canEdit) - MenuIconTheme( - child: PopupMenuButton( - itemBuilder: (context) => [ - ...commonActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), - if (formatSpecificActions.isNotEmpty) ...[ - const PopupMenuDivider(), - ...formatSpecificActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), - ], - if (!kReleaseMode) ...[ - const PopupMenuDivider(), - _toMenuItem(context, EntryAction.debug, enabled: true), - ] - ], - onSelected: (action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - actionDelegate.onActionSelected(context, entry, collection, action); - }, - ), - ), - ], + actions: useTvLayout + ? [] + : [ + IconButton( + icon: const Icon(AIcons.search), + onPressed: () => _goToSearch(context), + tooltip: MaterialLocalizations.of(context).searchFieldLabel, + ), + if (entry.canEdit) + MenuIconTheme( + child: PopupMenuButton( + itemBuilder: (context) => [ + ...commonActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), + if (formatSpecificActions.isNotEmpty) ...[ + const PopupMenuDivider(), + ...formatSpecificActions.map((action) => _toMenuItem(context, action, enabled: actionDelegate.canApply(entry, action))), + ], + if (!kReleaseMode) ...[ + const PopupMenuDivider(), + _toMenuItem(context, EntryAction.debug, enabled: true), + ] + ], + onSelected: (action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + actionDelegate.onActionSelected(context, entry, collection, action); + }, + ), + ), + ], floating: true, ); } diff --git a/lib/widgets/viewer/info/metadata/xmp_card.dart b/lib/widgets/viewer/info/metadata/xmp_card.dart index c4d00128c..840a46983 100644 --- a/lib/widgets/viewer/info/metadata/xmp_card.dart +++ b/lib/widgets/viewer/info/metadata/xmp_card.dart @@ -58,7 +58,7 @@ class _XmpCardState extends State { @override void didUpdateWidget(covariant XmpCard oldWidget) { super.didUpdateWidget(oldWidget); - if (_indexNotifier.value >= indexedStructCount) { + if (isIndexed && _indexNotifier.value >= indexedStructCount) { _indexNotifier.value = indexedStructCount - 1; } } diff --git a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart index 06639ab7d..53833c81b 100644 --- a/lib/widgets/viewer/info/metadata/xmp_ns/google.dart +++ b/lib/widgets/viewer/info/metadata/xmp_ns/google.dart @@ -70,7 +70,24 @@ class XmpGDepthNamespace extends XmpGoogleNamespace { } class XmpGDeviceNamespace extends XmpNamespace { - XmpGDeviceNamespace(String nsPrefix, Map rawProps) : super(Namespaces.gDevice, nsPrefix, rawProps); + XmpGDeviceNamespace(String nsPrefix, Map rawProps) : super(Namespaces.gDevice, nsPrefix, rawProps) { + final mimePattern = RegExp(nsPrefix + r'Container/Container:Directory\[(\d+)\]/Item:Mime'); + final originalProps = rawProps.entries.toList(); + originalProps.forEach((kv) { + final path = kv.key; + final match = mimePattern.firstMatch(path); + if (match != null) { + final indexString = match.group(1); + if (indexString != null) { + final index = int.tryParse(indexString); + if (index != null) { + final dataPath = '${nsPrefix}Container/Container:Directory[$index]/Item:Data'; + rawProps[dataPath] = '[skipped]'; + } + } + } + }); + } @override late final List cards = [ @@ -82,7 +99,23 @@ class XmpGDeviceNamespace extends XmpNamespace { XmpCardData(RegExp(r'Camera:ImagingModel/(.*)')), ], ), - XmpCardData(RegExp(nsPrefix + r'Container/Container:Directory\[(\d+)\]/(.*)')), + XmpCardData( + RegExp(nsPrefix + r'Container/Container:Directory\[(\d+)\]/(.*)'), + spanBuilders: (index, struct) { + if (struct.containsKey('Item:Data') && struct.containsKey('Item:DataURI')) { + final dataUriProp = struct['Item:DataURI']; + if (dataUriProp != null) { + return { + 'Data': InfoRowGroup.linkSpanBuilder( + linkText: (context) => context.l10n.viewerInfoOpenLinkText, + onTap: (context) => OpenEmbeddedDataNotification.googleDevice(dataUri: dataUriProp.value).dispatch(context), + ), + }; + } + } + return {}; + }, + ), XmpCardData(RegExp(nsPrefix + r'Profiles\[(\d+)\]/(.*)')), ]; } diff --git a/lib/widgets/viewer/overlay/bottom.dart b/lib/widgets/viewer/overlay/bottom.dart index 8523e3f4a..a48dbb36e 100644 --- a/lib/widgets/viewer/overlay/bottom.dart +++ b/lib/widgets/viewer/overlay/bottom.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'package:aves/app_mode.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -25,6 +24,7 @@ class ViewerBottomOverlay extends StatefulWidget { final int index; final CollectionLens? collection; final AnimationController animationController; + final Size availableSize; final EdgeInsets? viewInsets, viewPadding; final MultiPageController? multiPageController; @@ -34,6 +34,7 @@ class ViewerBottomOverlay extends StatefulWidget { required this.index, required this.collection, required this.animationController, + required this.availableSize, this.viewInsets, this.viewPadding, required this.multiPageController, @@ -72,6 +73,7 @@ class _ViewerBottomOverlayState extends State { mainEntry: mainEntry, pageEntry: pageEntry ?? mainEntry, collection: widget.collection, + availableSize: widget.availableSize, viewInsets: widget.viewInsets, viewPadding: widget.viewPadding, multiPageController: multiPageController, @@ -103,6 +105,7 @@ class _BottomOverlayContent extends StatefulWidget { final int index; final AvesEntry mainEntry, pageEntry; final CollectionLens? collection; + final Size availableSize; final EdgeInsets? viewInsets, viewPadding; final MultiPageController? multiPageController; final AnimationController animationController; @@ -113,6 +116,7 @@ class _BottomOverlayContent extends StatefulWidget { required this.mainEntry, required this.pageEntry, required this.collection, + required this.availableSize, required this.viewInsets, required this.viewPadding, required this.multiPageController, @@ -131,6 +135,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { void initState() { super.initState(); _registerWidget(widget); + WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus()); } @override @@ -158,11 +163,10 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { parent: animationController, curve: Curves.easeOutQuad, ); - animationController.addStatusListener(_onAnimationStatusChanged); } void _unregisterWidget(_BottomOverlayContent widget) { - widget.animationController.removeStatusListener(_onAnimationStatusChanged); + // nothing } @override @@ -178,99 +182,91 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> { pageEntry.metadataChangeNotifier, ]), builder: (context, child) { - return Selector( - selector: (context, mq) => mq.size.width, - builder: (context, mqWidth, child) { - final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); - final viewerButtonRow = FocusableActionDetector( - focusNode: _buttonRowFocusScopeNode, - shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, - actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, - child: SafeArea( - top: false, - bottom: false, - minimum: EdgeInsets.only( - left: viewInsetsPadding.left, - right: viewInsetsPadding.right, + final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero); + final viewerButtonRow = FocusableActionDetector( + focusNode: _buttonRowFocusScopeNode, + shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, + actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, + child: SafeArea( + top: false, + bottom: false, + minimum: EdgeInsets.only( + left: viewInsetsPadding.left, + right: viewInsetsPadding.right, + ), + child: isWallpaperMode + ? WallpaperButtons( + entry: pageEntry, + scale: _buttonScale, + ) + : ViewerButtons( + mainEntry: mainEntry, + pageEntry: pageEntry, + collection: widget.collection, + scale: _buttonScale, + ), + ), + ); + + final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; + final collapsedPageScroller = mainEntry.isMotionPhoto; + + final availableWidth = widget.availableSize.width; + return SizedBox( + width: availableWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showMultiPageOverlay && !collapsedPageScroller) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: FadeTransition( + opacity: _thumbnailOpacity, + child: MultiPageOverlay( + controller: multiPageController, + availableWidth: availableWidth, + scrollable: true, + ), + ), ), - child: isWallpaperMode - ? WallpaperButtons( - entry: pageEntry, - scale: _buttonScale, - ) - : ViewerButtons( - mainEntry: mainEntry, - pageEntry: pageEntry, - collection: widget.collection, - scale: _buttonScale, - ), - ), - ); - - final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null; - final collapsedPageScroller = mainEntry.isMotionPhoto; - - return SizedBox( - width: mqWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (showMultiPageOverlay && !collapsedPageScroller) - Padding( - padding: const EdgeInsets.only(bottom: 8), - child: FadeTransition( - opacity: _thumbnailOpacity, - child: MultiPageOverlay( - controller: multiPageController, - availableWidth: mqWidth, - scrollable: true, - ), - ), - ), - (showMultiPageOverlay && collapsedPageScroller) - ? Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SafeArea( - top: false, - bottom: false, - child: Padding( - padding: const EdgeInsets.only(bottom: 8), - child: MultiPageOverlay( - controller: multiPageController, - availableWidth: mqWidth, - scrollable: false, - ), - ), + (showMultiPageOverlay && collapsedPageScroller) + ? Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SafeArea( + top: false, + bottom: false, + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: MultiPageOverlay( + controller: multiPageController, + availableWidth: availableWidth, + scrollable: false, ), - Expanded(child: viewerButtonRow), - ], - ) - : viewerButtonRow, - if (settings.showOverlayThumbnailPreview && !isWallpaperMode) - FadeTransition( - opacity: _thumbnailOpacity, - child: ViewerThumbnailPreview( - availableWidth: mqWidth, - displayedIndex: widget.index, - entries: widget.entries, - ), - ), - ], - ), - ); - }, + ), + ), + Expanded(child: viewerButtonRow), + ], + ) + : viewerButtonRow, + if (settings.showOverlayThumbnailPreview && !isWallpaperMode) + FadeTransition( + opacity: _thumbnailOpacity, + child: ViewerThumbnailPreview( + availableWidth: availableWidth, + displayedIndex: widget.index, + entries: widget.entries, + ), + ), + ], + ), ); }, ); } - void _onAnimationStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.completed) { - _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus(); - } - } + void _requestFocus() => _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus(); } class ExtraBottomOverlay extends StatelessWidget { diff --git a/lib/widgets/viewer/overlay/details/description.dart b/lib/widgets/viewer/overlay/details/description.dart new file mode 100644 index 000000000..02a476fdf --- /dev/null +++ b/lib/widgets/viewer/overlay/details/description.dart @@ -0,0 +1,25 @@ +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/viewer/overlay/details/details.dart'; +import 'package:decorated_icon/decorated_icon.dart'; +import 'package:flutter/material.dart'; + +class OverlayDescriptionRow extends StatelessWidget { + final String description; + + const OverlayDescriptionRow({ + super.key, + required this.description, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + DecoratedIcon(AIcons.description, size: ViewerDetailOverlayContent.iconSize, shadows: ViewerDetailOverlayContent.shadows(context)), + const SizedBox(width: ViewerDetailOverlayContent.iconPadding), + Expanded(child: Text(description, strutStyle: Constants.overflowStrutStyle)), + ], + ); + } +} diff --git a/lib/widgets/viewer/overlay/details/details.dart b/lib/widgets/viewer/overlay/details/details.dart index 38ff26049..0e7be08e7 100644 --- a/lib/widgets/viewer/overlay/details/details.dart +++ b/lib/widgets/viewer/overlay/details/details.dart @@ -8,6 +8,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; import 'package:aves/widgets/viewer/overlay/details/date.dart'; +import 'package:aves/widgets/viewer/overlay/details/description.dart'; import 'package:aves/widgets/viewer/overlay/details/location.dart'; import 'package:aves/widgets/viewer/overlay/details/position_title.dart'; import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart'; @@ -22,6 +23,7 @@ class ViewerDetailOverlay extends StatefulWidget { final int index; final bool hasCollection; final MultiPageController? multiPageController; + final Size availableSize; const ViewerDetailOverlay({ super.key, @@ -29,6 +31,7 @@ class ViewerDetailOverlay extends StatefulWidget { required this.index, required this.hasCollection, required this.multiPageController, + required this.availableSize, }); @override @@ -43,9 +46,9 @@ class _ViewerDetailOverlayState extends State { return index < entries.length ? entries[index] : null; } - late Future _detailLoader; + late Future?> _detailLoader; AvesEntry? _lastEntry; - OverlayMetadata? _lastDetails; + List? _lastDetails; @override void initState() { @@ -63,7 +66,14 @@ class _ViewerDetailOverlayState extends State { void _initDetailLoader() { final requestEntry = entry; - _detailLoader = requestEntry != null ? metadataFetchService.getOverlayMetadata(requestEntry) : SynchronousFuture(null); + if (requestEntry == null) { + _detailLoader = SynchronousFuture(null); + } else { + _detailLoader = Future.wait([ + settings.showOverlayShootingDetails ? metadataFetchService.getOverlayMetadata(requestEntry) : Future.value(null), + settings.showOverlayDescription ? metadataFetchService.getDescription(requestEntry) : Future.value(null), + ]); + } } @override @@ -71,37 +81,35 @@ class _ViewerDetailOverlayState extends State { return SafeArea( top: false, bottom: false, - child: LayoutBuilder( - builder: (context, constraints) { - final availableWidth = constraints.maxWidth; + child: FutureBuilder?>( + future: _detailLoader, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) { + _lastDetails = snapshot.data; + _lastEntry = entry; + } + if (_lastEntry == null) return const SizedBox(); + final mainEntry = _lastEntry!; - return FutureBuilder( - future: _detailLoader, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) { - _lastDetails = snapshot.data; - _lastEntry = entry; - } - if (_lastEntry == null) return const SizedBox(); - final mainEntry = _lastEntry!; + final shootingDetails = _lastDetails![0]; + final description = _lastDetails![1]; - final multiPageController = widget.multiPageController; - Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent( - pageEntry: pageEntry ?? mainEntry, - details: _lastDetails, - position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null, - availableWidth: availableWidth, - multiPageController: multiPageController, - ); + final multiPageController = widget.multiPageController; + Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent( + pageEntry: pageEntry ?? mainEntry, + shootingDetails: shootingDetails, + description: description, + position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null, + availableWidth: widget.availableSize.width, + multiPageController: multiPageController, + ); - return multiPageController != null - ? PageEntryBuilder( - multiPageController: multiPageController, - builder: (pageEntry) => _buildContent(pageEntry: pageEntry), - ) - : _buildContent(); - }, - ); + return multiPageController != null + ? PageEntryBuilder( + multiPageController: multiPageController, + builder: (pageEntry) => _buildContent(pageEntry: pageEntry), + ) + : _buildContent(); }, ), ); @@ -110,7 +118,8 @@ class _ViewerDetailOverlayState extends State { class ViewerDetailOverlayContent extends StatelessWidget { final AvesEntry pageEntry; - final OverlayMetadata? details; + final OverlayMetadata? shootingDetails; + final String? description; final String? position; final double availableWidth; final MultiPageController? multiPageController; @@ -126,7 +135,8 @@ class ViewerDetailOverlayContent extends StatelessWidget { const ViewerDetailOverlayContent({ super.key, required this.pageEntry, - required this.details, + required this.shootingDetails, + required this.description, required this.position, required this.availableWidth, required this.multiPageController, @@ -136,7 +146,8 @@ class ViewerDetailOverlayContent extends StatelessWidget { Widget build(BuildContext context) { final infoMaxWidth = availableWidth - padding.horizontal; final showRatingTags = settings.showOverlayRatingTags; - final showShooting = settings.showOverlayShootingDetails; + final showShootingDetails = settings.showOverlayShootingDetails; + final showDescription = settings.showOverlayDescription; return AnimatedBuilder( animation: pageEntry.metadataChangeNotifier, @@ -156,8 +167,8 @@ class ViewerDetailOverlayContent extends StatelessWidget { builder: (context, orientation, child) { final twoColumns = orientation == Orientation.landscape && infoMaxWidth / 2 > _subRowMinWidth; final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth; - final collapsedShooting = twoColumns && showShooting; - final collapsedLocation = twoColumns && !showShooting; + final collapsedShooting = twoColumns && showShootingDetails; + final collapsedLocation = twoColumns && !showShootingDetails; final rows = []; if (positionTitle.isNotEmpty) { @@ -176,7 +187,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { ); } else { rows.add(_buildDateSubRow(subRowWidth)); - if (showShooting) { + if (showShootingDetails) { rows.add(_buildShootingFullRow(context, subRowWidth)); } } @@ -186,6 +197,9 @@ class ViewerDetailOverlayContent extends StatelessWidget { if (showRatingTags) { rows.add(_buildRatingTagsFullRow(context)); } + if (showDescription) { + rows.add(_buildDescriptionFullRow(context)); + } return Column( mainAxisSize: MainAxisSize.min, @@ -214,20 +228,26 @@ class ViewerDetailOverlayContent extends StatelessWidget { builder: (context) => OverlayRatingTagsRow(entry: pageEntry), ); + Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher( + context: context, + visible: description != null, + builder: (context) => OverlayDescriptionRow(description: description!), + ); + Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher( context: context, - visible: details != null && details!.isNotEmpty, + visible: shootingDetails != null && shootingDetails!.isNotEmpty, builder: (context) => SizedBox( width: subRowWidth, - child: OverlayShootingRow(details: details!), + child: OverlayShootingRow(details: shootingDetails!), ), ); Widget _buildShootingSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher( context: context, subRowWidth: subRowWidth, - visible: details != null && details!.isNotEmpty, - builder: (context) => OverlayShootingRow(details: details!), + visible: shootingDetails != null && shootingDetails!.isNotEmpty, + builder: (context) => OverlayShootingRow(details: shootingDetails!), ); Widget _buildLocationFullRow(BuildContext context) => _buildFullRowSwitcher( diff --git a/lib/widgets/viewer/overlay/details/shooting.dart b/lib/widgets/viewer/overlay/details/shooting.dart index e28dfb7c8..0d8ea39b9 100644 --- a/lib/widgets/viewer/overlay/details/shooting.dart +++ b/lib/widgets/viewer/overlay/details/shooting.dart @@ -10,7 +10,10 @@ import 'package:intl/intl.dart'; class OverlayShootingRow extends StatelessWidget { final OverlayMetadata details; - const OverlayShootingRow({super.key, required this.details}); + const OverlayShootingRow({ + super.key, + required this.details, + }); @override Widget build(BuildContext context) { diff --git a/lib/widgets/viewer/overlay/slideshow_buttons.dart b/lib/widgets/viewer/overlay/slideshow_buttons.dart index 645c6a6e5..d7d282d4a 100644 --- a/lib/widgets/viewer/overlay/slideshow_buttons.dart +++ b/lib/widgets/viewer/overlay/slideshow_buttons.dart @@ -1,5 +1,5 @@ import 'package:aves/model/actions/slideshow_actions.dart'; -import 'package:aves/model/device.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/identity/buttons/captioned_button.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; @@ -36,6 +36,7 @@ class _SlideshowButtonsState extends State { void initState() { super.initState(); _registerWidget(widget); + WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus()); } @override @@ -53,26 +54,24 @@ class _SlideshowButtonsState extends State { } void _registerWidget(SlideshowButtons widget) { - final animationController = widget.animationController; _buttonScale = CurvedAnimation( - parent: animationController, + parent: widget.animationController, // a little bounce at the top curve: Curves.easeOutBack, ); - animationController.addStatusListener(_onAnimationStatusChanged); } void _unregisterWidget(SlideshowButtons widget) { - widget.animationController.removeStatusListener(_onAnimationStatusChanged); + // nothing } @override Widget build(BuildContext context) { return FocusableActionDetector( focusNode: _buttonRowFocusScopeNode, - shortcuts: device.isTelevision ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, + shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null, actions: {TvShowLessInfoIntent: CallbackAction(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))}, - child: device.isTelevision + child: settings.useTvLayout ? Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, @@ -111,9 +110,5 @@ class _SlideshowButtonsState extends State { void _onAction(BuildContext context, SlideshowAction action) => SlideshowActionNotification(action).dispatch(context); - void _onAnimationStatusChanged(AnimationStatus status) { - if (status == AnimationStatus.completed) { - _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus(); - } - } + void _requestFocus() => _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus(); } diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index 657eee86d..3d3e8ddaa 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -16,6 +16,7 @@ class ViewerTopOverlay extends StatelessWidget { final AvesEntry mainEntry; final Animation scale; final bool hasCollection; + final Size availableSize; final EdgeInsets? viewInsets, viewPadding; const ViewerTopOverlay({ @@ -25,6 +26,7 @@ class ViewerTopOverlay extends StatelessWidget { required this.mainEntry, required this.scale, required this.hasCollection, + required this.availableSize, required this.viewInsets, required this.viewPadding, }); @@ -65,6 +67,7 @@ class ViewerTopOverlay extends StatelessWidget { entries: entries, hasCollection: hasCollection, multiPageController: multiPageController, + availableSize: availableSize, ), ), ), diff --git a/lib/widgets/viewer/overlay/viewer_buttons.dart b/lib/widgets/viewer/overlay/viewer_buttons.dart index 9d6bd33ee..3ce1e4c6d 100644 --- a/lib/widgets/viewer/overlay/viewer_buttons.dart +++ b/lib/widgets/viewer/overlay/viewer_buttons.dart @@ -1,5 +1,4 @@ import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -51,7 +50,7 @@ class ViewerButtons extends StatelessWidget { Widget build(BuildContext context) { final actionDelegate = EntryActionDelegate(mainEntry, pageEntry, collection); - if (device.isTelevision) { + if (settings.useTvLayout) { return _TvButtonRowContent( actionDelegate: actionDelegate, scale: scale, @@ -122,13 +121,14 @@ class _TvButtonRowContent extends StatelessWidget { final enabled = actionDelegate.canApply(action); return CaptionedButton( scale: scale, - iconButton: _buildButtonIcon( + iconButtonBuilder: (context, focusNode) => ViewerButtonRowContent._buildButtonIcon( context: context, action: action, mainEntry: mainEntry, pageEntry: pageEntry, videoController: videoController, actionDelegate: actionDelegate, + focusNode: focusNode, ), captionText: _buildButtonCaption( context: context, @@ -145,6 +145,39 @@ class _TvButtonRowContent extends StatelessWidget { }, ); } + + static Widget _buildButtonCaption({ + required BuildContext context, + required EntryAction action, + required AvesEntry mainEntry, + required AvesEntry pageEntry, + required AvesVideoController? videoController, + required bool enabled, + }) { + switch (action) { + case EntryAction.toggleFavourite: + final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; + return FavouriteTogglerCaption( + entries: {favouriteTargetEntry}, + enabled: enabled, + ); + case EntryAction.videoToggleMute: + return MuteTogglerCaption( + controller: videoController, + enabled: enabled, + ); + case EntryAction.videoTogglePlay: + return PlayTogglerCaption( + controller: videoController, + enabled: enabled, + ); + default: + return CaptionedButtonText( + text: action.getText(context), + enabled: enabled, + ); + } + } } class ViewerButtonRowContent extends StatelessWidget { @@ -375,139 +408,115 @@ class ViewerButtonRowContent extends StatelessWidget { ), ); } -} -Widget _buildButtonIcon({ - required BuildContext context, - required EntryAction action, - required AvesEntry mainEntry, - required AvesEntry pageEntry, - required AvesVideoController? videoController, - required EntryActionDelegate actionDelegate, -}) { - Widget? child; - void onPressed() => actionDelegate.onActionSelected(context, action); + static Widget _buildButtonIcon({ + required BuildContext context, + required EntryAction action, + required AvesEntry mainEntry, + required AvesEntry pageEntry, + required AvesVideoController? videoController, + required EntryActionDelegate actionDelegate, + FocusNode? focusNode, + }) { + Widget? child; + void onPressed() => actionDelegate.onActionSelected(context, action); - ValueListenableBuilder _buildFromListenable(ValueListenable? enabledNotifier) { - return ValueListenableBuilder( - valueListenable: enabledNotifier ?? ValueNotifier(false), - builder: (context, canDo, child) => IconButton( - icon: child!, - onPressed: canDo ? onPressed : null, - tooltip: action.getText(context), - ), - child: action.getIcon(), - ); - } + ValueListenableBuilder _buildFromListenable(ValueListenable? enabledNotifier) { + return ValueListenableBuilder( + valueListenable: enabledNotifier ?? ValueNotifier(false), + builder: (context, canDo, child) => IconButton( + icon: child!, + onPressed: canDo ? onPressed : null, + focusNode: focusNode, + tooltip: action.getText(context), + ), + child: action.getIcon(), + ); + } - final blurred = settings.enableBlurEffect; - switch (action) { - case EntryAction.copy: - child = MoveButton( - copy: true, - blurred: blurred, - onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: true), - onPressed: onPressed, - ); - break; - case EntryAction.move: - child = MoveButton( - copy: false, - blurred: blurred, - onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: false), - onPressed: onPressed, - ); - break; - case EntryAction.share: - child = ShareButton( - blurred: blurred, - entries: {mainEntry}, - onChooserValue: (action) => actionDelegate.quickShare(context, action), - onPressed: onPressed, - ); - break; - case EntryAction.toggleFavourite: - final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; - child = FavouriteToggler( - entries: {favouriteTargetEntry}, - onPressed: onPressed, - ); - break; - case EntryAction.videoToggleMute: - child = MuteToggler( - controller: videoController, - onPressed: onPressed, - ); - break; - case EntryAction.videoTogglePlay: - child = PlayToggler( - controller: videoController, - onPressed: onPressed, - ); - break; - case EntryAction.videoCaptureFrame: - child = _buildFromListenable(videoController?.canCaptureFrameNotifier); - break; - case EntryAction.videoSelectStreams: - child = _buildFromListenable(videoController?.canSelectStreamNotifier); - break; - case EntryAction.videoSetSpeed: - child = _buildFromListenable(videoController?.canSetSpeedNotifier); - break; - case EntryAction.editRating: - child = RateButton( - blurred: blurred, - onChooserValue: (rating) => actionDelegate.quickRate(context, rating), - onPressed: onPressed, - ); - break; - case EntryAction.editTags: - child = TagButton( - blurred: blurred, - onChooserValue: (filter) => actionDelegate.quickTag(context, filter), - onPressed: onPressed, - ); - break; - default: - child = IconButton( - icon: action.getIcon(), - onPressed: onPressed, - tooltip: action.getText(context), - ); - break; - } - return child; -} - -Widget _buildButtonCaption({ - required BuildContext context, - required EntryAction action, - required AvesEntry mainEntry, - required AvesEntry pageEntry, - required AvesVideoController? videoController, - required bool enabled, -}) { - switch (action) { - case EntryAction.toggleFavourite: - final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; - return FavouriteTogglerCaption( - entries: {favouriteTargetEntry}, - enabled: enabled, - ); - case EntryAction.videoToggleMute: - return MuteTogglerCaption( - controller: videoController, - enabled: enabled, - ); - case EntryAction.videoTogglePlay: - return PlayTogglerCaption( - controller: videoController, - enabled: enabled, - ); - default: - return CaptionedButtonText( - text: action.getText(context), - enabled: enabled, - ); + final blurred = settings.enableBlurEffect; + switch (action) { + case EntryAction.copy: + child = MoveButton( + copy: true, + blurred: blurred, + onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: true), + onPressed: onPressed, + ); + break; + case EntryAction.move: + child = MoveButton( + copy: false, + blurred: blurred, + onChooserValue: (album) => actionDelegate.quickMove(context, album, copy: false), + onPressed: onPressed, + ); + break; + case EntryAction.share: + child = ShareButton( + blurred: blurred, + entries: {mainEntry}, + onChooserValue: (action) => actionDelegate.quickShare(context, action), + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.toggleFavourite: + final favouriteTargetEntry = mainEntry.isBurst ? pageEntry : mainEntry; + child = FavouriteToggler( + entries: {favouriteTargetEntry}, + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.videoToggleMute: + child = MuteToggler( + controller: videoController, + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.videoTogglePlay: + child = PlayToggler( + controller: videoController, + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.videoCaptureFrame: + child = _buildFromListenable(videoController?.canCaptureFrameNotifier); + break; + case EntryAction.videoSelectStreams: + child = _buildFromListenable(videoController?.canSelectStreamNotifier); + break; + case EntryAction.videoSetSpeed: + child = _buildFromListenable(videoController?.canSetSpeedNotifier); + break; + case EntryAction.editRating: + child = RateButton( + blurred: blurred, + onChooserValue: (rating) => actionDelegate.quickRate(context, rating), + focusNode: focusNode, + onPressed: onPressed, + ); + break; + case EntryAction.editTags: + child = TagButton( + blurred: blurred, + onChooserValue: (filter) => actionDelegate.quickTag(context, filter), + focusNode: focusNode, + onPressed: onPressed, + ); + break; + default: + child = IconButton( + icon: action.getIcon(), + onPressed: onPressed, + focusNode: focusNode, + tooltip: action.getText(context), + ); + break; + } + return child; } } diff --git a/lib/widgets/viewer/panorama_page.dart b/lib/widgets/viewer/panorama_page.dart index a47584a8b..69a38bfd5 100644 --- a/lib/widgets/viewer/panorama_page.dart +++ b/lib/widgets/viewer/panorama_page.dart @@ -1,9 +1,9 @@ import 'dart:math'; -import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/model/panorama.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/common/basic/insets.dart'; @@ -110,7 +110,7 @@ class _PanoramaPageState extends State { } Widget _buildOverlay(BuildContext context) { - if (device.isTelevision) return const SizedBox(); + if (settings.useTvLayout) return const SizedBox(); return TooltipTheme( data: TooltipTheme.of(context).copyWith( diff --git a/lib/widgets/viewer/video/conductor.dart b/lib/widgets/viewer/video/conductor.dart index fb817ede9..08389e802 100644 --- a/lib/widgets/viewer/video/conductor.dart +++ b/lib/widgets/viewer/video/conductor.dart @@ -11,14 +11,13 @@ import 'package:collection/collection.dart'; class VideoConductor { final List _controllers = []; final List _subscriptions = []; - final bool persistPlayback; static const _defaultMaxControllerCount = 3; - VideoConductor({required this.persistPlayback}); + VideoConductor(); Future dispose() async { - await Future.forEach(_controllers, (controller) => controller.dispose()); + await disposeAll(); _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); @@ -33,7 +32,7 @@ class VideoConductor { if (controller != null) { _controllers.remove(controller); } else { - controller = IjkPlayerAvesVideoController(entry, persistPlayback: persistPlayback); + controller = IjkPlayerAvesVideoController(entry, persistPlayback: true); _subscriptions.add(controller.statusStream.listen(_onControllerStatusChanged)); } _controllers.insert(0, controller); @@ -55,6 +54,8 @@ class VideoConductor { Future _applyToAll(FutureOr Function(AvesVideoController controller) action) => Future.forEach(_controllers, action); + Future disposeAll() => _applyToAll((controller) => controller.dispose()); + Future pauseAll() => _applyToAll((controller) => controller.pause()); Future muteAll(bool muted) => _applyToAll((controller) => controller.mute(muted)); diff --git a/lib/widgets/viewer/video/controller.dart b/lib/widgets/viewer/video/controller.dart index fc544af7b..0e0f9f452 100644 --- a/lib/widgets/viewer/video/controller.dart +++ b/lib/widgets/viewer/video/controller.dart @@ -30,7 +30,6 @@ abstract class AvesVideoController { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); - await mediaSessionService.release(entry.uri); entry.visualChangeNotifier.removeListener(onVisualChanged); await _savePlaybackState(); } @@ -67,21 +66,19 @@ abstract class AvesVideoController { final resume = await showDialog( context: context, - builder: (context) { - return AvesDialog( - content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text(context.l10n.videoStartOverButtonLabel), - ), - TextButton( - onPressed: () => Navigator.pop(context, true), - child: Text(context.l10n.videoResumeButtonLabel), - ), - ], - ); - }, + builder: (context) => AvesDialog( + content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.videoStartOverButtonLabel), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.videoResumeButtonLabel), + ), + ], + ), ); if (resume == null || !resume) return 0; return resumeTime; diff --git a/lib/widgets/viewer/video/fijkplayer.dart b/lib/widgets/viewer/video/fijkplayer.dart index e2c62f5b9..9b7d10a6f 100644 --- a/lib/widgets/viewer/video/fijkplayer.dart +++ b/lib/widgets/viewer/video/fijkplayer.dart @@ -1,18 +1,21 @@ import 'dart:async'; -import 'dart:typed_data'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/enums/video_loop_mode.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/video/keys.dart'; import 'package:aves/model/video/metadata.dart'; +import 'package:aves/services/common/optional_event_channel.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:collection/collection.dart'; import 'package:fijkplayer/fijkplayer.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class IjkPlayerAvesVideoController extends AvesVideoController { + final EventChannel _eventChannel = const OptionalEventChannel('befovy.com/fijk/event'); + late FijkPlayer _instance; final List _subscriptions = []; final StreamController _valueStreamController = StreamController.broadcast(); @@ -94,6 +97,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { void _startListening() { _instance.addListener(_onValueChanged); + _subscriptions.add(_eventChannel.receiveBroadcastStream().listen((event) => _onPluginEvent(event as Map?))); _subscriptions.add(_valueStream.where((value) => value.state == FijkState.completed).listen((_) => _completedNotifier.notify())); _subscriptions.add(_instance.onTimedText.listen(_timedTextStreamController.add)); _subscriptions.add(settings.updateStream @@ -135,7 +139,13 @@ class IjkPlayerAvesVideoController extends AvesVideoController { void _applyOptions(int startMillis) { // FFmpeg options - // cf https://github.com/Bilibili/ijkplayer/blob/master/ijkmedia/ijkplayer/ff_ffplay_options.h + // `setHostOption`, cf: + // - https://fijkplayer.befovy.com/docs/zh/host-option.html + // - https://github.com/deckerst/fijkplayer/blob/master/android/src/main/java/com/befovy/fijkplayer/HostOption.java + // `setFormatOption`, cf https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/options_table.h + // `setCodecOption`, cf https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/options_table.h + // `setSwsOption`, cf https://github.com/FFmpeg/FFmpeg/blob/master/libswscale/options.c + // `setPlayerOption`, cf https://github.com/Bilibili/ijkplayer/blob/master/ijkmedia/ijkplayer/ff_ffplay_options.h // cf https://www.jianshu.com/p/843c86a9e9ad // cf https://www.jianshu.com/p/3649c073b346 @@ -166,10 +176,15 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // `enable-snapshot`: enable snapshot interface // default: 0, in [0, 1] - // cf https://fijkplayer.befovy.com/docs/zh/host-option.html // there is a performance cost, and it should be set up before playing options.setHostOption('enable-snapshot', captureFrameEnabled ? 1 : 0); + // default: 0, in [0, 1] + options.setHostOption('request-audio-focus', 1); + + // default: 0, in [0, 1] + options.setHostOption('release-audio-focus', 1); + // `accurate-seek-timeout`: accurate seek timeout // default: 5000 ms, in [0, 5000] options.setPlayerOption('accurate-seek-timeout', 1000); @@ -286,6 +301,31 @@ class IjkPlayerAvesVideoController extends AvesVideoController { } } + // cf https://developer.android.com/reference/android/media/AudioManager + static const int _audioFocusLoss = -1; + static const int _audioFocusRequestFailed = 0; + + void _onPluginEvent(Map? fields) { + if (fields == null) return; + final event = fields['event'] as String?; + switch (event) { + case 'volume': + // ignore + break; + case 'audiofocus': + final value = fields['value'] as int?; + if (value != null) { + switch (value) { + case _audioFocusLoss: + case _audioFocusRequestFailed: + pause(); + break; + } + } + break; + } + } + void _onValueChanged() { if (_instance.state == FijkState.prepared && _streams.isEmpty) { _fetchStreams(); diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index c263cad74..287a8caea 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -6,9 +6,12 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:aves/services/media/media_session_service.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/viewer/controller.dart'; import 'package:aves/widgets/viewer/hero.dart'; @@ -106,6 +109,7 @@ class _EntryPageViewState extends State with SingleTickerProvider _subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged)); _subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged)); if (entry.isVideo) { + _subscriptions.add(mediaSessionService.mediaCommands.listen(_onMediaCommand)); _videoCoverStreamListener = ImageStreamListener((image, _) => _videoCoverInfoNotifier.value = image); _videoCoverStream = videoCoverUriImage.resolve(ImageConfiguration.empty); _videoCoverStream!.addListener(_videoCoverStreamListener); @@ -147,6 +151,7 @@ class _EntryPageViewState extends State with SingleTickerProvider child = _buildRasterView(); } } + child ??= ErrorView( entry: entry, onTap: _onTap, @@ -155,6 +160,14 @@ class _EntryPageViewState extends State with SingleTickerProvider }, ); + if (!settings.viewerUseCutout) { + child = SafeCutoutArea( + child: ClipRect( + child: child, + ), + ); + } + final animate = context.select((v) => v.accessibilityAnimations.animate); if (animate) { child = Consumer( @@ -166,6 +179,7 @@ class _EntryPageViewState extends State with SingleTickerProvider child: child, ); } + return child; } @@ -419,6 +433,28 @@ class _EntryPageViewState extends State with SingleTickerProvider const ToggleOverlayNotification().dispatch(context); } + void _onMediaCommand(MediaCommandEvent event) { + final videoController = context.read().getController(entry); + if (videoController == null) return; + + switch (event.command) { + case MediaCommand.play: + videoController.play(); + break; + case MediaCommand.pause: + videoController.pause(); + break; + case MediaCommand.stop: + videoController.pause(); + break; + case MediaCommand.seek: + if (event is MediaSeekCommandEvent) { + videoController.seekTo(event.position); + } + break; + } + } + void _onViewStateChanged(MagnifierState v) { _viewStateNotifier.value = _viewStateNotifier.value.copyWith( position: v.position, diff --git a/lib/widgets/wallpaper_page.dart b/lib/widgets/wallpaper_page.dart index fbac45bbc..dc2773844 100644 --- a/lib/widgets/wallpaper_page.dart +++ b/lib/widgets/wallpaper_page.dart @@ -83,9 +83,6 @@ class _EntryEditorState extends State with EntryViewControllerMixin @override void initState() { super.initState(); - if (!settings.viewerUseCutout) { - windowService.setCutoutMode(false); - } if (settings.viewerMaxBrightness) { ScreenBrightness().setScreenBrightness(1); } @@ -134,25 +131,30 @@ class _EntryEditorState extends State with EntryViewControllerMixin } return true; }, - child: Stack( - children: [ - SingleEntryScroller( - entry: entry, - viewerController: _viewerController, - ), - Positioned( - bottom: 0, - child: _buildBottomOverlay(), - ), - const TopGestureAreaProtector(), - const SideGestureAreaProtector(), - const BottomGestureAreaProtector(), - ], + child: LayoutBuilder( + builder: (context, constraints) { + final viewSize = Size(constraints.maxWidth, constraints.maxHeight); + return Stack( + children: [ + SingleEntryScroller( + entry: entry, + viewerController: _viewerController, + ), + Positioned( + bottom: 0, + child: _buildBottomOverlay(viewSize), + ), + const TopGestureAreaProtector(), + const SideGestureAreaProtector(), + const BottomGestureAreaProtector(), + ], + ); + }, ), ); } - Widget _buildBottomOverlay() { + Widget _buildBottomOverlay(Size viewSize) { final mainEntry = entry; final multiPageController = mainEntry.isMultiPage ? context.read().getController(mainEntry) : null; @@ -210,6 +212,7 @@ class _EntryEditorState extends State with EntryViewControllerMixin index: 0, collection: null, animationController: _overlayAnimationController, + availableSize: viewSize, viewInsets: _frozenViewInsets, viewPadding: _frozenViewPadding, multiPageController: multiPageController, diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index c9e4cf468..8b867ab68 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -1,5 +1,4 @@ import 'package:aves/app_flavor.dart'; -import 'package:aves/model/device.dart'; import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/durations.dart'; @@ -71,7 +70,7 @@ class _WelcomePageState extends State { child: child, ), ), - children: device.isTelevision + children: settings.useTvLayout ? [ ..._buildHeader(context, isPortrait: isPortrait), Padding( diff --git a/plugins/aves_magnifier/lib/src/core/core.dart b/plugins/aves_magnifier/lib/src/core/core.dart index d8512c37c..eb24275ba 100644 --- a/plugins/aves_magnifier/lib/src/core/core.dart +++ b/plugins/aves_magnifier/lib/src/core/core.dart @@ -119,7 +119,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM if (_doubleTap) { // quick scale, aka one finger zoom // magic numbers from `davemorrissey/subsampling-scale-image-view` - final focalPointY = details.focalPoint.dy; + final focalPointY = details.localFocalPoint.dy; final distance = (focalPointY - _startFocalPoint!.dy).abs() * 2 + 20; _quickScaleLastDistance ??= distance; final spanDiff = (1 - (distance / _quickScaleLastDistance!)).abs() * .5; @@ -131,7 +131,7 @@ class _MagnifierCoreState extends State with TickerProviderStateM } else { newScale = _startScale! * details.scale; } - final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.focalPoint; + final scaleFocalPoint = _doubleTap ? _startFocalPoint! : details.localFocalPoint; final panPositionDelta = scaleFocalPoint - _lastViewportFocalPosition!; final scalePositionDelta = boundaries.viewportToStatePosition(controller, scaleFocalPoint) * (scale! / newScale - 1); diff --git a/plugins/aves_map/lib/src/controller.dart b/plugins/aves_map/lib/src/controller.dart index e196bcf2e..7f853c2a8 100644 --- a/plugins/aves_map/lib/src/controller.dart +++ b/plugins/aves_map/lib/src/controller.dart @@ -10,6 +10,8 @@ class AvesMapController { Stream get moveCommands => _events.where((event) => event is MapControllerMoveEvent).cast(); + Stream get zoomCommands => _events.where((event) => event is MapControllerZoomEvent).cast(); + Stream get idleUpdates => _events.where((event) => event is MapIdleUpdate).cast(); Stream get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast(); @@ -20,6 +22,8 @@ class AvesMapController { void moveTo(LatLng latLng) => _streamController.add(MapControllerMoveEvent(latLng)); + void zoomBy(double delta) => _streamController.add(MapControllerZoomEvent(delta)); + void notifyIdle(ZoomedBounds bounds) => _streamController.add(MapIdleUpdate(bounds)); void notifyMarkerLocationChange() => _streamController.add(MapMarkerLocationChangeEvent()); @@ -31,6 +35,12 @@ class MapControllerMoveEvent { MapControllerMoveEvent(this.latLng); } +class MapControllerZoomEvent { + final double delta; + + MapControllerZoomEvent(this.delta); +} + class MapIdleUpdate { final ZoomedBounds bounds; diff --git a/plugins/aves_map/lib/src/interface.dart b/plugins/aves_map/lib/src/interface.dart index 6bc04ef61..6aedd5397 100644 --- a/plugins/aves_map/lib/src/interface.dart +++ b/plugins/aves_map/lib/src/interface.dart @@ -3,7 +3,7 @@ import 'package:aves_map/src/marker/key.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; -typedef ButtonPanelBuilder = Widget Function(Future Function(double amount) zoomBy, VoidCallback resetRotation); +typedef ButtonPanelBuilder = Widget Function(VoidCallback resetRotation); typedef MarkerClusterBuilder = Map, GeoEntry> Function(); typedef MarkerWidgetBuilder = Widget Function(MarkerKey key); typedef MarkerImageReadyChecker = bool Function(MarkerKey key); diff --git a/plugins/aves_map/pubspec.lock b/plugins/aves_map/pubspec.lock index 52fb1e07f..9c5d491c8 100644 --- a/plugins/aves_map/pubspec.lock +++ b/plugins/aves_map/pubspec.lock @@ -75,7 +75,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" http: dependency: transitive description: diff --git a/plugins/aves_report_crashlytics/pubspec.lock b/plugins/aves_report_crashlytics/pubspec.lock index 975c4cb66..10253f93e 100644 --- a/plugins/aves_report_crashlytics/pubspec.lock +++ b/plugins/aves_report_crashlytics/pubspec.lock @@ -7,7 +7,7 @@ packages: name: _flutterfire_internals url: "https://pub.dartlang.org" source: hosted - version: "1.0.10" + version: "1.0.12" async: dependency: transitive description: @@ -43,20 +43,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" - cloud_firestore_platform_interface: - dependency: transitive - description: - name: cloud_firestore_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "5.9.1" - cloud_firestore_web: - dependency: transitive - description: - name: cloud_firestore_web - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.1" collection: dependency: "direct main" description: @@ -77,7 +63,7 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" firebase_core_platform_interface: dependency: transitive description: @@ -91,21 +77,21 @@ packages: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "3.0.7" + version: "3.0.10" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.3.8" + version: "3.3.10" flutter: dependency: "direct main" description: flutter diff --git a/plugins/aves_services/pubspec.lock b/plugins/aves_services/pubspec.lock index 58d687d8d..0db4e95e9 100644 --- a/plugins/aves_services/pubspec.lock +++ b/plugins/aves_services/pubspec.lock @@ -82,7 +82,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" http: dependency: transitive description: diff --git a/plugins/aves_services_google/lib/src/map.dart b/plugins/aves_services_google/lib/src/map.dart index 4007294be..b3f50548d 100644 --- a/plugins/aves_services_google/lib/src/map.dart +++ b/plugins/aves_services_google/lib/src/map.dart @@ -95,6 +95,7 @@ class _EntryGoogleMapState extends State> with WidgetsBindi final avesMapController = widget.controller; if (avesMapController != null) { _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); + _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta))); } widget.clusterListenable.addListener(_updateMarkers); } @@ -139,7 +140,7 @@ class _EntryGoogleMapState extends State> with WidgetsBindi }, ), widget.decoratorBuilder(context, _buildMap()), - widget.buttonPanelBuilder(_zoomBy, _resetRotation), + widget.buttonPanelBuilder(_resetRotation), ], ); } diff --git a/plugins/aves_services_google/pubspec.lock b/plugins/aves_services_google/pubspec.lock index aec163291..c7845b7fd 100644 --- a/plugins/aves_services_google/pubspec.lock +++ b/plugins/aves_services_google/pubspec.lock @@ -117,7 +117,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -143,28 +143,28 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "2.2.3" google_maps_flutter_android: dependency: "direct main" description: name: google_maps_flutter_android url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.2" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios url: "https://pub.dartlang.org" source: hosted - version: "2.1.12" + version: "2.1.13" google_maps_flutter_platform_interface: dependency: "direct main" description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.2.4" + version: "2.2.5" http: dependency: transitive description: @@ -351,7 +351,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.1.3" wkt_parser: dependency: transitive description: diff --git a/plugins/aves_services_huawei/lib/src/map.dart b/plugins/aves_services_huawei/lib/src/map.dart index 1616101fb..82d02d966 100644 --- a/plugins/aves_services_huawei/lib/src/map.dart +++ b/plugins/aves_services_huawei/lib/src/map.dart @@ -89,6 +89,7 @@ class _EntryHmsMapState extends State> { final avesMapController = widget.controller; if (avesMapController != null) { _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); + _subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta))); } widget.clusterListenable.addListener(_updateMarkers); } @@ -118,7 +119,7 @@ class _EntryHmsMapState extends State> { }, ), widget.decoratorBuilder(context, _buildMap()), - widget.buttonPanelBuilder(_zoomBy, _resetRotation), + widget.buttonPanelBuilder(_resetRotation), ], ); } diff --git a/plugins/aves_services_huawei/pubspec.lock b/plugins/aves_services_huawei/pubspec.lock index 2185469c7..12005e2fc 100644 --- a/plugins/aves_services_huawei/pubspec.lock +++ b/plugins/aves_services_huawei/pubspec.lock @@ -96,7 +96,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" http: dependency: transitive description: diff --git a/plugins/aves_services_none/pubspec.lock b/plugins/aves_services_none/pubspec.lock index 113e85699..0c8dfed63 100644 --- a/plugins/aves_services_none/pubspec.lock +++ b/plugins/aves_services_none/pubspec.lock @@ -89,7 +89,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" http: dependency: transitive description: diff --git a/pubspec.lock b/pubspec.lock index 081cfd241..af51869ef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: _flutterfire_internals url: "https://pub.dartlang.org" source: hosted - version: "1.0.10" + version: "1.0.12" analyzer: dependency: transitive description: @@ -123,9 +123,11 @@ packages: charts_flutter: dependency: "direct main" description: - name: charts_flutter - url: "https://pub.dartlang.org" - source: hosted + path: charts_flutter + ref: master + resolved-ref: de76a46c1908b0c35aaf60823100fe9bfa26ae4d + url: "https://github.com/fzyzcjy/charts.git" + source: git version: "0.12.0" clock: dependency: transitive @@ -134,20 +136,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.1" - cloud_firestore_platform_interface: - dependency: transitive - description: - name: cloud_firestore_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "5.9.1" - cloud_firestore_web: - dependency: transitive - description: - name: cloud_firestore_web - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.1" collection: dependency: "direct main" description: @@ -281,7 +269,7 @@ packages: description: path: "." ref: aves - resolved-ref: "556b3348a616c835d564b023f8ef6f056f67ef16" + resolved-ref: "69193da9a6b7f8b5cec5627253982adc303dbf46" url: "https://github.com/deckerst/fijkplayer.git" source: git version: "0.10.0" @@ -298,7 +286,7 @@ packages: name: firebase_core url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" firebase_core_platform_interface: dependency: transitive description: @@ -312,28 +300,28 @@ packages: name: firebase_core_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.0" firebase_crashlytics: dependency: transitive description: name: firebase_crashlytics url: "https://pub.dartlang.org" source: hosted - version: "3.0.7" + version: "3.0.10" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.3.8" + version: "3.3.10" flex_color_picker: dependency: "direct main" description: name: flex_color_picker url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.0.2" flex_seed_scheme: dependency: transitive description: @@ -366,7 +354,7 @@ packages: name: flutter_displaymode url: "https://pub.dartlang.org" source: hosted - version: "0.4.1" + version: "0.5.0" flutter_driver: dependency: "direct dev" description: flutter @@ -397,7 +385,7 @@ packages: name: flutter_map url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" flutter_markdown: dependency: "direct main" description: @@ -468,28 +456,28 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "2.2.3" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.2" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios url: "https://pub.dartlang.org" source: hosted - version: "2.1.12" + version: "2.1.13" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.2.4" + version: "2.2.5" highlight: dependency: transitive description: @@ -524,7 +512,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.2.2" + version: "3.3.0" intl: dependency: "direct main" description: @@ -622,7 +610,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" motion_sensors: dependency: transitive description: @@ -693,7 +681,7 @@ packages: description: path: "." ref: aves - resolved-ref: "47442e5102a8bdf5e756d6dfab60108b73d132f0" + resolved-ref: "89fbc3f1ecfd9be56cf9b4b674aa0aec15cae61e" url: "https://github.com/deckerst/aves_panorama.git" source: git version: "0.4.1" @@ -913,7 +901,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.15" + version: "2.0.16" shared_preferences_android: dependency: transitive description: @@ -921,10 +909,10 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.14" - shared_preferences_ios: + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_ios + name: shared_preferences_foundation url: "https://pub.dartlang.org" source: hosted version: "2.1.1" @@ -935,13 +923,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: @@ -1030,14 +1011,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.2.2" + version: "2.2.3" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.4.0+2" + version: "2.4.1" stack_trace: dependency: transitive description: @@ -1088,7 +1069,7 @@ packages: name: synchronized url: "https://pub.dartlang.org" source: hosted - version: "3.0.0+3" + version: "3.0.1" term_glyph: dependency: transitive description: @@ -1228,7 +1209,7 @@ packages: name: web_socket_channel url: "https://pub.dartlang.org" source: hosted - version: "2.2.0" + version: "2.3.0" webdriver: dependency: transitive description: @@ -1249,7 +1230,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.1.3" wkt_parser: dependency: transitive description: @@ -1263,7 +1244,7 @@ packages: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b932f6506..0e42057ba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,7 +7,7 @@ repository: https://github.com/deckerst/aves # - play changelog: /whatsnew/whatsnew-en-US # - izzy changelog: /fastlane/metadata/android/en-US/changelogs/XX01.txt # - libre changelog: /fastlane/metadata/android/en-US/changelogs/XX.txt -version: 1.7.8+88 +version: 1.7.9+89 publish_to: none environment: @@ -40,6 +40,10 @@ dependencies: aves_ui: path: plugins/aves_ui charts_flutter: + git: + url: https://github.com/fzyzcjy/charts.git + ref: master + path: charts_flutter collection: connectivity_plus: country_code: diff --git a/scripts/extract_apks.sh b/scripts/extract_apks.sh new file mode 100755 index 000000000..d7ae3bf4c --- /dev/null +++ b/scripts/extract_apks.sh @@ -0,0 +1,22 @@ +#!/bin/bash +if [ ! -d "scripts" ]; then + cd .. +fi + +BUNDLE="/home/tibo/Downloads/app-play-release.aab" + +# shellcheck disable=SC2001 +OUTPUT=$(sed "s|\.aab|\.apks|" <<<"$BUNDLE") + +KEYS_PATH="android/key.properties" +STORE_PATH=$(sed -n 's|.*storeFile=\(.*\)[\r\n]|\1|p' "$KEYS_PATH") +# shellcheck disable=SC1003 +STORE_PW=$(sed -n 's|.*storePassword=\(.*\)[\r\n]|\1|p' "$KEYS_PATH" | sed 's|\\'\''|'\''|g') +KEY_ALIAS=$(sed -n 's|.*keyAlias=\(.*\)[\r\n]|\1|p' "$KEYS_PATH") +# shellcheck disable=SC1003 +KEY_PW=$(sed -n 's|.*keyPassword=\(.*\)[\r\n]|\1|p' "$KEYS_PATH" | sed 's|\\'\''|'\''|g') + +echo "$BUNDLE -> $OUTPUT" +bundletool build-apks --bundle="$BUNDLE" --output="$OUTPUT" \ + --ks="$STORE_PATH" --ks-pass="pass:$STORE_PW" \ + --ks-key-alias="$KEY_ALIAS" --key-pass="pass:$KEY_PW" diff --git a/test/fake/window_service.dart b/test/fake/window_service.dart index 500d275c3..ea856236c 100644 --- a/test/fake/window_service.dart +++ b/test/fake/window_service.dart @@ -17,8 +17,8 @@ class FakeWindowService extends Fake implements WindowService { Future requestOrientation([Orientation? orientation]) => SynchronousFuture(null); @override - Future canSetCutoutMode() => SynchronousFuture(true); + Future isCutoutAware() => SynchronousFuture(true); @override - Future setCutoutMode(bool use) => SynchronousFuture(null); + Future getCutoutInsets() => SynchronousFuture(EdgeInsets.zero); } diff --git a/test/model/video/metadata_test.dart b/test/model/video/metadata_test.dart index 4423b775b..15a40f85c 100644 --- a/test/model/video/metadata_test.dart +++ b/test/model/video/metadata_test.dart @@ -16,6 +16,7 @@ void main() { expect(VideoMetadataFormatter.parseVideoDate('2022-01-28T5:07:46 p. m.Z'), DateTime(2022, 1, 28, 17, 7, 46).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2012-1-1T12:00:00Z'), DateTime(2012, 1, 1, 12, 0, 0).millisecondsSinceEpoch); expect(VideoMetadataFormatter.parseVideoDate('2020.10.14'), DateTime(2020, 10, 14).millisecondsSinceEpoch); + expect(VideoMetadataFormatter.parseVideoDate('2016:11:16 18:00:00'), DateTime(2016, 11, 16, 18, 0, 0).millisecondsSinceEpoch); }); test('Ambiguous date', () { diff --git a/test_driver/driver_screenshots.dart b/test_driver/driver_screenshots.dart index bad37cb77..abbbf3c3c 100644 --- a/test_driver/driver_screenshots.dart +++ b/test_driver/driver_screenshots.dart @@ -39,14 +39,15 @@ Future configureAndLaunch() async { ..collectionSortFactor = EntrySortFactor.date ..collectionBrowsingQuickActions = SettingsDefaults.collectionBrowsingQuickActions ..showThumbnailFavourite = false - ..showThumbnailTag = false - ..showThumbnailLocation = false + ..thumbnailLocationIcon = ThumbnailOverlayLocationIcon.none + ..thumbnailTagIcon = ThumbnailOverlayTagIcon.none ..hiddenFilters = {} // viewer ..viewerQuickActions = SettingsDefaults.viewerQuickActions ..showOverlayOnOpening = true ..showOverlayMinimap = false ..showOverlayInfo = true + ..showOverlayDescription = false ..showOverlayRatingTags = false ..showOverlayShootingDetails = false ..showOverlayThumbnailPreview = false diff --git a/untranslated.json b/untranslated.json index 917f19c09..4301f7282 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,34 +1,12 @@ { "ar": [ - "appName", - "welcomeMessage", - "welcomeOptional", - "welcomeTermsToggle", "itemCount", "columnCount", "timeSeconds", "timeMinutes", "timeDays", "focalLength", - "applyButtonLabel", - "deleteButtonLabel", - "nextButtonLabel", - "showButtonLabel", - "hideButtonLabel", - "continueButtonLabel", - "cancelTooltip", - "changeTooltip", - "clearTooltip", - "previousTooltip", - "nextTooltip", - "showTooltip", - "hideTooltip", - "actionRemove", - "resetTooltip", - "saveTooltip", "pickTooltip", - "doubleBackExitMessage", - "doNotAskAgain", "sourceStateLoading", "sourceStateCataloguing", "sourceStateLocatingCountries", @@ -96,8 +74,10 @@ "filterFavouriteLabel", "filterNoDateLabel", "filterNoAddressLabel", + "filterLocatedLabel", "filterNoLocationLabel", "filterNoRatingLabel", + "filterTaggedLabel", "filterNoTagLabel", "filterNoTitleLabel", "filterOnThisDayLabel", @@ -246,6 +226,7 @@ "genericSuccessFeedback", "genericFailureFeedback", "genericDangerWarningDialogMessage", + "tooManyItemsErrorDialogMessage", "menuActionConfigureView", "menuActionSelect", "menuActionSelectAll", @@ -387,6 +368,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -452,6 +434,7 @@ "settingsViewerShowInformationSubtitle", "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", "settingsViewerSlideshowTile", @@ -525,6 +508,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -583,30 +567,47 @@ "viewerInfoSearchSuggestionRights", "wallpaperUseScrollEffect", "tagEditorPageTitle", - "tagEditorPageNewTagFieldLabel", - "tagEditorPageAddTagTooltip", - "tagEditorSectionRecent", - "tagEditorSectionPlaceholders", - "filePickerUseThisFolder" + "tagEditorPageNewTagFieldLabel" + ], + + "cs": [ + "filterLocatedLabel", + "filterTaggedLabel", + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsDisplayUseTvInterface" ], "de": [ "columnCount", - "settingsAccessibilityShowPinchGestureAlternatives" + "filterLocatedLabel", + "filterTaggedLabel", + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" + ], + + "el": [ + "tooManyItemsErrorDialogMessage" + ], + + "es": [ + "tooManyItemsErrorDialogMessage" ], "fa": [ - "columnCount", "clearTooltip", - "entryActionCopyToClipboard", - "entryActionShowGeoTiffOnMap", "videoActionPause", "videoActionPlay", "videoActionSelectStreams", "slideshowActionResume", "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", - "filterOnThisDayLabel", + "filterLocatedLabel", + "filterTaggedLabel", "filterRatingRejectedLabel", "filterTypeAnimatedLabel", "filterTypeRawLabel", @@ -619,16 +620,6 @@ "coordinateDmsEast", "coordinateDmsWest", "videoControlsNone", - "mapStyleGoogleNormal", - "mapStyleGoogleHybrid", - "mapStyleGoogleTerrain", - "mapStyleHuaweiNormal", - "mapStyleHuaweiTerrain", - "mapStyleOsmHot", - "mapStyleStamenToner", - "mapStyleStamenWatercolor", - "nameConflictStrategyRename", - "nameConflictStrategyReplace", "nameConflictStrategySkip", "keepScreenOnNever", "keepScreenOnVideoPlayback", @@ -636,16 +627,7 @@ "keepScreenOnAlways", "accessibilityAnimationsRemove", "accessibilityAnimationsKeep", - "displayRefreshRatePreferHighest", - "displayRefreshRatePreferLowest", - "subtitlePositionTop", - "subtitlePositionBottom", "videoPlaybackSkip", - "videoPlaybackMuted", - "videoPlaybackWithSound", - "themeBrightnessLight", - "themeBrightnessDark", - "themeBrightnessBlack", "viewerTransitionSlide", "viewerTransitionParallax", "viewerTransitionFade", @@ -662,13 +644,9 @@ "albumTierNew", "albumTierPinned", "albumTierSpecial", - "albumTierApps", "albumTierRegular", - "storageVolumeDescriptionFallbackPrimary", - "storageVolumeDescriptionFallbackNonPrimary", "rootDirectoryDescription", "otherDirectoryDescription", - "storageAccessDialogMessage", "restrictedAccessDialogMessage", "notEnoughSpaceDialogMessage", "missingSystemFilePickerDialogMessage", @@ -682,8 +660,6 @@ "deleteEntriesConfirmationDialogMessage", "moveUndatedConfirmationDialogMessage", "moveUndatedConfirmationDialogSetDate", - "videoResumeDialogMessage", - "videoStartOverButtonLabel", "videoResumeButtonLabel", "setCoverDialogLatest", "setCoverDialogAuto", @@ -735,6 +711,7 @@ "genericSuccessFeedback", "genericFailureFeedback", "genericDangerWarningDialogMessage", + "tooManyItemsErrorDialogMessage", "menuActionConfigureView", "menuActionSelect", "menuActionSelectAll", @@ -853,6 +830,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionImport", @@ -914,6 +892,7 @@ "settingsViewerShowInformationSubtitle", "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", "settingsViewerSlideshowTile", @@ -987,6 +966,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -1068,6 +1048,8 @@ "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterNoAddressLabel", + "filterLocatedLabel", + "filterTaggedLabel", "keepScreenOnVideoPlayback", "accessibilityAnimationsRemove", "accessibilityAnimationsKeep", @@ -1173,6 +1155,7 @@ "genericSuccessFeedback", "genericFailureFeedback", "genericDangerWarningDialogMessage", + "tooManyItemsErrorDialogMessage", "menuActionConfigureView", "menuActionSelect", "menuActionSelectAll", @@ -1314,6 +1297,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -1379,6 +1363,7 @@ "settingsViewerShowInformationSubtitle", "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", "settingsViewerSlideshowTile", @@ -1452,6 +1437,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -1532,17 +1518,29 @@ "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterNoAddressLabel", + "filterLocatedLabel", + "filterTaggedLabel", "keepScreenOnVideoPlayback", "subtitlePositionTop", "subtitlePositionBottom", + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" ], "lt": [ "columnCount", + "filterLocatedLabel", + "filterTaggedLabel", "keepScreenOnVideoPlayback", - "settingsAccessibilityShowPinchGestureAlternatives" + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ], "nb": [ @@ -1550,8 +1548,14 @@ "entryActionShareImageOnly", "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", + "filterLocatedLabel", + "filterTaggedLabel", "keepScreenOnVideoPlayback", - "settingsAccessibilityShowPinchGestureAlternatives" + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ], "nl": [ @@ -1563,147 +1567,51 @@ "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterNoAddressLabel", + "filterLocatedLabel", + "filterTaggedLabel", "keepScreenOnVideoPlayback", "subtitlePositionTop", "subtitlePositionBottom", "widgetDisplayedItemRandom", "widgetDisplayedItemMostRecent", + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", "settingsViewerShowRatingTags", + "settingsViewerShowDescription", "settingsSubtitleThemeTextPositionTile", "settingsSubtitleThemeTextPositionDialogTitle", "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface", "settingsWidgetDisplayedItem" ], "nn": [ "columnCount", - "sourceStateLoading", "sourceStateCataloguing", - "entryActionShareImageOnly", - "entryActionShareVideoOnly", "entryInfoActionRemoveLocation", - "filterBinLabel", + "filterLocatedLabel", "filterNoLocationLabel", - "filterTypeAnimatedLabel", - "filterTypeMotionPhotoLabel", - "filterTypePanoramaLabel", - "mapStyleOsmHot", - "mapStyleStamenToner", - "keepScreenOnVideoPlayback", + "filterTaggedLabel", "accessibilityAnimationsKeep", "displayRefreshRatePreferHighest", "displayRefreshRatePreferLowest", - "themeBrightnessLight", - "themeBrightnessDark", - "themeBrightnessBlack", - "viewerTransitionSlide", - "viewerTransitionParallax", - "viewerTransitionFade", "wallpaperTargetHome", "wallpaperTargetHomeLock", - "widgetDisplayedItemRandom", - "widgetOpenPageHome", - "restrictedAccessDialogMessage", - "notEnoughSpaceDialogMessage", - "missingSystemFilePickerDialogMessage", - "unsupportedTypeDialogMessage", - "nameConflictDialogSingleSourceMessage", - "nameConflictDialogMultipleSourceMessage", - "addShortcutDialogLabel", - "addShortcutButtonLabel", - "noMatchingAppDialogMessage", - "binEntriesConfirmationDialogMessage", - "deleteEntriesConfirmationDialogMessage", - "moveUndatedConfirmationDialogMessage", - "moveUndatedConfirmationDialogSetDate", - "videoResumeDialogMessage", - "videoStartOverButtonLabel", - "videoResumeButtonLabel", - "setCoverDialogLatest", - "setCoverDialogAuto", "setCoverDialogCustom", - "hideFilterConfirmationDialogMessage", - "newAlbumDialogTitle", - "newAlbumDialogNameLabel", - "newAlbumDialogNameLabelAlreadyExistsHelper", - "newAlbumDialogStorageLabel", - "renameAlbumDialogLabel", - "renameAlbumDialogLabelAlreadyExistsHelper", - "renameEntrySetPageTitle", - "renameEntrySetPagePatternFieldLabel", - "renameEntrySetPageInsertTooltip", - "renameEntrySetPagePreviewSectionTitle", - "renameProcessorCounter", - "renameProcessorName", - "deleteSingleAlbumConfirmationDialogMessage", - "deleteMultiAlbumConfirmationDialogMessage", - "exportEntryDialogFormat", - "exportEntryDialogWidth", - "exportEntryDialogHeight", - "renameEntryDialogLabel", - "editEntryDialogCopyFromItem", "editEntryDialogTargetFieldsHeader", - "editEntryDateDialogTitle", "editEntryDateDialogSetCustom", - "editEntryDateDialogCopyField", - "editEntryDateDialogExtractFromTitle", - "editEntryDateDialogShift", - "editEntryDateDialogSourceFileModifiedDate", - "durationDialogHours", - "durationDialogMinutes", - "durationDialogSeconds", "editEntryLocationDialogTitle", "editEntryLocationDialogSetCustom", - "editEntryLocationDialogChooseOnMap", - "editEntryLocationDialogLatitude", - "editEntryLocationDialogLongitude", "locationPickerUseThisLocationButton", - "editEntryRatingDialogTitle", - "removeEntryMetadataDialogTitle", - "removeEntryMetadataDialogMore", "removeEntryMetadataMotionPhotoXmpWarningDialogMessage", - "videoSpeedDialogLabel", - "videoStreamSelectionDialogVideo", - "videoStreamSelectionDialogAudio", - "videoStreamSelectionDialogText", - "videoStreamSelectionDialogOff", - "videoStreamSelectionDialogTrack", - "videoStreamSelectionDialogNoSelection", - "genericSuccessFeedback", - "genericFailureFeedback", - "genericDangerWarningDialogMessage", - "menuActionConfigureView", - "menuActionSelect", - "menuActionSelectAll", - "menuActionSelectNone", - "menuActionMap", - "menuActionSlideshow", - "menuActionStats", + "tooManyItemsErrorDialogMessage", "viewDialogSortSectionTitle", - "viewDialogGroupSectionTitle", - "viewDialogLayoutSectionTitle", "viewDialogReverseSortOrder", - "tileLayoutMosaic", - "tileLayoutGrid", - "tileLayoutList", - "coverDialogTabCover", - "coverDialogTabApp", - "coverDialogTabColor", - "appPickDialogTitle", - "appPickDialogNone", - "aboutPageTitle", - "aboutLinkLicense", "aboutLinkPolicy", - "aboutBugSectionTitle", "aboutBugSaveLogInstruction", - "aboutBugCopyInfoInstruction", - "aboutBugCopyInfoButton", "aboutBugReportInstruction", "aboutBugReportButton", "aboutCreditsSectionTitle", - "aboutCreditsWorldAtlas1", - "aboutCreditsWorldAtlas2", - "aboutTranslatorsSectionTitle", "aboutLicensesSectionTitle", "aboutLicensesBanner", "aboutLicensesAndroidLibrariesSectionTitle", @@ -1813,6 +1721,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -1878,6 +1787,7 @@ "settingsViewerShowInformationSubtitle", "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", "settingsViewerSlideshowTile", @@ -1944,613 +1854,43 @@ "settingsRemoveAnimationsDialogTitle", "settingsTimeToTakeActionTile", "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplaySectionTitle", "settingsThemeBrightnessTile", "settingsThemeBrightnessDialogTitle", - "settingsThemeColorHighlights", "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", - "settingsLanguageSectionTitle", - "settingsLanguageTile", - "settingsLanguagePageTitle", - "settingsCoordinateFormatTile", - "settingsCoordinateFormatDialogTitle", - "settingsUnitSystemTile", - "settingsUnitSystemDialogTitle", - "settingsScreenSaverPageTitle", - "settingsWidgetPageTitle", - "settingsWidgetShowOutline", + "settingsDisplayUseTvInterface", "settingsWidgetOpenPage", - "settingsWidgetDisplayedItem", - "settingsCollectionTile", - "statsPageTitle", "statsWithGps", - "statsTopCountriesSectionTitle", - "statsTopPlacesSectionTitle", - "statsTopTagsSectionTitle", - "statsTopAlbumsSectionTitle", - "viewerOpenPanoramaButtonLabel", - "viewerSetWallpaperButtonLabel", - "viewerErrorUnknown", - "viewerErrorDoesNotExist", "viewerInfoPageTitle", - "viewerInfoBackToViewerTooltip", - "viewerInfoUnknown", "viewerInfoLabelDescription", - "viewerInfoLabelTitle", - "viewerInfoLabelDate", - "viewerInfoLabelResolution", - "viewerInfoLabelSize", - "viewerInfoLabelUri", - "viewerInfoLabelPath", - "viewerInfoLabelDuration", - "viewerInfoLabelOwner", - "viewerInfoLabelCoordinates", - "viewerInfoLabelAddress", - "mapStyleDialogTitle", - "mapStyleTooltip", - "mapZoomInTooltip", - "mapZoomOutTooltip", "mapPointNorthUpTooltip", "mapAttributionOsmHot", "mapAttributionStamen", - "openMapPageTooltip", "mapEmptyRegion", - "viewerInfoOpenEmbeddedFailureFeedback", - "viewerInfoOpenLinkText", - "viewerInfoViewXmlLinkText", - "viewerInfoSearchFieldLabel", - "viewerInfoSearchEmpty", - "viewerInfoSearchSuggestionDate", - "viewerInfoSearchSuggestionDescription", "viewerInfoSearchSuggestionDimensions", - "viewerInfoSearchSuggestionResolution", - "viewerInfoSearchSuggestionRights", - "wallpaperUseScrollEffect", - "tagEditorPageTitle", - "tagEditorPageNewTagFieldLabel", - "tagEditorPageAddTagTooltip", - "tagEditorSectionRecent", - "tagEditorSectionPlaceholders", - "tagPlaceholderCountry", - "tagPlaceholderPlace", - "panoramaEnableSensorControl", - "panoramaDisableSensorControl", - "sourceViewerPageTitle", - "filePickerShowHiddenFiles", - "filePickerDoNotShowHiddenFiles", - "filePickerOpenFrom", - "filePickerNoItems", - "filePickerUseThisFolder" + "wallpaperUseScrollEffect" ], "pl": [ - "itemCount", - "columnCount", - "timeSeconds", - "timeMinutes", - "timeDays", - "focalLength", - "entryActionShareImageOnly", - "entryActionShareVideoOnly", - "entryInfoActionExportMetadata", - "entryInfoActionRemoveLocation", - "filterAspectRatioLandscapeLabel", - "filterAspectRatioPortraitLabel", - "filterNoAddressLabel", - "filterTypeRawLabel", - "filterTypeSphericalVideoLabel", - "filterTypeGeotiffLabel", - "filterMimeImageLabel", - "filterMimeVideoLabel", - "coordinateFormatDms", - "coordinateFormatDecimal", - "coordinateDms", - "coordinateDmsNorth", - "coordinateDmsSouth", - "coordinateDmsEast", - "coordinateDmsWest", - "unitSystemMetric", - "unitSystemImperial", - "videoLoopModeNever", - "videoLoopModeShortOnly", - "videoLoopModeAlways", - "videoControlsPlay", - "videoControlsPlaySeek", - "videoControlsPlayOutside", - "videoControlsNone", - "mapStyleGoogleNormal", - "mapStyleGoogleHybrid", - "mapStyleGoogleTerrain", - "mapStyleHuaweiNormal", - "mapStyleHuaweiTerrain", - "mapStyleOsmHot", - "mapStyleStamenToner", - "mapStyleStamenWatercolor", - "nameConflictStrategyRename", - "nameConflictStrategyReplace", - "nameConflictStrategySkip", - "keepScreenOnNever", - "keepScreenOnVideoPlayback", - "keepScreenOnViewerOnly", - "keepScreenOnAlways", - "accessibilityAnimationsRemove", - "accessibilityAnimationsKeep", - "displayRefreshRatePreferHighest", - "displayRefreshRatePreferLowest", - "subtitlePositionTop", - "subtitlePositionBottom", - "videoPlaybackSkip", - "videoPlaybackMuted", - "videoPlaybackWithSound", - "themeBrightnessLight", - "themeBrightnessDark", - "themeBrightnessBlack", - "viewerTransitionSlide", - "viewerTransitionParallax", - "viewerTransitionFade", - "viewerTransitionZoomIn", - "viewerTransitionNone", - "wallpaperTargetHome", - "wallpaperTargetLock", - "wallpaperTargetHomeLock", - "widgetDisplayedItemRandom", - "widgetDisplayedItemMostRecent", - "widgetOpenPageHome", - "widgetOpenPageCollection", - "widgetOpenPageViewer", - "albumTierNew", - "albumTierPinned", - "albumTierSpecial", - "albumTierApps", - "albumTierRegular", - "storageVolumeDescriptionFallbackPrimary", - "storageVolumeDescriptionFallbackNonPrimary", - "rootDirectoryDescription", - "otherDirectoryDescription", - "storageAccessDialogMessage", - "restrictedAccessDialogMessage", - "notEnoughSpaceDialogMessage", - "missingSystemFilePickerDialogMessage", - "unsupportedTypeDialogMessage", - "nameConflictDialogSingleSourceMessage", - "nameConflictDialogMultipleSourceMessage", - "addShortcutDialogLabel", - "addShortcutButtonLabel", - "noMatchingAppDialogMessage", - "binEntriesConfirmationDialogMessage", - "deleteEntriesConfirmationDialogMessage", - "moveUndatedConfirmationDialogMessage", - "moveUndatedConfirmationDialogSetDate", - "videoResumeDialogMessage", - "videoStartOverButtonLabel", - "videoResumeButtonLabel", - "setCoverDialogLatest", - "setCoverDialogAuto", - "setCoverDialogCustom", - "hideFilterConfirmationDialogMessage", - "newAlbumDialogTitle", - "newAlbumDialogNameLabel", - "newAlbumDialogNameLabelAlreadyExistsHelper", - "newAlbumDialogStorageLabel", - "renameAlbumDialogLabel", - "renameAlbumDialogLabelAlreadyExistsHelper", - "renameEntrySetPageTitle", - "renameEntrySetPagePatternFieldLabel", - "renameEntrySetPageInsertTooltip", - "renameEntrySetPagePreviewSectionTitle", - "renameProcessorCounter", - "renameProcessorName", - "deleteSingleAlbumConfirmationDialogMessage", - "deleteMultiAlbumConfirmationDialogMessage", - "exportEntryDialogFormat", - "exportEntryDialogWidth", - "exportEntryDialogHeight", - "renameEntryDialogLabel", - "editEntryDialogCopyFromItem", - "editEntryDialogTargetFieldsHeader", - "editEntryDateDialogTitle", - "editEntryDateDialogSetCustom", - "editEntryDateDialogCopyField", - "editEntryDateDialogExtractFromTitle", - "editEntryDateDialogShift", - "editEntryDateDialogSourceFileModifiedDate", - "durationDialogHours", - "durationDialogMinutes", - "durationDialogSeconds", - "editEntryLocationDialogTitle", - "editEntryLocationDialogSetCustom", - "editEntryLocationDialogChooseOnMap", - "editEntryLocationDialogLatitude", - "editEntryLocationDialogLongitude", - "locationPickerUseThisLocationButton", - "editEntryRatingDialogTitle", - "removeEntryMetadataDialogTitle", - "removeEntryMetadataDialogMore", - "removeEntryMetadataMotionPhotoXmpWarningDialogMessage", - "videoSpeedDialogLabel", - "videoStreamSelectionDialogVideo", - "videoStreamSelectionDialogAudio", - "videoStreamSelectionDialogText", - "videoStreamSelectionDialogOff", - "videoStreamSelectionDialogTrack", - "videoStreamSelectionDialogNoSelection", - "genericSuccessFeedback", - "genericFailureFeedback", - "genericDangerWarningDialogMessage", - "menuActionConfigureView", - "menuActionSelect", - "menuActionSelectAll", - "menuActionSelectNone", - "menuActionMap", - "menuActionSlideshow", - "menuActionStats", - "viewDialogSortSectionTitle", - "viewDialogGroupSectionTitle", - "viewDialogLayoutSectionTitle", - "viewDialogReverseSortOrder", - "tileLayoutMosaic", - "tileLayoutGrid", - "tileLayoutList", - "coverDialogTabCover", - "coverDialogTabApp", - "coverDialogTabColor", - "appPickDialogTitle", - "appPickDialogNone", - "aboutPageTitle", - "aboutLinkLicense", - "aboutLinkPolicy", - "aboutBugSectionTitle", - "aboutBugSaveLogInstruction", - "aboutBugCopyInfoInstruction", - "aboutBugCopyInfoButton", - "aboutBugReportInstruction", - "aboutBugReportButton", - "aboutCreditsSectionTitle", - "aboutCreditsWorldAtlas1", - "aboutCreditsWorldAtlas2", - "aboutTranslatorsSectionTitle", - "aboutLicensesSectionTitle", - "aboutLicensesBanner", - "aboutLicensesAndroidLibrariesSectionTitle", - "aboutLicensesFlutterPluginsSectionTitle", - "aboutLicensesFlutterPackagesSectionTitle", - "aboutLicensesDartPackagesSectionTitle", - "aboutLicensesShowAllButtonLabel", - "policyPageTitle", - "collectionPageTitle", - "collectionPickPageTitle", - "collectionSelectPageTitle", - "collectionActionShowTitleSearch", - "collectionActionHideTitleSearch", - "collectionActionAddShortcut", - "collectionActionEmptyBin", - "collectionActionCopy", - "collectionActionMove", - "collectionActionRescan", - "collectionActionEdit", - "collectionSearchTitlesHintText", - "collectionGroupAlbum", - "collectionGroupMonth", - "collectionGroupDay", - "collectionGroupNone", - "sectionUnknown", - "dateToday", - "dateYesterday", - "dateThisMonth", - "collectionDeleteFailureFeedback", - "collectionCopyFailureFeedback", - "collectionMoveFailureFeedback", - "collectionRenameFailureFeedback", - "collectionEditFailureFeedback", - "collectionExportFailureFeedback", - "collectionCopySuccessFeedback", - "collectionMoveSuccessFeedback", - "collectionRenameSuccessFeedback", - "collectionEditSuccessFeedback", - "collectionEmptyFavourites", - "collectionEmptyVideos", - "collectionEmptyImages", - "collectionEmptyGrantAccessButtonLabel", - "collectionSelectSectionTooltip", - "collectionDeselectSectionTooltip", - "drawerAboutButton", - "drawerSettingsButton", - "drawerCollectionAll", - "drawerCollectionFavourites", - "drawerCollectionImages", - "drawerCollectionVideos", - "drawerCollectionAnimated", - "drawerCollectionMotionPhotos", - "drawerCollectionPanoramas", - "drawerCollectionRaws", - "drawerCollectionSphericalVideos", - "drawerAlbumPage", - "drawerCountryPage", - "drawerTagPage", - "sortByDate", - "sortByName", - "sortByItemCount", - "sortBySize", - "sortByAlbumFileName", - "sortByRating", - "sortOrderNewestFirst", - "sortOrderOldestFirst", - "sortOrderAtoZ", - "sortOrderZtoA", - "sortOrderHighestFirst", - "sortOrderLowestFirst", - "sortOrderLargestFirst", - "sortOrderSmallestFirst", - "albumGroupTier", - "albumGroupType", - "albumGroupVolume", - "albumGroupNone", - "albumMimeTypeMixed", - "albumPickPageTitleCopy", - "albumPickPageTitleExport", - "albumPickPageTitleMove", - "albumPickPageTitlePick", - "albumCamera", - "albumDownload", - "albumScreenshots", - "albumScreenRecordings", - "albumVideoCaptures", - "albumPageTitle", - "albumEmpty", - "createAlbumTooltip", - "createAlbumButtonLabel", - "newFilterBanner", - "countryPageTitle", - "countryEmpty", - "tagPageTitle", - "tagEmpty", - "binPageTitle", - "searchCollectionFieldHint", - "searchRecentSectionTitle", - "searchDateSectionTitle", - "searchAlbumsSectionTitle", - "searchCountriesSectionTitle", - "searchPlacesSectionTitle", - "searchTagsSectionTitle", - "searchRatingSectionTitle", - "searchMetadataSectionTitle", - "settingsPageTitle", - "settingsSystemDefault", - "settingsDefault", - "settingsDisabled", - "settingsSearchFieldLabel", - "settingsSearchEmpty", - "settingsActionExport", - "settingsActionExportDialogTitle", - "settingsActionImport", - "settingsActionImportDialogTitle", - "appExportCovers", - "appExportFavourites", - "appExportSettings", - "settingsNavigationSectionTitle", - "settingsHomeTile", - "settingsHomeDialogTitle", - "settingsShowBottomNavigationBar", - "settingsKeepScreenOnTile", - "settingsKeepScreenOnDialogTitle", - "settingsDoubleBackExit", - "settingsConfirmationTile", - "settingsConfirmationDialogTitle", - "settingsConfirmationBeforeDeleteItems", - "settingsConfirmationBeforeMoveToBinItems", - "settingsConfirmationBeforeMoveUndatedItems", - "settingsConfirmationAfterMoveToBinItems", - "settingsNavigationDrawerTile", - "settingsNavigationDrawerEditorPageTitle", - "settingsNavigationDrawerBanner", - "settingsNavigationDrawerTabTypes", - "settingsNavigationDrawerTabAlbums", - "settingsNavigationDrawerTabPages", - "settingsNavigationDrawerAddAlbum", - "settingsThumbnailSectionTitle", - "settingsThumbnailOverlayTile", - "settingsThumbnailOverlayPageTitle", - "settingsThumbnailShowFavouriteIcon", - "settingsThumbnailShowTagIcon", - "settingsThumbnailShowLocationIcon", - "settingsThumbnailShowMotionPhotoIcon", - "settingsThumbnailShowRating", - "settingsThumbnailShowRawIcon", - "settingsThumbnailShowVideoDuration", - "settingsCollectionQuickActionsTile", - "settingsCollectionQuickActionEditorPageTitle", - "settingsCollectionQuickActionTabBrowsing", - "settingsCollectionQuickActionTabSelecting", - "settingsCollectionBrowsingQuickActionEditorBanner", - "settingsCollectionSelectionQuickActionEditorBanner", - "settingsViewerSectionTitle", - "settingsViewerGestureSideTapNext", - "settingsViewerUseCutout", - "settingsViewerMaximumBrightness", - "settingsMotionPhotoAutoPlay", - "settingsImageBackground", - "settingsViewerQuickActionsTile", - "settingsViewerQuickActionEditorPageTitle", - "settingsViewerQuickActionEditorBanner", - "settingsViewerQuickActionEditorDisplayedButtonsSectionTitle", - "settingsViewerQuickActionEditorAvailableButtonsSectionTitle", - "settingsViewerQuickActionEmpty", - "settingsViewerOverlayTile", - "settingsViewerOverlayPageTitle", - "settingsViewerShowOverlayOnOpening", - "settingsViewerShowMinimap", - "settingsViewerShowInformation", - "settingsViewerShowInformationSubtitle", - "settingsViewerShowRatingTags", - "settingsViewerShowShootingDetails", - "settingsViewerShowOverlayThumbnails", - "settingsViewerEnableOverlayBlurEffect", - "settingsViewerSlideshowTile", - "settingsViewerSlideshowPageTitle", - "settingsSlideshowRepeat", - "settingsSlideshowShuffle", - "settingsSlideshowFillScreen", - "settingsSlideshowAnimatedZoomEffect", - "settingsSlideshowTransitionTile", - "settingsSlideshowIntervalTile", - "settingsSlideshowVideoPlaybackTile", - "settingsSlideshowVideoPlaybackDialogTitle", - "settingsVideoPageTitle", - "settingsVideoSectionTitle", - "settingsVideoShowVideos", - "settingsVideoEnableHardwareAcceleration", - "settingsVideoAutoPlay", - "settingsVideoLoopModeTile", - "settingsVideoLoopModeDialogTitle", - "settingsSubtitleThemeTile", - "settingsSubtitleThemePageTitle", - "settingsSubtitleThemeSample", - "settingsSubtitleThemeTextAlignmentTile", - "settingsSubtitleThemeTextAlignmentDialogTitle", - "settingsSubtitleThemeTextPositionTile", - "settingsSubtitleThemeTextPositionDialogTitle", - "settingsSubtitleThemeTextSize", - "settingsSubtitleThemeShowOutline", - "settingsSubtitleThemeTextColor", - "settingsSubtitleThemeTextOpacity", - "settingsSubtitleThemeBackgroundColor", - "settingsSubtitleThemeBackgroundOpacity", - "settingsSubtitleThemeTextAlignmentLeft", - "settingsSubtitleThemeTextAlignmentCenter", - "settingsSubtitleThemeTextAlignmentRight", - "settingsVideoControlsTile", - "settingsVideoControlsPageTitle", - "settingsVideoButtonsTile", - "settingsVideoGestureDoubleTapTogglePlay", - "settingsVideoGestureSideDoubleTapSeek", - "settingsPrivacySectionTitle", - "settingsAllowInstalledAppAccess", - "settingsAllowInstalledAppAccessSubtitle", - "settingsAllowErrorReporting", - "settingsSaveSearchHistory", - "settingsEnableBin", - "settingsEnableBinSubtitle", - "settingsAllowMediaManagement", - "settingsHiddenItemsTile", - "settingsHiddenItemsPageTitle", - "settingsHiddenItemsTabFilters", - "settingsHiddenFiltersBanner", - "settingsHiddenFiltersEmpty", - "settingsHiddenItemsTabPaths", - "settingsHiddenPathsBanner", - "addPathTooltip", - "settingsStorageAccessTile", - "settingsStorageAccessPageTitle", - "settingsStorageAccessBanner", - "settingsStorageAccessEmpty", - "settingsStorageAccessRevokeTooltip", - "settingsAccessibilitySectionTitle", - "settingsRemoveAnimationsTile", - "settingsRemoveAnimationsDialogTitle", - "settingsTimeToTakeActionTile", - "settingsAccessibilityShowPinchGestureAlternatives", - "settingsDisplaySectionTitle", - "settingsThemeBrightnessTile", - "settingsThemeBrightnessDialogTitle", - "settingsThemeColorHighlights", - "settingsThemeEnableDynamicColor", - "settingsDisplayRefreshRateModeTile", - "settingsDisplayRefreshRateModeDialogTitle", - "settingsLanguageSectionTitle", - "settingsLanguageTile", - "settingsLanguagePageTitle", - "settingsCoordinateFormatTile", - "settingsCoordinateFormatDialogTitle", - "settingsUnitSystemTile", - "settingsUnitSystemDialogTitle", - "settingsScreenSaverPageTitle", - "settingsWidgetPageTitle", - "settingsWidgetShowOutline", - "settingsWidgetOpenPage", - "settingsWidgetDisplayedItem", - "settingsCollectionTile", - "statsPageTitle", - "statsWithGps", - "statsTopCountriesSectionTitle", - "statsTopPlacesSectionTitle", - "statsTopTagsSectionTitle", - "statsTopAlbumsSectionTitle", - "viewerOpenPanoramaButtonLabel", - "viewerSetWallpaperButtonLabel", - "viewerErrorUnknown", - "viewerErrorDoesNotExist", - "viewerInfoPageTitle", - "viewerInfoBackToViewerTooltip", - "viewerInfoUnknown", - "viewerInfoLabelDescription", - "viewerInfoLabelTitle", - "viewerInfoLabelDate", - "viewerInfoLabelResolution", - "viewerInfoLabelSize", - "viewerInfoLabelUri", - "viewerInfoLabelPath", - "viewerInfoLabelDuration", - "viewerInfoLabelOwner", - "viewerInfoLabelCoordinates", - "viewerInfoLabelAddress", - "mapStyleDialogTitle", - "mapStyleTooltip", - "mapZoomInTooltip", - "mapZoomOutTooltip", - "mapPointNorthUpTooltip", - "mapAttributionOsmHot", - "mapAttributionStamen", - "openMapPageTooltip", - "mapEmptyRegion", - "viewerInfoOpenEmbeddedFailureFeedback", - "viewerInfoOpenLinkText", - "viewerInfoViewXmlLinkText", - "viewerInfoSearchFieldLabel", - "viewerInfoSearchEmpty", - "viewerInfoSearchSuggestionDate", - "viewerInfoSearchSuggestionDescription", - "viewerInfoSearchSuggestionDimensions", - "viewerInfoSearchSuggestionResolution", - "viewerInfoSearchSuggestionRights", - "wallpaperUseScrollEffect", - "tagEditorPageTitle", - "tagEditorPageNewTagFieldLabel", - "tagEditorPageAddTagTooltip", - "tagEditorSectionRecent", - "tagEditorSectionPlaceholders", - "tagPlaceholderCountry", - "tagPlaceholderPlace", - "panoramaEnableSensorControl", - "panoramaDisableSensorControl", - "sourceViewerPageTitle", - "filePickerShowHiddenFiles", - "filePickerDoNotShowHiddenFiles", - "filePickerOpenFrom", - "filePickerNoItems", - "filePickerUseThisFolder" + "tooManyItemsErrorDialogMessage" ], "pt": [ "columnCount", - "entryActionShareImageOnly", - "entryActionShareVideoOnly", - "entryInfoActionExportMetadata", - "entryInfoActionRemoveLocation", - "filterAspectRatioLandscapeLabel", - "filterAspectRatioPortraitLabel", - "filterNoAddressLabel", - "keepScreenOnVideoPlayback", - "subtitlePositionTop", - "subtitlePositionBottom", - "widgetDisplayedItemRandom", - "widgetDisplayedItemMostRecent", - "settingsViewerShowRatingTags", - "settingsSubtitleThemeTextPositionTile", - "settingsSubtitleThemeTextPositionDialogTitle", - "settingsAccessibilityShowPinchGestureAlternatives", - "settingsWidgetDisplayedItem" + "tooManyItemsErrorDialogMessage" + ], + + "ro": [ + "tooManyItemsErrorDialogMessage" + ], + + "ru": [ + "filterLocatedLabel", + "filterTaggedLabel", + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsDisplayUseTvInterface" ], "th": [ @@ -2569,6 +1909,8 @@ "filterAspectRatioLandscapeLabel", "filterAspectRatioPortraitLabel", "filterNoAddressLabel", + "filterLocatedLabel", + "filterTaggedLabel", "coordinateDms", "keepScreenOnVideoPlayback", "keepScreenOnViewerOnly", @@ -2585,6 +1927,7 @@ "editEntryDateDialogExtractFromTitle", "editEntryDateDialogShift", "removeEntryMetadataDialogTitle", + "tooManyItemsErrorDialogMessage", "collectionActionShowTitleSearch", "collectionActionHideTitleSearch", "collectionActionAddShortcut", @@ -2683,6 +2026,7 @@ "settingsSystemDefault", "settingsDefault", "settingsDisabled", + "settingsModificationWarningDialogMessage", "settingsSearchFieldLabel", "settingsSearchEmpty", "settingsActionExport", @@ -2748,6 +2092,7 @@ "settingsViewerShowInformationSubtitle", "settingsViewerShowRatingTags", "settingsViewerShowShootingDetails", + "settingsViewerShowDescription", "settingsViewerShowOverlayThumbnails", "settingsViewerEnableOverlayBlurEffect", "settingsViewerSlideshowTile", @@ -2821,6 +2166,7 @@ "settingsThemeEnableDynamicColor", "settingsDisplayRefreshRateModeTile", "settingsDisplayRefreshRateModeDialogTitle", + "settingsDisplayUseTvInterface", "settingsLanguageSectionTitle", "settingsLanguageTile", "settingsLanguagePageTitle", @@ -2895,12 +2241,28 @@ "filePickerUseThisFolder" ], + "uk": [ + "tooManyItemsErrorDialogMessage" + ], + "zh": [ - "settingsAccessibilityShowPinchGestureAlternatives" + "filterLocatedLabel", + "filterTaggedLabel", + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ], "zh_Hant": [ "columnCount", - "settingsAccessibilityShowPinchGestureAlternatives" + "filterLocatedLabel", + "filterTaggedLabel", + "tooManyItemsErrorDialogMessage", + "settingsModificationWarningDialogMessage", + "settingsViewerShowDescription", + "settingsAccessibilityShowPinchGestureAlternatives", + "settingsDisplayUseTvInterface" ] } diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index 6b14f2fdf..ec10c2286 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,5 +1,5 @@ -In v1.7.8: -- Android TV support -- long-press on the move/rate/tag quick actions in the viewer for quicker actions -- enjoy the app in Romanian, Lithuanian, Norwegian (Bokmål), Chinese (Traditional) & Ukrainian +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 \ No newline at end of file