Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2022-10-09 19:50:19 +02:00
commit c53d9b9cb6
236 changed files with 4991 additions and 2465 deletions

View file

@ -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.

View file

@ -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:

View file

@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
## <a id="v1.7.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

View file

@ -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')
}

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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) }
}
}
}

View file

@ -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()
}
}

View file

@ -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 -> {

View file

@ -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

View file

@ -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)
}

View file

@ -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)

View file

@ -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)
}
}
}
}

View file

@ -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(

View file

@ -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,

View file

@ -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)

View file

@ -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,
)

View file

@ -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

View file

@ -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) }
}

View file

@ -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

View file

@ -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) {

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 285 KiB

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 284 KiB

View file

@ -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",

View file

@ -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": "Προσθήκη ετικέτας",

View file

@ -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",

View file

@ -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",

View file

@ -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 daccueil",
"wallpaperTargetLock": "Écran de verrouillage",
"wallpaperTargetHomeLock": "Écrans accueil et verrouillage",
"widgetOpenPageHome": "Ouvrir la page daccueil",
"widgetOpenPageViewer": "Ouvrir la visionneuse",
"albumTierNew": "Nouveaux",
"albumTierPinned": "Épinglés",
"albumTierSpecial": "Standards",
@ -289,6 +295,7 @@
"viewDialogLayoutSectionTitle": "Vue",
"viewDialogReverseSortOrder": "Inverser lordre",
"tileLayoutMosaic": "Mosaïque",
"tileLayoutGrid": "Grille",
"tileLayoutList": "Liste",
@ -401,9 +408,12 @@
"sortOrderSmallestFirst": "Moins larges dabord",
"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 leffet de défilement sur lécran daccueil",
"tagEditorPageTitle": "Modifier les libellés",
"tagEditorPageNewTagFieldLabel": "Nouveau libellé",
"tagEditorPageAddTagTooltip": "Ajouter le libellé",

View file

@ -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",

View file

@ -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 dallalto",
@ -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",

View file

@ -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": "テーマ",

View file

@ -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": "태그 추가",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Добавить тег",

View file

@ -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",

View file

@ -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": "添加标签",

View file

@ -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));
}

View file

@ -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;
}

View file

@ -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,

View file

@ -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();

View file

@ -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(

View file

@ -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:

View file

@ -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) {

View file

@ -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;

View file

@ -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);

View file

@ -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;
}

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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';

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 }

View file

@ -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;
}
}
}

View 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;
}
}
}

View file

@ -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);
}
}
}

View 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;
}
}
}

View file

@ -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:

View file

@ -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:

View file

@ -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 {}

View file

@ -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 }

View file

@ -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;
}
}
}

View file

@ -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) {

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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;

View file

@ -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 = [

View file

@ -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',
};

View file

@ -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(

View file

@ -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),
));
}

View file

@ -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,
);

View file

@ -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,
);
}

View file

@ -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';

View file

@ -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;

View file

@ -64,7 +64,7 @@ class _FilterBarState extends State<FilterBar> {
),
);
}
: (context, animation) => _buildChip(filter),
: (context, animation) => const SizedBox(),
duration: animate ? Durations.filterBarRemovalAnimation : Duration.zero,
);
});

View file

@ -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),

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