Merge branch 'develop'
2
.github/workflows/check.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
# Available versions may lag behind https://github.com/flutter/flutter.git
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.3.2'
|
||||
flutter-version: '3.3.4'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Clone the repository.
|
||||
|
|
10
.github/workflows/release.yml
vendored
|
@ -19,7 +19,7 @@ jobs:
|
|||
# Available versions may lag behind https://github.com/flutter/flutter.git
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.3.2'
|
||||
flutter-version: '3.3.4'
|
||||
channel: 'stable'
|
||||
|
||||
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
|
||||
|
@ -56,15 +56,15 @@ jobs:
|
|||
rm release.keystore.asc
|
||||
mkdir outputs
|
||||
(cd scripts/; ./apply_flavor_play.sh)
|
||||
flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.2.sksl.json
|
||||
flutter build appbundle -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.4.sksl.json
|
||||
cp build/app/outputs/bundle/playRelease/*.aab outputs
|
||||
flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.2.sksl.json
|
||||
flutter build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders_3.3.4.sksl.json
|
||||
cp build/app/outputs/apk/play/release/*.apk outputs
|
||||
(cd scripts/; ./apply_flavor_huawei.sh)
|
||||
flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.3.2.sksl.json
|
||||
flutter build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders_3.3.4.sksl.json
|
||||
cp build/app/outputs/apk/huawei/release/*.apk outputs
|
||||
(cd scripts/; ./apply_flavor_izzy.sh)
|
||||
flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.3.2.sksl.json
|
||||
flutter build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi --bundle-sksl-path shaders_3.3.4.sksl.json
|
||||
cp build/app/outputs/apk/izzy/release/*.apk outputs
|
||||
rm $AVES_STORE_FILE
|
||||
env:
|
||||
|
|
26
CHANGELOG.md
|
@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
## <a id="v1.7.1"></a>[v1.7.1] - 2022-10-09
|
||||
|
||||
### Added
|
||||
|
||||
- mosaic layout
|
||||
- reverse filters to filter out/in
|
||||
- Collection: selection edit actions available as quick actions
|
||||
- Albums: group by content type
|
||||
- Info: improved display for XMP
|
||||
- Stats: top albums
|
||||
- Stats: open full top listings
|
||||
- Video: option for muted auto play
|
||||
- Slideshow / Screen saver: option for no transition
|
||||
- Slideshow / Screen saver: animated zoom effect
|
||||
- Widget: tap action setting
|
||||
- Wallpaper: scroll effect option
|
||||
|
||||
### Changed
|
||||
|
||||
- upgraded Flutter to stable v3.3.4
|
||||
|
||||
### Fixed
|
||||
|
||||
- restoring to missing Download subdir
|
||||
- crash when cataloguing PNG with large chunks
|
||||
|
||||
## <a id="v1.7.0"></a>[v1.7.0] - 2022-09-19
|
||||
|
||||
### Added
|
||||
|
|
|
@ -155,22 +155,22 @@ repositories {
|
|||
dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.3'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.4'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
||||
implementation 'com.commonsware.cwac:document:0.4.1'
|
||||
implementation 'com.commonsware.cwac:document:0.5.0'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.18.0'
|
||||
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||
implementation 'com.github.deckerst:Android-TiffBitmapFactory:876e53870a'
|
||||
// forked, built by JitPack, cf https://jitpack.io/p/deckerst/pixymeta-android
|
||||
implementation 'com.github.deckerst:pixymeta-android:706bd73d6e'
|
||||
implementation 'com.github.bumptech.glide:glide:4.13.2'
|
||||
implementation 'com.github.bumptech.glide:glide:4.14.2'
|
||||
|
||||
// huawei flavor only
|
||||
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.5.2.300'
|
||||
huaweiImplementation 'com.huawei.agconnect:agconnect-core:1.7.2.300'
|
||||
|
||||
kapt 'androidx.annotation:annotation:1.4.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.13.0'
|
||||
kapt 'androidx.annotation:annotation:1.5.0'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.14.2'
|
||||
|
||||
compileOnly rootProject.findProject(':streams_channel')
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
Android Studio Chipmunk (2021.2.1) recommends:
|
||||
Gradle v7.4 / Android Gradle Plugin v7.3.0 recommend:
|
||||
- removing "package" from AndroidManifest.xml
|
||||
- adding it as "namespace" in app/build.gradle
|
||||
This change eventually prevents building the app with Flutter v3.0.2.
|
||||
This change eventually prevents building the app with Flutter v3.3.3.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves
|
|||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
|
@ -21,7 +22,7 @@ import io.flutter.plugin.common.MethodCall
|
|||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
||||
class AnalysisService : Service() {
|
||||
private var flutterEngine: FlutterEngine? = null
|
||||
private var backgroundChannel: MethodChannel? = null
|
||||
private var serviceLooper: Looper? = null
|
||||
|
@ -30,35 +31,13 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
|||
|
||||
override fun onCreate() {
|
||||
Log.i(LOG_TAG, "Create analysis service")
|
||||
val context = this
|
||||
|
||||
runBlocking {
|
||||
FlutterUtils.initFlutterEngine(context, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
|
||||
FlutterUtils.initFlutterEngine(this@AnalysisService, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) {
|
||||
flutterEngine = it
|
||||
}
|
||||
}
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// channels for analysis
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
||||
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
|
||||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
|
||||
|
||||
// channel for service management
|
||||
backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
|
||||
setMethodCallHandler(context)
|
||||
}
|
||||
initChannels(this)
|
||||
|
||||
HandlerThread("Analysis service handler", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||
start()
|
||||
|
@ -94,7 +73,36 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
|||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
private fun detachAndStop() {
|
||||
analysisServiceBinder.detach()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun initChannels(context: Context) {
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// channels for analysis
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
|
||||
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context))
|
||||
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
|
||||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
|
||||
|
||||
// channel for service management
|
||||
backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
|
||||
setMethodCallHandler { call, result -> onMethodCall(call, result) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"initialized" -> {
|
||||
Log.d(LOG_TAG, "background channel is ready")
|
||||
|
@ -119,11 +127,6 @@ class AnalysisService : MethodChannel.MethodCallHandler, Service() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun detachAndStop() {
|
||||
analysisServiceBinder.detach()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun buildNotification(title: String? = null, message: String? = null): Notification {
|
||||
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
|
|
|
@ -3,7 +3,6 @@ package deckers.thibault.aves
|
|||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class HomeWidgetSettingsActivity : MainActivity() {
|
||||
|
@ -26,7 +25,7 @@ class HomeWidgetSettingsActivity : MainActivity() {
|
|||
}
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
MethodChannel(messenger, CHANNEL).setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
|
||||
MethodChannel(messenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"configure" -> {
|
||||
result.success(null)
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.appwidget.AppWidgetManager
|
|||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
@ -62,6 +64,18 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density
|
||||
|
||||
private fun getWidgetSizePx(context: Context, widgetInfo: Bundle): Pair<Int, Int> {
|
||||
val devicePixelRatio = getDevicePixelRatio()
|
||||
val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
|
||||
val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
|
||||
val widthPx = (widgetInfo.getInt(widthKey) * devicePixelRatio).roundToInt()
|
||||
val heightPx = (widgetInfo.getInt(heightKey) * devicePixelRatio).roundToInt()
|
||||
return Pair(widthPx, heightPx)
|
||||
}
|
||||
|
||||
private suspend fun getBytes(
|
||||
context: Context,
|
||||
widgetId: Int,
|
||||
|
@ -69,9 +83,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
drawEntryImage: Boolean,
|
||||
reuseEntry: Boolean = false,
|
||||
): ByteArray? {
|
||||
val devicePixelRatio = context.resources.displayMetrics.density
|
||||
val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt()
|
||||
val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt()
|
||||
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
|
||||
if (widthPx == 0 || heightPx == 0) return null
|
||||
|
||||
initFlutterEngine(context)
|
||||
|
@ -85,7 +97,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
"widgetId" to widgetId,
|
||||
"widthPx" to widthPx,
|
||||
"heightPx" to heightPx,
|
||||
"devicePixelRatio" to devicePixelRatio,
|
||||
"devicePixelRatio" to getDevicePixelRatio(),
|
||||
"drawEntryImage" to drawEntryImage,
|
||||
"reuseEntry" to reuseEntry,
|
||||
), object : MethodChannel.Result {
|
||||
|
@ -120,9 +132,8 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
) {
|
||||
bytes ?: return
|
||||
|
||||
val devicePixelRatio = context.resources.displayMetrics.density
|
||||
val widthPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) * devicePixelRatio).roundToInt()
|
||||
val heightPx = (widgetInfo.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) * devicePixelRatio).roundToInt()
|
||||
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
|
||||
if (widthPx == 0 || heightPx == 0) return
|
||||
|
||||
try {
|
||||
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
|
||||
|
@ -198,6 +209,5 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(context, args) }
|
||||
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(context, args) }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import deckers.thibault.aves.utils.ContextUtils.resourceUri
|
|||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
|
@ -74,14 +73,15 @@ class SearchSuggestionsProvider : ContentProvider() {
|
|||
}
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL)
|
||||
backgroundChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
|
||||
when (call.method) {
|
||||
"initialized" -> {
|
||||
Log.d(LOG_TAG, "background channel is ready")
|
||||
result.success(null)
|
||||
val backgroundChannel = MethodChannel(messenger, BACKGROUND_CHANNEL).apply {
|
||||
setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"initialized" -> {
|
||||
Log.d(LOG_TAG, "background channel is ready")
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
|
@ -16,50 +17,22 @@ import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
|||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.getParcelableExtraCompat
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class WallpaperActivity : FlutterActivity() {
|
||||
private lateinit var intentDataMap: MutableMap<String, Any?>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.i(LOG_TAG, "onCreate intent=$intent")
|
||||
intent.extras?.takeUnless { it.isEmpty }?.let {
|
||||
Log.i(LOG_TAG, "onCreate intent extras=$it")
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
|
||||
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
|
||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(context))
|
||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
|
||||
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
|
||||
// - need Activity
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
|
||||
|
||||
// intent handling
|
||||
// detail fetch: dart -> platform
|
||||
intentDataMap = extractIntentData(intent)
|
||||
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"getIntentData" -> {
|
||||
result.success(intentDataMap)
|
||||
intentDataMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initChannels(this)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -76,6 +49,41 @@ class WallpaperActivity : FlutterActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun initChannels(activity: Activity) {
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
|
||||
// dart -> platform -> dart
|
||||
// - need Context
|
||||
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(activity))
|
||||
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(activity))
|
||||
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(activity))
|
||||
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(activity))
|
||||
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(activity))
|
||||
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(activity))
|
||||
// - need ContextWrapper
|
||||
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(activity))
|
||||
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(activity))
|
||||
// - need Activity
|
||||
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(activity))
|
||||
|
||||
// result streaming: dart -> platform ->->-> dart
|
||||
// - need Context
|
||||
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(activity, args) }
|
||||
|
||||
// intent handling
|
||||
// detail fetch: dart -> platform
|
||||
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
|
||||
}
|
||||
|
||||
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getIntentData" -> {
|
||||
result.success(intentDataMap)
|
||||
intentDataMap.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
when (intent?.action) {
|
||||
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
|
||||
|
|
|
@ -21,7 +21,7 @@ class AvesAppGlideModule : AppGlideModule() {
|
|||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
// prevent ExifInterface error logs
|
||||
// cf https://github.com/bumptech/glide/issues/3383
|
||||
glide.registry.imageHeaderParsers.compatRemoveIf { parser: ImageHeaderParser? -> parser is ExifInterfaceImageHeaderParser }
|
||||
registry.imageHeaderParsers.compatRemoveIf { parser: ImageHeaderParser? -> parser is ExifInterfaceImageHeaderParser }
|
||||
}
|
||||
|
||||
override fun isManifestParsingEnabled(): Boolean = false
|
||||
|
|
|
@ -98,7 +98,7 @@ object XMP {
|
|||
if (MimeTypes.isHeic(mimeType) && !foundXmp && StorageUtils.isMediaStoreContentUri(uri) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
try {
|
||||
val xmpBytes = context.queryContentResolverProp(uri, mimeType, MediaStore.MediaColumns.XMP)
|
||||
if (xmpBytes is ByteArray) {
|
||||
if (xmpBytes is ByteArray && xmpBytes.size > 0) {
|
||||
val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS)
|
||||
processXmp(xmpMeta)
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ object Helper {
|
|||
|
||||
val metadata = when (fileType) {
|
||||
FileType.Jpeg -> safeReadJpeg(inputStream)
|
||||
FileType.Png -> safeReadPng(inputStream)
|
||||
FileType.Tiff,
|
||||
FileType.Arw,
|
||||
FileType.Cr2,
|
||||
|
@ -95,6 +96,10 @@ object Helper {
|
|||
return metadata
|
||||
}
|
||||
|
||||
private fun safeReadPng(input: InputStream): com.drew.metadata.Metadata {
|
||||
return SafePngMetadataReader.readMetadata(input)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, TiffProcessingException::class)
|
||||
fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata {
|
||||
val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, safeReadStreamLength)
|
||||
|
|
|
@ -0,0 +1,302 @@
|
|||
package deckers.thibault.aves.metadata.metadataextractor
|
||||
|
||||
import android.util.Log
|
||||
import com.drew.imaging.png.*
|
||||
import com.drew.imaging.tiff.TiffProcessingException
|
||||
import com.drew.imaging.tiff.TiffReader
|
||||
import com.drew.lang.*
|
||||
import com.drew.lang.annotations.NotNull
|
||||
import com.drew.metadata.ErrorDirectory
|
||||
import com.drew.metadata.Metadata
|
||||
import com.drew.metadata.StringValue
|
||||
import com.drew.metadata.exif.ExifTiffHandler
|
||||
import com.drew.metadata.icc.IccReader
|
||||
import com.drew.metadata.png.PngChromaticitiesDirectory
|
||||
import com.drew.metadata.png.PngDirectory
|
||||
import com.drew.metadata.xmp.XmpReader
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.util.zip.InflaterInputStream
|
||||
import java.util.zip.ZipException
|
||||
|
||||
// adapted from `PngMetadataReader` to prevent reading OOM from large chunks
|
||||
// as of `metadata-extractor` v2.18.0, there is no way to customize the reader
|
||||
// without copying `desiredChunkTypes` and the whole `processChunk` function
|
||||
object SafePngMetadataReader {
|
||||
private val LOG_TAG = LogUtils.createTag<SafePngMetadataReader>()
|
||||
|
||||
// arbitrary size to detect chunks that may yield an OOM
|
||||
private const val chunkSizeDangerThreshold = SafeXmpReader.segmentTypeSizeDangerThreshold
|
||||
|
||||
private val latin1Encoding = Charsets.ISO_8859_1
|
||||
private val desiredChunkTypes: Set<PngChunkType> = hashSetOf(
|
||||
PngChunkType.IHDR,
|
||||
PngChunkType.PLTE,
|
||||
PngChunkType.tRNS,
|
||||
PngChunkType.cHRM,
|
||||
PngChunkType.sRGB,
|
||||
PngChunkType.gAMA,
|
||||
PngChunkType.iCCP,
|
||||
PngChunkType.bKGD,
|
||||
PngChunkType.tEXt,
|
||||
PngChunkType.zTXt,
|
||||
PngChunkType.iTXt,
|
||||
PngChunkType.tIME,
|
||||
PngChunkType.pHYs,
|
||||
PngChunkType.sBIT,
|
||||
PngChunkType.eXIf,
|
||||
)
|
||||
|
||||
@Throws(IOException::class, PngProcessingException::class)
|
||||
fun readMetadata(inputStream: InputStream): Metadata {
|
||||
val chunks = PngChunkReader().extract(StreamReader(inputStream), desiredChunkTypes)
|
||||
val metadata = Metadata()
|
||||
for (chunk in chunks) {
|
||||
try {
|
||||
processChunk(metadata, chunk)
|
||||
} catch (e: Exception) {
|
||||
metadata.addDirectory(ErrorDirectory("Exception reading PNG chunk: " + e.message))
|
||||
}
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
@Throws(PngProcessingException::class, IOException::class)
|
||||
private fun processChunk(@NotNull metadata: Metadata, @NotNull chunk: PngChunk) {
|
||||
val chunkType = chunk.type
|
||||
val bytes = chunk.bytes
|
||||
|
||||
// TLAD insert start
|
||||
if (bytes.size > chunkSizeDangerThreshold) {
|
||||
Log.w(LOG_TAG, "PNG chunk $chunkType is too large, with a size of ${bytes.size} B")
|
||||
return
|
||||
}
|
||||
// TLAD insert end
|
||||
|
||||
if (chunkType == PngChunkType.IHDR) {
|
||||
val header = PngHeader(bytes)
|
||||
val directory = PngDirectory(PngChunkType.IHDR)
|
||||
directory.setInt(PngDirectory.TAG_IMAGE_WIDTH, header.imageWidth)
|
||||
directory.setInt(PngDirectory.TAG_IMAGE_HEIGHT, header.imageHeight)
|
||||
directory.setInt(PngDirectory.TAG_BITS_PER_SAMPLE, header.bitsPerSample.toInt())
|
||||
directory.setInt(PngDirectory.TAG_COLOR_TYPE, header.colorType.numericValue)
|
||||
directory.setInt(PngDirectory.TAG_COMPRESSION_TYPE, header.compressionType.toInt() and 0xFF) // make sure it's unsigned
|
||||
directory.setInt(PngDirectory.TAG_FILTER_METHOD, header.filterMethod.toInt())
|
||||
directory.setInt(PngDirectory.TAG_INTERLACE_METHOD, header.interlaceMethod.toInt())
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.PLTE) {
|
||||
val directory = PngDirectory(PngChunkType.PLTE)
|
||||
directory.setInt(PngDirectory.TAG_PALETTE_SIZE, bytes.size / 3)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.tRNS) {
|
||||
val directory = PngDirectory(PngChunkType.tRNS)
|
||||
directory.setInt(PngDirectory.TAG_PALETTE_HAS_TRANSPARENCY, 1)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.sRGB) {
|
||||
val srgbRenderingIntent = bytes[0].toInt()
|
||||
val directory = PngDirectory(PngChunkType.sRGB)
|
||||
directory.setInt(PngDirectory.TAG_SRGB_RENDERING_INTENT, srgbRenderingIntent)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.cHRM) {
|
||||
val chromaticities = PngChromaticities(bytes)
|
||||
val directory = PngChromaticitiesDirectory()
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_X, chromaticities.whitePointX)
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_WHITE_POINT_Y, chromaticities.whitePointY)
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_RED_X, chromaticities.redX)
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_RED_Y, chromaticities.redY)
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_X, chromaticities.greenX)
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_GREEN_Y, chromaticities.greenY)
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_X, chromaticities.blueX)
|
||||
directory.setInt(PngChromaticitiesDirectory.TAG_BLUE_Y, chromaticities.blueY)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.gAMA) {
|
||||
val gammaInt = ByteConvert.toInt32BigEndian(bytes)
|
||||
SequentialByteArrayReader(bytes).int32
|
||||
val directory = PngDirectory(PngChunkType.gAMA)
|
||||
directory.setDouble(PngDirectory.TAG_GAMMA, gammaInt / 100000.0)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.iCCP) {
|
||||
val reader: SequentialReader = SequentialByteArrayReader(bytes)
|
||||
|
||||
// Profile Name is 1-79 bytes, followed by the 1 byte null character
|
||||
val profileNameBytes = reader.getNullTerminatedBytes(79 + 1)
|
||||
val directory = PngDirectory(PngChunkType.iCCP)
|
||||
directory.setStringValue(PngDirectory.TAG_ICC_PROFILE_NAME, StringValue(profileNameBytes, latin1Encoding))
|
||||
val compressionMethod = reader.int8
|
||||
// Only compression method allowed by the spec is zero: deflate
|
||||
if (compressionMethod.toInt() == 0) {
|
||||
// bytes left for compressed text is:
|
||||
// total bytes length - (profilenamebytes length + null byte + compression method byte)
|
||||
val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1)
|
||||
val compressedProfile = reader.getBytes(bytesLeft)
|
||||
try {
|
||||
val inflateStream = InflaterInputStream(ByteArrayInputStream(compressedProfile))
|
||||
IccReader().extract(RandomAccessStreamReader(inflateStream), metadata, directory)
|
||||
inflateStream.close()
|
||||
} catch (zex: ZipException) {
|
||||
directory.addError(String.format("Exception decompressing PNG iCCP chunk : %s", zex.message))
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
} else {
|
||||
directory.addError("Invalid compression method value")
|
||||
}
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.bKGD) {
|
||||
val directory = PngDirectory(PngChunkType.bKGD)
|
||||
directory.setByteArray(PngDirectory.TAG_BACKGROUND_COLOR, bytes)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.tEXt) {
|
||||
val reader: SequentialReader = SequentialByteArrayReader(bytes)
|
||||
|
||||
// Keyword is 1-79 bytes, followed by the 1 byte null character
|
||||
val keywordsv = reader.getNullTerminatedStringValue(79 + 1, latin1Encoding)
|
||||
val keyword = keywordsv.toString()
|
||||
|
||||
// bytes left for text is:
|
||||
// total bytes length - (Keyword length + null byte)
|
||||
val bytesLeft = bytes.size - (keywordsv.bytes.size + 1)
|
||||
val value = reader.getNullTerminatedStringValue(bytesLeft, latin1Encoding)
|
||||
val textPairs: MutableList<KeyValuePair> = ArrayList()
|
||||
textPairs.add(KeyValuePair(keyword, value))
|
||||
val directory = PngDirectory(PngChunkType.tEXt)
|
||||
directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.zTXt) {
|
||||
val reader: SequentialReader = SequentialByteArrayReader(bytes)
|
||||
|
||||
// Keyword is 1-79 bytes, followed by the 1 byte null character
|
||||
val keywordsv = reader.getNullTerminatedStringValue(79 + 1, latin1Encoding)
|
||||
val keyword = keywordsv.toString()
|
||||
val compressionMethod = reader.int8
|
||||
|
||||
// bytes left for compressed text is:
|
||||
// total bytes length - (Keyword length + null byte + compression method byte)
|
||||
val bytesLeft = bytes.size - (keywordsv.bytes.size + 1 + 1)
|
||||
var textBytes: ByteArray? = null
|
||||
if (compressionMethod.toInt() == 0) {
|
||||
try {
|
||||
textBytes = StreamUtil.readAllBytes(InflaterInputStream(ByteArrayInputStream(bytes, bytes.size - bytesLeft, bytesLeft)))
|
||||
} catch (zex: ZipException) {
|
||||
val directory = PngDirectory(PngChunkType.zTXt)
|
||||
directory.addError(String.format("Exception decompressing PNG zTXt chunk with keyword \"%s\": %s", keyword, zex.message))
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
} else {
|
||||
val directory = PngDirectory(PngChunkType.zTXt)
|
||||
directory.addError("Invalid compression method value")
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
if (textBytes != null) {
|
||||
if (keyword == "XML:com.adobe.xmp") {
|
||||
// NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
|
||||
XmpReader().extract(textBytes, metadata)
|
||||
} else {
|
||||
val textPairs: MutableList<KeyValuePair> = ArrayList()
|
||||
textPairs.add(KeyValuePair(keyword, StringValue(textBytes, latin1Encoding)))
|
||||
val directory = PngDirectory(PngChunkType.zTXt)
|
||||
directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs)
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
}
|
||||
} else if (chunkType == PngChunkType.iTXt) {
|
||||
val reader: SequentialReader = SequentialByteArrayReader(bytes)
|
||||
|
||||
// Keyword is 1-79 bytes, followed by the 1 byte null character
|
||||
val keywordsv = reader.getNullTerminatedStringValue(79 + 1, latin1Encoding)
|
||||
val keyword = keywordsv.toString()
|
||||
val compressionFlag = reader.int8
|
||||
val compressionMethod = reader.int8
|
||||
// TODO we currently ignore languageTagBytes and translatedKeywordBytes
|
||||
val languageTagBytes = reader.getNullTerminatedBytes(bytes.size)
|
||||
val translatedKeywordBytes = reader.getNullTerminatedBytes(bytes.size)
|
||||
|
||||
// bytes left for compressed text is:
|
||||
// total bytes length - (Keyword length + null byte + comp flag byte + comp method byte + lang length + null byte + translated length + null byte)
|
||||
val bytesLeft = bytes.size - (keywordsv.bytes.size + 1 + 1 + 1 + languageTagBytes.size + 1 + translatedKeywordBytes.size + 1)
|
||||
var textBytes: ByteArray? = null
|
||||
if (compressionFlag.toInt() == 0) {
|
||||
textBytes = reader.getNullTerminatedBytes(bytesLeft)
|
||||
} else if (compressionFlag.toInt() == 1) {
|
||||
if (compressionMethod.toInt() == 0) {
|
||||
try {
|
||||
textBytes = StreamUtil.readAllBytes(InflaterInputStream(ByteArrayInputStream(bytes, bytes.size - bytesLeft, bytesLeft)))
|
||||
} catch (zex: ZipException) {
|
||||
val directory = PngDirectory(PngChunkType.iTXt)
|
||||
directory.addError(String.format("Exception decompressing PNG iTXt chunk with keyword \"%s\": %s", keyword, zex.message))
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
} else {
|
||||
val directory = PngDirectory(PngChunkType.iTXt)
|
||||
directory.addError("Invalid compression method value")
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
} else {
|
||||
val directory = PngDirectory(PngChunkType.iTXt)
|
||||
directory.addError("Invalid compression flag value")
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
if (textBytes != null) {
|
||||
if (keyword == "XML:com.adobe.xmp") {
|
||||
// NOTE in testing images, the XMP has parsed successfully, but we are not extracting tags from it as necessary
|
||||
XmpReader().extract(textBytes, metadata)
|
||||
} else {
|
||||
val textPairs: MutableList<KeyValuePair> = ArrayList()
|
||||
textPairs.add(KeyValuePair(keyword, StringValue(textBytes, latin1Encoding)))
|
||||
val directory = PngDirectory(PngChunkType.iTXt)
|
||||
directory.setObject(PngDirectory.TAG_TEXTUAL_DATA, textPairs)
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
}
|
||||
} else if (chunkType == PngChunkType.tIME) {
|
||||
val reader = SequentialByteArrayReader(bytes)
|
||||
val year = reader.uInt16
|
||||
val month = reader.uInt8.toInt()
|
||||
val day = reader.uInt8.toInt()
|
||||
val hour = reader.uInt8.toInt()
|
||||
val minute = reader.uInt8.toInt()
|
||||
val second = reader.uInt8.toInt()
|
||||
val directory = PngDirectory(PngChunkType.tIME)
|
||||
if (DateUtil.isValidDate(year, month - 1, day) && DateUtil.isValidTime(hour, minute, second)) {
|
||||
val dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", year, month, day, hour, minute, second)
|
||||
directory.setString(PngDirectory.TAG_LAST_MODIFICATION_TIME, dateString)
|
||||
} else {
|
||||
directory.addError(
|
||||
String.format(
|
||||
"PNG tIME data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d",
|
||||
year, month, day, hour, minute, second
|
||||
)
|
||||
)
|
||||
}
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.pHYs) {
|
||||
val reader = SequentialByteArrayReader(bytes)
|
||||
val pixelsPerUnitX = reader.int32
|
||||
val pixelsPerUnitY = reader.int32
|
||||
val unitSpecifier = reader.int8
|
||||
val directory = PngDirectory(PngChunkType.pHYs)
|
||||
directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_X, pixelsPerUnitX)
|
||||
directory.setInt(PngDirectory.TAG_PIXELS_PER_UNIT_Y, pixelsPerUnitY)
|
||||
directory.setInt(PngDirectory.TAG_UNIT_SPECIFIER, unitSpecifier.toInt())
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.sBIT) {
|
||||
val directory = PngDirectory(PngChunkType.sBIT)
|
||||
directory.setByteArray(PngDirectory.TAG_SIGNIFICANT_BITS, bytes)
|
||||
metadata.addDirectory(directory)
|
||||
} else if (chunkType == PngChunkType.eXIf) {
|
||||
try {
|
||||
val handler = ExifTiffHandler(metadata, null)
|
||||
TiffReader().processTiff(ByteArrayReader(bytes), handler, 0)
|
||||
} catch (ex: TiffProcessingException) {
|
||||
val directory = PngDirectory(PngChunkType.eXIf)
|
||||
directory.addError(ex.message)
|
||||
metadata.addDirectory(directory)
|
||||
} catch (ex: IOException) {
|
||||
val directory = PngDirectory(PngChunkType.eXIf)
|
||||
directory.addError(ex.message)
|
||||
metadata.addDirectory(directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -135,7 +135,7 @@ class SafeXmpReader : XmpReader() {
|
|||
private val LOG_TAG = LogUtils.createTag<SafeXmpReader>()
|
||||
|
||||
// arbitrary size to detect extended XMP that may yield an OOM
|
||||
private const val segmentTypeSizeDangerThreshold = 3 * (1 shl 20) // MB
|
||||
const val segmentTypeSizeDangerThreshold = 3 * (1 shl 20) // MB
|
||||
|
||||
// tighter node limits for faster loading
|
||||
val PARSE_OPTIONS: ParseOptions = ParseOptions().setXMPNodesToLimit(
|
||||
|
|
|
@ -41,6 +41,7 @@ class SourceEntry {
|
|||
var height: Int? = null
|
||||
private var sourceRotationDegrees: Int? = null
|
||||
private var sizeBytes: Long? = null
|
||||
private var dateAddedSecs: Long? = null
|
||||
private var dateModifiedSecs: Long? = null
|
||||
private var sourceDateTakenMillis: Long? = null
|
||||
private var durationMillis: Long? = null
|
||||
|
@ -61,6 +62,7 @@ class SourceEntry {
|
|||
sourceRotationDegrees = map["sourceRotationDegrees"] as Int?
|
||||
sizeBytes = toLong(map["sizeBytes"])
|
||||
title = map["title"] as String?
|
||||
dateAddedSecs = toLong(map["dateAddedSecs"])
|
||||
dateModifiedSecs = toLong(map["dateModifiedSecs"])
|
||||
sourceDateTakenMillis = toLong(map["sourceDateTakenMillis"])
|
||||
durationMillis = toLong(map["durationMillis"])
|
||||
|
@ -83,6 +85,7 @@ class SourceEntry {
|
|||
"sourceRotationDegrees" to (sourceRotationDegrees ?: 0),
|
||||
"sizeBytes" to sizeBytes,
|
||||
"title" to title,
|
||||
"dateAddedSecs" to dateAddedSecs,
|
||||
"dateModifiedSecs" to dateModifiedSecs,
|
||||
"sourceDateTakenMillis" to sourceDateTakenMillis,
|
||||
"durationMillis" to durationMillis,
|
||||
|
|
|
@ -22,7 +22,12 @@ internal class FileImageProvider : ImageProvider() {
|
|||
try {
|
||||
val file = File(path)
|
||||
if (file.exists()) {
|
||||
entry.initFromFile(path, file.name, file.length(), file.lastModified() / 1000)
|
||||
entry.initFromFile(
|
||||
path = path,
|
||||
title = file.name,
|
||||
sizeBytes = file.length(),
|
||||
dateModifiedSecs = file.lastModified() / 1000,
|
||||
)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
callback.onFailure(e)
|
||||
|
|
|
@ -7,7 +7,6 @@ import android.content.*
|
|||
import android.media.MediaScannerConnection
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
|
@ -87,7 +86,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val alwaysValid: NewEntryChecker = fun(_: Int, _: Int): Boolean = true
|
||||
val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
|
||||
if (id != null) {
|
||||
if (!found && (sourceMimeType == null || isImage(sourceMimeType))) {
|
||||
if (sourceMimeType == null || isImage(sourceMimeType)) {
|
||||
val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id)
|
||||
found = fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION)
|
||||
}
|
||||
|
@ -190,6 +189,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
|
||||
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
|
||||
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
|
||||
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
|
||||
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||
val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN)
|
||||
|
||||
|
@ -225,6 +225,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
"height" to height,
|
||||
"sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
|
||||
"sizeBytes" to cursor.getLong(sizeColumn),
|
||||
"dateAddedSecs" to cursor.getInt(dateAddedColumn),
|
||||
"dateModifiedSecs" to dateModifiedSecs,
|
||||
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
|
||||
"durationMillis" to durationMillis,
|
||||
|
@ -391,8 +392,13 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
effectiveTargetDir = targetDir
|
||||
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
|
||||
if (!File(targetDir).exists()) {
|
||||
callback.onFailure(Exception("failed to create directory at path=$targetDir"))
|
||||
return
|
||||
val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir)
|
||||
val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath)
|
||||
// download subdirectories can be created later by Media Store insertion
|
||||
if (!isDownloadSubdir) {
|
||||
callback.onFailure(Exception("failed to create directory at path=$targetDir"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -535,54 +541,57 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetNameWithoutExtension: String,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && isDownloadDir(activity, targetDir)) {
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
val resolver = activity.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir)
|
||||
val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath)
|
||||
if (isDownloadSubdir) {
|
||||
val volumePath = StorageUtils.getVolumePath(activity, targetDir)
|
||||
val relativePath = targetDir.substring(volumePath?.length ?: 0)
|
||||
|
||||
uri?.let {
|
||||
resolver.openOutputStream(uri)?.use(write)
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
} ?: throw Exception("MediaStore failed for some reason")
|
||||
|
||||
File(targetDir, targetFileName).path
|
||||
} else {
|
||||
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
|
||||
|
||||
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
|
||||
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
|
||||
// through a document URI, not a tree URI
|
||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
|
||||
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
|
||||
try {
|
||||
targetDocFile.openOutputStream().use(write)
|
||||
} catch (e: Exception) {
|
||||
// remove empty file
|
||||
if (targetDocFile.exists()) {
|
||||
targetDocFile.delete()
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, targetFileName)
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
put(MediaStore.MediaColumns.IS_PENDING, 1)
|
||||
}
|
||||
throw e
|
||||
val resolver = activity.contentResolver
|
||||
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
|
||||
uri?.let {
|
||||
resolver.openOutputStream(uri)?.use(write)
|
||||
values.clear()
|
||||
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||
resolver.update(uri, values, null, null)
|
||||
} ?: throw Exception("MediaStore failed for some reason")
|
||||
|
||||
return File(targetDir, targetFileName).path
|
||||
}
|
||||
|
||||
// the source file name and the created document file name can be different when:
|
||||
// - a file with the same name already exists, some implementations give a suffix like ` (1)`, some *do not*
|
||||
// - the original extension does not match the extension added by the underlying provider
|
||||
val fileName = targetDocFile.name
|
||||
targetDir + fileName
|
||||
}
|
||||
}
|
||||
|
||||
private fun isDownloadDir(context: Context, dirPath: String): Boolean {
|
||||
val relativeDir = removeTrailingSeparator(PathSegments(context, dirPath).relativeDir ?: "")
|
||||
return relativeDir == Environment.DIRECTORY_DOWNLOADS
|
||||
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
|
||||
|
||||
// the file created from a `TreeDocumentFile` is also a `TreeDocumentFile`
|
||||
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
|
||||
// through a document URI, not a tree URI
|
||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
|
||||
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
|
||||
try {
|
||||
targetDocFile.openOutputStream().use(write)
|
||||
} catch (e: Exception) {
|
||||
// remove empty file
|
||||
if (targetDocFile.exists()) {
|
||||
targetDocFile.delete()
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
// the source file name and the created document file name can be different when:
|
||||
// - a file with the same name already exists, some implementations give a suffix like ` (1)`, some *do not*
|
||||
// - the original extension does not match the extension added by the underlying provider
|
||||
val fileName = targetDocFile.name
|
||||
return targetDir + fileName
|
||||
}
|
||||
|
||||
override suspend fun renameMultiple(
|
||||
|
@ -782,6 +791,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
|
||||
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns.DATE_ADDED,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
)
|
||||
try {
|
||||
|
@ -791,6 +801,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
newFields["uri"] = uri.toString()
|
||||
newFields["contentId"] = uri.tryParseId()
|
||||
newFields["path"] = path
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) }
|
||||
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
|
||||
cursor.close()
|
||||
return newFields
|
||||
|
@ -864,6 +875,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
MediaStore.MediaColumns.SIZE,
|
||||
MediaStore.MediaColumns.WIDTH,
|
||||
MediaStore.MediaColumns.HEIGHT,
|
||||
MediaStore.MediaColumns.DATE_ADDED,
|
||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
||||
MediaColumns.DATE_TAKEN,
|
||||
)
|
||||
|
|
|
@ -104,9 +104,8 @@ object MimeTypes {
|
|||
else -> false
|
||||
}
|
||||
|
||||
// as of androidx.exifinterface:exifinterface:1.3.3
|
||||
// as of androidx.exifinterface:exifinterface:1.3.4
|
||||
fun canEditExif(mimeType: String) = when (mimeType) {
|
||||
DNG,
|
||||
JPEG,
|
||||
PNG,
|
||||
WEBP -> true
|
||||
|
|
|
@ -105,7 +105,11 @@ object PermissionManager {
|
|||
val primaryDir = dirSegments.firstOrNull()
|
||||
if (getRestrictedPrimaryDirectories().contains(primaryDir) && dirSegments.size > 1) {
|
||||
// request secondary directory (if any) for restricted primary directory
|
||||
dirSet.add(dirSegments.take(2).joinToString(File.separator))
|
||||
val dir = dirSegments.take(2).joinToString(File.separator)
|
||||
// only register directories that exist on storage, so they can be selected for access grant
|
||||
if (File(volumePath, dir).exists()) {
|
||||
dirSet.add(dir)
|
||||
}
|
||||
} else {
|
||||
primaryDir?.let { dirSet.add(it) }
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.content.pm.PackageManager
|
|||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
|
@ -93,6 +94,10 @@ object StorageUtils {
|
|||
return getVolumePaths(context).firstOrNull { anyPath.startsWith(it) }
|
||||
}
|
||||
|
||||
fun getDownloadDirPath(context: Context, anyPath: String): String? {
|
||||
return getVolumePath(context, anyPath)?.let { volumePath -> ensureTrailingSeparator(File(volumePath, Environment.DIRECTORY_DOWNLOADS).path) }
|
||||
}
|
||||
|
||||
private fun getPathStepIterator(context: Context, anyPath: String, root: String?): Iterator<String?>? {
|
||||
val rootLength = (root ?: getVolumePath(context, anyPath))?.length ?: return null
|
||||
|
||||
|
|
|
@ -7,13 +7,13 @@ buildscript {
|
|||
maven { url 'https://developer.huawei.com/repo/' }
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
// GMS & Firebase Crashlytics (used by some flavors only)
|
||||
classpath 'com.google.gms:google-services:4.3.14'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.2'
|
||||
// HMS (used by some flavors only)
|
||||
classpath 'com.huawei.agconnect:agcp:1.5.2.300'
|
||||
classpath 'com.huawei.agconnect:agcp:1.7.2.300'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ allprojects {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {url 'https://developer.huawei.com/repo/'}
|
||||
maven { url 'https://developer.huawei.com/repo/' }
|
||||
}
|
||||
// gradle.projectsEvaluated {
|
||||
// tasks.withType(JavaCompile) {
|
||||
|
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||
|
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 283 KiB |
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 285 KiB |
5
fastlane/metadata/android/en-US/changelogs/1081.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
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
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 283 KiB |
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 285 KiB |
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 283 KiB |
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 283 KiB |
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 284 KiB |
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "Anzeigen in Alben",
|
||||
"chipActionGoToCountryPage": "Anzeigen in Ländern",
|
||||
"chipActionGoToTagPage": "Zeige in Tags",
|
||||
"chipActionFilterOut": "Filtern ohne",
|
||||
"chipActionFilterIn": "Filtern mit",
|
||||
"chipActionHide": "Ausblenden",
|
||||
"chipActionPin": "Oben Anpinnen",
|
||||
"chipActionUnpin": "Nicht mehr Anpinen",
|
||||
|
@ -88,15 +90,18 @@
|
|||
|
||||
"entryInfoActionEditDate": "Datum & Uhrzeit bearbeiten",
|
||||
"entryInfoActionEditLocation": "Standort bearbeiten",
|
||||
"entryInfoActionEditTitleDescription": "Titel und Beschreibung bearbeiten",
|
||||
"entryInfoActionEditRating": "Bewertung bearbeiten",
|
||||
"entryInfoActionEditTags": "Tags bearbeiten",
|
||||
"entryInfoActionRemoveMetadata": "Metadaten entfernen",
|
||||
|
||||
"filterBinLabel": "Papierkorb",
|
||||
"filterFavouriteLabel": "Favorit",
|
||||
"filterNoDateLabel": "Undatiert",
|
||||
"filterNoLocationLabel": "Ungeortet",
|
||||
"filterNoRatingLabel": "Nicht bewertet",
|
||||
"filterNoTagLabel": "Unmarkiert",
|
||||
"filterNoTitleLabel": "Unbenannt",
|
||||
"filterOnThisDayLabel": "Am heutigen Tag",
|
||||
"filterRecentlyAddedLabel": "Kürzlich hinzugefügt",
|
||||
"filterRatingRejectedLabel": "Verworfen",
|
||||
|
@ -152,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "Höchste Rate",
|
||||
"displayRefreshRatePreferLowest": "Niedrigste Rate",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Überspringen",
|
||||
"slideshowVideoPlaybackMuted": "Stumm abspielen",
|
||||
"slideshowVideoPlaybackWithSound": "Mit Ton abspielen",
|
||||
"videoPlaybackSkip": "Überspringen",
|
||||
"videoPlaybackMuted": "Stumm abspielen",
|
||||
"videoPlaybackWithSound": "Mit Ton abspielen",
|
||||
|
||||
"themeBrightnessLight": "Hell",
|
||||
"themeBrightnessDark": "Dunkel",
|
||||
|
@ -164,11 +169,15 @@
|
|||
"viewerTransitionParallax": "Parallaxe",
|
||||
"viewerTransitionFade": "Ausblenden",
|
||||
"viewerTransitionZoomIn": "Heranzoomen",
|
||||
"viewerTransitionNone": "Keine",
|
||||
|
||||
"wallpaperTargetHome": "Startbildschirm",
|
||||
"wallpaperTargetLock": "Sperrbildschirm",
|
||||
"wallpaperTargetHomeLock": "Start- und Sperrbildschirm",
|
||||
|
||||
"widgetOpenPageHome": "Startseite öffnen",
|
||||
"widgetOpenPageViewer": "Viewer öffnen",
|
||||
|
||||
"albumTierNew": "Neu",
|
||||
"albumTierPinned": "Angeheftet",
|
||||
"albumTierSpecial": "Häufig verwendet",
|
||||
|
@ -284,7 +293,9 @@
|
|||
"viewDialogSortSectionTitle": "Sortieren",
|
||||
"viewDialogGroupSectionTitle": "Gruppe",
|
||||
"viewDialogLayoutSectionTitle": "Layout",
|
||||
"viewDialogReverseSortOrder": "Umgekehrte Sortierung",
|
||||
|
||||
"tileLayoutMosaic": "Mosaik",
|
||||
"tileLayoutGrid": "Kacheln",
|
||||
"tileLayoutList": "Liste",
|
||||
|
||||
|
@ -387,10 +398,22 @@
|
|||
"sortByAlbumFileName": "Nach Album & Dateiname",
|
||||
"sortByRating": "Nach Bewertung",
|
||||
|
||||
"sortOrderNewestFirst": "Neueste zuerst",
|
||||
"sortOrderOldestFirst": "Älteste zuerst",
|
||||
"sortOrderAtoZ": "A zu Z",
|
||||
"sortOrderZtoA": "Z zu A",
|
||||
"sortOrderHighestFirst": "Höchste zuerst",
|
||||
"sortOrderLowestFirst": "Niedrigste zuerst",
|
||||
"sortOrderLargestFirst": "Größtes zuerst",
|
||||
"sortOrderSmallestFirst": "Kleinste zuerst",
|
||||
|
||||
"albumGroupTier": "Nach Ebene",
|
||||
"albumGroupType": "Nach Typ",
|
||||
"albumGroupVolume": "Nach Speichervolumen",
|
||||
"albumGroupNone": "Nicht gruppieren",
|
||||
|
||||
"albumMimeTypeMixed": "Gemischt",
|
||||
|
||||
"albumPickPageTitleCopy": "In Album kopieren",
|
||||
"albumPickPageTitleExport": "In Album exportieren",
|
||||
"albumPickPageTitleMove": "Zum Album verschieben",
|
||||
|
@ -424,10 +447,12 @@
|
|||
"searchPlacesSectionTitle": "Orte",
|
||||
"searchTagsSectionTitle": "Tags",
|
||||
"searchRatingSectionTitle": "Bewertungen",
|
||||
"searchMetadataSectionTitle": "Metadaten",
|
||||
|
||||
"settingsPageTitle": "Einstellungen",
|
||||
"settingsSystemDefault": "System",
|
||||
"settingsDefault": "Standard",
|
||||
"settingsDisabled": "Deaktiviert",
|
||||
|
||||
"settingsSearchFieldLabel": "Einstellungen durchsuchen",
|
||||
"settingsSearchEmpty": "Keine passende Einstellung",
|
||||
|
@ -510,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "Wiederholung",
|
||||
"settingsSlideshowShuffle": "Mischen",
|
||||
"settingsSlideshowFillScreen": "Bildschirm ausfüllen",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Animierter Zoomeffekt",
|
||||
"settingsSlideshowTransitionTile": "Übergang",
|
||||
"settingsSlideshowTransitionDialogTitle": "Übergang",
|
||||
"settingsSlideshowIntervalTile": "Intervall",
|
||||
"settingsSlideshowIntervalDialogTitle": "Intervall",
|
||||
"settingsSlideshowVideoPlaybackTile": "Videowiedergabe",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Videowiedergabe",
|
||||
|
||||
|
@ -521,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Videos anzeigen",
|
||||
"settingsVideoEnableHardwareAcceleration": "Hardware-Beschleunigung",
|
||||
"settingsVideoEnableAutoPlay": "Automatische Wiedergabe",
|
||||
"settingsVideoAutoPlay": "Automatische Wiedergabe",
|
||||
"settingsVideoLoopModeTile": "Schleifen-Modus",
|
||||
"settingsVideoLoopModeDialogTitle": "Schleifen-Modus",
|
||||
|
||||
|
@ -543,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "Steuerung",
|
||||
"settingsVideoControlsPageTitle": "Steuerung",
|
||||
"settingsVideoButtonsTile": "Schaltflächen",
|
||||
"settingsVideoButtonsDialogTitle": "Schaltflächen",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Doppeltippen zum Abspielen/Pausieren",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Doppeltippen auf die Bildschirmränder zum Rückwärts-/Vorwärtsspringen",
|
||||
|
||||
|
@ -576,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "Animationen entfernen",
|
||||
"settingsRemoveAnimationsDialogTitle": "Animationen entfernen",
|
||||
"settingsTimeToTakeActionTile": "Zeit zum Reagieren",
|
||||
"settingsTimeToTakeActionDialogTitle": "Zeit zum Reagieren",
|
||||
|
||||
"settingsDisplaySectionTitle": "Anzeige",
|
||||
"settingsThemeBrightnessTile": "Thema",
|
||||
|
@ -598,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Bilderrahmen",
|
||||
"settingsWidgetShowOutline": "Gliederung",
|
||||
"settingsWidgetOpenPage": "Beim Tippen auf das Widget",
|
||||
|
||||
"settingsCollectionTile": "Sammlung",
|
||||
|
||||
|
@ -606,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "Top-Länder",
|
||||
"statsTopPlacesSectionTitle": "Top-Plätze",
|
||||
"statsTopTagsSectionTitle": "Top-Tags",
|
||||
"statsTopAlbumsSectionTitle": "Top-Alben",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "ÖFFNE PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "HINTERGRUNDBILD EINSTELLEN",
|
||||
|
@ -650,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Auflösung",
|
||||
"viewerInfoSearchSuggestionRights": "Rechte",
|
||||
|
||||
"wallpaperUseScrollEffect": "Scroll-Effekt auf dem Startbildschirm verwenden",
|
||||
|
||||
"tagEditorPageTitle": "Tags bearbeiten",
|
||||
"tagEditorPageNewTagFieldLabel": "Neuer Tag",
|
||||
"tagEditorPageAddTagTooltip": "Tag hinzufügen",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "Εμφάνιση στα Άλμπουμ",
|
||||
"chipActionGoToCountryPage": "Εμφάνιση στις χώρες",
|
||||
"chipActionGoToTagPage": "Εμφάνιση στις ετικέτες",
|
||||
"chipActionFilterOut": "Χωρίς φιλτράρισμα",
|
||||
"chipActionFilterIn": "Με φιλτράρισμα",
|
||||
"chipActionHide": "Απόκρυψη",
|
||||
"chipActionPin": "Καρφίτσωμα στην κορυφή",
|
||||
"chipActionUnpin": "Ξέκαρφίτσωμα από την κορυφή",
|
||||
|
@ -155,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "Υψηλότερος ρυθμός",
|
||||
"displayRefreshRatePreferLowest": "Χαμηλότερος ρυθμός",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Παράλειψη",
|
||||
"slideshowVideoPlaybackMuted": "Αναπαραγωγή σε σίγαση",
|
||||
"slideshowVideoPlaybackWithSound": "Αναπαραγωγή με ήχο",
|
||||
"videoPlaybackSkip": "Παράλειψη",
|
||||
"videoPlaybackMuted": "Αναπαραγωγή σε σίγαση",
|
||||
"videoPlaybackWithSound": "Αναπαραγωγή με ήχο",
|
||||
|
||||
"themeBrightnessLight": "Φωτεινό",
|
||||
"themeBrightnessDark": "Σκούρο",
|
||||
|
@ -167,11 +169,15 @@
|
|||
"viewerTransitionParallax": "Παράλλαξη",
|
||||
"viewerTransitionFade": "Ξεθώριασμα",
|
||||
"viewerTransitionZoomIn": "Μεγέθυνση",
|
||||
"viewerTransitionNone": "Καμία",
|
||||
|
||||
"wallpaperTargetHome": "Αρχική οθόνη",
|
||||
"wallpaperTargetLock": "Οθόνη κλειδώματος",
|
||||
"wallpaperTargetHomeLock": "Αρχική οθόνη και οθόνη κλειδώματος",
|
||||
|
||||
"widgetOpenPageHome": "Άνοιγμα αρχικής σελίδας",
|
||||
"widgetOpenPageViewer": "Άνοιγμα προβολέα αρχείων",
|
||||
|
||||
"albumTierNew": "Νέα",
|
||||
"albumTierPinned": "Καρφιτσωμένα",
|
||||
"albumTierSpecial": "Συστήματος",
|
||||
|
@ -289,6 +295,7 @@
|
|||
"viewDialogLayoutSectionTitle": "Διαταξη",
|
||||
"viewDialogReverseSortOrder": "Αντίστροφη σειρά ταξινόμησης",
|
||||
|
||||
"tileLayoutMosaic": "Ψηφιδωτό",
|
||||
"tileLayoutGrid": "Πλέγμα",
|
||||
"tileLayoutList": "Λίστα",
|
||||
|
||||
|
@ -401,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "Τα μικρότερα πρώτα",
|
||||
|
||||
"albumGroupTier": "Ανά βαθμίδα",
|
||||
"albumGroupType": "Ανά τύπο",
|
||||
"albumGroupVolume": "Ανά αποθηκευτική μονάδα",
|
||||
"albumGroupNone": "Να μην γίνει ομαδοποίηση",
|
||||
|
||||
"albumMimeTypeMixed": "Μικτα",
|
||||
|
||||
"albumPickPageTitleCopy": "Αντιγραφή στο άλμπουμ",
|
||||
"albumPickPageTitleExport": "Εξαγωγή στο άλμπουμ",
|
||||
"albumPickPageTitleMove": "Μετακίνηση στο άλμπουμ",
|
||||
|
@ -442,6 +452,7 @@
|
|||
"settingsPageTitle": "Ρυθμισεις",
|
||||
"settingsSystemDefault": "Σύστημα",
|
||||
"settingsDefault": "Προεπιλογή",
|
||||
"settingsDisabled": "Απενεργοποιημένο",
|
||||
|
||||
"settingsSearchFieldLabel": "Αναζήτηση ρυθμίσεων",
|
||||
"settingsSearchEmpty": "Δεν υπάρχει αντίστοιχη ρύθμιση",
|
||||
|
@ -524,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "Επανάληψη",
|
||||
"settingsSlideshowShuffle": "Τυχαία σειρά",
|
||||
"settingsSlideshowFillScreen": "Χρησιμοποίηση πλήρης οθόνης",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Εφέ κινούμενου ζουμ",
|
||||
"settingsSlideshowTransitionTile": "Μετάβαση",
|
||||
"settingsSlideshowTransitionDialogTitle": "Μεταβαση",
|
||||
"settingsSlideshowIntervalTile": "Διάρκεια",
|
||||
"settingsSlideshowIntervalDialogTitle": "Διαρκεια",
|
||||
"settingsSlideshowVideoPlaybackTile": "Αναπαραγωγή βίντεο",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Αναπαραγωγη Βιντεο",
|
||||
|
||||
|
@ -535,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "Βιντεο",
|
||||
"settingsVideoShowVideos": "Εμφάνιση των βίντεο στη συλλογή",
|
||||
"settingsVideoEnableHardwareAcceleration": "Επιτάχυνση υλισμικού",
|
||||
"settingsVideoEnableAutoPlay": "Αυτόματη αναπαραγωγή κατά το άνοιγμα",
|
||||
"settingsVideoAutoPlay": "Αυτόματη αναπαραγωγή κατά το άνοιγμα",
|
||||
"settingsVideoLoopModeTile": "Επανάληψη αυτόματα στο τέλος κάθε βίντεο",
|
||||
"settingsVideoLoopModeDialogTitle": "Επαναληψη Αυτοματα στο Τελος Καθε Βιντεο",
|
||||
|
||||
|
@ -557,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "Έλεγχος",
|
||||
"settingsVideoControlsPageTitle": "Ελεγχος",
|
||||
"settingsVideoButtonsTile": "Κουμπιά",
|
||||
"settingsVideoButtonsDialogTitle": "Κουμπια",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Αγγίξτε την οθόνη δύο φορές για αναπαραγωγή/παύση",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Αγγίξτε δύο φορές τις άκρες της οθόνης για να πάτε πίσω/εμπρός",
|
||||
|
||||
|
@ -590,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "Κατάργηση κινούμενων εικόνων",
|
||||
"settingsRemoveAnimationsDialogTitle": "Καταργηση Κινουμενων Εικονων",
|
||||
"settingsTimeToTakeActionTile": "Χρόνος λήψης ενεργειών",
|
||||
"settingsTimeToTakeActionDialogTitle": "Χρονος Ληψης Ενεργειων",
|
||||
|
||||
"settingsDisplaySectionTitle": "Οθονη",
|
||||
"settingsThemeBrightnessTile": "Θέμα",
|
||||
|
@ -612,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Κορνιζα",
|
||||
"settingsWidgetShowOutline": "Περίγραμμα",
|
||||
"settingsWidgetOpenPage": "Όταν πατάτε στο γραφικό στοιχείο",
|
||||
|
||||
"settingsCollectionTile": "Συλλογή",
|
||||
|
||||
|
@ -620,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "Κορυφαιες Χωρες",
|
||||
"statsTopPlacesSectionTitle": "Κορυφαια Μερη",
|
||||
"statsTopTagsSectionTitle": "Κορυφαιες Ετικετες",
|
||||
"statsTopAlbumsSectionTitle": "Κορυφαια Αλμπουμ",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "Άνοιγμα πανοραμικών",
|
||||
"viewerSetWallpaperButtonLabel": "ΟΡΙΣΜΟΣ ΤΑΠΕΤΣΑΡΙΑΣ",
|
||||
|
@ -664,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Ανάλυση",
|
||||
"viewerInfoSearchSuggestionRights": "Δικαιώματα",
|
||||
|
||||
"wallpaperUseScrollEffect": "Εφέ κύλισης στην αρχική οθόνη",
|
||||
|
||||
"tagEditorPageTitle": "Επεξεργασια Ετικετων",
|
||||
"tagEditorPageNewTagFieldLabel": "Νέα ετικέτα",
|
||||
"tagEditorPageAddTagTooltip": "Προσθήκη ετικέτας",
|
||||
|
|
|
@ -69,6 +69,8 @@
|
|||
"chipActionGoToAlbumPage": "Show in Albums",
|
||||
"chipActionGoToCountryPage": "Show in Countries",
|
||||
"chipActionGoToTagPage": "Show in Tags",
|
||||
"chipActionFilterOut": "Filter out",
|
||||
"chipActionFilterIn": "Filter in",
|
||||
"chipActionHide": "Hide",
|
||||
"chipActionPin": "Pin to top",
|
||||
"chipActionUnpin": "Unpin from top",
|
||||
|
@ -195,9 +197,9 @@
|
|||
"displayRefreshRatePreferHighest": "Highest rate",
|
||||
"displayRefreshRatePreferLowest": "Lowest rate",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Skip",
|
||||
"slideshowVideoPlaybackMuted": "Play muted",
|
||||
"slideshowVideoPlaybackWithSound": "Play with sound",
|
||||
"videoPlaybackSkip": "Skip",
|
||||
"videoPlaybackMuted": "Play muted",
|
||||
"videoPlaybackWithSound": "Play with sound",
|
||||
|
||||
"themeBrightnessLight": "Light",
|
||||
"themeBrightnessDark": "Dark",
|
||||
|
@ -207,11 +209,15 @@
|
|||
"viewerTransitionParallax": "Parallax",
|
||||
"viewerTransitionFade": "Fade",
|
||||
"viewerTransitionZoomIn": "Zoom in",
|
||||
"viewerTransitionNone": "None",
|
||||
|
||||
"wallpaperTargetHome": "Home screen",
|
||||
"wallpaperTargetLock": "Lock screen",
|
||||
"wallpaperTargetHomeLock": "Home and lock screens",
|
||||
|
||||
"widgetOpenPageHome": "Open home",
|
||||
"widgetOpenPageViewer": "Open viewer",
|
||||
|
||||
"albumTierNew": "New",
|
||||
"albumTierPinned": "Pinned",
|
||||
"albumTierSpecial": "Common",
|
||||
|
@ -419,6 +425,7 @@
|
|||
"viewDialogLayoutSectionTitle": "Layout",
|
||||
"viewDialogReverseSortOrder": "Reverse sort order",
|
||||
|
||||
"tileLayoutMosaic": "Mosaic",
|
||||
"tileLayoutGrid": "Grid",
|
||||
"tileLayoutList": "List",
|
||||
|
||||
|
@ -581,9 +588,12 @@
|
|||
"sortOrderSmallestFirst": "Smallest first",
|
||||
|
||||
"albumGroupTier": "By tier",
|
||||
"albumGroupType": "By type",
|
||||
"albumGroupVolume": "By storage volume",
|
||||
"albumGroupNone": "Do not group",
|
||||
|
||||
"albumMimeTypeMixed": "Mixed",
|
||||
|
||||
"albumPickPageTitleCopy": "Copy to Album",
|
||||
"albumPickPageTitleExport": "Export to Album",
|
||||
"albumPickPageTitleMove": "Move to Album",
|
||||
|
@ -620,8 +630,9 @@
|
|||
"searchMetadataSectionTitle": "Metadata",
|
||||
|
||||
"settingsPageTitle": "Settings",
|
||||
"settingsSystemDefault": "System",
|
||||
"settingsSystemDefault": "System default",
|
||||
"settingsDefault": "Default",
|
||||
"settingsDisabled": "Disabled",
|
||||
|
||||
"settingsSearchFieldLabel": "Search settings",
|
||||
"settingsSearchEmpty": "No matching setting",
|
||||
|
@ -704,10 +715,9 @@
|
|||
"settingsSlideshowRepeat": "Repeat",
|
||||
"settingsSlideshowShuffle": "Shuffle",
|
||||
"settingsSlideshowFillScreen": "Fill screen",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Animated zoom effect",
|
||||
"settingsSlideshowTransitionTile": "Transition",
|
||||
"settingsSlideshowTransitionDialogTitle": "Transition",
|
||||
"settingsSlideshowIntervalTile": "Interval",
|
||||
"settingsSlideshowIntervalDialogTitle": "Interval",
|
||||
"settingsSlideshowVideoPlaybackTile": "Video playback",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Video Playback",
|
||||
|
||||
|
@ -715,7 +725,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Show videos",
|
||||
"settingsVideoEnableHardwareAcceleration": "Hardware acceleration",
|
||||
"settingsVideoEnableAutoPlay": "Auto play",
|
||||
"settingsVideoAutoPlay": "Auto play",
|
||||
"settingsVideoLoopModeTile": "Loop mode",
|
||||
"settingsVideoLoopModeDialogTitle": "Loop Mode",
|
||||
|
||||
|
@ -737,7 +747,6 @@
|
|||
"settingsVideoControlsTile": "Controls",
|
||||
"settingsVideoControlsPageTitle": "Controls",
|
||||
"settingsVideoButtonsTile": "Buttons",
|
||||
"settingsVideoButtonsDialogTitle": "Buttons",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Double tap to play/pause",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Double tap on screen edges to seek backward/forward",
|
||||
|
||||
|
@ -770,7 +779,6 @@
|
|||
"settingsRemoveAnimationsTile": "Remove animations",
|
||||
"settingsRemoveAnimationsDialogTitle": "Remove Animations",
|
||||
"settingsTimeToTakeActionTile": "Time to take action",
|
||||
"settingsTimeToTakeActionDialogTitle": "Time to Take Action",
|
||||
|
||||
"settingsDisplaySectionTitle": "Display",
|
||||
"settingsThemeBrightnessTile": "Theme",
|
||||
|
@ -792,6 +800,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Photo Frame",
|
||||
"settingsWidgetShowOutline": "Outline",
|
||||
"settingsWidgetOpenPage": "When tapping on the widget",
|
||||
|
||||
"settingsCollectionTile": "Collection",
|
||||
|
||||
|
@ -805,6 +814,7 @@
|
|||
"statsTopCountriesSectionTitle": "Top Countries",
|
||||
"statsTopPlacesSectionTitle": "Top Places",
|
||||
"statsTopTagsSectionTitle": "Top Tags",
|
||||
"statsTopAlbumsSectionTitle": "Top Albums",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "SET WALLPAPER",
|
||||
|
@ -849,6 +859,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Resolution",
|
||||
"viewerInfoSearchSuggestionRights": "Rights",
|
||||
|
||||
"wallpaperUseScrollEffect": "Use scroll effect on home screen",
|
||||
|
||||
"tagEditorPageTitle": "Edit Tags",
|
||||
"tagEditorPageNewTagFieldLabel": "New tag",
|
||||
"tagEditorPageAddTagTooltip": "Add tag",
|
||||
|
|
|
@ -151,9 +151,9 @@
|
|||
"displayRefreshRatePreferHighest": "Alta tasa",
|
||||
"displayRefreshRatePreferLowest": "Baja tasa",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Saltear",
|
||||
"slideshowVideoPlaybackMuted": "Reproducir sin sonido",
|
||||
"slideshowVideoPlaybackWithSound": "Reproducir con sonido",
|
||||
"videoPlaybackSkip": "Saltear",
|
||||
"videoPlaybackMuted": "Reproducir sin sonido",
|
||||
"videoPlaybackWithSound": "Reproducir con sonido",
|
||||
|
||||
"themeBrightnessLight": "Claro",
|
||||
"themeBrightnessDark": "Obscuro",
|
||||
|
@ -509,9 +509,7 @@
|
|||
"settingsSlideshowShuffle": "Mezclar",
|
||||
"settingsSlideshowFillScreen": "Llenar pantalla",
|
||||
"settingsSlideshowTransitionTile": "Transición",
|
||||
"settingsSlideshowTransitionDialogTitle": "Transición",
|
||||
"settingsSlideshowIntervalTile": "Intervalo",
|
||||
"settingsSlideshowIntervalDialogTitle": "Intervalo",
|
||||
"settingsSlideshowVideoPlaybackTile": "Reproducción de video",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Reproducción de video",
|
||||
|
||||
|
@ -519,7 +517,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Mostrar videos",
|
||||
"settingsVideoEnableHardwareAcceleration": "Aceleración por hardware",
|
||||
"settingsVideoEnableAutoPlay": "Reproducción automática",
|
||||
"settingsVideoAutoPlay": "Reproducción automática",
|
||||
"settingsVideoLoopModeTile": "Modo bucle",
|
||||
"settingsVideoLoopModeDialogTitle": "Modo bucle",
|
||||
|
||||
|
@ -541,7 +539,6 @@
|
|||
"settingsVideoControlsTile": "Controles",
|
||||
"settingsVideoControlsPageTitle": "Controles",
|
||||
"settingsVideoButtonsTile": "Botones",
|
||||
"settingsVideoButtonsDialogTitle": "Botones",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Doble toque para reproducir/pausar",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Doble toque en los bordes de la pantalla para buscar atrás/adelante",
|
||||
|
||||
|
@ -574,7 +571,6 @@
|
|||
"settingsRemoveAnimationsTile": "Remover animaciones",
|
||||
"settingsRemoveAnimationsDialogTitle": "Remover animaciones",
|
||||
"settingsTimeToTakeActionTile": "Retraso para ejecutar una acción",
|
||||
"settingsTimeToTakeActionDialogTitle": "Retraso para ejecutar una acción",
|
||||
|
||||
"settingsDisplaySectionTitle": "Pantalla",
|
||||
"settingsThemeBrightnessTile": "Tema",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "Afficher dans Albums",
|
||||
"chipActionGoToCountryPage": "Afficher dans Pays",
|
||||
"chipActionGoToTagPage": "Afficher dans Libellés",
|
||||
"chipActionFilterOut": "Exclure",
|
||||
"chipActionFilterIn": "Inclure",
|
||||
"chipActionHide": "Masquer",
|
||||
"chipActionPin": "Épingler",
|
||||
"chipActionUnpin": "Retirer",
|
||||
|
@ -155,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "Fréquence maximale",
|
||||
"displayRefreshRatePreferLowest": "Fréquence minimale",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Passer",
|
||||
"slideshowVideoPlaybackMuted": "Jouer sans son",
|
||||
"slideshowVideoPlaybackWithSound": "Jouer avec son",
|
||||
"videoPlaybackSkip": "Passer",
|
||||
"videoPlaybackMuted": "Jouer sans son",
|
||||
"videoPlaybackWithSound": "Jouer avec son",
|
||||
|
||||
"themeBrightnessLight": "Clair",
|
||||
"themeBrightnessDark": "Sombre",
|
||||
|
@ -167,11 +169,15 @@
|
|||
"viewerTransitionParallax": "Parallaxe",
|
||||
"viewerTransitionFade": "Fondu",
|
||||
"viewerTransitionZoomIn": "Zoom",
|
||||
"viewerTransitionNone": "Aucune",
|
||||
|
||||
"wallpaperTargetHome": "Écran d’accueil",
|
||||
"wallpaperTargetLock": "Écran de verrouillage",
|
||||
"wallpaperTargetHomeLock": "Écrans accueil et verrouillage",
|
||||
|
||||
"widgetOpenPageHome": "Ouvrir la page d’accueil",
|
||||
"widgetOpenPageViewer": "Ouvrir la visionneuse",
|
||||
|
||||
"albumTierNew": "Nouveaux",
|
||||
"albumTierPinned": "Épinglés",
|
||||
"albumTierSpecial": "Standards",
|
||||
|
@ -289,6 +295,7 @@
|
|||
"viewDialogLayoutSectionTitle": "Vue",
|
||||
"viewDialogReverseSortOrder": "Inverser l’ordre",
|
||||
|
||||
"tileLayoutMosaic": "Mosaïque",
|
||||
"tileLayoutGrid": "Grille",
|
||||
"tileLayoutList": "Liste",
|
||||
|
||||
|
@ -401,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "Moins larges d’abord",
|
||||
|
||||
"albumGroupTier": "par importance",
|
||||
"albumGroupType": "par type",
|
||||
"albumGroupVolume": "par volume de stockage",
|
||||
"albumGroupNone": "ne pas grouper",
|
||||
|
||||
"albumMimeTypeMixed": "Mixte",
|
||||
|
||||
"albumPickPageTitleCopy": "Copie",
|
||||
"albumPickPageTitleExport": "Export",
|
||||
"albumPickPageTitleMove": "Déplacement",
|
||||
|
@ -442,6 +452,7 @@
|
|||
"settingsPageTitle": "Réglages",
|
||||
"settingsSystemDefault": "Système",
|
||||
"settingsDefault": "Par défaut",
|
||||
"settingsDisabled": "Désactivé",
|
||||
|
||||
"settingsSearchFieldLabel": "Recherche de réglages",
|
||||
"settingsSearchEmpty": "Aucun réglage correspondant",
|
||||
|
@ -524,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "Répéter",
|
||||
"settingsSlideshowShuffle": "Aléatoire",
|
||||
"settingsSlideshowFillScreen": "Remplir l’écran",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Effet de zoom animé",
|
||||
"settingsSlideshowTransitionTile": "Transition",
|
||||
"settingsSlideshowTransitionDialogTitle": "Transition",
|
||||
"settingsSlideshowIntervalTile": "Intervalle",
|
||||
"settingsSlideshowIntervalDialogTitle": "Intervalle",
|
||||
"settingsSlideshowVideoPlaybackTile": "Lecture de vidéos",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Lecture de vidéos",
|
||||
|
||||
|
@ -535,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "Vidéo",
|
||||
"settingsVideoShowVideos": "Afficher les vidéos",
|
||||
"settingsVideoEnableHardwareAcceleration": "Accélération matérielle",
|
||||
"settingsVideoEnableAutoPlay": "Lecture automatique",
|
||||
"settingsVideoAutoPlay": "Lecture automatique",
|
||||
"settingsVideoLoopModeTile": "Lecture répétée",
|
||||
"settingsVideoLoopModeDialogTitle": "Lecture répétée",
|
||||
|
||||
|
@ -557,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "Contrôles",
|
||||
"settingsVideoControlsPageTitle": "Contrôles",
|
||||
"settingsVideoButtonsTile": "Boutons",
|
||||
"settingsVideoButtonsDialogTitle": "Boutons",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Appuyer deux fois pour lire ou mettre en pause",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Appuyer deux fois sur les bords de l’écran pour reculer ou avancer",
|
||||
|
||||
|
@ -590,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "Suppression des animations",
|
||||
"settingsRemoveAnimationsDialogTitle": "Suppression des animations",
|
||||
"settingsTimeToTakeActionTile": "Délai pour effectuer une action",
|
||||
"settingsTimeToTakeActionDialogTitle": "Délai pour effectuer une action",
|
||||
|
||||
"settingsDisplaySectionTitle": "Affichage",
|
||||
"settingsThemeBrightnessTile": "Thème",
|
||||
|
@ -612,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Cadre photo",
|
||||
"settingsWidgetShowOutline": "Contours",
|
||||
"settingsWidgetOpenPage": "Quand vous appuyez sur le widget",
|
||||
|
||||
"settingsCollectionTile": "Collection",
|
||||
|
||||
|
@ -620,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "Top pays",
|
||||
"statsTopPlacesSectionTitle": "Top lieux",
|
||||
"statsTopTagsSectionTitle": "Top libellés",
|
||||
"statsTopAlbumsSectionTitle": "Top albums",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "OUVRIR LE PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "APPLIQUER",
|
||||
|
@ -664,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Résolution",
|
||||
"viewerInfoSearchSuggestionRights": "Droits",
|
||||
|
||||
"wallpaperUseScrollEffect": "Utiliser l’effet de défilement sur l’écran d’accueil",
|
||||
|
||||
"tagEditorPageTitle": "Modifier les libellés",
|
||||
"tagEditorPageNewTagFieldLabel": "Nouveau libellé",
|
||||
"tagEditorPageAddTagTooltip": "Ajouter le libellé",
|
||||
|
|
|
@ -155,9 +155,9 @@
|
|||
"displayRefreshRatePreferHighest": "Penyegaran tertinggi",
|
||||
"displayRefreshRatePreferLowest": "Penyegaran terendah",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Lewati",
|
||||
"slideshowVideoPlaybackMuted": "Mainkan bisu",
|
||||
"slideshowVideoPlaybackWithSound": "Mainkan dengan suara",
|
||||
"videoPlaybackSkip": "Lewati",
|
||||
"videoPlaybackMuted": "Mainkan bisu",
|
||||
"videoPlaybackWithSound": "Mainkan dengan suara",
|
||||
|
||||
"themeBrightnessLight": "Terang",
|
||||
"themeBrightnessDark": "Gelap",
|
||||
|
@ -525,9 +525,7 @@
|
|||
"settingsSlideshowShuffle": "Acak",
|
||||
"settingsSlideshowFillScreen": "Isi layar",
|
||||
"settingsSlideshowTransitionTile": "Transisi",
|
||||
"settingsSlideshowTransitionDialogTitle": "Transisi",
|
||||
"settingsSlideshowIntervalTile": "Interval",
|
||||
"settingsSlideshowIntervalDialogTitle": "Interval",
|
||||
"settingsSlideshowVideoPlaybackTile": "Putaran ulang video",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Putaran Ulang Video",
|
||||
|
||||
|
@ -535,7 +533,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Tampilkan video",
|
||||
"settingsVideoEnableHardwareAcceleration": "Akselerasi perangkat keras",
|
||||
"settingsVideoEnableAutoPlay": "Putar otomatis",
|
||||
"settingsVideoAutoPlay": "Putar otomatis",
|
||||
"settingsVideoLoopModeTile": "Putar ulang",
|
||||
"settingsVideoLoopModeDialogTitle": "Putar Ulang",
|
||||
|
||||
|
@ -557,7 +555,6 @@
|
|||
"settingsVideoControlsTile": "Kontrol",
|
||||
"settingsVideoControlsPageTitle": "Kontrol",
|
||||
"settingsVideoButtonsTile": "Tombol",
|
||||
"settingsVideoButtonsDialogTitle": "Tombol",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Ketuk dua kali untuk mainkan/hentikan",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Ketuk dua kali di tepi layar untuk mencari kebelakang/kedepan",
|
||||
|
||||
|
@ -590,7 +587,6 @@
|
|||
"settingsRemoveAnimationsTile": "Hapus animasi",
|
||||
"settingsRemoveAnimationsDialogTitle": "Hapus Animasi",
|
||||
"settingsTimeToTakeActionTile": "Waktu untuk mengambil tindakan",
|
||||
"settingsTimeToTakeActionDialogTitle": "Saatnya Bertindak",
|
||||
|
||||
"settingsDisplaySectionTitle": "Tampilan",
|
||||
"settingsThemeBrightnessTile": "Tema",
|
||||
|
|
|
@ -40,7 +40,9 @@
|
|||
"chipActionDelete": "Elimina",
|
||||
"chipActionGoToAlbumPage": "Mostra negli album",
|
||||
"chipActionGoToCountryPage": "Mostra nei Paesi",
|
||||
"chipActionGoToTagPage": "Mostra nelle etichette",
|
||||
"chipActionGoToTagPage": "Mostra nelle Etichette",
|
||||
"chipActionFilterOut": "Escludi",
|
||||
"chipActionFilterIn": "Includi",
|
||||
"chipActionHide": "Nascondi",
|
||||
"chipActionPin": "Fissa in alto",
|
||||
"chipActionUnpin": "Rimuovi dall’alto",
|
||||
|
@ -155,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "Frequenza massima",
|
||||
"displayRefreshRatePreferLowest": "Frequenza minima",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Salta",
|
||||
"slideshowVideoPlaybackMuted": "Riproduci senza audio",
|
||||
"slideshowVideoPlaybackWithSound": "Riproduci con audio",
|
||||
"videoPlaybackSkip": "Salta",
|
||||
"videoPlaybackMuted": "Riproduci senza audio",
|
||||
"videoPlaybackWithSound": "Riproduci con audio",
|
||||
|
||||
"themeBrightnessLight": "Chiaro",
|
||||
"themeBrightnessDark": "Scuro",
|
||||
|
@ -167,11 +169,15 @@
|
|||
"viewerTransitionParallax": "Parallasse",
|
||||
"viewerTransitionFade": "Dissolvenza",
|
||||
"viewerTransitionZoomIn": "Ingrandisci",
|
||||
"viewerTransitionNone": "Nessuna",
|
||||
|
||||
"wallpaperTargetHome": "Schermata iniziale",
|
||||
"wallpaperTargetLock": "Schermata di blocco",
|
||||
"wallpaperTargetHomeLock": "Schermata iniziale e di blocco",
|
||||
|
||||
"widgetOpenPageHome": "Apri pagina iniziale",
|
||||
"widgetOpenPageViewer": "Apri visualizzazione",
|
||||
|
||||
"albumTierNew": "Nuovi",
|
||||
"albumTierPinned": "Fissati",
|
||||
"albumTierSpecial": "Frequenti",
|
||||
|
@ -289,6 +295,7 @@
|
|||
"viewDialogLayoutSectionTitle": "Layout",
|
||||
"viewDialogReverseSortOrder": "Inverti ordinamento",
|
||||
|
||||
"tileLayoutMosaic": "Mosaico",
|
||||
"tileLayoutGrid": "Griglia",
|
||||
"tileLayoutList": "Lista",
|
||||
|
||||
|
@ -390,6 +397,7 @@
|
|||
"sortBySize": "Per dimensione",
|
||||
"sortByAlbumFileName": "Per album e nome del file",
|
||||
"sortByRating": "Per valutazione",
|
||||
|
||||
"sortOrderNewestFirst": "Prima i più nuovi",
|
||||
"sortOrderOldestFirst": "Prima i più vecchi",
|
||||
"sortOrderAtoZ": "Dalla A alla Z",
|
||||
|
@ -400,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "Prima i più piccoli",
|
||||
|
||||
"albumGroupTier": "Per importanza",
|
||||
"albumGroupType": "Per tipo",
|
||||
"albumGroupVolume": "Per volume di archiviazione",
|
||||
"albumGroupNone": "Non raggruppare",
|
||||
|
||||
"albumMimeTypeMixed": "Misto",
|
||||
|
||||
"albumPickPageTitleCopy": "Copia",
|
||||
"albumPickPageTitleExport": "Esporta",
|
||||
"albumPickPageTitleMove": "Sposta",
|
||||
|
@ -441,6 +452,7 @@
|
|||
"settingsPageTitle": "Impostazioni",
|
||||
"settingsSystemDefault": "Sistema",
|
||||
"settingsDefault": "Predefinite",
|
||||
"settingsDisabled": "Disabilitato",
|
||||
|
||||
"settingsSearchFieldLabel": "Ricerca impostazioni",
|
||||
"settingsSearchEmpty": "Nessuna impostazione corrispondente",
|
||||
|
@ -523,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "Ripeti",
|
||||
"settingsSlideshowShuffle": "Ordine casuale",
|
||||
"settingsSlideshowFillScreen": "Riempi schermo",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Effetto ingrandimento animato",
|
||||
"settingsSlideshowTransitionTile": "Transizione",
|
||||
"settingsSlideshowTransitionDialogTitle": "Transizione",
|
||||
"settingsSlideshowIntervalTile": "Intervallo",
|
||||
"settingsSlideshowIntervalDialogTitle": "Intervallo",
|
||||
"settingsSlideshowVideoPlaybackTile": "Riproduzione video",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Riproduzione video",
|
||||
|
||||
|
@ -534,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Mostra video",
|
||||
"settingsVideoEnableHardwareAcceleration": "Accelerazione hardware",
|
||||
"settingsVideoEnableAutoPlay": "Riproduzione automatica",
|
||||
"settingsVideoAutoPlay": "Riproduzione automatica",
|
||||
"settingsVideoLoopModeTile": "Modalità loop",
|
||||
"settingsVideoLoopModeDialogTitle": "Modalità loop",
|
||||
|
||||
|
@ -556,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "Controlli",
|
||||
"settingsVideoControlsPageTitle": "Controlli",
|
||||
"settingsVideoButtonsTile": "Pulsanti",
|
||||
"settingsVideoButtonsDialogTitle": "Pulsanti",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Doppio tocco per play/pausa",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Doppio tocco sui bordi dello schermo per cercare avanti/indietro",
|
||||
|
||||
|
@ -589,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "Rimuovi animazioni",
|
||||
"settingsRemoveAnimationsDialogTitle": "Rimuovi animazioni",
|
||||
"settingsTimeToTakeActionTile": "Tempo di reazione",
|
||||
"settingsTimeToTakeActionDialogTitle": "Tempo di reazione",
|
||||
|
||||
"settingsDisplaySectionTitle": "Schermo",
|
||||
"settingsThemeBrightnessTile": "Tema",
|
||||
|
@ -611,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Cornice foto",
|
||||
"settingsWidgetShowOutline": "Contorno",
|
||||
"settingsWidgetOpenPage": "Se tocchi il widget",
|
||||
|
||||
"settingsCollectionTile": "Collezione",
|
||||
|
||||
|
@ -619,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "Paesi più frequenti",
|
||||
"statsTopPlacesSectionTitle": "Luoghi più frequenti",
|
||||
"statsTopTagsSectionTitle": "Etichette più frequenti",
|
||||
"statsTopAlbumsSectionTitle": "Album più frequenti",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "APRI PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "IMPOSTA SFONDO",
|
||||
|
@ -663,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Risoluzione",
|
||||
"viewerInfoSearchSuggestionRights": "Diritti",
|
||||
|
||||
"wallpaperUseScrollEffect": "Usa effetto di scorrimento nella schermata iniziale",
|
||||
|
||||
"tagEditorPageTitle": "Modifica etichette",
|
||||
"tagEditorPageNewTagFieldLabel": "Nuova etichetta",
|
||||
"tagEditorPageAddTagTooltip": "Aggiungi etichetta",
|
||||
|
|
|
@ -151,9 +151,9 @@
|
|||
"displayRefreshRatePreferHighest": "高レート",
|
||||
"displayRefreshRatePreferLowest": "低レート",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "スキップ",
|
||||
"slideshowVideoPlaybackMuted": "ミュート再生",
|
||||
"slideshowVideoPlaybackWithSound": "音声あり再生",
|
||||
"videoPlaybackSkip": "スキップ",
|
||||
"videoPlaybackMuted": "ミュート再生",
|
||||
"videoPlaybackWithSound": "音声あり再生",
|
||||
|
||||
"themeBrightnessLight": "ライト",
|
||||
"themeBrightnessDark": "ダーク",
|
||||
|
@ -508,9 +508,7 @@
|
|||
"settingsSlideshowShuffle": "シャッフル",
|
||||
"settingsSlideshowFillScreen": "画面いっぱいに表示",
|
||||
"settingsSlideshowTransitionTile": "トランジション",
|
||||
"settingsSlideshowTransitionDialogTitle": "トランジション",
|
||||
"settingsSlideshowIntervalTile": "間隔",
|
||||
"settingsSlideshowIntervalDialogTitle": "間隔",
|
||||
"settingsSlideshowVideoPlaybackTile": "動画を再生",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "動画再生",
|
||||
|
||||
|
@ -518,7 +516,7 @@
|
|||
"settingsVideoSectionTitle": "動画",
|
||||
"settingsVideoShowVideos": "動画を表示",
|
||||
"settingsVideoEnableHardwareAcceleration": "ハードウェア アクセラレーション",
|
||||
"settingsVideoEnableAutoPlay": "自動再生",
|
||||
"settingsVideoAutoPlay": "自動再生",
|
||||
"settingsVideoLoopModeTile": "ループ モード",
|
||||
"settingsVideoLoopModeDialogTitle": "ループ モード",
|
||||
|
||||
|
@ -540,7 +538,6 @@
|
|||
"settingsVideoControlsTile": "操作",
|
||||
"settingsVideoControlsPageTitle": "操作",
|
||||
"settingsVideoButtonsTile": "ボタン",
|
||||
"settingsVideoButtonsDialogTitle": "ボタン",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "2回タップして再生/一時停止",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "画面の角を2回タップして早送り/早戻し",
|
||||
|
||||
|
@ -573,7 +570,6 @@
|
|||
"settingsRemoveAnimationsTile": "アニメーションの削除",
|
||||
"settingsRemoveAnimationsDialogTitle": "アニメーションの削除",
|
||||
"settingsTimeToTakeActionTile": "操作までの時間",
|
||||
"settingsTimeToTakeActionDialogTitle": "操作までの時間",
|
||||
|
||||
"settingsDisplaySectionTitle": "ディスプレイ",
|
||||
"settingsThemeBrightnessTile": "テーマ",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "앨범 페이지에서 보기",
|
||||
"chipActionGoToCountryPage": "국가 페이지에서 보기",
|
||||
"chipActionGoToTagPage": "태그 페이지에서 보기",
|
||||
"chipActionFilterOut": "제외하기",
|
||||
"chipActionFilterIn": "포함시키기",
|
||||
"chipActionHide": "숨기기",
|
||||
"chipActionPin": "고정",
|
||||
"chipActionUnpin": "고정 해제",
|
||||
|
@ -155,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "가장 높은 재생률",
|
||||
"displayRefreshRatePreferLowest": "가장 낮은 재생률",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "생략",
|
||||
"slideshowVideoPlaybackMuted": "음소거 재생",
|
||||
"slideshowVideoPlaybackWithSound": "일반 재생",
|
||||
"videoPlaybackSkip": "생략",
|
||||
"videoPlaybackMuted": "음소거 재생",
|
||||
"videoPlaybackWithSound": "일반 재생",
|
||||
|
||||
"themeBrightnessLight": "라이트",
|
||||
"themeBrightnessDark": "다크",
|
||||
|
@ -167,11 +169,15 @@
|
|||
"viewerTransitionParallax": "시차",
|
||||
"viewerTransitionFade": "페이드",
|
||||
"viewerTransitionZoomIn": "확대",
|
||||
"viewerTransitionNone": "없음",
|
||||
|
||||
"wallpaperTargetHome": "홈 화면",
|
||||
"wallpaperTargetLock": "잠금화면",
|
||||
"wallpaperTargetHomeLock": "홈 및 잠금화면",
|
||||
|
||||
"widgetOpenPageHome": "홈 열기",
|
||||
"widgetOpenPageViewer": "뷰어 열기",
|
||||
|
||||
"albumTierNew": "신규",
|
||||
"albumTierPinned": "고정",
|
||||
"albumTierSpecial": "기본",
|
||||
|
@ -289,6 +295,7 @@
|
|||
"viewDialogLayoutSectionTitle": "배치",
|
||||
"viewDialogReverseSortOrder": "순서를 뒤바꾸기",
|
||||
|
||||
"tileLayoutMosaic": "모자이크",
|
||||
"tileLayoutGrid": "바둑판",
|
||||
"tileLayoutList": "목록",
|
||||
|
||||
|
@ -401,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "작은 파일순",
|
||||
|
||||
"albumGroupTier": "단계별로",
|
||||
"albumGroupType": "유형별로",
|
||||
"albumGroupVolume": "저장공간별로",
|
||||
"albumGroupNone": "묶음 없음",
|
||||
|
||||
"albumMimeTypeMixed": "혼합",
|
||||
|
||||
"albumPickPageTitleCopy": "앨범으로 복사",
|
||||
"albumPickPageTitleExport": "앨범으로 내보내기",
|
||||
"albumPickPageTitleMove": "앨범으로 이동",
|
||||
|
@ -440,8 +450,9 @@
|
|||
"searchMetadataSectionTitle": "메타데이터",
|
||||
|
||||
"settingsPageTitle": "설정",
|
||||
"settingsSystemDefault": "시스템",
|
||||
"settingsSystemDefault": "시스템 기본값",
|
||||
"settingsDefault": "기본",
|
||||
"settingsDisabled": "사용 안함",
|
||||
|
||||
"settingsSearchFieldLabel": "설정 검색",
|
||||
"settingsSearchEmpty": "결과가 없습니다",
|
||||
|
@ -524,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "반복",
|
||||
"settingsSlideshowShuffle": "순서섞기",
|
||||
"settingsSlideshowFillScreen": "화면 채우기",
|
||||
"settingsSlideshowAnimatedZoomEffect": "애니메이션 확대/축소 효과",
|
||||
"settingsSlideshowTransitionTile": "전환 효과",
|
||||
"settingsSlideshowTransitionDialogTitle": "전환 효과",
|
||||
"settingsSlideshowIntervalTile": "교체 주기",
|
||||
"settingsSlideshowIntervalDialogTitle": "교체 주기",
|
||||
"settingsSlideshowVideoPlaybackTile": "동영상 재생",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "동영상 재생",
|
||||
|
||||
|
@ -535,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "동영상",
|
||||
"settingsVideoShowVideos": "미디어에 동영상 표시",
|
||||
"settingsVideoEnableHardwareAcceleration": "하드웨어 가속",
|
||||
"settingsVideoEnableAutoPlay": "자동 재생",
|
||||
"settingsVideoAutoPlay": "자동 재생",
|
||||
"settingsVideoLoopModeTile": "반복 모드",
|
||||
"settingsVideoLoopModeDialogTitle": "반복 모드",
|
||||
|
||||
|
@ -557,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "제어",
|
||||
"settingsVideoControlsPageTitle": "제어",
|
||||
"settingsVideoButtonsTile": "버튼",
|
||||
"settingsVideoButtonsDialogTitle": "버튼",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "두 번 탭해서 재생이나 일시정지하기",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "화면 측면에서 두 번 탭해서 앞뒤로 가기",
|
||||
|
||||
|
@ -590,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "애니메이션 삭제",
|
||||
"settingsRemoveAnimationsDialogTitle": "애니메이션 삭제",
|
||||
"settingsTimeToTakeActionTile": "액션 취하기 전 대기 시간",
|
||||
"settingsTimeToTakeActionDialogTitle": "액션 취하기 전 대기 시간",
|
||||
|
||||
"settingsDisplaySectionTitle": "디스플레이",
|
||||
"settingsThemeBrightnessTile": "테마",
|
||||
|
@ -612,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "사진 액자",
|
||||
"settingsWidgetShowOutline": "윤곽",
|
||||
"settingsWidgetOpenPage": "위젯을 탭하면",
|
||||
|
||||
"settingsCollectionTile": "미디어",
|
||||
|
||||
|
@ -620,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "국가 랭킹",
|
||||
"statsTopPlacesSectionTitle": "장소 랭킹",
|
||||
"statsTopTagsSectionTitle": "태그 랭킹",
|
||||
"statsTopAlbumsSectionTitle": "앨범 랭킹",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "파노라마 열기",
|
||||
"viewerSetWallpaperButtonLabel": "설정",
|
||||
|
@ -664,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "해상도",
|
||||
"viewerInfoSearchSuggestionRights": "권리",
|
||||
|
||||
"wallpaperUseScrollEffect": "홈 화면에 스크롤 효과 사용",
|
||||
|
||||
"tagEditorPageTitle": "태그 수정",
|
||||
"tagEditorPageNewTagFieldLabel": "새 태그",
|
||||
"tagEditorPageAddTagTooltip": "태그 추가",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "Tonen Albums",
|
||||
"chipActionGoToCountryPage": "Tonen in Landen",
|
||||
"chipActionGoToTagPage": "Tonen in Labels",
|
||||
"chipActionFilterOut": "Uitfilteren",
|
||||
"chipActionFilterIn": "Infilteren",
|
||||
"chipActionHide": "Verbergen",
|
||||
"chipActionPin": "Bovenaan pinnen",
|
||||
"chipActionUnpin": "Unpinnen",
|
||||
|
@ -155,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "Hoogste waardering",
|
||||
"displayRefreshRatePreferLowest": "Laagste waardering",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Overslaan",
|
||||
"slideshowVideoPlaybackMuted": "Gedempte afspelen",
|
||||
"slideshowVideoPlaybackWithSound": "Met geluid afspelen",
|
||||
"videoPlaybackSkip": "Overslaan",
|
||||
"videoPlaybackMuted": "Gedempte afspelen",
|
||||
"videoPlaybackWithSound": "Met geluid afspelen",
|
||||
|
||||
"themeBrightnessLight": "Licht",
|
||||
"themeBrightnessDark": "Donker",
|
||||
|
@ -167,11 +169,15 @@
|
|||
"viewerTransitionParallax": "Parallax",
|
||||
"viewerTransitionFade": "Vervagen",
|
||||
"viewerTransitionZoomIn": "Inzoomen",
|
||||
"viewerTransitionNone": "Geen",
|
||||
|
||||
"wallpaperTargetHome": "Home scherm",
|
||||
"wallpaperTargetLock": "Vergrendel scherm",
|
||||
"wallpaperTargetHomeLock": "Home and Vergrendel schermen",
|
||||
|
||||
"widgetOpenPageHome": "Open startscherm",
|
||||
"widgetOpenPageViewer": "Open viewer",
|
||||
|
||||
"albumTierNew": "Nieuw",
|
||||
"albumTierPinned": "Gepint",
|
||||
"albumTierSpecial": "Veelgebruikt",
|
||||
|
@ -289,6 +295,7 @@
|
|||
"viewDialogLayoutSectionTitle": "Layout",
|
||||
"viewDialogReverseSortOrder": "Draai sorteerrichting om",
|
||||
|
||||
"tileLayoutMosaic": "Mozaïek",
|
||||
"tileLayoutGrid": "Raster",
|
||||
"tileLayoutList": "Lijst",
|
||||
|
||||
|
@ -401,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "Kleinste eerst",
|
||||
|
||||
"albumGroupTier": "Op rang",
|
||||
"albumGroupType": "Op type",
|
||||
"albumGroupVolume": "Op opslagvolume",
|
||||
"albumGroupNone": "Niet groeperen",
|
||||
|
||||
"albumMimeTypeMixed": "Gemengd",
|
||||
|
||||
"albumPickPageTitleCopy": "Kopieer naar Album",
|
||||
"albumPickPageTitleExport": "Exporteer naar Album",
|
||||
"albumPickPageTitleMove": "Verplaats naar Album",
|
||||
|
@ -442,6 +452,7 @@
|
|||
"settingsPageTitle": "Instellingen",
|
||||
"settingsSystemDefault": "Systeem",
|
||||
"settingsDefault": "Standaard",
|
||||
"settingsDisabled": "Uitgeschakeld",
|
||||
|
||||
"settingsSearchFieldLabel": "Instellingen doorzoeken",
|
||||
"settingsSearchEmpty": "Geen instellingen gevonden",
|
||||
|
@ -524,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "Herhalen",
|
||||
"settingsSlideshowShuffle": "Shuffle",
|
||||
"settingsSlideshowFillScreen": "Volledig scherm",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Geanimeerd zoomeffect",
|
||||
"settingsSlideshowTransitionTile": "Overgang",
|
||||
"settingsSlideshowTransitionDialogTitle": "Overgang",
|
||||
"settingsSlideshowIntervalTile": "Interval",
|
||||
"settingsSlideshowIntervalDialogTitle": "Interval",
|
||||
"settingsSlideshowVideoPlaybackTile": "Video afspelen",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Video afspelen",
|
||||
|
||||
|
@ -535,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Videos",
|
||||
"settingsVideoEnableHardwareAcceleration": "Hardware acceleratie",
|
||||
"settingsVideoEnableAutoPlay": "Automatisch afspelen",
|
||||
"settingsVideoAutoPlay": "Automatisch afspelen",
|
||||
"settingsVideoLoopModeTile": "Herhaald afspelen",
|
||||
"settingsVideoLoopModeDialogTitle": "Herhaald afspelen",
|
||||
|
||||
|
@ -557,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "Bediening",
|
||||
"settingsVideoControlsPageTitle": "Bediening",
|
||||
"settingsVideoButtonsTile": "Knoppen",
|
||||
"settingsVideoButtonsDialogTitle": "Knoppen",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Dubbeltik om te spelen/pauzeren",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Dubbeltik op schermranden om achteruit/vooruit te zoeken",
|
||||
|
||||
|
@ -590,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "Animaties verwijderen",
|
||||
"settingsRemoveAnimationsDialogTitle": "Animaties verwijderen",
|
||||
"settingsTimeToTakeActionTile": "Tijd om actie te ondernemen",
|
||||
"settingsTimeToTakeActionDialogTitle": "Tijd om actie te ondernemen",
|
||||
|
||||
"settingsDisplaySectionTitle": "Scherm",
|
||||
"settingsThemeBrightnessTile": "Thema",
|
||||
|
@ -612,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Foto Lijstje",
|
||||
"settingsWidgetShowOutline": "Contour",
|
||||
"settingsWidgetOpenPage": "Wanneer u op de widget tikt",
|
||||
|
||||
"settingsCollectionTile": "Verzameling",
|
||||
|
||||
|
@ -620,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "Top Landen",
|
||||
"statsTopPlacesSectionTitle": "Top Plaatsen",
|
||||
"statsTopTagsSectionTitle": "Top Labels",
|
||||
"statsTopAlbumsSectionTitle": "Top Albums",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "OPEN PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "ALS ACHTERGROND INSTELLEN",
|
||||
|
@ -664,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Resolutie",
|
||||
"viewerInfoSearchSuggestionRights": "Rechten",
|
||||
|
||||
"wallpaperUseScrollEffect": "Scroll-effect gebruiken op startscherm",
|
||||
|
||||
"tagEditorPageTitle": "Wijzig Labels",
|
||||
"tagEditorPageNewTagFieldLabel": "Nieuw label",
|
||||
"tagEditorPageAddTagTooltip": "Label toevoegen",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "Mostrar nos Álbuns",
|
||||
"chipActionGoToCountryPage": "Mostrar em Países",
|
||||
"chipActionGoToTagPage": "Mostrar em Etiquetas",
|
||||
"chipActionFilterOut": "Filtrar dentro",
|
||||
"chipActionFilterIn": "Filtrar fora",
|
||||
"chipActionHide": "Ocultar",
|
||||
"chipActionPin": "Fixar no topo",
|
||||
"chipActionUnpin": "Desafixar do topo",
|
||||
|
@ -88,15 +90,18 @@
|
|||
|
||||
"entryInfoActionEditDate": "Editar data e hora",
|
||||
"entryInfoActionEditLocation": "Editar localização",
|
||||
"entryInfoActionEditTitleDescription": "Editar título e descrição",
|
||||
"entryInfoActionEditRating": "Editar classificação",
|
||||
"entryInfoActionEditTags": "Editar etiquetas",
|
||||
"entryInfoActionRemoveMetadata": "Remover metadados",
|
||||
|
||||
"filterBinLabel": "Lixeira",
|
||||
"filterFavouriteLabel": "Favorito",
|
||||
"filterNoDateLabel": "Sem data",
|
||||
"filterNoLocationLabel": "Não localizado",
|
||||
"filterNoRatingLabel": "Sem classificação",
|
||||
"filterNoTagLabel": "Sem etiqueta",
|
||||
"filterNoTitleLabel": "Sem título",
|
||||
"filterOnThisDayLabel": "Neste dia",
|
||||
"filterRecentlyAddedLabel": "Adicionado recentemente",
|
||||
"filterRatingRejectedLabel": "Rejeitado",
|
||||
|
@ -152,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "Taxa mais alta",
|
||||
"displayRefreshRatePreferLowest": "Taxa mais baixa",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Pular",
|
||||
"slideshowVideoPlaybackMuted": "Reproduzir sem som",
|
||||
"slideshowVideoPlaybackWithSound": "Reproduzir com som",
|
||||
"videoPlaybackSkip": "Pular",
|
||||
"videoPlaybackMuted": "Reproduzir sem som",
|
||||
"videoPlaybackWithSound": "Reproduzir com som",
|
||||
|
||||
"themeBrightnessLight": "Claro",
|
||||
"themeBrightnessDark": "Escuro",
|
||||
|
@ -164,11 +169,15 @@
|
|||
"viewerTransitionParallax": "Parallax",
|
||||
"viewerTransitionFade": "Desvaneça",
|
||||
"viewerTransitionZoomIn": "Mais zoom",
|
||||
"viewerTransitionNone": "Nenhum",
|
||||
|
||||
"wallpaperTargetHome": "Tela inicial",
|
||||
"wallpaperTargetLock": "Tela de bloqueio",
|
||||
"wallpaperTargetHomeLock": "Telas iniciais e de bloqueio",
|
||||
|
||||
"widgetOpenPageHome": "Abrir inicial",
|
||||
"widgetOpenPageViewer": "Abrir visualizador",
|
||||
|
||||
"albumTierNew": "Novo",
|
||||
"albumTierPinned": "Fixada",
|
||||
"albumTierSpecial": "Comum",
|
||||
|
@ -284,7 +293,9 @@
|
|||
"viewDialogSortSectionTitle": "Organizar",
|
||||
"viewDialogGroupSectionTitle": "Grupo",
|
||||
"viewDialogLayoutSectionTitle": "Layout",
|
||||
"viewDialogReverseSortOrder": "Ordem de classificação inversa",
|
||||
|
||||
"tileLayoutMosaic": "Mosaico",
|
||||
"tileLayoutGrid": "Grid",
|
||||
"tileLayoutList": "Lista",
|
||||
|
||||
|
@ -387,10 +398,22 @@
|
|||
"sortByAlbumFileName": "Por álbum e nome de arquivo",
|
||||
"sortByRating": "Por classificação",
|
||||
|
||||
"sortOrderNewestFirst": "Os mais novos primeiro",
|
||||
"sortOrderOldestFirst": "Mais velhos primeiro",
|
||||
"sortOrderAtoZ": "A a Z",
|
||||
"sortOrderZtoA": "Z a A",
|
||||
"sortOrderHighestFirst": "Mais alto primeiro",
|
||||
"sortOrderLowestFirst": "Mais baixo primeiro",
|
||||
"sortOrderLargestFirst": "Maior primeiro",
|
||||
"sortOrderSmallestFirst": "Menor primeiro",
|
||||
|
||||
"albumGroupType": "Por tipo",
|
||||
"albumGroupTier": "Por nível",
|
||||
"albumGroupVolume": "Por volume de armazenamento",
|
||||
"albumGroupNone": "Não agrupe",
|
||||
|
||||
"albumMimeTypeMixed": "Misturado",
|
||||
|
||||
"albumPickPageTitleCopy": "Copiar para o álbum",
|
||||
"albumPickPageTitleExport": "Exportar para o álbum",
|
||||
"albumPickPageTitleMove": "Mover para o álbum",
|
||||
|
@ -424,10 +447,12 @@
|
|||
"searchPlacesSectionTitle": "Locais",
|
||||
"searchTagsSectionTitle": "Etiquetas",
|
||||
"searchRatingSectionTitle": "Classificações",
|
||||
"searchMetadataSectionTitle": "Metadados",
|
||||
|
||||
"settingsPageTitle": "Configurações",
|
||||
"settingsSystemDefault": "Sistema",
|
||||
"settingsDefault": "Padrão",
|
||||
"settingsDisabled": "Desativado",
|
||||
|
||||
"settingsSearchFieldLabel": "Pesquisar configuração",
|
||||
"settingsSearchEmpty": "Nenhuma configuração correspondente",
|
||||
|
@ -510,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "Repetir",
|
||||
"settingsSlideshowShuffle": "Embaralhar",
|
||||
"settingsSlideshowFillScreen": "Preencher tela",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Efeito de zoom animado",
|
||||
"settingsSlideshowTransitionTile": "Transição",
|
||||
"settingsSlideshowTransitionDialogTitle": "Transição",
|
||||
"settingsSlideshowIntervalTile": "Intervalo",
|
||||
"settingsSlideshowIntervalDialogTitle": "Intervalo",
|
||||
"settingsSlideshowVideoPlaybackTile": "Reprodução de vídeo",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Reprodução de vídeo",
|
||||
|
||||
|
@ -521,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "Vídeo",
|
||||
"settingsVideoShowVideos": "Mostrar vídeos",
|
||||
"settingsVideoEnableHardwareAcceleration": "Aceleraçao do hardware",
|
||||
"settingsVideoEnableAutoPlay": "Reprodução automática",
|
||||
"settingsVideoAutoPlay": "Reprodução automática",
|
||||
"settingsVideoLoopModeTile": "Modo de loop",
|
||||
"settingsVideoLoopModeDialogTitle": "Modo de loop",
|
||||
|
||||
|
@ -543,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "Controles",
|
||||
"settingsVideoControlsPageTitle": "Controles",
|
||||
"settingsVideoButtonsTile": "Botões",
|
||||
"settingsVideoButtonsDialogTitle": "Botões",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Toque duas vezes para reproduzir/pausar",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Toque duas vezes nas bordas da tela buscar para trás/para frente",
|
||||
|
||||
|
@ -576,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "Remover animações",
|
||||
"settingsRemoveAnimationsDialogTitle": "Remover Animações",
|
||||
"settingsTimeToTakeActionTile": "Tempo para executar uma ação",
|
||||
"settingsTimeToTakeActionDialogTitle": "Tempo para executar uma ação",
|
||||
|
||||
"settingsDisplaySectionTitle": "Tela",
|
||||
"settingsThemeBrightnessTile": "Tema",
|
||||
|
@ -598,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Porta-retratos",
|
||||
"settingsWidgetShowOutline": "Contorno",
|
||||
"settingsWidgetOpenPage": "Ao tocar no widget",
|
||||
|
||||
"settingsCollectionTile": "Coleção",
|
||||
|
||||
|
@ -606,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "Principais Países",
|
||||
"statsTopPlacesSectionTitle": "Principais Lugares",
|
||||
"statsTopTagsSectionTitle": "Principais Etiquetas",
|
||||
"statsTopAlbumsSectionTitle": "Principais Álbuns",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "ABRIR PANORAMA",
|
||||
"viewerSetWallpaperButtonLabel": "DEFINIR PAPEL DE PAREDE",
|
||||
|
@ -650,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Resolução",
|
||||
"viewerInfoSearchSuggestionRights": "Direitos",
|
||||
|
||||
"wallpaperUseScrollEffect": "Use o efeito de rolagem na tela inicial",
|
||||
|
||||
"tagEditorPageTitle": "Editar etiquetas",
|
||||
"tagEditorPageNewTagFieldLabel": "Nova etiqueta",
|
||||
"tagEditorPageAddTagTooltip": "Adicionar etiqueta",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "Показать в Альбомах",
|
||||
"chipActionGoToCountryPage": "Показать в Странах",
|
||||
"chipActionGoToTagPage": "Показать в тегах",
|
||||
"chipActionFilterOut": "Исключить",
|
||||
"chipActionFilterIn": "Включить",
|
||||
"chipActionHide": "Скрыть",
|
||||
"chipActionPin": "Закрепить",
|
||||
"chipActionUnpin": "Открепить",
|
||||
|
@ -155,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "Наивысшая частота",
|
||||
"displayRefreshRatePreferLowest": "Наименьшая частота",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "Пропустить",
|
||||
"slideshowVideoPlaybackMuted": "Играть без звука",
|
||||
"slideshowVideoPlaybackWithSound": "Играть со звуком",
|
||||
"videoPlaybackSkip": "Пропустить",
|
||||
"videoPlaybackMuted": "Играть без звука",
|
||||
"videoPlaybackWithSound": "Играть со звуком",
|
||||
|
||||
"themeBrightnessLight": "Светлая",
|
||||
"themeBrightnessDark": "Тёмная",
|
||||
|
@ -167,11 +169,15 @@
|
|||
"viewerTransitionParallax": "Параллакс",
|
||||
"viewerTransitionFade": "Затухание",
|
||||
"viewerTransitionZoomIn": "Приближение",
|
||||
"viewerTransitionNone": "Нет",
|
||||
|
||||
"wallpaperTargetHome": "Домашний экран",
|
||||
"wallpaperTargetLock": "Экран блокировки",
|
||||
"wallpaperTargetHomeLock": "Домашний экран и экран блокировки",
|
||||
|
||||
"widgetOpenPageHome": "Открыть главную страницу",
|
||||
"widgetOpenPageViewer": "Просмотр текущего",
|
||||
|
||||
"albumTierNew": "Новые",
|
||||
"albumTierPinned": "Закрепленные",
|
||||
"albumTierSpecial": "Стандартные",
|
||||
|
@ -289,6 +295,7 @@
|
|||
"viewDialogLayoutSectionTitle": "Макет",
|
||||
"viewDialogReverseSortOrder": "Обратный порядок сортировки",
|
||||
|
||||
"tileLayoutMosaic": "Мозайка",
|
||||
"tileLayoutGrid": "Сетка",
|
||||
"tileLayoutList": "Список",
|
||||
|
||||
|
@ -401,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "Сначала маленькие",
|
||||
|
||||
"albumGroupTier": "По уровню",
|
||||
"albumGroupType": "По типу",
|
||||
"albumGroupVolume": "По накопителю",
|
||||
"albumGroupNone": "Не группировать",
|
||||
|
||||
"albumMimeTypeMixed": "Разное",
|
||||
|
||||
"albumPickPageTitleCopy": "Копировать в альбом",
|
||||
"albumPickPageTitleExport": "Экспорт в альбом",
|
||||
"albumPickPageTitleMove": "Переместить в альбом",
|
||||
|
@ -442,6 +452,7 @@
|
|||
"settingsPageTitle": "Настройки",
|
||||
"settingsSystemDefault": "Система",
|
||||
"settingsDefault": "По умолчанию",
|
||||
"settingsDisabled": "Выключено",
|
||||
|
||||
"settingsSearchFieldLabel": "Поиск настроек",
|
||||
"settingsSearchEmpty": "Нет соответствующих настроек",
|
||||
|
@ -524,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "Повтор",
|
||||
"settingsSlideshowShuffle": "Вперемешку",
|
||||
"settingsSlideshowFillScreen": "Полный экран",
|
||||
"settingsSlideshowAnimatedZoomEffect": "Анимация зум эффекта",
|
||||
"settingsSlideshowTransitionTile": "Эффект перехода",
|
||||
"settingsSlideshowTransitionDialogTitle": "Эффект Перехода",
|
||||
"settingsSlideshowIntervalTile": "Интервал",
|
||||
"settingsSlideshowIntervalDialogTitle": "Интервал",
|
||||
"settingsSlideshowVideoPlaybackTile": "Проигрывание видео",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "Проигрывание Видео",
|
||||
|
||||
|
@ -535,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "Видео",
|
||||
"settingsVideoShowVideos": "Показать видео",
|
||||
"settingsVideoEnableHardwareAcceleration": "Аппаратное ускорение",
|
||||
"settingsVideoEnableAutoPlay": "Автозапуск воспроизведения",
|
||||
"settingsVideoAutoPlay": "Автозапуск воспроизведения",
|
||||
"settingsVideoLoopModeTile": "Циклический режим",
|
||||
"settingsVideoLoopModeDialogTitle": "Цикличный режим",
|
||||
|
||||
|
@ -557,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "Элементы управления",
|
||||
"settingsVideoControlsPageTitle": "Элементы управления",
|
||||
"settingsVideoButtonsTile": "Кнопки",
|
||||
"settingsVideoButtonsDialogTitle": "Кнопки",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Двойное нажатие для воспроизведения/паузы",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Двойное нажатие на края экрана для перехода назад/вперёд",
|
||||
|
||||
|
@ -590,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "Удалить анимацию",
|
||||
"settingsRemoveAnimationsDialogTitle": "Удалить анимацию",
|
||||
"settingsTimeToTakeActionTile": "Время на выполнение действия",
|
||||
"settingsTimeToTakeActionDialogTitle": "Время на выполнение действия",
|
||||
|
||||
"settingsDisplaySectionTitle": "Отображение",
|
||||
"settingsThemeBrightnessTile": "Тема",
|
||||
|
@ -612,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "Фоторамка",
|
||||
"settingsWidgetShowOutline": "Выделение",
|
||||
"settingsWidgetOpenPage": "При нажатии на виджет",
|
||||
|
||||
"settingsCollectionTile": "Коллекция",
|
||||
|
||||
|
@ -620,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "Топ стран",
|
||||
"statsTopPlacesSectionTitle": "Топ локаций",
|
||||
"statsTopTagsSectionTitle": "Топ тегов",
|
||||
"statsTopAlbumsSectionTitle": "Топ альбомов",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "ОТКРЫТЬ ПАНОРАМУ",
|
||||
"viewerSetWallpaperButtonLabel": "УСТАНОВИТЬ КАК ОБОИ",
|
||||
|
@ -664,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "Разрешение",
|
||||
"viewerInfoSearchSuggestionRights": "Права",
|
||||
|
||||
"wallpaperUseScrollEffect": "Эффект прокрутки на домашнем экране",
|
||||
|
||||
"tagEditorPageTitle": "Изменить теги",
|
||||
"tagEditorPageNewTagFieldLabel": "Новый тег",
|
||||
"tagEditorPageAddTagTooltip": "Добавить тег",
|
||||
|
|
|
@ -502,7 +502,7 @@
|
|||
"settingsVideoSectionTitle": "Video",
|
||||
"settingsVideoShowVideos": "Videoları göster",
|
||||
"settingsVideoEnableHardwareAcceleration": "Donanım hızlandırma",
|
||||
"settingsVideoEnableAutoPlay": "Otomatik oynat",
|
||||
"settingsVideoAutoPlay": "Otomatik oynat",
|
||||
"settingsVideoLoopModeTile": "Döngü modu",
|
||||
"settingsVideoLoopModeDialogTitle": "Döngü Modu",
|
||||
|
||||
|
@ -524,7 +524,6 @@
|
|||
"settingsVideoControlsTile": "Kontroller",
|
||||
"settingsVideoControlsPageTitle": "Kontroller",
|
||||
"settingsVideoButtonsTile": "Düğmeler",
|
||||
"settingsVideoButtonsDialogTitle": "Düğmeler",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "Oynatmak/duraklatmak için çift dokunun",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Geri/ileri aramak için ekran kenarlarına çift dokunun",
|
||||
|
||||
|
@ -557,7 +556,6 @@
|
|||
"settingsRemoveAnimationsTile": "Animasyonları kaldır",
|
||||
"settingsRemoveAnimationsDialogTitle": "Animasyonları Kaldır",
|
||||
"settingsTimeToTakeActionTile": "Harekete geçme zamanı",
|
||||
"settingsTimeToTakeActionDialogTitle": "Harekete Geçme Zamanı",
|
||||
|
||||
"settingsDisplaySectionTitle": "Ekran",
|
||||
"settingsThemeBrightnessTile": "Tema",
|
||||
|
|
|
@ -41,6 +41,8 @@
|
|||
"chipActionGoToAlbumPage": "在相册中显示",
|
||||
"chipActionGoToCountryPage": "在国家中显示",
|
||||
"chipActionGoToTagPage": "在标签中显示",
|
||||
"chipActionFilterOut": "滤除",
|
||||
"chipActionFilterIn": "筛选",
|
||||
"chipActionHide": "隐藏",
|
||||
"chipActionPin": "置顶",
|
||||
"chipActionUnpin": "取消置顶",
|
||||
|
@ -155,9 +157,9 @@
|
|||
"displayRefreshRatePreferHighest": "最高刷新率",
|
||||
"displayRefreshRatePreferLowest": "最低刷新率",
|
||||
|
||||
"slideshowVideoPlaybackSkip": "跳过",
|
||||
"slideshowVideoPlaybackMuted": "静音播放",
|
||||
"slideshowVideoPlaybackWithSound": "带音播放",
|
||||
"videoPlaybackSkip": "跳过",
|
||||
"videoPlaybackMuted": "静音播放",
|
||||
"videoPlaybackWithSound": "带音播放",
|
||||
|
||||
"themeBrightnessLight": "浅色",
|
||||
"themeBrightnessDark": "深色",
|
||||
|
@ -167,11 +169,15 @@
|
|||
"viewerTransitionParallax": "视差滚动",
|
||||
"viewerTransitionFade": "淡入淡出",
|
||||
"viewerTransitionZoomIn": "放大",
|
||||
"viewerTransitionNone": "无",
|
||||
|
||||
"wallpaperTargetHome": "主屏幕",
|
||||
"wallpaperTargetLock": "锁屏界面",
|
||||
"wallpaperTargetHomeLock": "主屏幕 + 锁屏界面",
|
||||
|
||||
"widgetOpenPageHome": "打开主页",
|
||||
"widgetOpenPageViewer": "打开查看器",
|
||||
|
||||
"albumTierNew": "新的",
|
||||
"albumTierPinned": "钉选",
|
||||
"albumTierSpecial": "普通",
|
||||
|
@ -289,6 +295,7 @@
|
|||
"viewDialogLayoutSectionTitle": "布局",
|
||||
"viewDialogReverseSortOrder": "反向排序",
|
||||
|
||||
"tileLayoutMosaic": "马赛克",
|
||||
"tileLayoutGrid": "网格",
|
||||
"tileLayoutList": "列表",
|
||||
|
||||
|
@ -401,9 +408,12 @@
|
|||
"sortOrderSmallestFirst": "由小到大",
|
||||
|
||||
"albumGroupTier": "按层级",
|
||||
"albumGroupType": "按类型",
|
||||
"albumGroupVolume": "按存储卷",
|
||||
"albumGroupNone": "不分组",
|
||||
|
||||
"albumMimeTypeMixed": "混合",
|
||||
|
||||
"albumPickPageTitleCopy": "复制到相册",
|
||||
"albumPickPageTitleExport": "导出到相册",
|
||||
"albumPickPageTitleMove": "移至相册",
|
||||
|
@ -442,6 +452,7 @@
|
|||
"settingsPageTitle": "设置",
|
||||
"settingsSystemDefault": "系统",
|
||||
"settingsDefault": "默认",
|
||||
"settingsDisabled": "禁用",
|
||||
|
||||
"settingsSearchFieldLabel": "搜索设置",
|
||||
"settingsSearchEmpty": "无匹配设置项",
|
||||
|
@ -524,10 +535,9 @@
|
|||
"settingsSlideshowRepeat": "重复",
|
||||
"settingsSlideshowShuffle": "随机播放",
|
||||
"settingsSlideshowFillScreen": "填充屏幕",
|
||||
"settingsSlideshowAnimatedZoomEffect": "动画缩放效果",
|
||||
"settingsSlideshowTransitionTile": "过渡动画",
|
||||
"settingsSlideshowTransitionDialogTitle": "过渡动画",
|
||||
"settingsSlideshowIntervalTile": "时间间隔",
|
||||
"settingsSlideshowIntervalDialogTitle": "时间间隔",
|
||||
"settingsSlideshowVideoPlaybackTile": "视频回放",
|
||||
"settingsSlideshowVideoPlaybackDialogTitle": "视频回放",
|
||||
|
||||
|
@ -535,7 +545,7 @@
|
|||
"settingsVideoSectionTitle": "视频",
|
||||
"settingsVideoShowVideos": "显示视频",
|
||||
"settingsVideoEnableHardwareAcceleration": "硬件加速",
|
||||
"settingsVideoEnableAutoPlay": "自动播放",
|
||||
"settingsVideoAutoPlay": "自动播放",
|
||||
"settingsVideoLoopModeTile": "循环模式",
|
||||
"settingsVideoLoopModeDialogTitle": "循环模式",
|
||||
|
||||
|
@ -557,7 +567,6 @@
|
|||
"settingsVideoControlsTile": "控件",
|
||||
"settingsVideoControlsPageTitle": "控件",
|
||||
"settingsVideoButtonsTile": "按钮",
|
||||
"settingsVideoButtonsDialogTitle": "按钮",
|
||||
"settingsVideoGestureDoubleTapTogglePlay": "双击播放/暂停",
|
||||
"settingsVideoGestureSideDoubleTapSeek": "双击屏幕边缘步进/步退",
|
||||
|
||||
|
@ -590,7 +599,6 @@
|
|||
"settingsRemoveAnimationsTile": "移除动画",
|
||||
"settingsRemoveAnimationsDialogTitle": "移除动画",
|
||||
"settingsTimeToTakeActionTile": "生效时间",
|
||||
"settingsTimeToTakeActionDialogTitle": "生效时间",
|
||||
|
||||
"settingsDisplaySectionTitle": "显示",
|
||||
"settingsThemeBrightnessTile": "主题",
|
||||
|
@ -612,6 +620,7 @@
|
|||
|
||||
"settingsWidgetPageTitle": "相框",
|
||||
"settingsWidgetShowOutline": "轮廓",
|
||||
"settingsWidgetOpenPage": "轻触小部件时",
|
||||
|
||||
"settingsCollectionTile": "媒体集",
|
||||
|
||||
|
@ -620,6 +629,7 @@
|
|||
"statsTopCountriesSectionTitle": "热门国家",
|
||||
"statsTopPlacesSectionTitle": "热门地点",
|
||||
"statsTopTagsSectionTitle": "热门标签",
|
||||
"statsTopAlbumsSectionTitle": "热门相册",
|
||||
|
||||
"viewerOpenPanoramaButtonLabel": "打开全景",
|
||||
"viewerSetWallpaperButtonLabel": "设置壁纸",
|
||||
|
@ -664,6 +674,8 @@
|
|||
"viewerInfoSearchSuggestionResolution": "分辨率",
|
||||
"viewerInfoSearchSuggestionRights": "所有权",
|
||||
|
||||
"wallpaperUseScrollEffect": "在主屏幕上使用滚动效果",
|
||||
|
||||
"tagEditorPageTitle": "编辑标签",
|
||||
"tagEditorPageNewTagFieldLabel": "新标签",
|
||||
"tagEditorPageAddTagTooltip": "添加标签",
|
||||
|
|
|
@ -33,6 +33,7 @@ void mainCommon(AppFlavor flavor) {
|
|||
// - in profile/release mode: plain grey background
|
||||
// This can be modified via `ErrorWidget.builder`
|
||||
// ErrorWidget.builder = (details) => ErrorWidget(details.exception);
|
||||
// cf https://docs.flutter.dev/testing/errors
|
||||
|
||||
runApp(AvesApp(flavor: flavor));
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ enum ChipAction {
|
|||
goToAlbumPage,
|
||||
goToCountryPage,
|
||||
goToTagPage,
|
||||
reverse,
|
||||
hide,
|
||||
}
|
||||
|
||||
|
@ -18,6 +19,9 @@ extension ExtraChipAction on ChipAction {
|
|||
return context.l10n.chipActionGoToCountryPage;
|
||||
case ChipAction.goToTagPage:
|
||||
return context.l10n.chipActionGoToTagPage;
|
||||
case ChipAction.reverse:
|
||||
// different data depending on state
|
||||
return context.l10n.chipActionFilterOut;
|
||||
case ChipAction.hide:
|
||||
return context.l10n.chipActionHide;
|
||||
}
|
||||
|
@ -33,6 +37,8 @@ extension ExtraChipAction on ChipAction {
|
|||
return AIcons.location;
|
||||
case ChipAction.goToTagPage:
|
||||
return AIcons.tag;
|
||||
case ChipAction.reverse:
|
||||
return AIcons.reverse;
|
||||
case ChipAction.hide:
|
||||
return AIcons.hide;
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ class EntrySetActions {
|
|||
];
|
||||
|
||||
// exclude bin related actions
|
||||
static const collectionEditorSelection = [
|
||||
static const collectionEditorSelectionRegular = [
|
||||
EntrySetAction.share,
|
||||
EntrySetAction.delete,
|
||||
EntrySetAction.copy,
|
||||
|
@ -97,6 +97,18 @@ class EntrySetActions {
|
|||
// editing actions are in their subsection
|
||||
];
|
||||
|
||||
static const collectionEditorSelectionEdit = [
|
||||
EntrySetAction.rotateCCW,
|
||||
EntrySetAction.rotateCW,
|
||||
EntrySetAction.flip,
|
||||
EntrySetAction.editDate,
|
||||
EntrySetAction.editLocation,
|
||||
EntrySetAction.editTitleDescription,
|
||||
EntrySetAction.editRating,
|
||||
EntrySetAction.editTags,
|
||||
EntrySetAction.removeMetadata,
|
||||
];
|
||||
|
||||
static const edit = [
|
||||
EntrySetAction.editDate,
|
||||
EntrySetAction.editLocation,
|
||||
|
|
|
@ -10,8 +10,6 @@ import 'package:aves/model/video_playback.dart';
|
|||
abstract class MetadataDb {
|
||||
int get nextId;
|
||||
|
||||
int get timestampSecs;
|
||||
|
||||
Future<void> init();
|
||||
|
||||
Future<int> dbFileSize();
|
||||
|
|
|
@ -34,9 +34,6 @@ class SqfliteMetadataDb implements MetadataDb {
|
|||
@override
|
||||
int get nextId => ++_lastId;
|
||||
|
||||
@override
|
||||
int get timestampSecs => DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
_db = await openDatabase(
|
||||
|
|
|
@ -26,7 +26,7 @@ import 'package:country_code/country_code.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
enum EntryDataType { basic, catalog, address, references }
|
||||
enum EntryDataType { basic, aspectRatio, catalog, address, references }
|
||||
|
||||
class AvesEntry {
|
||||
// `sizeBytes`, `dateModifiedSecs` can be missing in viewer mode
|
||||
|
@ -150,6 +150,7 @@ class AvesEntry {
|
|||
'sourceRotationDegrees': sourceRotationDegrees,
|
||||
'sizeBytes': sizeBytes,
|
||||
'title': sourceTitle,
|
||||
'dateAddedSecs': dateAddedSecs,
|
||||
'dateModifiedSecs': dateModifiedSecs,
|
||||
'sourceDateTakenMillis': sourceDateTakenMillis,
|
||||
'durationMillis': durationMillis,
|
||||
|
@ -277,6 +278,8 @@ class AvesEntry {
|
|||
|
||||
bool get isMediaStoreContent => uri.startsWith('content://media/');
|
||||
|
||||
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
|
||||
|
||||
bool get canEdit => path != null && !trashed && isMediaStoreContent;
|
||||
|
||||
bool get canEditDate => canEdit && (canEditExif || canEditXmp);
|
||||
|
@ -291,9 +294,9 @@ class AvesEntry {
|
|||
|
||||
bool get canRotateAndFlip => canEdit && canEditExif;
|
||||
|
||||
// as of androidx.exifinterface:exifinterface:1.3.3
|
||||
// `exifinterface` declares support for DNG, but `exifinterface` strips non-standard Exif tags when saving attributes,
|
||||
// and DNG requires DNG-specific tags saved along standard Exif. So `exifinterface` actually breaks DNG files.
|
||||
// `exifinterface` v1.3.3 declared support for DNG, but it strips non-standard Exif tags when saving attributes,
|
||||
// and DNG requires DNG-specific tags saved along standard Exif. So it was actually breaking DNG files.
|
||||
// as of androidx.exifinterface:exifinterface:1.3.4
|
||||
bool get canEditExif {
|
||||
switch (mimeType.toLowerCase()) {
|
||||
case MimeTypes.jpeg:
|
||||
|
|
|
@ -126,6 +126,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
if (newFields.isNotEmpty) {
|
||||
dataTypes.addAll({
|
||||
EntryDataType.basic,
|
||||
EntryDataType.aspectRatio,
|
||||
EntryDataType.catalog,
|
||||
});
|
||||
}
|
||||
|
@ -309,14 +310,18 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
}
|
||||
|
||||
Future<Set<EntryDataType>> removeMetadata(Set<MetadataType> types) async {
|
||||
final Set<EntryDataType> dataTypes = {};
|
||||
|
||||
final newFields = await metadataEditService.removeTypes(this, types);
|
||||
return newFields.isEmpty
|
||||
? {}
|
||||
: {
|
||||
EntryDataType.basic,
|
||||
EntryDataType.catalog,
|
||||
EntryDataType.address,
|
||||
};
|
||||
if (newFields.isNotEmpty) {
|
||||
dataTypes.addAll({
|
||||
EntryDataType.basic,
|
||||
EntryDataType.aspectRatio,
|
||||
EntryDataType.catalog,
|
||||
EntryDataType.address,
|
||||
});
|
||||
}
|
||||
return dataTypes;
|
||||
}
|
||||
|
||||
static void editIptcValues(List<Map<String, dynamic>> iptc, int record, int tag, Set<String> values) {
|
||||
|
|
|
@ -14,16 +14,20 @@ class AlbumFilter extends CoveredCollectionFilter {
|
|||
|
||||
final String album;
|
||||
final String? displayName;
|
||||
late final EntryFilter _test;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [album];
|
||||
List<Object?> get props => [album, reversed];
|
||||
|
||||
const AlbumFilter(this.album, this.displayName);
|
||||
AlbumFilter(this.album, this.displayName, {super.reversed = false}) {
|
||||
_test = (entry) => entry.directory == album;
|
||||
}
|
||||
|
||||
factory AlbumFilter.fromMap(Map<String, dynamic> json) {
|
||||
return AlbumFilter(
|
||||
json['album'],
|
||||
json['uniqueName'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -32,10 +36,14 @@ class AlbumFilter extends CoveredCollectionFilter {
|
|||
'type': type,
|
||||
'album': album,
|
||||
'uniqueName': displayName,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => entry.directory == album;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => true;
|
||||
|
||||
@override
|
||||
String get universalLabel => displayName ?? pContext.split(album).last;
|
||||
|
|
|
@ -17,16 +17,20 @@ class CoordinateFilter extends CollectionFilter {
|
|||
final LatLng sw;
|
||||
final LatLng ne;
|
||||
final bool minuteSecondPadding;
|
||||
late final EntryFilter _test;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [sw, ne];
|
||||
List<Object?> get props => [sw, ne, reversed];
|
||||
|
||||
const CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false});
|
||||
CoordinateFilter(this.sw, this.ne, {this.minuteSecondPadding = false, super.reversed = false}) {
|
||||
_test = (entry) => GeoUtils.contains(sw, ne, entry.latLng);
|
||||
}
|
||||
|
||||
factory CoordinateFilter.fromMap(Map<String, dynamic> json) {
|
||||
return CoordinateFilter(
|
||||
LatLng.fromJson(json['sw']),
|
||||
LatLng.fromJson(json['ne']),
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -35,10 +39,11 @@ class CoordinateFilter extends CollectionFilter {
|
|||
'type': type,
|
||||
'sw': sw.toJson(),
|
||||
'ne': ne.toJson(),
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => GeoUtils.contains(sw, ne, entry.latLng);
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
String _formatBounds(AppLocalizations l10n, CoordinateFormat format) {
|
||||
String s(LatLng latLng) => format.format(
|
||||
|
@ -50,6 +55,9 @@ class CoordinateFilter extends CollectionFilter {
|
|||
return '${s(ne)}\n${s(sw)}';
|
||||
}
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => _formatBounds(lookupAppLocalizations(AppLocalizations.supportedLocales.first), CoordinateFormat.decimal);
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ class DateFilter extends CollectionFilter {
|
|||
static final onThisDay = DateFilter(DateLevel.md, null);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [level, date];
|
||||
List<Object?> get props => [level, date, reversed];
|
||||
|
||||
DateFilter(this.level, this.date) {
|
||||
DateFilter(this.level, this.date, {super.reversed = false}) {
|
||||
_effectiveDate = date ?? DateTime.now();
|
||||
switch (level) {
|
||||
case DateLevel.y:
|
||||
|
@ -56,6 +56,7 @@ class DateFilter extends CollectionFilter {
|
|||
return DateFilter(
|
||||
DateLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? DateLevel.ymd,
|
||||
dateString != null ? DateTime.tryParse(dateString) : null,
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -64,15 +65,20 @@ class DateFilter extends CollectionFilter {
|
|||
'type': type,
|
||||
'level': level.toString(),
|
||||
'date': date?.toIso8601String(),
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => _test;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => true;
|
||||
|
||||
@override
|
||||
bool isCompatible(CollectionFilter other) {
|
||||
if (other is DateFilter) {
|
||||
return isCompatibleLevel(level, other.level);
|
||||
if (reversed != other.reversed && this == other.reverse()) return false;
|
||||
return reversed || other.reversed || isCompatibleLevel(level, other.level);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -9,20 +10,32 @@ import 'package:provider/provider.dart';
|
|||
class FavouriteFilter extends CollectionFilter {
|
||||
static const type = 'favourite';
|
||||
|
||||
static bool _test(AvesEntry entry) => entry.isFavourite;
|
||||
|
||||
static const instance = FavouriteFilter._private();
|
||||
static const instanceReversed = FavouriteFilter._private(reversed: true);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => [reversed];
|
||||
|
||||
const FavouriteFilter._private();
|
||||
const FavouriteFilter._private({super.reversed = false});
|
||||
|
||||
factory FavouriteFilter.fromMap(Map<String, dynamic> json) {
|
||||
final reversed = json['reversed'] ?? false;
|
||||
return reversed ? instanceReversed : instance;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => entry.isFavourite;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => type;
|
||||
|
|
|
@ -42,9 +42,44 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
PathFilter.type,
|
||||
];
|
||||
|
||||
final bool not;
|
||||
final bool reversed;
|
||||
|
||||
const CollectionFilter({this.not = false});
|
||||
const CollectionFilter({required this.reversed});
|
||||
|
||||
static CollectionFilter? _fromMap(Map<String, dynamic> jsonMap) {
|
||||
final type = jsonMap['type'];
|
||||
switch (type) {
|
||||
case AlbumFilter.type:
|
||||
return AlbumFilter.fromMap(jsonMap);
|
||||
case CoordinateFilter.type:
|
||||
return CoordinateFilter.fromMap(jsonMap);
|
||||
case DateFilter.type:
|
||||
return DateFilter.fromMap(jsonMap);
|
||||
case FavouriteFilter.type:
|
||||
return FavouriteFilter.fromMap(jsonMap);
|
||||
case LocationFilter.type:
|
||||
return LocationFilter.fromMap(jsonMap);
|
||||
case MimeFilter.type:
|
||||
return MimeFilter.fromMap(jsonMap);
|
||||
case MissingFilter.type:
|
||||
return MissingFilter.fromMap(jsonMap);
|
||||
case PathFilter.type:
|
||||
return PathFilter.fromMap(jsonMap);
|
||||
case QueryFilter.type:
|
||||
return QueryFilter.fromMap(jsonMap);
|
||||
case RatingFilter.type:
|
||||
return RatingFilter.fromMap(jsonMap);
|
||||
case RecentlyAddedFilter.type:
|
||||
return RecentlyAddedFilter.fromMap(jsonMap);
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromMap(jsonMap);
|
||||
case TypeFilter.type:
|
||||
return TypeFilter.fromMap(jsonMap);
|
||||
case TrashFilter.type:
|
||||
return TrashFilter.fromMap(jsonMap);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static CollectionFilter? fromJson(String jsonString) {
|
||||
if (jsonString.isEmpty) return null;
|
||||
|
@ -52,37 +87,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
try {
|
||||
final jsonMap = jsonDecode(jsonString);
|
||||
if (jsonMap is Map<String, dynamic>) {
|
||||
final type = jsonMap['type'];
|
||||
switch (type) {
|
||||
case AlbumFilter.type:
|
||||
return AlbumFilter.fromMap(jsonMap);
|
||||
case CoordinateFilter.type:
|
||||
return CoordinateFilter.fromMap(jsonMap);
|
||||
case DateFilter.type:
|
||||
return DateFilter.fromMap(jsonMap);
|
||||
case FavouriteFilter.type:
|
||||
return FavouriteFilter.instance;
|
||||
case LocationFilter.type:
|
||||
return LocationFilter.fromMap(jsonMap);
|
||||
case MimeFilter.type:
|
||||
return MimeFilter.fromMap(jsonMap);
|
||||
case MissingFilter.type:
|
||||
return MissingFilter.fromMap(jsonMap);
|
||||
case PathFilter.type:
|
||||
return PathFilter.fromMap(jsonMap);
|
||||
case QueryFilter.type:
|
||||
return QueryFilter.fromMap(jsonMap);
|
||||
case RatingFilter.type:
|
||||
return RatingFilter.fromMap(jsonMap);
|
||||
case RecentlyAddedFilter.type:
|
||||
return RecentlyAddedFilter.instance;
|
||||
case TagFilter.type:
|
||||
return TagFilter.fromMap(jsonMap);
|
||||
case TypeFilter.type:
|
||||
return TypeFilter.fromMap(jsonMap);
|
||||
case TrashFilter.type:
|
||||
return TrashFilter.instance;
|
||||
}
|
||||
return _fromMap(jsonMap);
|
||||
}
|
||||
} catch (error, stack) {
|
||||
debugPrint('failed to parse filter from json=$jsonString error=$error\n$stack');
|
||||
|
@ -95,9 +100,21 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
|
||||
String toJson() => jsonEncode(toMap());
|
||||
|
||||
EntryFilter get test;
|
||||
EntryFilter get positiveTest;
|
||||
|
||||
bool isCompatible(CollectionFilter other) => category != other.category;
|
||||
EntryFilter get test => reversed ? (v) => !positiveTest(v) : positiveTest;
|
||||
|
||||
CollectionFilter reverse() => _fromMap(toMap()..['reversed'] = !reversed)!;
|
||||
|
||||
bool get exclusiveProp;
|
||||
|
||||
bool isCompatible(CollectionFilter other) {
|
||||
if (category != other.category) return true;
|
||||
if (!reversed && !other.reversed) return !exclusiveProp;
|
||||
if (reversed && other.reversed) return true;
|
||||
if (this == other.reverse()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
String get universalLabel;
|
||||
|
||||
|
@ -129,7 +146,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
|
|||
|
||||
@immutable
|
||||
abstract class CoveredCollectionFilter extends CollectionFilter {
|
||||
const CoveredCollectionFilter({bool not = false}) : super(not: not);
|
||||
const CoveredCollectionFilter({required super.reversed});
|
||||
|
||||
@override
|
||||
Future<Color> color(BuildContext context) {
|
||||
|
|
|
@ -15,9 +15,9 @@ class LocationFilter extends CoveredCollectionFilter {
|
|||
late final EntryFilter _test;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [level, _location, _countryCode];
|
||||
List<Object?> get props => [level, _location, _countryCode, reversed];
|
||||
|
||||
LocationFilter(this.level, String location) {
|
||||
LocationFilter(this.level, String location, {super.reversed = false}) {
|
||||
final split = location.split(locationSeparator);
|
||||
_location = split.isNotEmpty ? split[0] : location;
|
||||
_countryCode = split.length > 1 ? split[1] : null;
|
||||
|
@ -35,6 +35,7 @@ class LocationFilter extends CoveredCollectionFilter {
|
|||
return LocationFilter(
|
||||
LocationLevel.values.firstWhereOrNull((v) => v.toString() == json['level']) ?? LocationLevel.place,
|
||||
json['location'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -43,6 +44,7 @@ class LocationFilter extends CoveredCollectionFilter {
|
|||
'type': type,
|
||||
'level': level.toString(),
|
||||
'location': _countryCode != null ? countryNameAndCode : _location,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
String get countryNameAndCode => '$_location$locationSeparator$_countryCode';
|
||||
|
@ -50,7 +52,10 @@ class LocationFilter extends CoveredCollectionFilter {
|
|||
String? get countryCode => _countryCode;
|
||||
|
||||
@override
|
||||
EntryFilter get test => _test;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => true;
|
||||
|
||||
@override
|
||||
String get universalLabel => _location;
|
||||
|
|
|
@ -14,17 +14,17 @@ class MimeFilter extends CollectionFilter {
|
|||
static const type = 'mime';
|
||||
|
||||
final String mime;
|
||||
late final EntryFilter _test;
|
||||
late final String _label;
|
||||
late final IconData _icon;
|
||||
late final EntryFilter _test;
|
||||
|
||||
static final image = MimeFilter(MimeTypes.anyImage);
|
||||
static final video = MimeFilter(MimeTypes.anyVideo);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mime];
|
||||
List<Object?> get props => [mime, reversed];
|
||||
|
||||
MimeFilter(this.mime) {
|
||||
MimeFilter(this.mime, {super.reversed = false}) {
|
||||
IconData? icon;
|
||||
var lowMime = mime.toLowerCase();
|
||||
if (lowMime.endsWith('/*')) {
|
||||
|
@ -46,6 +46,7 @@ class MimeFilter extends CollectionFilter {
|
|||
factory MimeFilter.fromMap(Map<String, dynamic> json) {
|
||||
return MimeFilter(
|
||||
json['mime'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -53,10 +54,14 @@ class MimeFilter extends CollectionFilter {
|
|||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'mime': mime,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => _test;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => true;
|
||||
|
||||
@override
|
||||
String get universalLabel => _label;
|
||||
|
|
|
@ -10,16 +10,16 @@ class MissingFilter extends CollectionFilter {
|
|||
static const _title = 'title';
|
||||
|
||||
final String metadataType;
|
||||
late final EntryFilter _test;
|
||||
late final IconData _icon;
|
||||
late final EntryFilter _test;
|
||||
|
||||
static final date = MissingFilter._private(_date);
|
||||
static final title = MissingFilter._private(_title);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [metadataType];
|
||||
List<Object?> get props => [metadataType, reversed];
|
||||
|
||||
MissingFilter._private(this.metadataType) {
|
||||
MissingFilter._private(this.metadataType, {super.reversed = false}) {
|
||||
switch (metadataType) {
|
||||
case _date:
|
||||
_test = (entry) => (entry.catalogMetadata?.dateMillis ?? 0) == 0;
|
||||
|
@ -35,6 +35,7 @@ class MissingFilter extends CollectionFilter {
|
|||
factory MissingFilter.fromMap(Map<String, dynamic> json) {
|
||||
return MissingFilter._private(
|
||||
json['metadataType'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -42,10 +43,14 @@ class MissingFilter extends CollectionFilter {
|
|||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'metadataType': metadataType,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => _test;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => metadataType;
|
||||
|
|
|
@ -10,14 +10,24 @@ class PathFilter extends CollectionFilter {
|
|||
// without trailing separator
|
||||
final String _rootAlbum;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [path];
|
||||
late final EntryFilter _test;
|
||||
|
||||
PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1);
|
||||
@override
|
||||
List<Object?> get props => [path, reversed];
|
||||
|
||||
PathFilter(this.path, {super.reversed = false}) : _rootAlbum = path.substring(0, path.length - 1) {
|
||||
_test = (entry) {
|
||||
final dir = entry.directory;
|
||||
if (dir == null) return false;
|
||||
// avoid string building in most cases
|
||||
return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path);
|
||||
};
|
||||
}
|
||||
|
||||
factory PathFilter.fromMap(Map<String, dynamic> json) {
|
||||
return PathFilter(
|
||||
json['path'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -25,15 +35,14 @@ class PathFilter extends CollectionFilter {
|
|||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'path': path,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) {
|
||||
final dir = entry.directory;
|
||||
if (dir == null) return false;
|
||||
// avoid string building in most cases
|
||||
return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path);
|
||||
};
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => true;
|
||||
|
||||
@override
|
||||
String get universalLabel => path;
|
||||
|
|
|
@ -18,7 +18,7 @@ class QueryFilter extends CollectionFilter {
|
|||
late final EntryFilter _test;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [query, live];
|
||||
List<Object?> get props => [query, live, reversed];
|
||||
|
||||
static final _fieldPattern = RegExp(r'(.+)([=<>])(.+)');
|
||||
static final _fileSizePattern = RegExp(r'(\d+)([KMG])?');
|
||||
|
@ -33,7 +33,7 @@ class QueryFilter extends CollectionFilter {
|
|||
static const opLower = '<';
|
||||
static const opGreater = '>';
|
||||
|
||||
QueryFilter(this.query, {this.colorful = true, this.live = false}) {
|
||||
QueryFilter(this.query, {this.colorful = true, this.live = false, super.reversed = false}) {
|
||||
var upQuery = query.toUpperCase();
|
||||
|
||||
final test = fieldTest(upQuery);
|
||||
|
@ -62,6 +62,7 @@ class QueryFilter extends CollectionFilter {
|
|||
factory QueryFilter.fromMap(Map<String, dynamic> json) {
|
||||
return QueryFilter(
|
||||
json['query'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -69,13 +70,14 @@ class QueryFilter extends CollectionFilter {
|
|||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'query': query,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => _test;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool isCompatible(CollectionFilter other) => true;
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => query;
|
||||
|
|
|
@ -7,15 +7,19 @@ class RatingFilter extends CollectionFilter {
|
|||
static const type = 'rating';
|
||||
|
||||
final int rating;
|
||||
late final EntryFilter _test;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [rating];
|
||||
List<Object?> get props => [rating, reversed];
|
||||
|
||||
const RatingFilter(this.rating);
|
||||
RatingFilter(this.rating, {super.reversed = false}) {
|
||||
_test = (entry) => entry.rating == rating;
|
||||
}
|
||||
|
||||
factory RatingFilter.fromMap(Map<String, dynamic> json) {
|
||||
return RatingFilter(
|
||||
json['rating'] ?? 0,
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -23,10 +27,14 @@ class RatingFilter extends CollectionFilter {
|
|||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'rating': rating,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => entry.rating == rating;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => true;
|
||||
|
||||
@override
|
||||
String get universalLabel => '$rating';
|
||||
|
|
|
@ -6,30 +6,43 @@ import 'package:flutter/material.dart';
|
|||
class RecentlyAddedFilter extends CollectionFilter {
|
||||
static const type = 'recently_added';
|
||||
|
||||
static late EntryFilter _test;
|
||||
|
||||
static final instance = RecentlyAddedFilter._private();
|
||||
static final instanceReversed = RecentlyAddedFilter._private(reversed: true);
|
||||
|
||||
static late int nowSecs;
|
||||
|
||||
static void updateNow() {
|
||||
nowSecs = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
_test = (entry) => (nowSecs - (entry.dateAddedSecs ?? 0)) < _dayInSecs;
|
||||
}
|
||||
|
||||
static const _dayInSecs = 24 * 60 * 60;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => [reversed];
|
||||
|
||||
RecentlyAddedFilter._private() {
|
||||
RecentlyAddedFilter._private({super.reversed = false}) {
|
||||
updateNow();
|
||||
}
|
||||
|
||||
factory RecentlyAddedFilter.fromMap(Map<String, dynamic> json) {
|
||||
final reversed = json['reversed'] ?? false;
|
||||
return reversed ? instanceReversed : instance;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => (nowSecs - (entry.dateAddedSecs ?? 0)) < _dayInSecs;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => type;
|
||||
|
|
|
@ -10,20 +10,20 @@ class TagFilter extends CoveredCollectionFilter {
|
|||
late final EntryFilter _test;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [tag];
|
||||
List<Object?> get props => [tag, reversed];
|
||||
|
||||
TagFilter(this.tag, {bool not = false}) : super(not: not) {
|
||||
TagFilter(this.tag, {super.reversed = false}) {
|
||||
if (tag.isEmpty) {
|
||||
_test = not ? (entry) => entry.tags.isNotEmpty : (entry) => entry.tags.isEmpty;
|
||||
_test = (entry) => entry.tags.isEmpty;
|
||||
} else {
|
||||
_test = not ? (entry) => !entry.tags.contains(tag) : (entry) => entry.tags.contains(tag);
|
||||
_test = (entry) => entry.tags.contains(tag);
|
||||
}
|
||||
}
|
||||
|
||||
factory TagFilter.fromMap(Map<String, dynamic> json) {
|
||||
return TagFilter(
|
||||
json['tag'],
|
||||
not: json['not'] ?? false,
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -31,14 +31,14 @@ class TagFilter extends CoveredCollectionFilter {
|
|||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'tag': tag,
|
||||
'not': not,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => _test;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool isCompatible(CollectionFilter other) => true;
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => tag;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
@ -6,20 +7,32 @@ import 'package:flutter/material.dart';
|
|||
class TrashFilter extends CollectionFilter {
|
||||
static const type = 'trash';
|
||||
|
||||
static bool _test(AvesEntry entry) => entry.trashed;
|
||||
|
||||
static const instance = TrashFilter._private();
|
||||
static const instanceReversed = TrashFilter._private(reversed: true);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => [reversed];
|
||||
|
||||
const TrashFilter._private();
|
||||
const TrashFilter._private({super.reversed = false});
|
||||
|
||||
factory TrashFilter.fromMap(Map<String, dynamic> json) {
|
||||
final reversed = json['reversed'] ?? false;
|
||||
return reversed ? instanceReversed : instance;
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => (entry) => entry.trashed;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => type;
|
||||
|
|
|
@ -17,8 +17,8 @@ class TypeFilter extends CollectionFilter {
|
|||
static const _sphericalVideo = 'spherical_video'; // subset of videos
|
||||
|
||||
final String itemType;
|
||||
late final EntryFilter _test;
|
||||
late final IconData _icon;
|
||||
late final EntryFilter _test;
|
||||
|
||||
static final animated = TypeFilter._private(_animated);
|
||||
static final geotiff = TypeFilter._private(_geotiff);
|
||||
|
@ -28,9 +28,9 @@ class TypeFilter extends CollectionFilter {
|
|||
static final sphericalVideo = TypeFilter._private(_sphericalVideo);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [itemType];
|
||||
List<Object?> get props => [itemType, reversed];
|
||||
|
||||
TypeFilter._private(this.itemType) {
|
||||
TypeFilter._private(this.itemType, {super.reversed = false}) {
|
||||
switch (itemType) {
|
||||
case _animated:
|
||||
_test = (entry) => entry.isAnimated;
|
||||
|
@ -62,6 +62,7 @@ class TypeFilter extends CollectionFilter {
|
|||
factory TypeFilter.fromMap(Map<String, dynamic> json) {
|
||||
return TypeFilter._private(
|
||||
json['itemType'],
|
||||
reversed: json['reversed'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -69,10 +70,14 @@ class TypeFilter extends CollectionFilter {
|
|||
Map<String, dynamic> toMap() => {
|
||||
'type': type,
|
||||
'itemType': itemType,
|
||||
'reversed': reversed,
|
||||
};
|
||||
|
||||
@override
|
||||
EntryFilter get test => _test;
|
||||
EntryFilter get positiveTest => _test;
|
||||
|
||||
@override
|
||||
bool get exclusiveProp => false;
|
||||
|
||||
@override
|
||||
String get universalLabel => itemType;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/actions/entry_set_actions.dart';
|
||||
import 'package:aves/model/filters/favourite.dart';
|
||||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/filters/recent.dart';
|
||||
import 'package:aves/model/naming_pattern.dart';
|
||||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
|
@ -40,8 +38,6 @@ class SettingsDefaults {
|
|||
static const setMetadataDateBeforeFileOp = false;
|
||||
static final drawerTypeBookmarks = [
|
||||
null,
|
||||
MimeFilter.video,
|
||||
FavouriteFilter.instance,
|
||||
RecentlyAddedFilter.instance,
|
||||
];
|
||||
static const drawerPageBookmarks = [
|
||||
|
@ -93,7 +89,7 @@ class SettingsDefaults {
|
|||
|
||||
// video
|
||||
static const enableVideoHardwareAcceleration = true;
|
||||
static const enableVideoAutoPlay = false;
|
||||
static const videoAutoPlayMode = VideoAutoPlayMode.disabled;
|
||||
static const videoLoopMode = VideoLoopMode.shortOnly;
|
||||
static const videoShowRawTimedText = false;
|
||||
static const videoControls = VideoControls.play;
|
||||
|
@ -133,6 +129,7 @@ class SettingsDefaults {
|
|||
static const slideshowRepeat = false;
|
||||
static const slideshowShuffle = false;
|
||||
static const slideshowFillScreen = false;
|
||||
static const slideshowAnimatedZoomEffect = true;
|
||||
static const slideshowTransition = ViewerTransition.fade;
|
||||
static const slideshowVideoPlayback = SlideshowVideoPlayback.playMuted;
|
||||
static const slideshowInterval = SlideshowInterval.s5;
|
||||
|
@ -140,6 +137,7 @@ class SettingsDefaults {
|
|||
// widget
|
||||
static const widgetOutline = false;
|
||||
static const widgetShape = WidgetShape.rrect;
|
||||
static const widgetOpenPage = WidgetOpenPage.viewer;
|
||||
|
||||
// platform settings
|
||||
static const isRotationLocked = false;
|
||||
|
|
|
@ -28,6 +28,10 @@ enum VideoControls { play, playSeek, playOutside, none }
|
|||
|
||||
enum VideoLoopMode { never, shortOnly, always }
|
||||
|
||||
enum ViewerTransition { slide, parallax, fade, zoomIn }
|
||||
enum VideoAutoPlayMode { disabled, playMuted, playWithSound }
|
||||
|
||||
enum WidgetShape { rrect, circle, heart }
|
||||
enum ViewerTransition { slide, parallax, fade, zoomIn, none }
|
||||
|
||||
enum WidgetOpenPage { home, viewer }
|
||||
|
||||
enum WidgetShape { rrect, circle, heart }
|
||||
|
|
|
@ -7,11 +7,11 @@ extension ExtraSlideshowVideoPlayback on SlideshowVideoPlayback {
|
|||
String getName(BuildContext context) {
|
||||
switch (this) {
|
||||
case SlideshowVideoPlayback.skip:
|
||||
return context.l10n.slideshowVideoPlaybackSkip;
|
||||
return context.l10n.videoPlaybackSkip;
|
||||
case SlideshowVideoPlayback.playMuted:
|
||||
return context.l10n.slideshowVideoPlaybackMuted;
|
||||
return context.l10n.videoPlaybackMuted;
|
||||
case SlideshowVideoPlayback.playWithSound:
|
||||
return context.l10n.slideshowVideoPlaybackWithSound;
|
||||
return context.l10n.videoPlaybackWithSound;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
17
lib/model/settings/enums/video_auto_play_mode.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
|
||||
extension ExtraSlideshowVideoPlayback on VideoAutoPlayMode {
|
||||
String getName(BuildContext context) {
|
||||
switch (this) {
|
||||
case VideoAutoPlayMode.disabled:
|
||||
return context.l10n.settingsDisabled;
|
||||
case VideoAutoPlayMode.playMuted:
|
||||
return context.l10n.videoPlaybackMuted;
|
||||
case VideoAutoPlayMode.playWithSound:
|
||||
return context.l10n.videoPlaybackWithSound;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,8 @@ extension ExtraViewerTransition on ViewerTransition {
|
|||
return context.l10n.viewerTransitionFade;
|
||||
case ViewerTransition.zoomIn:
|
||||
return context.l10n.viewerTransitionZoomIn;
|
||||
case ViewerTransition.none:
|
||||
return context.l10n.viewerTransitionNone;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,6 +30,8 @@ extension ExtraViewerTransition on ViewerTransition {
|
|||
return PageTransitionEffects.fade(pageController, index, zoomIn: false);
|
||||
case ViewerTransition.zoomIn:
|
||||
return PageTransitionEffects.fade(pageController, index, zoomIn: true);
|
||||
case ViewerTransition.none:
|
||||
return PageTransitionEffects.none(pageController, index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
lib/model/settings/enums/widget_open_action.dart
Normal file
|
@ -0,0 +1,14 @@
|
|||
import 'package:aves/model/settings/enums/enums.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
extension ExtraWidgetOpenPage on WidgetOpenPage {
|
||||
String getName(BuildContext context) {
|
||||
switch (this) {
|
||||
case WidgetOpenPage.home:
|
||||
return context.l10n.widgetOpenPageHome;
|
||||
case WidgetOpenPage.viewer:
|
||||
return context.l10n.widgetOpenPageViewer;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -112,7 +112,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// video
|
||||
static const enableVideoHardwareAccelerationKey = 'video_hwaccel_mediacodec';
|
||||
static const enableVideoAutoPlayKey = 'video_auto_play';
|
||||
static const videoAutoPlayModeKey = 'video_auto_play_mode';
|
||||
static const videoLoopModeKey = 'video_loop';
|
||||
static const videoShowRawTimedTextKey = 'video_show_raw_timed_text';
|
||||
static const videoControlsKey = 'video_controls';
|
||||
|
@ -151,6 +151,7 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// screen saver
|
||||
static const screenSaverFillScreenKey = 'screen_saver_fill_screen';
|
||||
static const screenSaverAnimatedZoomEffectKey = 'screen_saver_animated_zoom_effect';
|
||||
static const screenSaverTransitionKey = 'screen_saver_transition';
|
||||
static const screenSaverVideoPlaybackKey = 'screen_saver_video_playback';
|
||||
static const screenSaverIntervalKey = 'screen_saver_interval';
|
||||
|
@ -160,6 +161,7 @@ class Settings extends ChangeNotifier {
|
|||
static const slideshowRepeatKey = 'slideshow_loop';
|
||||
static const slideshowShuffleKey = 'slideshow_shuffle';
|
||||
static const slideshowFillScreenKey = 'slideshow_fill_screen';
|
||||
static const slideshowAnimatedZoomEffectKey = 'slideshow_animated_zoom_effect';
|
||||
static const slideshowTransitionKey = 'slideshow_transition';
|
||||
static const slideshowVideoPlaybackKey = 'slideshow_video_playback';
|
||||
static const slideshowIntervalKey = 'slideshow_interval';
|
||||
|
@ -168,6 +170,7 @@ class Settings extends ChangeNotifier {
|
|||
static const widgetOutlinePrefixKey = '${_widgetKeyPrefix}outline_';
|
||||
static const widgetShapePrefixKey = '${_widgetKeyPrefix}shape_';
|
||||
static const widgetCollectionFiltersPrefixKey = '${_widgetKeyPrefix}collection_filters_';
|
||||
static const widgetOpenPagePrefixKey = '${_widgetKeyPrefix}open_page_';
|
||||
static const widgetUriPrefixKey = '${_widgetKeyPrefix}uri_';
|
||||
|
||||
// platform settings
|
||||
|
@ -533,9 +536,9 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set enableVideoHardwareAcceleration(bool newValue) => setAndNotify(enableVideoHardwareAccelerationKey, newValue);
|
||||
|
||||
bool get enableVideoAutoPlay => getBoolOrDefault(enableVideoAutoPlayKey, SettingsDefaults.enableVideoAutoPlay);
|
||||
VideoAutoPlayMode get videoAutoPlayMode => getEnumOrDefault(videoAutoPlayModeKey, SettingsDefaults.videoAutoPlayMode, VideoAutoPlayMode.values);
|
||||
|
||||
set enableVideoAutoPlay(bool newValue) => setAndNotify(enableVideoAutoPlayKey, newValue);
|
||||
set videoAutoPlayMode(VideoAutoPlayMode newValue) => setAndNotify(videoAutoPlayModeKey, newValue.toString());
|
||||
|
||||
VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, SettingsDefaults.videoLoopMode, VideoLoopMode.values);
|
||||
|
||||
|
@ -648,6 +651,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set screenSaverFillScreen(bool newValue) => setAndNotify(screenSaverFillScreenKey, newValue);
|
||||
|
||||
bool get screenSaverAnimatedZoomEffect => getBoolOrDefault(screenSaverAnimatedZoomEffectKey, SettingsDefaults.slideshowAnimatedZoomEffect);
|
||||
|
||||
set screenSaverAnimatedZoomEffect(bool newValue) => setAndNotify(screenSaverAnimatedZoomEffectKey, newValue);
|
||||
|
||||
ViewerTransition get screenSaverTransition => getEnumOrDefault(screenSaverTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values);
|
||||
|
||||
set screenSaverTransition(ViewerTransition newValue) => setAndNotify(screenSaverTransitionKey, newValue.toString());
|
||||
|
@ -678,6 +685,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set slideshowFillScreen(bool newValue) => setAndNotify(slideshowFillScreenKey, newValue);
|
||||
|
||||
bool get slideshowAnimatedZoomEffect => getBoolOrDefault(slideshowAnimatedZoomEffectKey, SettingsDefaults.slideshowAnimatedZoomEffect);
|
||||
|
||||
set slideshowAnimatedZoomEffect(bool newValue) => setAndNotify(slideshowAnimatedZoomEffectKey, newValue);
|
||||
|
||||
ViewerTransition get slideshowTransition => getEnumOrDefault(slideshowTransitionKey, SettingsDefaults.slideshowTransition, ViewerTransition.values);
|
||||
|
||||
set slideshowTransition(ViewerTransition newValue) => setAndNotify(slideshowTransitionKey, newValue.toString());
|
||||
|
@ -707,6 +718,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
void setWidgetCollectionFilters(int widgetId, Set<CollectionFilter> newValue) => setAndNotify('$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());
|
||||
|
||||
String? getWidgetUri(int widgetId) => getString('$widgetUriPrefixKey$widgetId');
|
||||
|
||||
void setWidgetUri(int widgetId, String? newValue) => setAndNotify('$widgetUriPrefixKey$widgetId', newValue);
|
||||
|
@ -869,16 +884,17 @@ class Settings extends ChangeNotifier {
|
|||
case viewerMaxBrightnessKey:
|
||||
case enableMotionPhotoAutoPlayKey:
|
||||
case enableVideoHardwareAccelerationKey:
|
||||
case enableVideoAutoPlayKey:
|
||||
case videoGestureDoubleTapTogglePlayKey:
|
||||
case videoGestureSideDoubleTapSeekKey:
|
||||
case subtitleShowOutlineKey:
|
||||
case saveSearchHistoryKey:
|
||||
case filePickerShowHiddenFilesKey:
|
||||
case screenSaverFillScreenKey:
|
||||
case screenSaverAnimatedZoomEffectKey:
|
||||
case slideshowRepeatKey:
|
||||
case slideshowShuffleKey:
|
||||
case slideshowFillScreenKey:
|
||||
case slideshowAnimatedZoomEffectKey:
|
||||
if (newValue is bool) {
|
||||
settingsStore.setBool(key, newValue);
|
||||
} else {
|
||||
|
@ -898,6 +914,7 @@ class Settings extends ChangeNotifier {
|
|||
case countrySortFactorKey:
|
||||
case tagSortFactorKey:
|
||||
case imageBackgroundKey:
|
||||
case videoAutoPlayModeKey:
|
||||
case videoLoopModeKey:
|
||||
case videoControlsKey:
|
||||
case subtitleTextAlignmentKey:
|
||||
|
|
|
@ -128,7 +128,7 @@ class CollectionLens with ChangeNotifier {
|
|||
}
|
||||
|
||||
bool get showHeaders {
|
||||
bool showAlbumHeaders() => !filters.any((f) => f is AlbumFilter);
|
||||
bool showAlbumHeaders() => !filters.any((v) => v is AlbumFilter && !v.reversed);
|
||||
|
||||
switch (sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
|
|
|
@ -310,7 +310,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
contentId: newFields['contentId'] as int?,
|
||||
// title can change when moved files are automatically renamed to avoid conflict
|
||||
title: newFields['title'] as String?,
|
||||
dateAddedSecs: metadataDb.timestampSecs,
|
||||
dateAddedSecs: newFields['dateAddedSecs'] as int?,
|
||||
dateModifiedSecs: newFields['dateModifiedSecs'] as int?,
|
||||
));
|
||||
} else {
|
||||
|
@ -395,16 +395,25 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
Future<void> refreshEntry(AvesEntry entry, Set<EntryDataType> dataTypes) async {
|
||||
await entry.refresh(background: false, persist: true, dataTypes: dataTypes, geocoderLocale: settings.appliedLocale);
|
||||
|
||||
// update/delete in DB
|
||||
final id = entry.id;
|
||||
if (dataTypes.contains(EntryDataType.catalog)) {
|
||||
await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata);
|
||||
onCatalogMetadataChanged();
|
||||
}
|
||||
if (dataTypes.contains(EntryDataType.address)) {
|
||||
await metadataDb.updateAddress(id, entry.addressDetails);
|
||||
onAddressMetadataChanged();
|
||||
}
|
||||
await Future.forEach(EntryDataType.values, (dataType) async {
|
||||
switch (dataType) {
|
||||
case EntryDataType.aspectRatio:
|
||||
onAspectRatioChanged();
|
||||
break;
|
||||
case EntryDataType.catalog:
|
||||
await metadataDb.updateCatalogMetadata(id, entry.catalogMetadata);
|
||||
onCatalogMetadataChanged();
|
||||
break;
|
||||
case EntryDataType.address:
|
||||
await metadataDb.updateAddress(id, entry.addressDetails);
|
||||
onAddressMetadataChanged();
|
||||
break;
|
||||
case EntryDataType.basic:
|
||||
case EntryDataType.references:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
updateDerivedFilters({entry});
|
||||
eventBus.fire(EntryRefreshedEvent({entry}));
|
||||
|
@ -449,6 +458,8 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
state = SourceState.ready;
|
||||
}
|
||||
|
||||
void onAspectRatioChanged() => eventBus.fire(AspectRatioChangedEvent());
|
||||
|
||||
// monitoring
|
||||
|
||||
bool _monitoring = true;
|
||||
|
@ -502,3 +513,5 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AspectRatioChangedEvent {}
|
||||
|
|
|
@ -2,10 +2,10 @@ enum SourceState { loading, cataloguing, locatingCountries, locatingPlaces, read
|
|||
|
||||
enum ChipSortFactor { date, name, count, size }
|
||||
|
||||
enum AlbumChipGroupFactor { none, importance, volume }
|
||||
enum AlbumChipGroupFactor { none, importance, mimeType, volume }
|
||||
|
||||
enum EntrySortFactor { date, name, rating, size }
|
||||
|
||||
enum EntryGroupFactor { none, album, month, day }
|
||||
|
||||
enum TileLayout { grid, list }
|
||||
enum TileLayout { mosaic, grid, list }
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
@ -18,6 +19,19 @@ extension ExtraEntrySortFactor on EntrySortFactor {
|
|||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case EntrySortFactor.date:
|
||||
return AIcons.date;
|
||||
case EntrySortFactor.name:
|
||||
return AIcons.name;
|
||||
case EntrySortFactor.rating:
|
||||
return AIcons.rating;
|
||||
case EntrySortFactor.size:
|
||||
return AIcons.size;
|
||||
}
|
||||
}
|
||||
|
||||
String getOrderName(BuildContext context, bool reverse) {
|
||||
final l10n = context.l10n;
|
||||
switch (this) {
|
||||
|
@ -48,6 +62,19 @@ extension ExtraChipSortFactor on ChipSortFactor {
|
|||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case ChipSortFactor.date:
|
||||
return AIcons.date;
|
||||
case ChipSortFactor.name:
|
||||
return AIcons.name;
|
||||
case ChipSortFactor.count:
|
||||
return AIcons.count;
|
||||
case ChipSortFactor.size:
|
||||
return AIcons.size;
|
||||
}
|
||||
}
|
||||
|
||||
String getOrderName(BuildContext context, bool reverse) {
|
||||
final l10n = context.l10n;
|
||||
switch (this) {
|
||||
|
@ -76,6 +103,19 @@ extension ExtraEntryGroupFactor on EntryGroupFactor {
|
|||
return l10n.collectionGroupNone;
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case EntryGroupFactor.album:
|
||||
return AIcons.album;
|
||||
case EntryGroupFactor.month:
|
||||
return AIcons.dateByMonth;
|
||||
case EntryGroupFactor.day:
|
||||
return AIcons.dateByDay;
|
||||
case EntryGroupFactor.none:
|
||||
return AIcons.clear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor {
|
||||
|
@ -84,22 +124,50 @@ extension ExtraAlbumChipGroupFactor on AlbumChipGroupFactor {
|
|||
switch (this) {
|
||||
case AlbumChipGroupFactor.importance:
|
||||
return l10n.albumGroupTier;
|
||||
case AlbumChipGroupFactor.mimeType:
|
||||
return l10n.albumGroupType;
|
||||
case AlbumChipGroupFactor.volume:
|
||||
return l10n.albumGroupVolume;
|
||||
case AlbumChipGroupFactor.none:
|
||||
return l10n.albumGroupNone;
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case AlbumChipGroupFactor.importance:
|
||||
return AIcons.important;
|
||||
case AlbumChipGroupFactor.mimeType:
|
||||
return AIcons.mimeType;
|
||||
case AlbumChipGroupFactor.volume:
|
||||
return AIcons.removableStorage;
|
||||
case AlbumChipGroupFactor.none:
|
||||
return AIcons.clear;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ExtraTileLayout on TileLayout {
|
||||
String getName(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
switch (this) {
|
||||
case TileLayout.mosaic:
|
||||
return l10n.tileLayoutMosaic;
|
||||
case TileLayout.grid:
|
||||
return l10n.tileLayoutGrid;
|
||||
case TileLayout.list:
|
||||
return l10n.tileLayoutList;
|
||||
}
|
||||
}
|
||||
|
||||
IconData get icon {
|
||||
switch (this) {
|
||||
case TileLayout.mosaic:
|
||||
return AIcons.layoutMosaic;
|
||||
case TileLayout.grid:
|
||||
return AIcons.layoutGrid;
|
||||
case TileLayout.list:
|
||||
return AIcons.layoutList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,13 +159,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
// reuse known entry ID to overwrite it while preserving favourites, etc.
|
||||
final contentId = entry.contentId;
|
||||
final existingEntry = knownContentIds.contains(contentId) ? knownLiveEntries.firstWhereOrNull((entry) => entry.contentId == contentId) : null;
|
||||
if (existingEntry != null) {
|
||||
entry.id = existingEntry.id;
|
||||
entry.dateAddedSecs = existingEntry.dateAddedSecs;
|
||||
} else {
|
||||
entry.id = metadataDb.nextId;
|
||||
entry.dateAddedSecs = metadataDb.timestampSecs;
|
||||
}
|
||||
entry.id = existingEntry?.id ?? metadataDb.nextId;
|
||||
|
||||
pendingNewEntries.add(entry);
|
||||
if (pendingNewEntries.length >= refreshCount) {
|
||||
|
@ -250,13 +244,7 @@ class MediaStoreSource extends CollectionSource {
|
|||
final newPath = sourceEntry.path;
|
||||
final volume = newPath != null ? androidFileUtils.getStorageVolume(newPath) : null;
|
||||
if (volume != null) {
|
||||
if (existingEntry != null) {
|
||||
sourceEntry.id = existingEntry.id;
|
||||
sourceEntry.dateAddedSecs = existingEntry.dateAddedSecs;
|
||||
} else {
|
||||
sourceEntry.id = metadataDb.nextId;
|
||||
sourceEntry.dateAddedSecs = metadataDb.timestampSecs;
|
||||
}
|
||||
sourceEntry.id = existingEntry?.id ?? metadataDb.nextId;
|
||||
newEntries.add(sourceEntry);
|
||||
final existingDirectory = existingEntry?.directory;
|
||||
if (existingDirectory != null) {
|
||||
|
|
|
@ -140,7 +140,7 @@ class VideoMetadataFormatter {
|
|||
hour = int.tryParse(match.group(5)!) ?? 0;
|
||||
minute = int.tryParse(match.group(6)!) ?? 0;
|
||||
second = int.tryParse(match.group(7)!) ?? 0;
|
||||
pm = match.group(9) == 'pm';
|
||||
pm = {'pm', 'p. m.'}.contains(match.group(9));
|
||||
}
|
||||
|
||||
final date = DateTime(year, month, day, hour + (pm ? 12 : 0), minute, second, 0);
|
||||
|
|
|
@ -81,9 +81,8 @@ class PlatformMediaFetchService implements MediaFetchService {
|
|||
}) as Map;
|
||||
return AvesEntry.fromMap(result);
|
||||
} on PlatformException catch (e, stack) {
|
||||
// do not report issues with simple parameter-less media content
|
||||
// as it is likely an obsolete Media Store entry
|
||||
if (!uri.startsWith('content://media/') || uri.contains('?')) {
|
||||
// do not report issues with media content as it is likely an obsolete Media Store entry
|
||||
if (!uri.startsWith('content://media/')) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,11 +24,12 @@ class Durations {
|
|||
static const chipDecorationAnimation = Duration(milliseconds: 200);
|
||||
static const highlightScrollAnimationMinMillis = 400;
|
||||
static const highlightScrollAnimationMaxMillis = 2000;
|
||||
static const scalingGridBackgroundAnimation = Duration(milliseconds: 200);
|
||||
static const scalingGridPositionAnimation = Duration(milliseconds: 150);
|
||||
|
||||
// collection animations
|
||||
static const filterBarRemovalAnimation = Duration(milliseconds: 400);
|
||||
static const collectionOpOverlayAnimation = Duration(milliseconds: 300);
|
||||
static const collectionScalingBackgroundAnimation = Duration(milliseconds: 200);
|
||||
static const sectionHeaderAnimation = Duration(milliseconds: 200);
|
||||
static const thumbnailOverlayAnimation = Duration(milliseconds: 200);
|
||||
|
||||
|
@ -40,6 +41,7 @@ class Durations {
|
|||
static const thumbnailScrollerShadeAnimation = Duration(milliseconds: 150);
|
||||
static const viewerVideoPlayerTransition = Duration(milliseconds: 500);
|
||||
static const viewerActionFeedbackAnimation = Duration(milliseconds: 600);
|
||||
static const viewerHorizontalPageAnimation = Duration(seconds: 1);
|
||||
|
||||
// info animations
|
||||
static const mapStyleSwitchAnimation = Duration(milliseconds: 300);
|
||||
|
@ -95,6 +97,7 @@ class DurationsData {
|
|||
// common animations
|
||||
final Duration expansionTileAnimation;
|
||||
final Duration formTransition;
|
||||
final Duration formTextStyleTransition;
|
||||
final Duration chartTransition;
|
||||
final Duration iconAnimation;
|
||||
final Duration staggeredAnimation;
|
||||
|
@ -111,6 +114,7 @@ class DurationsData {
|
|||
const DurationsData({
|
||||
this.expansionTileAnimation = const Duration(milliseconds: 200),
|
||||
this.formTransition = const Duration(milliseconds: 200),
|
||||
this.formTextStyleTransition = const Duration(milliseconds: 800),
|
||||
this.chartTransition = const Duration(milliseconds: 400),
|
||||
this.iconAnimation = const Duration(milliseconds: 300),
|
||||
this.staggeredAnimation = const Duration(milliseconds: 375),
|
||||
|
@ -125,6 +129,7 @@ class DurationsData {
|
|||
// as of Flutter v2.5.1, `ExpansionPanelList` throws if animation duration is zero
|
||||
expansionTileAnimation: const Duration(microseconds: 1),
|
||||
formTransition: Duration.zero,
|
||||
formTextStyleTransition: Duration.zero,
|
||||
chartTransition: Duration.zero,
|
||||
iconAnimation: Duration.zero,
|
||||
staggeredAnimation: Duration.zero,
|
||||
|
|
|
@ -14,8 +14,11 @@ class AIcons {
|
|||
static const IconData bin = Icons.delete_outlined;
|
||||
static const IconData broken = Icons.broken_image_outlined;
|
||||
static const IconData checked = Icons.done_outlined;
|
||||
static const IconData count = MdiIcons.counter;
|
||||
static const IconData counter = Icons.plus_one_outlined;
|
||||
static const IconData date = Icons.calendar_today_outlined;
|
||||
static const IconData dateByDay = Icons.today_outlined;
|
||||
static const IconData dateByMonth = Icons.calendar_month_outlined;
|
||||
static const IconData dateRecent = Icons.today_outlined;
|
||||
static const IconData dateUndated = Icons.event_busy_outlined;
|
||||
static const IconData description = Icons.description_outlined;
|
||||
|
@ -31,6 +34,7 @@ class AIcons {
|
|||
static const IconData location = Icons.place_outlined;
|
||||
static const IconData locationUnlocated = Icons.location_off_outlined;
|
||||
static const IconData mainStorage = Icons.smartphone_outlined;
|
||||
static const IconData mimeType = Icons.code_outlined;
|
||||
static const IconData opacity = Icons.opacity;
|
||||
static const IconData privacy = MdiIcons.shieldAccountOutline;
|
||||
static const IconData rating = Icons.star_border_outlined;
|
||||
|
@ -43,6 +47,7 @@ class AIcons {
|
|||
static const IconData sensorControlEnabled = Icons.explore_outlined;
|
||||
static const IconData sensorControlDisabled = Icons.explore_off_outlined;
|
||||
static const IconData settings = Icons.settings_outlined;
|
||||
static const IconData size = Icons.data_usage_outlined;
|
||||
static const IconData text = Icons.format_quote_outlined;
|
||||
static const IconData tag = Icons.local_offer_outlined;
|
||||
static const IconData tagUntagged = MdiIcons.tagOffOutline;
|
||||
|
@ -50,6 +55,9 @@ class AIcons {
|
|||
// view
|
||||
static const IconData group = Icons.group_work_outlined;
|
||||
static const IconData layout = Icons.grid_view_outlined;
|
||||
static const IconData layoutMosaic = Icons.view_compact_outlined;
|
||||
static const IconData layoutGrid = Icons.view_comfy_outlined;
|
||||
static const IconData layoutList = Icons.list_outlined;
|
||||
static const IconData sort = Icons.sort_outlined;
|
||||
static const IconData sortOrder = Icons.swap_vert_outlined;
|
||||
|
||||
|
@ -97,6 +105,7 @@ class AIcons {
|
|||
static const IconData print = Icons.print_outlined;
|
||||
static const IconData refresh = Icons.refresh_outlined;
|
||||
static const IconData replay10 = Icons.replay_10_outlined;
|
||||
static const IconData reverse = Icons.invert_colors_outlined;
|
||||
static const IconData skip10 = Icons.forward_10_outlined;
|
||||
static const IconData reset = Icons.restart_alt_outlined;
|
||||
static const IconData restore = Icons.restore_outlined;
|
||||
|
@ -111,7 +120,7 @@ class AIcons {
|
|||
static const IconData show = Icons.visibility_outlined;
|
||||
static const IconData slideshow = Icons.slideshow_outlined;
|
||||
static const IconData speed = Icons.speed_outlined;
|
||||
static const IconData stats = Icons.pie_chart_outline_outlined;
|
||||
static const IconData stats = Icons.donut_small_outlined;
|
||||
static const IconData streams = Icons.translate_outlined;
|
||||
static const IconData streamVideo = Icons.movie_outlined;
|
||||
static const IconData streamAudio = Icons.audiotrack_outlined;
|
||||
|
|
|
@ -5,6 +5,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
class Constants {
|
||||
// `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`)
|
||||
// when used in gradients or lerping to it
|
||||
static const transparentWhite = Color(0x00FFFFFF);
|
||||
static const transparentBlack = Colors.transparent;
|
||||
|
||||
// as of Flutter v2.8.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
|
||||
// so we give it a `strutStyle` with a slightly larger height
|
||||
static const overflowStrutStyle = StrutStyle(height: 1.3);
|
||||
|
@ -307,6 +312,11 @@ class Constants {
|
|||
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<Dependency> dartPackages = [
|
||||
|
|
|
@ -10,6 +10,7 @@ class Namespaces {
|
|||
static const container = 'http://ns.google.com/photos/1.0/container/';
|
||||
static const creatorAtom = 'http://ns.adobe.com/creatorAtom/1.0/';
|
||||
static const crd = 'http://ns.adobe.com/camera-raw-defaults/1.0/';
|
||||
static const crlcp = 'http://ns.adobe.com/camera-raw-embedded-lens-profile/1.0/';
|
||||
static const crs = 'http://ns.adobe.com/camera-raw-settings/1.0/';
|
||||
static const crss = 'http://ns.adobe.com/camera-raw-saved-settings/1.0/';
|
||||
static const darktable = 'http://darktable.sf.net/';
|
||||
|
@ -30,7 +31,8 @@ class Namespaces {
|
|||
static const gettyImagesGift = 'http://xmp.gettyimages.com/gift/1.0/';
|
||||
static const gFocus = 'http://ns.google.com/photos/1.0/focus/';
|
||||
static const gImage = 'http://ns.google.com/photos/1.0/image/';
|
||||
static const gimp = 'http://www.gimp.org/ns/2.10/';
|
||||
static const gimp210 = 'http://www.gimp.org/ns/2.10/';
|
||||
static const gimpXmp = 'http://www.gimp.org/xmp/';
|
||||
static const gPano = 'http://ns.google.com/photos/1.0/panorama/';
|
||||
static const gSpherical = 'http://ns.google.com/videos/1.0/spherical/';
|
||||
static const illustrator = 'http://ns.adobe.com/illustrator/1.0/';
|
||||
|
@ -56,6 +58,7 @@ class Namespaces {
|
|||
static const plus = 'http://ns.useplus.org/ldf/xmp/1.0/';
|
||||
static const pmtm = 'http://www.hdrsoft.com/photomatix_settings01';
|
||||
static const rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
||||
static const stCamera = 'http://ns.adobe.com/photoshop/1.0/camera-profile';
|
||||
static const stEvt = 'http://ns.adobe.com/xap/1.0/sType/ResourceEvent#';
|
||||
static const stRef = 'http://ns.adobe.com/xap/1.0/sType/ResourceRef#';
|
||||
static const tiff = 'http://ns.adobe.com/tiff/1.0/';
|
||||
|
@ -96,7 +99,8 @@ class Namespaces {
|
|||
gDepth: 'Google Depth',
|
||||
gFocus: 'Google Focus',
|
||||
gImage: 'Google Image',
|
||||
gimp: 'GIMP',
|
||||
gimp210: 'GIMP 2.10',
|
||||
gimpXmp: 'GIMP',
|
||||
gPano: 'Google Panorama',
|
||||
gSpherical: 'Google Spherical',
|
||||
illustrator: 'Illustrator',
|
||||
|
@ -122,6 +126,7 @@ class Namespaces {
|
|||
xmpBJ: 'Basic Job Ticket',
|
||||
xmpDM: 'Dynamic Media',
|
||||
xmpMM: 'Media Management',
|
||||
xmpNote: 'Note',
|
||||
xmpRights: 'Rights Management',
|
||||
xmpTPg: 'Paged-Text',
|
||||
};
|
||||
|
|
|
@ -128,7 +128,7 @@ class LicenseRow extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final bodyTextStyle = textTheme.bodyText2!;
|
||||
final bodyTextStyle = textTheme.bodyMedium!;
|
||||
final subColor = bodyTextStyle.color!.withOpacity(.6);
|
||||
|
||||
return Padding(
|
||||
|
|
|
@ -105,11 +105,11 @@ class AvesApp extends StatefulWidget {
|
|||
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||
final ValueNotifier<AppMode> appModeNotifier = ValueNotifier(AppMode.main);
|
||||
late final Future<void> _appSetup;
|
||||
late final Size _screenSize;
|
||||
late final Future<CorePalette?> _dynamicColorPaletteLoader;
|
||||
final CollectionSource _mediaStoreSource = MediaStoreSource();
|
||||
final Debouncer _mediaStoreChangeDebouncer = Debouncer(delay: Durations.mediaContentChangeDebounceDelay);
|
||||
final Set<String> _changedUris = {};
|
||||
Size? _screenSize;
|
||||
|
||||
// observers are not registered when using the same list object with different items
|
||||
// the list itself needs to be reassigned
|
||||
|
@ -119,15 +119,13 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
final EventChannel _analysisCompletionChannel = const OptionalEventChannel('deckers.thibault/aves/analysis_events');
|
||||
final EventChannel _errorChannel = const OptionalEventChannel('deckers.thibault/aves/error');
|
||||
|
||||
Widget getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
EquatableConfig.stringify = true;
|
||||
_appSetup = _setup();
|
||||
// remember screen size to use it later, when `context` and `window` are no longer reliable
|
||||
_screenSize = window.physicalSize / window.devicePixelRatio;
|
||||
_screenSize = _getScreenSize();
|
||||
_dynamicColorPaletteLoader = DynamicColorPlugin.getCorePalette();
|
||||
_mediaStoreChangeChannel.receiveBroadcastStream().listen((event) => _onMediaStoreChange(event as String?));
|
||||
_newIntentChannel.receiveBroadcastStream().listen((event) => _onNewIntent(event as Map?));
|
||||
|
@ -159,7 +157,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
AvesApp.showSystemUI();
|
||||
}
|
||||
final home = initialized
|
||||
? getFirstPage()
|
||||
? _getFirstPage()
|
||||
: Scaffold(
|
||||
body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(),
|
||||
);
|
||||
|
@ -291,23 +289,31 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
|
||||
|
||||
Size? _getScreenSize() {
|
||||
final physicalSize = window.physicalSize;
|
||||
final ratio = window.devicePixelRatio;
|
||||
return physicalSize > Size.zero && ratio > 0 ? physicalSize / ratio : null;
|
||||
}
|
||||
|
||||
// save IDs of entries visible at the top of the collection page with current layout settings
|
||||
void _saveTopEntries() {
|
||||
if (!settings.initialized) return;
|
||||
|
||||
final stopwatch = Stopwatch()..start();
|
||||
final screenSize = _screenSize ?? _getScreenSize();
|
||||
if (screenSize == null) return;
|
||||
|
||||
var tileExtent = settings.getTileExtent(CollectionPage.routeName);
|
||||
if (tileExtent == 0) {
|
||||
tileExtent = _screenSize.shortestSide / CollectionGrid.columnCountDefault;
|
||||
tileExtent = screenSize.shortestSide / CollectionGrid.columnCountDefault;
|
||||
}
|
||||
final rows = (_screenSize.height / tileExtent).ceil();
|
||||
final columns = (_screenSize.width / tileExtent).ceil();
|
||||
final rows = (screenSize.height / tileExtent).ceil();
|
||||
final columns = (screenSize.width / tileExtent).ceil();
|
||||
final count = rows * columns;
|
||||
final collection = CollectionLens(source: _mediaStoreSource, listenToSource: false);
|
||||
settings.topEntryIds = collection.sortedEntries.take(count).map((entry) => entry.id).toList();
|
||||
collection.dispose();
|
||||
debugPrint('Saved $count top entries in ${stopwatch.elapsed.inMilliseconds}ms');
|
||||
}
|
||||
|
||||
// setup before the first page is displayed. keep it short
|
||||
|
@ -374,10 +380,10 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
'locales': WidgetsBinding.instance.window.locales.join(', '),
|
||||
'time_zone': '${now.timeZoneName} (${now.timeZoneOffset})',
|
||||
});
|
||||
_navigatorObservers = [
|
||||
AvesApp.pageRouteObserver,
|
||||
ReportingRouteTracker(),
|
||||
];
|
||||
setState(() => _navigatorObservers = [
|
||||
AvesApp.pageRouteObserver,
|
||||
ReportingRouteTracker(),
|
||||
]);
|
||||
}
|
||||
|
||||
void _onNewIntent(Map? intentData) {
|
||||
|
@ -389,7 +395,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
reportService.log('New intent');
|
||||
AvesApp.navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
|
||||
settings: const RouteSettings(name: HomePage.routeName),
|
||||
builder: (_) => getFirstPage(intentData: intentData),
|
||||
builder: (_) => _getFirstPage(intentData: intentData),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
|||
import 'package:aves/widgets/common/identity/aves_app_bar.dart';
|
||||
import 'package:aves/widgets/common/search/route.dart';
|
||||
import 'package:aves/widgets/dialogs/tile_view_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
|
||||
import 'package:aves/widgets/search/search_delegate.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -83,6 +84,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
];
|
||||
|
||||
static const _layoutOptions = [
|
||||
TileLayout.mosaic,
|
||||
TileLayout.grid,
|
||||
TileLayout.list,
|
||||
];
|
||||
|
@ -167,10 +169,16 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
bottom: Column(
|
||||
children: [
|
||||
if (showFilterBar)
|
||||
FilterBar(
|
||||
filters: visibleFilters,
|
||||
removable: removableFilters,
|
||||
onTap: removableFilters ? collection.removeFilter : null,
|
||||
NotificationListener<ReverseFilterNotification>(
|
||||
onNotification: (notification) {
|
||||
collection.addFilter(notification.reversedFilter);
|
||||
return true;
|
||||
},
|
||||
child: FilterBar(
|
||||
filters: visibleFilters,
|
||||
removable: removableFilters,
|
||||
onTap: removableFilters ? collection.removeFilter : null,
|
||||
),
|
||||
),
|
||||
if (queryEnabled)
|
||||
EntryQueryBar(
|
||||
|
@ -310,7 +318,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
title: context.l10n.collectionActionEdit,
|
||||
items: [
|
||||
_buildRotateAndFlipMenuItems(context, canApply: canApply),
|
||||
...EntrySetActions.edit.where(isVisible).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
|
||||
...EntrySetActions.edit.where((v) => isVisible(v) && !selectionQuickActions.contains(v)).map((action) => _toMenuItem(action, enabled: canApply(action), selection: selection)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -537,9 +545,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
builder: (context) {
|
||||
return TileViewDialog<EntrySortFactor, EntryGroupFactor, TileLayout>(
|
||||
initialValue: initialValue,
|
||||
sortOptions: Map.fromEntries(_sortOptions.map((v) => MapEntry(v, v.getName(context)))),
|
||||
groupOptions: Map.fromEntries(_groupOptions.map((v) => MapEntry(v, v.getName(context)))),
|
||||
layoutOptions: Map.fromEntries(_layoutOptions.map((v) => MapEntry(v, v.getName(context)))),
|
||||
sortOptions: _sortOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||
groupOptions: _groupOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||
layoutOptions: _layoutOptions.map((v) => TileViewDialogOption(value: v, title: v.getName(context), icon: v.icon)).toList(),
|
||||
sortOrder: (factor, reverse) => factor.getOrderName(context, reverse),
|
||||
canGroup: (s, g, l) => s == EntrySortFactor.date,
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/filters/favourite.dart';
|
|||
import 'package:aves/model/filters/mime.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/enums/enums.dart';
|
||||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
|
@ -25,7 +26,9 @@ import 'package:aves/widgets/common/extensions/media_query.dart';
|
|||
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/common/grid/item_tracker.dart';
|
||||
import 'package:aves/widgets/common/grid/scaling.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/common/grid/sections/fixed/scale_grid.dart';
|
||||
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||
import 'package:aves/widgets/common/grid/sections/section_layout.dart';
|
||||
import 'package:aves/widgets/common/grid/selector.dart';
|
||||
import 'package:aves/widgets/common/grid/sliver.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
|
@ -34,6 +37,7 @@ import 'package:aves/widgets/common/identity/empty.dart';
|
|||
import 'package:aves/widgets/common/identity/scroll_thumb.dart';
|
||||
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
@ -50,7 +54,8 @@ class CollectionGrid extends StatefulWidget {
|
|||
static const int columnCountDefault = 4;
|
||||
static const double extentMin = 46;
|
||||
static const double extentMax = 300;
|
||||
static const double spacing = 2;
|
||||
static const double fixedExtentLayoutSpacing = 2;
|
||||
static const double mosaicLayoutSpacing = 4;
|
||||
|
||||
const CollectionGrid({
|
||||
super.key,
|
||||
|
@ -64,6 +69,8 @@ class CollectionGrid extends StatefulWidget {
|
|||
class _CollectionGridState extends State<CollectionGrid> {
|
||||
TileExtentController? _tileExtentController;
|
||||
|
||||
String get settingsRouteKey => widget.settingsRouteKey;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tileExtentController?.dispose();
|
||||
|
@ -72,14 +79,17 @@ class _CollectionGridState extends State<CollectionGrid> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_tileExtentController ??= TileExtentController(
|
||||
settingsRouteKey: widget.settingsRouteKey,
|
||||
columnCountDefault: CollectionGrid.columnCountDefault,
|
||||
extentMin: CollectionGrid.extentMin,
|
||||
extentMax: CollectionGrid.extentMax,
|
||||
spacing: CollectionGrid.spacing,
|
||||
horizontalPadding: 2,
|
||||
);
|
||||
final spacing = context.select<Settings, double>((s) => s.getTileLayout(settingsRouteKey) == TileLayout.mosaic ? CollectionGrid.mosaicLayoutSpacing : CollectionGrid.fixedExtentLayoutSpacing);
|
||||
if (_tileExtentController?.spacing != spacing) {
|
||||
_tileExtentController = TileExtentController(
|
||||
settingsRouteKey: settingsRouteKey,
|
||||
columnCountDefault: CollectionGrid.columnCountDefault,
|
||||
extentMin: CollectionGrid.extentMin,
|
||||
extentMax: CollectionGrid.extentMax,
|
||||
spacing: spacing,
|
||||
horizontalPadding: 2,
|
||||
);
|
||||
}
|
||||
return TileExtentControllerProvider(
|
||||
controller: _tileExtentController!,
|
||||
child: _CollectionGridContent(),
|
||||
|
@ -108,12 +118,13 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
final columnCount = c.item2;
|
||||
final tileSpacing = c.item3;
|
||||
final horizontalPadding = c.item4;
|
||||
final source = collection.source;
|
||||
return GridTheme(
|
||||
extent: thumbnailExtent,
|
||||
child: EntryListDetailsTheme(
|
||||
extent: thumbnailExtent,
|
||||
child: ValueListenableBuilder<SourceState>(
|
||||
valueListenable: collection.source.stateNotifier,
|
||||
valueListenable: source.stateNotifier,
|
||||
builder: (context, sourceState, child) {
|
||||
late final Duration tileAnimationDelay;
|
||||
if (sourceState == SourceState.ready) {
|
||||
|
@ -123,30 +134,37 @@ class _CollectionGridContent extends StatelessWidget {
|
|||
} else {
|
||||
tileAnimationDelay = Duration.zero;
|
||||
}
|
||||
return SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
selectable: selectable,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileLayout: tileLayout,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
tileExtent: thumbnailExtent,
|
||||
tileBuilder: (entry) => AnimatedBuilder(
|
||||
animation: favourites,
|
||||
builder: (context, child) {
|
||||
return InteractiveTile(
|
||||
key: ValueKey(entry.id),
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
thumbnailExtent: thumbnailExtent,
|
||||
tileLayout: tileLayout,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
|
||||
return StreamBuilder(
|
||||
stream: source.eventBus.on<AspectRatioChangedEvent>(),
|
||||
builder: (context, snapshot) => SectionedEntryListLayoutProvider(
|
||||
collection: collection,
|
||||
selectable: selectable,
|
||||
scrollableWidth: scrollableWidth,
|
||||
tileLayout: tileLayout,
|
||||
columnCount: columnCount,
|
||||
spacing: tileSpacing,
|
||||
horizontalPadding: horizontalPadding,
|
||||
tileExtent: thumbnailExtent,
|
||||
tileBuilder: (entry, tileSize) {
|
||||
final extent = tileSize.shortestSide;
|
||||
return AnimatedBuilder(
|
||||
animation: favourites,
|
||||
builder: (context, child) {
|
||||
return InteractiveTile(
|
||||
key: ValueKey(entry.id),
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
thumbnailExtent: extent,
|
||||
tileLayout: tileLayout,
|
||||
isScrollingNotifier: _isScrollingNotifier,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: child!,
|
||||
),
|
||||
tileAnimationDelay: tileAnimationDelay,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
|
@ -260,12 +278,13 @@ class _CollectionScaler extends StatelessWidget {
|
|||
final metrics = context.select<TileExtentController, Tuple2<double, double>>((v) => Tuple2(v.spacing, v.horizontalPadding));
|
||||
final tileSpacing = metrics.item1;
|
||||
final horizontalPadding = metrics.item2;
|
||||
final brightness = Theme.of(context).brightness;
|
||||
return GridScaleGestureDetector<AvesEntry>(
|
||||
scrollableKey: scrollableKey,
|
||||
tileLayout: tileLayout,
|
||||
heightForWidth: (width) => width,
|
||||
gridBuilder: (center, tileSize, child) => CustomPaint(
|
||||
painter: GridPainter(
|
||||
painter: FixedExtentGridPainter(
|
||||
tileLayout: tileLayout,
|
||||
tileCenter: center,
|
||||
tileSize: tileSize,
|
||||
|
@ -278,7 +297,7 @@ class _CollectionScaler extends StatelessWidget {
|
|||
),
|
||||
child: child,
|
||||
),
|
||||
scaledBuilder: (entry, tileSize) => EntryListDetailsTheme(
|
||||
scaledItemBuilder: (entry, tileSize) => EntryListDetailsTheme(
|
||||
extent: tileSize.height,
|
||||
child: Tile(
|
||||
entry: entry,
|
||||
|
@ -286,6 +305,15 @@ class _CollectionScaler extends StatelessWidget {
|
|||
tileLayout: tileLayout,
|
||||
),
|
||||
),
|
||||
mosaicItemBuilder: (index, targetExtent) => DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: ThumbnailImage.computeLoadingBackgroundColor(index * 10, brightness).withOpacity(.9),
|
||||
border: Border.all(
|
||||
color: DecoratedThumbnail.borderColor,
|
||||
width: DecoratedThumbnail.borderWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import 'package:aves/model/source/enums/enums.dart';
|
|||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/grid/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/common/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/common/grid/sections/list_layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
|
|
@ -370,6 +370,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
Set<String> obsoleteTags = todoItems.expand((entry) => entry.tags).toSet();
|
||||
Set<String> obsoleteCountryCodes = todoItems.where((entry) => entry.hasAddress).map((entry) => entry.addressDetails?.countryCode).whereNotNull().toSet();
|
||||
|
||||
final Set<EntryDataType> dataTypes = {};
|
||||
final source = context.read<CollectionSource>();
|
||||
source.pauseMonitoring();
|
||||
var cancelled = false;
|
||||
|
@ -379,8 +380,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
if (cancelled) {
|
||||
return ImageOpEvent(success: true, skipped: true, uri: entry.uri);
|
||||
} else {
|
||||
final dataTypes = await op(entry);
|
||||
return ImageOpEvent(success: dataTypes.isNotEmpty, skipped: false, uri: entry.uri);
|
||||
final opDataTypes = await op(entry);
|
||||
dataTypes.addAll(opDataTypes);
|
||||
return ImageOpEvent(success: opDataTypes.isNotEmpty, skipped: false, uri: entry.uri);
|
||||
}
|
||||
}).asBroadcastStream(),
|
||||
itemCount: todoCount,
|
||||
|
@ -402,6 +404,10 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
}
|
||||
}));
|
||||
|
||||
if (dataTypes.contains(EntryDataType.aspectRatio)) {
|
||||
source.onAspectRatioChanged();
|
||||
}
|
||||
|
||||
if (showResult) {
|
||||
final l10n = context.l10n;
|
||||
final successCount = successOps.length;
|
||||
|
|
|
@ -64,7 +64,7 @@ class _FilterBarState extends State<FilterBar> {
|
|||
),
|
||||
);
|
||||
}
|
||||
: (context, animation) => _buildChip(filter),
|
||||
: (context, animation) => const SizedBox(),
|
||||
duration: animate ? Durations.filterBarRemovalAnimation : Duration.zero,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -29,8 +29,8 @@ class EntryListDetailsTheme extends StatelessWidget {
|
|||
final textScaleFactor = mq.textScaleFactor;
|
||||
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final titleStyle = textTheme.bodyText2!;
|
||||
final captionStyle = textTheme.caption!;
|
||||
final titleStyle = textTheme.bodyMedium!;
|
||||
final captionStyle = textTheme.bodySmall!;
|
||||
|
||||
final titleLineHeight = (RenderParagraph(
|
||||
TextSpan(text: 'Fake Title', style: titleStyle),
|
||||
|
|