Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-07-09 00:18:12 +02:00
commit a396635639
161 changed files with 2024 additions and 962 deletions

View file

@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.11.4"></a>[v1.11.4] - 2024-07-09
### Added
- Collection: stack RAW and JPEG with same file names
- Collection: ask to rename/replace/skip when converting items with name conflict
- Export: bulk converting motion photos to still images
- Explorer: view folder tree and filter paths
### Fixed
- switching to PiP when changing device orientation on Android >=13
- handling wallpaper intent without URI
- sizing widgets with some launchers on Android >=12
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17 ## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17
### Added ### Added

View file

@ -196,9 +196,9 @@ repositories {
dependencies { dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat:1.7.0"
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.lifecycle:lifecycle-process:2.8.0' implementation 'androidx.lifecycle:lifecycle-process:2.8.2'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'androidx.security:security-crypto:1.1.0-alpha06'

View file

@ -120,6 +120,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:showWhenLocked="true"
android:supportsRtl="true" android:supportsRtl="true"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
@ -143,6 +144,7 @@
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" /> <action android:name="android.provider.action.REVIEW" />
<action android:name="android.provider.action.REVIEW_SECURE" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />
@ -163,6 +165,7 @@
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" /> <action android:name="android.provider.action.REVIEW" />
<action android:name="android.provider.action.REVIEW_SECURE" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />

View file

@ -27,6 +27,10 @@ import deckers.thibault.aves.utils.LogUtils
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -34,13 +38,17 @@ import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class AnalysisWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { class AnalysisWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var workCont: Continuation<Any?>? = null private var workCont: Continuation<Any?>? = null
private var flutterEngine: FlutterEngine? = null private var flutterEngine: FlutterEngine? = null
private var backgroundChannel: MethodChannel? = null private var backgroundChannel: MethodChannel? = null
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
createNotificationChannel() defaultScope.launch {
setForeground(createForegroundInfo()) // prevent ANR triggered by slow operations in main thread
createNotificationChannel()
setForeground(createForegroundInfo())
}
suspendCoroutine { cont -> suspendCoroutine { cont ->
workCont = cont workCont = cont
onStart() onStart()

View file

@ -12,6 +12,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.util.SizeF
import android.widget.RemoteViews import android.widget.RemoteViews
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
@ -40,12 +41,16 @@ class HomeWidgetProvider : AppWidgetProvider() {
for (widgetId in appWidgetIds) { for (widgetId in appWidgetIds) {
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId) val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
defaultScope.launch { goAsync().run {
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false) defaultScope.launch {
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundProps) val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false) val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps) updateWidgetImage(context, appWidgetManager, widgetId, imageProps)
finish()
}
} }
} }
} }
@ -61,20 +66,32 @@ class HomeWidgetProvider : AppWidgetProvider() {
imageByteFetchJob = defaultScope.launch { imageByteFetchJob = defaultScope.launch {
delay(500) delay(500)
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true) val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps) updateWidgetImage(context, appWidgetManager, widgetId, imageProps)
} }
} }
private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density
private fun getWidgetSizePx(context: Context, widgetInfo: Bundle): Pair<Int, Int> { private fun getWidgetSizesDip(context: Context, widgetInfo: Bundle): List<FieldMap> {
val devicePixelRatio = getDevicePixelRatio() var sizes: List<SizeF>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF::class.java)
val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT @Suppress("DEPRECATION")
val widthPx = (widgetInfo.getInt(widthKey) * devicePixelRatio).roundToInt() widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES)
val heightPx = (widgetInfo.getInt(heightKey) * devicePixelRatio).roundToInt() } else {
return Pair(widthPx, heightPx) null
}
if (sizes.isNullOrEmpty()) {
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 widthDip = widgetInfo.getInt(widthKey)
val heightDip = widgetInfo.getInt(heightKey)
sizes = listOf(SizeF(widthDip.toFloat(), heightDip.toFloat()))
}
return sizes.map { size -> hashMapOf("widthDip" to size.width, "heightDip" to size.height) }
} }
private suspend fun getProps( private suspend fun getProps(
@ -84,8 +101,11 @@ class HomeWidgetProvider : AppWidgetProvider() {
drawEntryImage: Boolean, drawEntryImage: Boolean,
reuseEntry: Boolean = false, reuseEntry: Boolean = false,
): FieldMap? { ): FieldMap? {
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) val sizesDip = getWidgetSizesDip(context, widgetInfo)
if (widthPx == 0 || heightPx == 0) return null if (sizesDip.isEmpty()) return null
val sizeDip = sizesDip.first()
if (sizeDip["widthDip"] == 0 || sizeDip["heightDip"] == 0) return null
val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
@ -98,13 +118,16 @@ class HomeWidgetProvider : AppWidgetProvider() {
FlutterUtils.runOnUiThread { FlutterUtils.runOnUiThread {
channel.invokeMethod("drawWidget", hashMapOf( channel.invokeMethod("drawWidget", hashMapOf(
"widgetId" to widgetId, "widgetId" to widgetId,
"widthPx" to widthPx, "sizesDip" to sizesDip,
"heightPx" to heightPx,
"devicePixelRatio" to getDevicePixelRatio(), "devicePixelRatio" to getDevicePixelRatio(),
"drawEntryImage" to drawEntryImage, "drawEntryImage" to drawEntryImage,
"reuseEntry" to reuseEntry, "reuseEntry" to reuseEntry,
"isSystemThemeDark" to isNightModeOn, "isSystemThemeDark" to isNightModeOn,
), object : MethodChannel.Result { ).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
put("cornerRadiusPx", context.resources.getDimension(android.R.dimen.system_app_widget_background_radius))
}
}, object : MethodChannel.Result {
override fun success(result: Any?) { override fun success(result: Any?) {
cont.resume(result) cont.resume(result)
} }
@ -123,7 +146,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
return props as FieldMap? return props as FieldMap?
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e) Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId sizesPx=$sizesDip", e)
} }
return null return null
} }
@ -132,36 +155,83 @@ class HomeWidgetProvider : AppWidgetProvider() {
context: Context, context: Context,
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
widgetId: Int, widgetId: Int,
widgetInfo: Bundle,
props: FieldMap?, props: FieldMap?,
) { ) {
props ?: return props ?: return
val bytes = props["bytes"] as ByteArray? val bytesBySizeDip = (props["bytesBySizeDip"] as List<*>?)?.mapNotNull {
if (it is Map<*, *>) {
val widthDip = (it["widthDip"] as Number?)?.toFloat()
val heightDip = (it["heightDip"] as Number?)?.toFloat()
val bytes = it["bytes"] as ByteArray?
if (widthDip != null && heightDip != null && bytes != null) {
Pair(SizeF(widthDip, heightDip), bytes)
} else null
} else null
}
val updateOnTap = props["updateOnTap"] as Boolean? val updateOnTap = props["updateOnTap"] as Boolean?
if (bytes == null || updateOnTap == null) { if (bytesBySizeDip == null || updateOnTap == null) {
Log.e(LOG_TAG, "missing arguments") Log.e(LOG_TAG, "missing arguments")
return return
} }
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) if (bytesBySizeDip.isEmpty()) {
if (widthPx == 0 || heightPx == 0) return Log.e(LOG_TAG, "empty image list")
return
}
val bitmaps = ArrayList<Bitmap>()
fun createRemoteViewsForSize(
context: Context,
widgetId: Int,
sizeDip: SizeF,
bytes: ByteArray,
updateOnTap: Boolean,
): RemoteViews? {
val devicePixelRatio = getDevicePixelRatio()
val widthPx = (sizeDip.width * devicePixelRatio).roundToInt()
val heightPx = (sizeDip.height * devicePixelRatio).roundToInt()
try {
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888).also {
bitmaps.add(it)
it.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
}
val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId)
return RemoteViews(context.packageName, R.layout.app_widget).apply {
setImageViewBitmap(R.id.widget_img, bitmap)
setOnClickPendingIntent(R.id.widget_img, pendingIntent)
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget", e)
}
return null
}
try { try {
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes)) // multiple rendering for all possible sizes
val views = RemoteViews(
val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId) bytesBySizeDip.associateBy(
{ (sizeDip, _) -> sizeDip },
val views = RemoteViews(context.packageName, R.layout.app_widget).apply { { (sizeDip, bytes) -> createRemoteViewsForSize(context, widgetId, sizeDip, bytes, updateOnTap) },
setImageViewBitmap(R.id.widget_img, bitmap) ).filterValues { it != null }.mapValues { (_, view) -> view!! }
setOnClickPendingIntent(R.id.widget_img, pendingIntent) )
appWidgetManager.updateAppWidget(widgetId, views)
} else {
// single rendering
val (sizeDip, bytes) = bytesBySizeDip.first()
val views = createRemoteViewsForSize(context, widgetId, sizeDip, bytes, updateOnTap)
appWidgetManager.updateAppWidget(widgetId, views)
} }
appWidgetManager.updateAppWidget(widgetId, views)
bitmap.recycle()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget", e) Log.e(LOG_TAG, "failed to draw widget", e)
} finally {
bitmaps.forEach { it.recycle() }
bitmaps.clear()
} }
} }

View file

@ -10,6 +10,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.TransactionTooLargeException
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -21,6 +22,7 @@ import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.AccessibilityHandler import deckers.thibault.aves.channel.calls.AccessibilityHandler
import deckers.thibault.aves.channel.calls.AnalysisHandler import deckers.thibault.aves.channel.calls.AnalysisHandler
import deckers.thibault.aves.channel.calls.AppAdapterHandler import deckers.thibault.aves.channel.calls.AppAdapterHandler
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.DebugHandler import deckers.thibault.aves.channel.calls.DebugHandler
import deckers.thibault.aves.channel.calls.DeviceHandler import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.EmbeddedDataHandler import deckers.thibault.aves.channel.calls.EmbeddedDataHandler
@ -36,6 +38,7 @@ import deckers.thibault.aves.channel.calls.MetadataEditHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.calls.SecurityHandler import deckers.thibault.aves.channel.calls.SecurityHandler
import deckers.thibault.aves.channel.calls.StorageHandler import deckers.thibault.aves.channel.calls.StorageHandler
import deckers.thibault.aves.channel.calls.WallpaperHandler
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ActivityResultStreamHandler import deckers.thibault.aves.channel.streams.ActivityResultStreamHandler
@ -135,6 +138,7 @@ open class MainActivity : FlutterFragmentActivity() {
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this)) MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity // - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
@ -168,7 +172,7 @@ open class MainActivity : FlutterFragmentActivity() {
intentDataMap.clear() intentDataMap.clear()
} }
"submitPickedItems" -> submitPickedItems(call) "submitPickedItems" -> safe(call, result, ::submitPickedItems)
"submitPickedCollectionFilters" -> submitPickedCollectionFilters(call) "submitPickedCollectionFilters" -> submitPickedCollectionFilters(call)
} }
} }
@ -301,16 +305,32 @@ open class MainActivity : FlutterFragmentActivity() {
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
Intent.ACTION_SEND, Intent.ACTION_SEND,
MediaStore.ACTION_REVIEW, MediaStore.ACTION_REVIEW,
MediaStore.ACTION_REVIEW_SECURE,
"com.android.camera.action.REVIEW", "com.android.camera.action.REVIEW",
"com.android.camera.action.SPLIT_SCREEN_REVIEW" -> { "com.android.camera.action.SPLIT_SCREEN_REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri -> (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional // MIME type is optional
val type = intent.type ?: intent.resolveType(this) val type = intent.type ?: intent.resolveType(this)
return hashMapOf( val fields = hashMapOf<String, Any?>(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW, INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
INTENT_DATA_KEY_MIME_TYPE to type, INTENT_DATA_KEY_MIME_TYPE to type,
INTENT_DATA_KEY_URI to uri.toString(), INTENT_DATA_KEY_URI to uri.toString(),
) )
if (action == MediaStore.ACTION_REVIEW_SECURE) {
val uris = ArrayList<String>()
intent.clipData?.let { clipData ->
for (i in 0 until clipData.itemCount) {
clipData.getItemAt(i).uri?.let { uris.add(it.toString()) }
}
}
fields[INTENT_DATA_KEY_SECURE_URIS] = uris
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && intent.hasExtra(MediaStore.EXTRA_BRIGHTNESS)) {
fields[INTENT_DATA_KEY_BRIGHTNESS] = intent.getFloatExtra(MediaStore.EXTRA_BRIGHTNESS, 0f)
}
return fields
} }
} }
@ -390,28 +410,36 @@ open class MainActivity : FlutterFragmentActivity() {
return null return null
} }
private fun submitPickedItems(call: MethodCall) { open fun submitPickedItems(call: MethodCall, result: MethodChannel.Result) {
val pickedUris = call.argument<List<String>>("uris") val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) { try {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) } if (!pickedUris.isNullOrEmpty()) {
val intent = Intent().apply { val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
val firstUri = toUri(pickedUris.first()) val intent = Intent().apply {
if (pickedUris.size == 1) { val firstUri = toUri(pickedUris.first())
data = firstUri if (pickedUris.size == 1) {
} else { data = firstUri
clipData = ClipData.newUri(contentResolver, null, firstUri).apply { } else {
pickedUris.drop(1).forEach { clipData = ClipData.newUri(contentResolver, null, firstUri).apply {
addItem(ClipData.Item(toUri(it))) pickedUris.drop(1).forEach {
addItem(ClipData.Item(toUri(it)))
}
} }
} }
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) setResult(RESULT_OK, intent)
} else {
setResult(RESULT_CANCELED)
}
finish()
} catch (e: Exception) {
if (e is TransactionTooLargeException || e.cause is TransactionTooLargeException) {
result.error("submitPickedItems-large", "transaction too large with ${pickedUris?.size} URIs", e)
} else {
result.error("submitPickedItems-exception", "failed to pick ${pickedUris?.size} URIs", e)
} }
setResult(RESULT_OK, intent)
} else {
setResult(RESULT_CANCELED)
} }
finish()
} }
private fun submitPickedCollectionFilters(call: MethodCall) { private fun submitPickedCollectionFilters(call: MethodCall) {
@ -498,11 +526,13 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_ACTION = "action" const val INTENT_DATA_KEY_ACTION = "action"
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
const val INTENT_DATA_KEY_BRIGHTNESS = "brightness"
const val INTENT_DATA_KEY_FILTERS = "filters" const val INTENT_DATA_KEY_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page" const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query" const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_DATA_KEY_SAFE_MODE = "safeMode" const val INTENT_DATA_KEY_SAFE_MODE = "safeMode"
const val INTENT_DATA_KEY_SECURE_URIS = "secureUris"
const val INTENT_DATA_KEY_URI = "uri" const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId" const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"

View file

@ -2,132 +2,54 @@ package deckers.thibault.aves
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import deckers.thibault.aves.channel.calls.AppAdapterHandler
import android.os.Handler
import android.os.Looper
import android.util.Log
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.AccessibilityHandler
import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.EmbeddedDataHandler
import deckers.thibault.aves.channel.calls.MediaFetchBytesHandler
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
import deckers.thibault.aves.channel.calls.MediaSessionHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.calls.StorageHandler
import deckers.thibault.aves.channel.calls.WallpaperHandler
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterFragmentActivity() { class WallpaperActivity : MainActivity() {
private lateinit var intentDataMap: FieldMap private var originalIntent: String? = null
private lateinit var mediaSessionHandler: MediaSessionHandler
override fun onCreate(savedInstanceState: Bundle?) { override fun extractIntentData(intent: Intent?): FieldMap {
super.onCreate(savedInstanceState) if (intent != null) {
when (intent.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_SET_WALLPAPER,
INTENT_DATA_KEY_MIME_TYPE to type,
INTENT_DATA_KEY_URI to uri.toString(),
)
}
Log.i(LOG_TAG, "onCreate intent=$intent") // if the media URI is not provided we need to pick one first
intent.extras?.takeUnless { it.isEmpty }?.let { originalIntent = intent.action
Log.i(LOG_TAG, "onCreate intent extras=$it") intent.action = Intent.ACTION_PICK
}
intentDataMap = extractIntentData(intent)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
// notification: platform -> dart
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
}
// dart -> platform -> dart
// - need Context
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
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
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
}
override fun onStart() {
Log.i(LOG_TAG, "onStart")
super.onStart()
// as of Flutter v3.0.1, the window `viewInsets` and `viewPadding`
// are incorrect on startup in some environments (e.g. API 29 emulator),
// so we manually request to apply the insets to update the window metrics
Handler(Looper.getMainLooper()).postDelayed({
window.decorView.requestApplyInsets()
}, 100)
}
override fun onDestroy() {
mediaSessionHandler.dispose()
super.onDestroy()
}
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
intentDataMap.clear()
}
}
}
private fun extractIntentData(intent: Intent?): FieldMap {
when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER,
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type,
MainActivity.INTENT_DATA_KEY_URI to uri.toString(),
)
} }
} }
Intent.ACTION_RUN -> {
// flutter run
}
else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
}
} }
return HashMap()
return super.extractIntentData(intent)
} }
companion object { override fun submitPickedItems(call: MethodCall, result: MethodChannel.Result) {
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>() if (originalIntent != null) {
val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
onNewIntent(Intent().apply {
action = originalIntent
data = toUri(pickedUris.first())
})
} else {
setResult(RESULT_CANCELED)
finish()
}
} else {
super.submitPickedItems(call, result)
}
} }
} }

View file

@ -39,6 +39,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
"revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess) "revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess)
"deleteEmptyDirectories" -> ioScope.launch { safe(call, result, ::deleteEmptyDirectories) } "deleteEmptyDirectories" -> ioScope.launch { safe(call, result, ::deleteEmptyDirectories) }
"deleteTempDirectory" -> ioScope.launch { safe(call, result, ::deleteTempDirectory) } "deleteTempDirectory" -> ioScope.launch { safe(call, result, ::deleteTempDirectory) }
"deleteExternalCache" -> ioScope.launch { safe(call, result, ::deleteExternalCache) }
"canRequestMediaFileBulkAccess" -> safe(call, result, ::canRequestMediaFileBulkAccess) "canRequestMediaFileBulkAccess" -> safe(call, result, ::canRequestMediaFileBulkAccess)
"canInsertMedia" -> safe(call, result, ::canInsertMedia) "canInsertMedia" -> safe(call, result, ::canInsertMedia)
else -> result.notImplemented() else -> result.notImplemented()
@ -49,16 +50,17 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
var internalCache = getFolderSize(context.cacheDir) var internalCache = getFolderSize(context.cacheDir)
internalCache += getFolderSize(context.codeCacheDir) internalCache += getFolderSize(context.codeCacheDir)
val externalCache = context.externalCacheDirs.map(::getFolderSize).sum() val externalCache = context.externalCacheDirs.map(::getFolderSize).sum()
val externalFilesDirs = context.getExternalFilesDirs(null)
val dataDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) context.dataDir else File(context.applicationInfo.dataDir) val dataDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) context.dataDir else File(context.applicationInfo.dataDir)
val database = getFolderSize(File(dataDir, "databases")) val database = getFolderSize(File(dataDir, "databases"))
val flutter = getFolderSize(File(PathUtils.getDataDirectory(context))) val flutter = getFolderSize(File(PathUtils.getDataDirectory(context)))
val vaults = getFolderSize(File(StorageUtils.getVaultRoot(context))) val vaults = getFolderSize(File(StorageUtils.getVaultRoot(context)))
val trash = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }.map(::getFolderSize).sum() val trash = externalFilesDirs.mapNotNull { StorageUtils.trashDirFor(context, it.path) }.map(::getFolderSize).sum()
val internalData = getFolderSize(dataDir) - internalCache val internalData = getFolderSize(dataDir) - internalCache
val externalData = context.getExternalFilesDirs(null).map(::getFolderSize).sum() val externalData = externalFilesDirs.map(::getFolderSize).sum()
val miscData = internalData + externalData - (database + flutter + vaults + trash) val miscData = internalData + externalData - (database + flutter + vaults + trash)
result.success( result.success(
@ -224,6 +226,11 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
result.success(StorageUtils.deleteTempDirectory(context)) result.success(StorageUtils.deleteTempDirectory(context))
} }
private fun deleteExternalCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
context.externalCacheDirs.filter { it.exists() }.forEach { it.deleteRecursively() }
result.success(true)
}
private fun canRequestMediaFileBulkAccess(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun canRequestMediaFileBulkAccess(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
} }

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.model package deckers.thibault.aves.model
import java.io.File
enum class NameConflictStrategy { enum class NameConflictStrategy {
RENAME, REPLACE, SKIP; RENAME, REPLACE, SKIP;
@ -10,3 +12,5 @@ enum class NameConflictStrategy {
} }
} }
} }
class NameConflictResolution(var nameWithoutExtension: String?, var replacementFile: File?)

View file

@ -41,6 +41,7 @@ import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictResolution
import deckers.thibault.aves.model.NameConflictStrategy import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils
@ -147,13 +148,14 @@ abstract class ImageProvider {
val oldFile = File(sourcePath) val oldFile = File(sourcePath)
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) { if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
oldFile.parent?.let { dir -> oldFile.parent?.let { dir ->
resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = dir, dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
conflictStrategy = NameConflictStrategy.RENAME, conflictStrategy = NameConflictStrategy.RENAME,
)?.let { targetNameWithoutExtension -> )
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
val newFile = File(dir, targetFileName) val newFile = File(dir, targetFileName)
if (oldFile != newFile) { if (oldFile != newFile) {
@ -266,7 +268,7 @@ abstract class ImageProvider {
exportMimeType: String, exportMimeType: String,
): FieldMap { ): FieldMap {
val sourceMimeType = sourceEntry.mimeType val sourceMimeType = sourceEntry.mimeType
val sourceUri = sourceEntry.uri var sourceUri = sourceEntry.uri
val pageId = sourceEntry.pageId val pageId = sourceEntry.pageId
var desiredNameWithoutExtension = if (sourceEntry.path != null) { var desiredNameWithoutExtension = if (sourceEntry.path != null) {
@ -279,13 +281,17 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
} }
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType, mimeType = exportMimeType,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) ?: return skippedFieldMap )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
resolution.replacementFile?.let { file ->
sourceUri = Uri.fromFile(file)
}
val targetMimeType: String val targetMimeType: String
val write: (OutputStream) -> Unit val write: (OutputStream) -> Unit
@ -391,6 +397,8 @@ abstract class ImageProvider {
} finally { } finally {
// clearing Glide target should happen after effectively writing the bitmap // clearing Glide target should happen after effectively writing the bitmap
Glide.with(activity).clear(target) Glide.with(activity).clear(target)
resolution.replacementFile?.delete()
} }
} }
@ -470,7 +478,7 @@ abstract class ImageProvider {
} }
val captureMimeType = MimeTypes.JPEG val captureMimeType = MimeTypes.JPEG
val targetNameWithoutExtension = try { val resolution = try {
resolveTargetFileNameWithoutExtension( resolveTargetFileNameWithoutExtension(
contextWrapper = contextWrapper, contextWrapper = contextWrapper,
dir = targetDir, dir = targetDir,
@ -483,6 +491,7 @@ abstract class ImageProvider {
return return
} }
val targetNameWithoutExtension = resolution.nameWithoutExtension
if (targetNameWithoutExtension == null) { if (targetNameWithoutExtension == null) {
// skip it // skip it
callback.onSuccess(skippedFieldMap) callback.onSuccess(skippedFieldMap)
@ -568,10 +577,13 @@ abstract class ImageProvider {
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
mimeType: String, mimeType: String,
conflictStrategy: NameConflictStrategy, conflictStrategy: NameConflictStrategy,
): String? { ): NameConflictResolution {
var resolvedName: String? = desiredNameWithoutExtension
var replacementFile: File? = null
val extension = extensionFor(mimeType) val extension = extensionFor(mimeType)
val targetFile = File(dir, "$desiredNameWithoutExtension$extension") val targetFile = File(dir, "$desiredNameWithoutExtension$extension")
return when (conflictStrategy) { when (conflictStrategy) {
NameConflictStrategy.RENAME -> { NameConflictStrategy.RENAME -> {
var nameWithoutExtension = desiredNameWithoutExtension var nameWithoutExtension = desiredNameWithoutExtension
var i = 0 var i = 0
@ -579,24 +591,28 @@ abstract class ImageProvider {
i++ i++
nameWithoutExtension = "$desiredNameWithoutExtension ($i)" nameWithoutExtension = "$desiredNameWithoutExtension ($i)"
} }
nameWithoutExtension resolvedName = nameWithoutExtension
} }
NameConflictStrategy.REPLACE -> { NameConflictStrategy.REPLACE -> {
if (targetFile.exists()) { if (targetFile.exists()) {
// move replaced file to temp storage
// so that it can be used as a source for conversion or metadata copy
replacementFile = StorageUtils.createTempFile(contextWrapper).apply {
targetFile.transferTo(outputStream())
}
deletePath(contextWrapper, targetFile.path, mimeType) deletePath(contextWrapper, targetFile.path, mimeType)
} }
desiredNameWithoutExtension
} }
NameConflictStrategy.SKIP -> { NameConflictStrategy.SKIP -> {
if (targetFile.exists()) { if (targetFile.exists()) {
null resolvedName = null
} else {
desiredNameWithoutExtension
} }
} }
} }
return NameConflictResolution(resolvedName, replacementFile)
} }
// cf `MetadataFetchHandler.getCatalogMetadataByMetadataExtractor()` for a more thorough check // cf `MetadataFetchHandler.getCatalogMetadataByMetadataExtractor()` for a more thorough check

View file

@ -562,13 +562,14 @@ class MediaStoreImageProvider : ImageProvider() {
} }
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) ?: return skippedFieldMap )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri) val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
val targetPath = createSingle( val targetPath = createSingle(

View file

@ -6,32 +6,32 @@
<path <path
android:fillColor="#ef435a" android:fillColor="#ef435a"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m40.44,57.44 l8.74,8.74a1.54,1.54 0,0 1,0 2.18l-4.18,4.18a7.99,7.99 0,0 1,-11.3 0l-4.18,-4.18a1.54,1.54 0,0 1,0 -2.18l8.74,-8.74a1.54,1.54 0,0 1,2.18 0z" android:pathData="m41.03,57.12 l8.31,8.31a1.47,1.47 0,0 1,0 2.08l-3.97,3.97a7.6,7.6 0,0 1,-10.75 0l-3.97,-3.97a1.47,1.47 0,0 1,0 -2.08l8.31,-8.31a1.47,1.47 0,0 1,2.08 0z"
android:strokeWidth="1.61863" android:strokeWidth="1.53903"
android:strokeColor="#000000" android:strokeColor="#000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />
<path <path
android:fillColor="#e0e0e0" android:fillColor="#e0e0e0"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m53.48,44.4 l8.74,8.74a1.54,1.54 0,0 1,-0 2.18l-8.74,8.74a1.54,1.54 0,0 1,-2.18 0l-8.74,-8.74a1.54,1.54 0,0 1,0 -2.18l8.74,-8.74a1.54,1.54 0,0 1,2.18 0z" android:pathData="m53.43,44.71 l8.31,8.31a1.46,1.46 0,0 1,-0 2.08l-8.31,8.31a1.46,1.46 0,0 1,-2.08 0l-8.31,-8.31a1.46,1.46 0,0 1,0 -2.08l8.31,-8.31a1.46,1.46 0,0 1,2.08 0z"
android:strokeWidth="1.61862" android:strokeWidth="1.53902"
android:strokeColor="#000000" android:strokeColor="#000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />
<path <path
android:fillColor="#ffc11f" android:fillColor="#ffc11f"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m55.61,40.09a1.54,1.54 0,0 0,0 2.18l8.74,8.74a1.54,1.54 0,0 0,2.18 0l8.74,-8.74a1.54,1.54 0,0 0,0 -2.18l-4.34,-4.34a7.77,7.77 0,0 0,-10.98 0zM64.23,39.98a1.71,1.71 0,0 1,2.41 0,1.71 1.71,0 0,1 0,2.41 1.71,1.71 0,0 1,-2.41 0,1.71 1.71,0 0,1 0,-2.41z" android:pathData="m55.45,40.62a1.47,1.47 0,0 0,0 2.08l8.31,8.31a1.47,1.47 0,0 0,2.08 0l8.31,-8.31a1.47,1.47 0,0 0,0 -2.08l-4.12,-4.12a7.39,7.39 0,0 0,-10.44 0zM63.65,40.51a1.62,1.62 0,0 1,2.29 0,1.62 1.62,0 0,1 0,2.29 1.62,1.62 0,0 1,-2.29 0,1.62 1.62,0 0,1 0,-2.29z"
android:strokeWidth="1.61862" android:strokeWidth="1.53902"
android:strokeColor="#000000" android:strokeColor="#000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />
<path <path
android:fillColor="@color/ic_launcher_flavour" android:fillColor="@color/ic_launcher_flavour"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m36.47,27.39 l12.76,12.76a1.54,1.54 0,0 1,0 2.18l-8.74,8.74a1.54,1.54 0,0 1,-2.18 0l-5.67,-5.67a12.06,12.06 0,0 1,0 -17.06l0.95,-0.95a2.04,2.04 0,0 1,2.88 0z" android:pathData="m37.26,28.54 l12.13,12.13a1.47,1.47 0,0 1,0 2.08l-8.31,8.31a1.47,1.47 0,0 1,-2.08 0l-5.39,-5.39a11.47,11.47 0,0 1,0 -16.22l0.9,-0.9a1.94,1.94 0,0 1,2.74 0z"
android:strokeWidth="1.61863" android:strokeWidth="1.53903"
android:strokeColor="#000000" android:strokeColor="#000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />

View file

@ -6,32 +6,32 @@
<path <path
android:fillColor="#000000" android:fillColor="#000000"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m40.44,57.44 l8.74,8.74a1.54,1.54 0,0 1,0 2.18l-4.18,4.18a7.99,7.99 0,0 1,-11.3 0l-4.18,-4.18a1.54,1.54 0,0 1,0 -2.18l8.74,-8.74a1.54,1.54 0,0 1,2.18 0z" android:pathData="m41.03,57.12 l8.31,8.31a1.47,1.47 0,0 1,0 2.08l-3.97,3.97a7.6,7.6 0,0 1,-10.75 0l-3.97,-3.97a1.47,1.47 0,0 1,0 -2.08l8.3,-8.3a1.47,1.47 0,0 1,2.08 0z"
android:strokeWidth="1.61863" android:strokeWidth="1.53871"
android:strokeColor="#00000000" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />
<path <path
android:fillColor="#000000" android:fillColor="#000000"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m53.48,44.4 l8.74,8.74a1.54,1.54 0,0 1,-0 2.18l-8.74,8.74a1.54,1.54 0,0 1,-2.18 0l-8.74,-8.74a1.54,1.54 0,0 1,0 -2.18l8.74,-8.74a1.54,1.54 0,0 1,2.18 0z" android:pathData="m53.43,44.72 l8.31,8.31a1.46,1.46 0,0 1,-0 2.08l-8.31,8.31a1.46,1.46 0,0 1,-2.08 0l-8.31,-8.31a1.46,1.46 0,0 1,0 -2.08L51.35,44.72a1.46,1.46 0,0 1,2.08 0z"
android:strokeWidth="1.61862" android:strokeWidth="1.5387"
android:strokeColor="#00000000" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />
<path <path
android:fillColor="#000000" android:fillColor="#000000"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m55.61,40.09a1.54,1.54 0,0 0,0 2.18l8.74,8.74a1.54,1.54 0,0 0,2.18 0l8.74,-8.74a1.54,1.54 0,0 0,0 -2.18l-4.34,-4.34a7.77,7.77 0,0 0,-10.98 0zM64.23,39.98a1.71,1.71 0,0 1,2.41 0,1.71 1.71,0 0,1 0,2.41 1.71,1.71 0,0 1,-2.41 0,1.71 1.71,0 0,1 0,-2.41z" android:pathData="m55.45,40.62a1.47,1.47 0,0 0,0 2.08l8.31,8.3a1.47,1.47 0,0 0,2.08 0l8.3,-8.3a1.47,1.47 0,0 0,0 -2.08l-4.12,-4.12a7.38,7.38 0,0 0,-10.44 0zM63.65,40.51a1.62,1.62 0,0 1,2.29 0,1.62 1.62,0 0,1 0,2.29 1.62,1.62 0,0 1,-2.29 0,1.62 1.62,0 0,1 0,-2.29z"
android:strokeWidth="1.61862" android:strokeWidth="1.5387"
android:strokeColor="#00000000" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />
<path <path
android:fillColor="#000000" android:fillColor="#000000"
android:fillType="evenOdd" android:fillType="evenOdd"
android:pathData="m36.47,27.39 l12.76,12.76a1.54,1.54 0,0 1,0 2.18l-8.74,8.74a1.54,1.54 0,0 1,-2.18 0l-5.67,-5.67a12.06,12.06 0,0 1,0 -17.06l0.95,-0.95a2.04,2.04 0,0 1,2.88 0z" android:pathData="m37.26,28.54 l12.13,12.13a1.47,1.47 0,0 1,0 2.08l-8.31,8.3a1.47,1.47 0,0 1,-2.08 0L33.62,45.67a11.47,11.47 0,0 1,0 -16.22l0.9,-0.9a1.94,1.94 0,0 1,2.74 0z"
android:strokeWidth="1.61863" android:strokeWidth="1.53871"
android:strokeColor="#00000000" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineCap="round"
android:strokeLineJoin="round" /> android:strokeLineJoin="round" />

View file

@ -1,4 +0,0 @@
In v1.10.6:
- detect HDR videos (but do not play them in their full HDR glory)
- removing animations also applies to pop up menus
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.10.6:
- detect HDR videos (but do not play them in their full HDR glory)
- removing animations also applies to pop up menus
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.10.7:
- detect HDR videos (but do not play them in their full HDR glory)
- removing animations also applies to pop up menus
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.10.7:
- detect HDR videos (but do not play them in their full HDR glory)
- removing animations also applies to pop up menus
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.10.8:
- rename in bulk using tags
- repeat a section section section of a video
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.10.8:
- rename in bulk using tags
- repeat a section section section of a video
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.10.9:
- rename in bulk using tags
- repeat a section section section of a video
Full changelog available on GitHub

View file

@ -1,4 +0,0 @@
In v1.10.9:
- rename in bulk using tags
- repeat a section section section of a video
Full changelog available on GitHub

View file

@ -1,3 +0,0 @@
In v1.11.0:
- watch videos with SRT subtitle files
Full changelog available on GitHub

View file

@ -1,3 +0,0 @@
In v1.11.0:
- watch videos with SRT subtitle files
Full changelog available on GitHub

View file

@ -0,0 +1,4 @@
In v1.11.4:
- explore your collection with the... explorer
- convert your motion photos to stills in bulk
Full changelog available on GitHub

View file

@ -0,0 +1,4 @@
In v1.11.4:
- explore your collection with the... explorer
- convert your motion photos to stills in bulk
Full changelog available on GitHub

View file

@ -1195,7 +1195,7 @@
"@collectionActionAddShortcut": {}, "@collectionActionAddShortcut": {},
"settingsViewerShowMinimap": "إظهار الخريطة المصغرة", "settingsViewerShowMinimap": "إظهار الخريطة المصغرة",
"@settingsViewerShowMinimap": {}, "@settingsViewerShowMinimap": {},
"settingsCollectionBurstPatternsTile": "أنماط الانفجار", "settingsCollectionBurstPatternsTile": "أنماط الصور المتتابعة",
"@settingsCollectionBurstPatternsTile": {}, "@settingsCollectionBurstPatternsTile": {},
"viewerInfoLabelPath": "المسار", "viewerInfoLabelPath": "المسار",
"@viewerInfoLabelPath": {}, "@viewerInfoLabelPath": {},
@ -1538,5 +1538,9 @@
"renameProcessorHash": "تجزئة", "renameProcessorHash": "تجزئة",
"@renameProcessorHash": {}, "@renameProcessorHash": {},
"chipActionShowCollection": "عرض في المجموعة", "chipActionShowCollection": "عرض في المجموعة",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "عرض في المستكشف",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "المستكشف",
"@explorerPageTitle": {}
} }

View file

@ -5,7 +5,7 @@
"@welcomeTermsToggle": {}, "@welcomeTermsToggle": {},
"welcomeOptional": "Неабавязковыя", "welcomeOptional": "Неабавязковыя",
"@welcomeOptional": {}, "@welcomeOptional": {},
"welcomeMessage": "Сардэчна запрашаем ў Aves", "welcomeMessage": "Сардэчна запрашаем у Aves",
"@welcomeMessage": {}, "@welcomeMessage": {},
"itemCount": "{count, plural, =1{{count} элемент} other{{count} элементаў}}", "itemCount": "{count, plural, =1{{count} элемент} other{{count} элементаў}}",
"@itemCount": { "@itemCount": {
@ -38,7 +38,7 @@
"@saveTooltip": {}, "@saveTooltip": {},
"doNotAskAgain": "Больш не пытайся", "doNotAskAgain": "Больш не пытайся",
"@doNotAskAgain": {}, "@doNotAskAgain": {},
"chipActionGoToCountryPage": "Паказаць ў Краінах", "chipActionGoToCountryPage": "Паказаць у Краінах",
"@chipActionGoToCountryPage": {}, "@chipActionGoToCountryPage": {},
"chipActionFilterOut": "Адфільтраваць", "chipActionFilterOut": "Адфільтраваць",
"@chipActionFilterOut": {}, "@chipActionFilterOut": {},
@ -56,27 +56,27 @@
"@sourceStateCataloguing": {}, "@sourceStateCataloguing": {},
"chipActionDelete": "Выдаліць", "chipActionDelete": "Выдаліць",
"@chipActionDelete": {}, "@chipActionDelete": {},
"chipActionGoToAlbumPage": "Паказаць ў Альбомах", "chipActionGoToAlbumPage": "Паказаць у Альбомах",
"@chipActionGoToAlbumPage": {}, "@chipActionGoToAlbumPage": {},
"chipActionHide": "Схаваць", "chipActionHide": "Схаваць",
"@chipActionHide": {}, "@chipActionHide": {},
"chipActionCreateVault": "Стварыце сховішча", "chipActionCreateVault": "Стварыце сховішча",
"@chipActionCreateVault": {}, "@chipActionCreateVault": {},
"chipActionGoToPlacePage": "Паказаць ў Лакацыях", "chipActionGoToPlacePage": "Паказаць у Лакацыях",
"@chipActionGoToPlacePage": {}, "@chipActionGoToPlacePage": {},
"chipActionUnpin": "Адмацаваць зверху", "chipActionUnpin": "Адмацаваць зверху",
"@chipActionUnpin": {}, "@chipActionUnpin": {},
"chipActionGoToTagPage": "Паказаць ў Тэгах", "chipActionGoToTagPage": "Паказаць у Тэгах",
"@chipActionGoToTagPage": {}, "@chipActionGoToTagPage": {},
"chipActionLock": "Заблакаваць", "chipActionLock": "Заблакаваць",
"@chipActionLock": {}, "@chipActionLock": {},
"chipActionSetCover": "Ўсталяваць вокладку", "chipActionSetCover": "Усталяваць вокладку",
"@chipActionSetCover": {}, "@chipActionSetCover": {},
"chipActionRename": "Перайменаваць", "chipActionRename": "Перайменаваць",
"@chipActionRename": {}, "@chipActionRename": {},
"chipActionConfigureVault": "Наладзіць сховішча", "chipActionConfigureVault": "Наладзіць сховішча",
"@chipActionConfigureVault": {}, "@chipActionConfigureVault": {},
"entryActionCopyToClipboard": "Скапіяваць ў буфер абмену", "entryActionCopyToClipboard": "Скапіяваць у буфер абмену",
"@entryActionCopyToClipboard": {}, "@entryActionCopyToClipboard": {},
"entryActionDelete": "Выдаліць", "entryActionDelete": "Выдаліць",
"@entryActionDelete": {}, "@entryActionDelete": {},
@ -120,15 +120,15 @@
"@entryActionRotateScreen": {}, "@entryActionRotateScreen": {},
"entryActionViewSource": "Паглядзець крыніцу", "entryActionViewSource": "Паглядзець крыніцу",
"@entryActionViewSource": {}, "@entryActionViewSource": {},
"entryActionConvertMotionPhotoToStillImage": "Пераўтварыць ў нерухомую выяву", "entryActionConvertMotionPhotoToStillImage": "Канвертаваць у статычны малюнак",
"@entryActionConvertMotionPhotoToStillImage": {}, "@entryActionConvertMotionPhotoToStillImage": {},
"entryActionViewMotionPhotoVideo": "Адкрыць відэа", "entryActionViewMotionPhotoVideo": "Адкрыць відэа",
"@entryActionViewMotionPhotoVideo": {}, "@entryActionViewMotionPhotoVideo": {},
"entryActionSetAs": "Ўсталяваць як", "entryActionSetAs": "Усталяваць як",
"@entryActionSetAs": {}, "@entryActionSetAs": {},
"entryActionAddFavourite": "Дадаць ў абранае", "entryActionAddFavourite": "Дадаць у абранае",
"@entryActionAddFavourite": {}, "@entryActionAddFavourite": {},
"videoActionUnmute": "Ўключыць гук", "videoActionUnmute": "Уключыць гук",
"@videoActionUnmute": {}, "@videoActionUnmute": {},
"videoActionCaptureFrame": "Захоп кадра", "videoActionCaptureFrame": "Захоп кадра",
"@videoActionCaptureFrame": {}, "@videoActionCaptureFrame": {},
@ -188,11 +188,11 @@
"@entryActionEdit": {}, "@entryActionEdit": {},
"entryActionOpen": "Адкрыць з дапамогай", "entryActionOpen": "Адкрыць з дапамогай",
"@entryActionOpen": {}, "@entryActionOpen": {},
"entryActionOpenMap": "Паказаць ў праграме карты", "entryActionOpenMap": "Паказаць у праграме карты",
"@entryActionOpenMap": {}, "@entryActionOpenMap": {},
"videoActionMute": "Адключыць гук", "videoActionMute": "Адключыць гук",
"@videoActionMute": {}, "@videoActionMute": {},
"slideshowActionShowInCollection": "Паказаць ў Калекцыі", "slideshowActionShowInCollection": "Паказаць у Калекцыі",
"@slideshowActionShowInCollection": {}, "@slideshowActionShowInCollection": {},
"entryInfoActionEditDate": "Рэдагаваць дату і час", "entryInfoActionEditDate": "Рэдагаваць дату і час",
"@entryInfoActionEditDate": {}, "@entryInfoActionEditDate": {},
@ -228,7 +228,7 @@
"@filterTypeSphericalVideoLabel": {}, "@filterTypeSphericalVideoLabel": {},
"filterNoTitleLabel": "Без назвы", "filterNoTitleLabel": "Без назвы",
"@filterNoTitleLabel": {}, "@filterNoTitleLabel": {},
"filterOnThisDayLabel": "Ў гэты дзень", "filterOnThisDayLabel": "У гэты дзень",
"@filterOnThisDayLabel": {}, "@filterOnThisDayLabel": {},
"filterRatingRejectedLabel": "Адхілена", "filterRatingRejectedLabel": "Адхілена",
"@filterRatingRejectedLabel": {}, "@filterRatingRejectedLabel": {},
@ -363,7 +363,7 @@
"@vaultLockTypePassword": {}, "@vaultLockTypePassword": {},
"settingsVideoEnablePip": "Карцінка ў карцінцы", "settingsVideoEnablePip": "Карцінка ў карцінцы",
"@settingsVideoEnablePip": {}, "@settingsVideoEnablePip": {},
"videoControlsPlayOutside": "Адкрыць ў іншым прайгравальніку", "videoControlsPlayOutside": "Адкрыць у іншым прайгравальніку",
"@videoControlsPlayOutside": {}, "@videoControlsPlayOutside": {},
"videoControlsPlay": "Прайграванне", "videoControlsPlay": "Прайграванне",
"@videoControlsPlay": {}, "@videoControlsPlay": {},
@ -449,7 +449,7 @@
"@wallpaperTargetHomeLock": {}, "@wallpaperTargetHomeLock": {},
"widgetTapUpdateWidget": "Абнавіць віджэт", "widgetTapUpdateWidget": "Абнавіць віджэт",
"@widgetTapUpdateWidget": {}, "@widgetTapUpdateWidget": {},
"storageVolumeDescriptionFallbackPrimary": "Ўнутраная памяць", "storageVolumeDescriptionFallbackPrimary": "Унутраная памяць",
"@storageVolumeDescriptionFallbackPrimary": {}, "@storageVolumeDescriptionFallbackPrimary": {},
"restrictedAccessDialogMessage": "Гэтай праграме забаронена змяняць файлы ў {directory} «{volume}».\n\nКаб перамясціць элементы ў іншую дырэкторыю, выкарыстоўвайце папярэдне ўсталяваны дыспетчар файлаў або праграму галерэі.", "restrictedAccessDialogMessage": "Гэтай праграме забаронена змяняць файлы ў {directory} «{volume}».\n\nКаб перамясціць элементы ў іншую дырэкторыю, выкарыстоўвайце папярэдне ўсталяваны дыспетчар файлаў або праграму галерэі.",
"@restrictedAccessDialogMessage": { "@restrictedAccessDialogMessage": {
@ -465,7 +465,7 @@
} }
} }
}, },
"missingSystemFilePickerDialogMessage": "Сродак выбару сістэмных файлаў адсутнічае або адключаны. Ўключыце яго і паўтарыце спробу.", "missingSystemFilePickerDialogMessage": "Сістэмная праграма выбару файлаў адсутнічае ці адключана. Калі ласка, уключыце яе і паспрабуйце яшчэ раз.",
"@missingSystemFilePickerDialogMessage": {}, "@missingSystemFilePickerDialogMessage": {},
"unsupportedTypeDialogMessage": "{count, plural, =1{Гэта аперацыя не падтрымліваецца для элементаў наступнага тыпу: {types}.} other{Гэта аперацыя не падтрымліваецца для элементаў наступных тыпаў: {types}.}}", "unsupportedTypeDialogMessage": "{count, plural, =1{Гэта аперацыя не падтрымліваецца для элементаў наступнага тыпу: {types}.} other{Гэта аперацыя не падтрымліваецца для элементаў наступных тыпаў: {types}.}}",
"@unsupportedTypeDialogMessage": { "@unsupportedTypeDialogMessage": {
@ -488,7 +488,7 @@
"@moveUndatedConfirmationDialogMessage": {}, "@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "Захаваць даты", "moveUndatedConfirmationDialogSetDate": "Захаваць даты",
"@moveUndatedConfirmationDialogSetDate": {}, "@moveUndatedConfirmationDialogSetDate": {},
"videoResumeDialogMessage": "Вы хочаце аднавіць гульню ў {time}?", "videoResumeDialogMessage": "Вы хочаце аднавіць прайграванне на {time}?",
"@videoResumeDialogMessage": { "@videoResumeDialogMessage": {
"placeholders": { "placeholders": {
"time": { "time": {
@ -517,15 +517,15 @@
"@configureVaultDialogTitle": {}, "@configureVaultDialogTitle": {},
"vaultDialogLockTypeLabel": "Тып блакіроўкі", "vaultDialogLockTypeLabel": "Тып блакіроўкі",
"@vaultDialogLockTypeLabel": {}, "@vaultDialogLockTypeLabel": {},
"pinDialogEnter": "Ўвядзіце PIN-код", "pinDialogEnter": "Увядзіце PIN-код",
"@pinDialogEnter": {}, "@pinDialogEnter": {},
"patternDialogEnter": "Ўвядзіце графічны ключ", "patternDialogEnter": "Увядзіце ключ",
"@patternDialogEnter": {}, "@patternDialogEnter": {},
"patternDialogConfirm": "Пацвердзіце графічны ключ", "patternDialogConfirm": "Пацвердзіце графічны ключ",
"@patternDialogConfirm": {}, "@patternDialogConfirm": {},
"pinDialogConfirm": "Пацвердзіце PIN-код", "pinDialogConfirm": "Пацвердзіце PIN-код",
"@pinDialogConfirm": {}, "@pinDialogConfirm": {},
"passwordDialogEnter": "Ўвядзіце пароль", "passwordDialogEnter": "Увядзіце пароль",
"@passwordDialogEnter": {}, "@passwordDialogEnter": {},
"passwordDialogConfirm": "Пацвердзіце пароль", "passwordDialogConfirm": "Пацвердзіце пароль",
"@passwordDialogConfirm": {}, "@passwordDialogConfirm": {},
@ -551,7 +551,7 @@
"@mapPointNorthUpTooltip": {}, "@mapPointNorthUpTooltip": {},
"viewerInfoLabelCoordinates": "Каардынаты", "viewerInfoLabelCoordinates": "Каардынаты",
"@viewerInfoLabelCoordinates": {}, "@viewerInfoLabelCoordinates": {},
"viewerInfoLabelOwner": "Ўладальнік", "viewerInfoLabelOwner": "Уладальнік",
"@viewerInfoLabelOwner": {}, "@viewerInfoLabelOwner": {},
"viewerInfoLabelDuration": "Працягласць", "viewerInfoLabelDuration": "Працягласць",
"@viewerInfoLabelDuration": {}, "@viewerInfoLabelDuration": {},
@ -577,7 +577,7 @@
"@sourceViewerPageTitle": {}, "@sourceViewerPageTitle": {},
"panoramaDisableSensorControl": "Адключыць сэнсарнае кіраванне", "panoramaDisableSensorControl": "Адключыць сэнсарнае кіраванне",
"@panoramaDisableSensorControl": {}, "@panoramaDisableSensorControl": {},
"panoramaEnableSensorControl": "Ўключыць сэнсарнае кіраванне", "panoramaEnableSensorControl": "Уключыць сэнсарнае кіраванне",
"@panoramaEnableSensorControl": {}, "@panoramaEnableSensorControl": {},
"tagPlaceholderPlace": "Месца", "tagPlaceholderPlace": "Месца",
"@tagPlaceholderPlace": {}, "@tagPlaceholderPlace": {},
@ -601,7 +601,7 @@
"@videoControlsNone": {}, "@videoControlsNone": {},
"viewerErrorUnknown": "Ой!", "viewerErrorUnknown": "Ой!",
"@viewerErrorUnknown": {}, "@viewerErrorUnknown": {},
"viewerSetWallpaperButtonLabel": "ЎСТАНАВІЦЬ ШПАЛЕРЫ", "viewerSetWallpaperButtonLabel": "УСТАНАВІЦЬ ШПАЛЕРЫ",
"@viewerSetWallpaperButtonLabel": {}, "@viewerSetWallpaperButtonLabel": {},
"statsTopAlbumsSectionTitle": "Лепшыя альбомы", "statsTopAlbumsSectionTitle": "Лепшыя альбомы",
"@statsTopAlbumsSectionTitle": {}, "@statsTopAlbumsSectionTitle": {},
@ -625,7 +625,7 @@
"@mapZoomOutTooltip": {}, "@mapZoomOutTooltip": {},
"openMapPageTooltip": "Паглядзець на старонцы карты", "openMapPageTooltip": "Паглядзець на старонцы карты",
"@openMapPageTooltip": {}, "@openMapPageTooltip": {},
"mapEmptyRegion": "Ў гэтым рэгіёне няма малюнкаў", "mapEmptyRegion": "Няма малюнкаў у гэтым рэгіёне",
"@mapEmptyRegion": {}, "@mapEmptyRegion": {},
"viewerInfoSearchEmpty": "Няма адпаведных ключоў", "viewerInfoSearchEmpty": "Няма адпаведных ключоў",
"@viewerInfoSearchEmpty": {}, "@viewerInfoSearchEmpty": {},
@ -685,19 +685,19 @@
"@aboutBugCopyInfoInstruction": {}, "@aboutBugCopyInfoInstruction": {},
"vaultBinUsageDialogMessage": "Некаторыя сховішчы выкарыстоўваюць сметніцу.", "vaultBinUsageDialogMessage": "Некаторыя сховішчы выкарыстоўваюць сметніцу.",
"@vaultBinUsageDialogMessage": {}, "@vaultBinUsageDialogMessage": {},
"aboutBugSaveLogInstruction": "Захаваць журналы праграмы ў файл", "aboutBugSaveLogInstruction": "Захавайце логі праграмы ў файл",
"@aboutBugSaveLogInstruction": {}, "@aboutBugSaveLogInstruction": {},
"aboutBugReportInstruction": "Адправіць справаздачу аб памылцы на GitHub разам з журналамі і сістэмнай інфармацыяй", "aboutBugReportInstruction": "Адправіць справаздачу аб памылцы на GitHub разам з журналамі і сістэмнай інфармацыяй",
"@aboutBugReportInstruction": {}, "@aboutBugReportInstruction": {},
"entryActionCast": "Трансляцыя", "entryActionCast": "Трансляцыя",
"@entryActionCast": {}, "@entryActionCast": {},
"hideFilterConfirmationDialogMessage": "Адпаведныя фота і відэа будуць схаваны з вашай калекцыі. Вы можаце убачыць іх зноў ў наладах «Прыватнасць».\n\nВы ўпэўнены, што хочаце іх схаваць?", "hideFilterConfirmationDialogMessage": "Адпаведныя фота і відэа будуць схаваны з вашай калекцыі. Вы можаце паказаць іх зноў у наладах «Прыватнасць».\n\nВы ўпэўнены, што хочаце іх схаваць?",
"@hideFilterConfirmationDialogMessage": {}, "@hideFilterConfirmationDialogMessage": {},
"renameEntrySetPagePatternFieldLabel": "Шаблон наймення", "renameEntrySetPagePatternFieldLabel": "Шаблон наймення",
"@renameEntrySetPagePatternFieldLabel": {}, "@renameEntrySetPagePatternFieldLabel": {},
"renameAlbumDialogLabel": "Новая назва", "renameAlbumDialogLabel": "Новая назва",
"@renameAlbumDialogLabel": {}, "@renameAlbumDialogLabel": {},
"renameAlbumDialogLabelAlreadyExistsHelper": "Каталог ўжо ёсць", "renameAlbumDialogLabelAlreadyExistsHelper": "Каталог ужо існуе",
"@renameAlbumDialogLabelAlreadyExistsHelper": {}, "@renameAlbumDialogLabelAlreadyExistsHelper": {},
"aboutBugReportButton": "Адправіць справаздачу", "aboutBugReportButton": "Адправіць справаздачу",
"@aboutBugReportButton": {}, "@aboutBugReportButton": {},
@ -707,7 +707,7 @@
"@aboutBugSectionTitle": {}, "@aboutBugSectionTitle": {},
"aboutBugCopyInfoButton": "Скапіяваць", "aboutBugCopyInfoButton": "Скапіяваць",
"@aboutBugCopyInfoButton": {}, "@aboutBugCopyInfoButton": {},
"binEntriesConfirmationDialogMessage": "{count, plural, =1{Перамясціць гэты элемент ў сметніцу?} few{Перамясціць гэтыя {count} элемента ў сметніцу?} other{Перамясціць гэтыя {count} элементаў ў сметніцу?}}", "binEntriesConfirmationDialogMessage": "{count, plural, =1{Перамясціць гэты элемент у сметніцу?} few{Перамясціць гэтыя {count} элемента ў сметніцу?} other{Перамясціць гэтыя {count} элементаў у сметніцу?}}",
"@binEntriesConfirmationDialogMessage": { "@binEntriesConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -797,7 +797,7 @@
"@settingsCollectionTile": {}, "@settingsCollectionTile": {},
"settingsThemeBrightnessDialogTitle": "Тэма", "settingsThemeBrightnessDialogTitle": "Тэма",
"@settingsThemeBrightnessDialogTitle": {}, "@settingsThemeBrightnessDialogTitle": {},
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэты альбом і элемент ў ім?} few{Выдаліць гэты альбом і {count} элементы ў ім?} other{Выдаліць гэты альбом і {count} элементаў ў ім?}}", "deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэты альбом і элемент у ім?} few{Выдаліць гэты альбом і {count} элементы ў ім?} other{Выдаліць гэты альбом і {count} элементаў у ім?}}",
"@deleteSingleAlbumConfirmationDialogMessage": { "@deleteSingleAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -819,7 +819,7 @@
"@aboutDataUsageMisc": {}, "@aboutDataUsageMisc": {},
"albumVideoCaptures": "Відэазапісы", "albumVideoCaptures": "Відэазапісы",
"@albumVideoCaptures": {}, "@albumVideoCaptures": {},
"editEntryDateDialogSetCustom": "Ўсталяваць карыстацкую дату", "editEntryDateDialogSetCustom": "Устанавіць дату",
"@editEntryDateDialogSetCustom": {}, "@editEntryDateDialogSetCustom": {},
"settingsSearchEmpty": "Няма адпаведнай налады", "settingsSearchEmpty": "Няма адпаведнай налады",
"@settingsSearchEmpty": {}, "@settingsSearchEmpty": {},
@ -845,7 +845,7 @@
"@collectionSelectSectionTooltip": {}, "@collectionSelectSectionTooltip": {},
"aboutLicensesBanner": "Гэта праграма выкарыстоўвае наступныя пакеты і бібліятэкі з адкрытым зыходным кодам.", "aboutLicensesBanner": "Гэта праграма выкарыстоўвае наступныя пакеты і бібліятэкі з адкрытым зыходным кодам.",
"@aboutLicensesBanner": {}, "@aboutLicensesBanner": {},
"dateYesterday": "Ўчора", "dateYesterday": "Учора",
"@dateYesterday": {}, "@dateYesterday": {},
"aboutDataUsageDatabase": "База дадзеных", "aboutDataUsageDatabase": "База дадзеных",
"@aboutDataUsageDatabase": {}, "@aboutDataUsageDatabase": {},
@ -853,7 +853,7 @@
"@tileLayoutMosaic": {}, "@tileLayoutMosaic": {},
"collectionDeselectSectionTooltip": "Адмяніць выбар раздзела", "collectionDeselectSectionTooltip": "Адмяніць выбар раздзела",
"@collectionDeselectSectionTooltip": {}, "@collectionDeselectSectionTooltip": {},
"settingsKeepScreenOnTile": "Трымаць экран ўключаным", "settingsKeepScreenOnTile": "Трымаць экран уключаным",
"@settingsKeepScreenOnTile": {}, "@settingsKeepScreenOnTile": {},
"tileLayoutGrid": "Сетка", "tileLayoutGrid": "Сетка",
"@tileLayoutGrid": {}, "@tileLayoutGrid": {},
@ -879,11 +879,11 @@
"@videoStreamSelectionDialogAudio": {}, "@videoStreamSelectionDialogAudio": {},
"videoSpeedDialogLabel": "Хуткасць прайгравання", "videoSpeedDialogLabel": "Хуткасць прайгравання",
"@videoSpeedDialogLabel": {}, "@videoSpeedDialogLabel": {},
"editEntryLocationDialogSetCustom": "Ўстанавіць карыстацкае месцазнаходжанне", "editEntryLocationDialogSetCustom": "Рэдагаваць месцазнаходжанне",
"@editEntryLocationDialogSetCustom": {}, "@editEntryLocationDialogSetCustom": {},
"placeEmpty": "Няма месцаў", "placeEmpty": "Няма месцаў",
"@placeEmpty": {}, "@placeEmpty": {},
"editEntryDateDialogExtractFromTitle": "Выняць з загалоўка", "editEntryDateDialogExtractFromTitle": "Выняць з назвы",
"@editEntryDateDialogExtractFromTitle": {}, "@editEntryDateDialogExtractFromTitle": {},
"aboutLinkLicense": "Ліцэнзія", "aboutLinkLicense": "Ліцэнзія",
"@aboutLinkLicense": {}, "@aboutLinkLicense": {},
@ -925,7 +925,7 @@
"@drawerAlbumPage": {}, "@drawerAlbumPage": {},
"settingsActionImport": "Імпарт", "settingsActionImport": "Імпарт",
"@settingsActionImport": {}, "@settingsActionImport": {},
"locationPickerUseThisLocationButton": "Выкарыстоўваць гэтае месца", "locationPickerUseThisLocationButton": "Выкарыстоўваць гэтае месцазнаходжанне",
"@locationPickerUseThisLocationButton": {}, "@locationPickerUseThisLocationButton": {},
"collectionGroupNone": "Не групаваць", "collectionGroupNone": "Не групаваць",
"@collectionGroupNone": {}, "@collectionGroupNone": {},
@ -937,7 +937,7 @@
"@settingsActionImportDialogTitle": {}, "@settingsActionImportDialogTitle": {},
"albumGroupTier": "Па ўзроўні", "albumGroupTier": "Па ўзроўні",
"@albumGroupTier": {}, "@albumGroupTier": {},
"drawerCollectionAll": "Ўся калекцыя", "drawerCollectionAll": "Уся калекцыя",
"@drawerCollectionAll": {}, "@drawerCollectionAll": {},
"sortByItemCount": "Па колькасці элементаў", "sortByItemCount": "Па колькасці элементаў",
"@sortByItemCount": {}, "@sortByItemCount": {},
@ -953,7 +953,7 @@
"@albumPickPageTitlePick": {}, "@albumPickPageTitlePick": {},
"menuActionMap": "Карта", "menuActionMap": "Карта",
"@menuActionMap": {}, "@menuActionMap": {},
"collectionActionMove": "Перамясціць ў альбом", "collectionActionMove": "Перамясціць у альбом",
"@collectionActionMove": {}, "@collectionActionMove": {},
"searchAlbumsSectionTitle": "Альбомы", "searchAlbumsSectionTitle": "Альбомы",
"@searchAlbumsSectionTitle": {}, "@searchAlbumsSectionTitle": {},
@ -1013,9 +1013,9 @@
"@albumPageTitle": {}, "@albumPageTitle": {},
"editEntryLocationDialogTitle": "Месцазнаходжанне", "editEntryLocationDialogTitle": "Месцазнаходжанне",
"@editEntryLocationDialogTitle": {}, "@editEntryLocationDialogTitle": {},
"albumPickPageTitleCopy": "Скапіяваць ў альбом", "albumPickPageTitleCopy": "Капіяваць у альбом",
"@albumPickPageTitleCopy": {}, "@albumPickPageTitleCopy": {},
"collectionActionCopy": "Скапіяваць ў альбом", "collectionActionCopy": "Скапіяваць у альбом",
"@collectionActionCopy": {}, "@collectionActionCopy": {},
"viewDialogReverseSortOrder": "Адваротны парадак сартавання", "viewDialogReverseSortOrder": "Адваротны парадак сартавання",
"@viewDialogReverseSortOrder": {}, "@viewDialogReverseSortOrder": {},
@ -1033,7 +1033,7 @@
"@tagEmpty": {}, "@tagEmpty": {},
"collectionActionShowTitleSearch": "Паказаць фільтр загалоўка", "collectionActionShowTitleSearch": "Паказаць фільтр загалоўка",
"@collectionActionShowTitleSearch": {}, "@collectionActionShowTitleSearch": {},
"menuActionSelectAll": "Выбраць ўсё", "menuActionSelectAll": "Выбраць усе",
"@menuActionSelectAll": {}, "@menuActionSelectAll": {},
"settingsConfirmationTile": "Дыялогі пацверджання", "settingsConfirmationTile": "Дыялогі пацверджання",
"@settingsConfirmationTile": {}, "@settingsConfirmationTile": {},
@ -1059,7 +1059,7 @@
"@drawerCollectionAnimated": {}, "@drawerCollectionAnimated": {},
"durationDialogHours": "Гадзіны", "durationDialogHours": "Гадзіны",
"@durationDialogHours": {}, "@durationDialogHours": {},
"settingsKeepScreenOnDialogTitle": "Трымаць экран ўключаным", "settingsKeepScreenOnDialogTitle": "Трымаць экран уключаным",
"@settingsKeepScreenOnDialogTitle": {}, "@settingsKeepScreenOnDialogTitle": {},
"drawerPlacePage": "Месцы", "drawerPlacePage": "Месцы",
"@drawerPlacePage": {}, "@drawerPlacePage": {},
@ -1077,7 +1077,7 @@
"@appExportFavourites": {}, "@appExportFavourites": {},
"collectionEmptyImages": "Няма выяў", "collectionEmptyImages": "Няма выяў",
"@collectionEmptyImages": {}, "@collectionEmptyImages": {},
"albumPickPageTitleExport": "Экспартаваць ў альбом", "albumPickPageTitleExport": "Экспарт у альбом",
"@albumPickPageTitleExport": {}, "@albumPickPageTitleExport": {},
"settingsActionExportDialogTitle": "Экспарт", "settingsActionExportDialogTitle": "Экспарт",
"@settingsActionExportDialogTitle": {}, "@settingsActionExportDialogTitle": {},
@ -1127,7 +1127,7 @@
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
"searchStatesSectionTitle": "Штаты", "searchStatesSectionTitle": "Штаты",
"@searchStatesSectionTitle": {}, "@searchStatesSectionTitle": {},
"dateThisMonth": "Ў гэтым месяцы", "dateThisMonth": "У гэтым месяцы",
"@dateThisMonth": {}, "@dateThisMonth": {},
"aboutPageTitle": "Пра нас", "aboutPageTitle": "Пра нас",
"@aboutPageTitle": {}, "@aboutPageTitle": {},
@ -1141,7 +1141,7 @@
"@genericFailureFeedback": {}, "@genericFailureFeedback": {},
"aboutDataUsageData": "Дадзеныя", "aboutDataUsageData": "Дадзеныя",
"@aboutDataUsageData": {}, "@aboutDataUsageData": {},
"aboutDataUsageInternal": "Ўнутраны", "aboutDataUsageInternal": "Унутранае",
"@aboutDataUsageInternal": {}, "@aboutDataUsageInternal": {},
"albumDownload": "Загрузкі", "albumDownload": "Загрузкі",
"@albumDownload": {}, "@albumDownload": {},
@ -1149,7 +1149,7 @@
"@coverDialogTabColor": {}, "@coverDialogTabColor": {},
"genericSuccessFeedback": "Гатова!", "genericSuccessFeedback": "Гатова!",
"@genericSuccessFeedback": {}, "@genericSuccessFeedback": {},
"aboutLicensesShowAllButtonLabel": "Паказаць ўсе ліцэнзіі", "aboutLicensesShowAllButtonLabel": "Паказаць усе ліцэнзіі",
"@aboutLicensesShowAllButtonLabel": {}, "@aboutLicensesShowAllButtonLabel": {},
"sortOrderNewestFirst": "Спачатку самае новае", "sortOrderNewestFirst": "Спачатку самае новае",
"@sortOrderNewestFirst": {}, "@sortOrderNewestFirst": {},
@ -1175,7 +1175,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"appPickDialogTitle": "Выбраць праграму", "appPickDialogTitle": "Выбраць праграму",
"@appPickDialogTitle": {}, "@appPickDialogTitle": {},
"albumPickPageTitleMove": "Перамясціць ў альбом", "albumPickPageTitleMove": "Перамясціць у альбом",
"@albumPickPageTitleMove": {}, "@albumPickPageTitleMove": {},
"coverDialogTabCover": "Вокладка", "coverDialogTabCover": "Вокладка",
"@coverDialogTabCover": {}, "@coverDialogTabCover": {},
@ -1183,7 +1183,7 @@
"@settingsConfirmationBeforeDeleteItems": {}, "@settingsConfirmationBeforeDeleteItems": {},
"settingsConfirmationBeforeMoveUndatedItems": "Спытаць, перш чым перамяшчаць прадметы без даты", "settingsConfirmationBeforeMoveUndatedItems": "Спытаць, перш чым перамяшчаць прадметы без даты",
"@settingsConfirmationBeforeMoveUndatedItems": {}, "@settingsConfirmationBeforeMoveUndatedItems": {},
"settingsConfirmationAfterMoveToBinItems": "Паказваць паведамленне пасля перамяшчэння элементаў ў сметніцу", "settingsConfirmationAfterMoveToBinItems": "Паказваць паведамленне пасля перамяшчэння элементаў у сметніцу",
"@settingsConfirmationAfterMoveToBinItems": {}, "@settingsConfirmationAfterMoveToBinItems": {},
"settingsConfirmationBeforeMoveToBinItems": "Спытаць перад тым, як пераносіць элементы ў сметніцу", "settingsConfirmationBeforeMoveToBinItems": "Спытаць перад тым, як пераносіць элементы ў сметніцу",
"@settingsConfirmationBeforeMoveToBinItems": {}, "@settingsConfirmationBeforeMoveToBinItems": {},
@ -1387,7 +1387,7 @@
"@settingsNavigationDrawerTile": {}, "@settingsNavigationDrawerTile": {},
"settingsHiddenItemsPageTitle": "Схаваныя элементы", "settingsHiddenItemsPageTitle": "Схаваныя элементы",
"@settingsHiddenItemsPageTitle": {}, "@settingsHiddenItemsPageTitle": {},
"settingsHiddenPathsBanner": "Фатаграфіі і відэа ў гэтых папках або ў любой з іх укладзеных папак не будуць адлюстроўвацца ў вашай калекцыі.", "settingsHiddenPathsBanner": "Фатаграфіі і відэа ў гэтых тэчках або ў любой з іх укладзеных тэчках не будуць адлюстроўвацца ў вашай калекцыі.",
"@settingsHiddenPathsBanner": {}, "@settingsHiddenPathsBanner": {},
"settingsViewerShowOverlayOnOpening": "Паказаць на адкрыцці", "settingsViewerShowOverlayOnOpening": "Паказаць на адкрыцці",
"@settingsViewerShowOverlayOnOpening": {}, "@settingsViewerShowOverlayOnOpening": {},
@ -1405,7 +1405,7 @@
"@settingsStorageAccessEmpty": {}, "@settingsStorageAccessEmpty": {},
"settingsRemoveAnimationsTile": "Выдаліць анімацыі", "settingsRemoveAnimationsTile": "Выдаліць анімацыі",
"@settingsRemoveAnimationsTile": {}, "@settingsRemoveAnimationsTile": {},
"settingsStorageAccessBanner": "Некаторыя каталогі патрабуюць відавочнага дазволу на змяненне файлаў ў іх. Тут вы можаце прагледзець каталогі, да якіх вы раней далі доступ.", "settingsStorageAccessBanner": "Некаторыя каталогі патрабуюць відавочнага дазволу на змяненне файлаў у іх. Тут вы можаце прагледзець каталогі, да якіх вы раней далі доступ.",
"@settingsStorageAccessBanner": {}, "@settingsStorageAccessBanner": {},
"collectionCopySuccessFeedback": "{count, plural, =1{1 элемент скапіяваны} few{{count} элементы скапіявана} other{{count} элементаў скапіявана}}", "collectionCopySuccessFeedback": "{count, plural, =1{1 элемент скапіяваны} few{{count} элементы скапіявана} other{{count} элементаў скапіявана}}",
"@collectionCopySuccessFeedback": { "@collectionCopySuccessFeedback": {
@ -1467,7 +1467,7 @@
"@settingsSubtitleThemeTextPositionTile": {}, "@settingsSubtitleThemeTextPositionTile": {},
"settingsVideoBackgroundModeDialogTitle": "Фонавы рэжым", "settingsVideoBackgroundModeDialogTitle": "Фонавы рэжым",
"@settingsVideoBackgroundModeDialogTitle": {}, "@settingsVideoBackgroundModeDialogTitle": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэтыя альбомы і элемент ў іх?} few{Выдаліць гэтыя альбомы і {count} элементы ў іх?} other{Выдаліць гэтыя альбомы і {count} элементаў ў іх?}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэтыя альбомы і элемент у іх?} few{Выдаліць гэтыя альбомы і {count} элементы ў іх?} other{Выдаліць гэтыя альбомы і {count} элементаў у іх?}}",
"@deleteMultiAlbumConfirmationDialogMessage": { "@deleteMultiAlbumConfirmationDialogMessage": {
"placeholders": { "placeholders": {
"count": {} "count": {}
@ -1519,24 +1519,28 @@
"minutes": {} "minutes": {}
} }
}, },
"collectionActionSetHome": "Ўсталяваць як галоўную", "collectionActionSetHome": "Усталяваць як галоўную",
"@collectionActionSetHome": {}, "@collectionActionSetHome": {},
"setHomeCustomCollection": "Ўласная калекцыя", "setHomeCustomCollection": "Уласная калекцыя",
"@setHomeCustomCollection": {}, "@setHomeCustomCollection": {},
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR", "settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"videoRepeatActionSetEnd": "Ўсталяваць канец", "videoRepeatActionSetEnd": "Усталяваць канец",
"@videoRepeatActionSetEnd": {}, "@videoRepeatActionSetEnd": {},
"stopTooltip": "Спыніць", "stopTooltip": "Спыніць",
"@stopTooltip": {}, "@stopTooltip": {},
"videoActionABRepeat": "Паўтарыць ад А да Б", "videoActionABRepeat": "Паўтарыць ад А да Б",
"@videoActionABRepeat": {}, "@videoActionABRepeat": {},
"videoRepeatActionSetStart": "Ўсталяваць пачатак", "videoRepeatActionSetStart": "Усталяваць пачатак",
"@videoRepeatActionSetStart": {}, "@videoRepeatActionSetStart": {},
"renameProcessorHash": "Хэш", "renameProcessorHash": "Хэш",
"@renameProcessorHash": {}, "@renameProcessorHash": {},
"settingsForceWesternArabicNumeralsTile": "Прымусовыя арабскія лічбы", "settingsForceWesternArabicNumeralsTile": "Прымусовыя арабскія лічбы",
"@settingsForceWesternArabicNumeralsTile": {}, "@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "Паказаць ў Калекцыі", "chipActionShowCollection": "Паказаць у Калекцыі",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "Паказаць у Правадыру",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Правадыр",
"@explorerPageTitle": {}
} }

View file

@ -90,6 +90,7 @@
"chipActionGoToCountryPage": "Show in Countries", "chipActionGoToCountryPage": "Show in Countries",
"chipActionGoToPlacePage": "Show in Places", "chipActionGoToPlacePage": "Show in Places",
"chipActionGoToTagPage": "Show in Tags", "chipActionGoToTagPage": "Show in Tags",
"chipActionGoToExplorerPage": "Show in Explorer",
"chipActionFilterOut": "Filter out", "chipActionFilterOut": "Filter out",
"chipActionFilterIn": "Filter in", "chipActionFilterIn": "Filter in",
"chipActionHide": "Hide", "chipActionHide": "Hide",
@ -771,6 +772,8 @@
"binPageTitle": "Recycle Bin", "binPageTitle": "Recycle Bin",
"explorerPageTitle": "Explorer",
"searchCollectionFieldHint": "Search collection", "searchCollectionFieldHint": "Search collection",
"searchRecentSectionTitle": "Recent", "searchRecentSectionTitle": "Recent",
"searchDateSectionTitle": "Date", "searchDateSectionTitle": "Date",

View file

@ -1380,5 +1380,9 @@
"renameProcessorHash": "Hash", "renameProcessorHash": "Hash",
"@renameProcessorHash": {}, "@renameProcessorHash": {},
"chipActionShowCollection": "Mostrar en Colección", "chipActionShowCollection": "Mostrar en Colección",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"explorerPageTitle": "Explorar",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Mostrar en el explorador",
"@chipActionGoToExplorerPage": {}
} }

View file

@ -302,5 +302,137 @@
"filterNoDateLabel": "Päiväämätön", "filterNoDateLabel": "Päiväämätön",
"@filterNoDateLabel": {}, "@filterNoDateLabel": {},
"chipActionShowCollection": "Näytä kokoelmassa", "chipActionShowCollection": "Näytä kokoelmassa",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"widgetDisplayedItemMostRecent": "Viimeisin",
"@widgetDisplayedItemMostRecent": {},
"otherDirectoryDescription": "“{name}” kansio",
"@otherDirectoryDescription": {
"placeholders": {
"name": {
"type": "String",
"example": "Pictures",
"description": "the name of a specific directory"
}
}
},
"videoActionABRepeat": "A-B toisto",
"@videoActionABRepeat": {},
"videoRepeatActionSetStart": "Aseta alku",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "Aseta loppu",
"@videoRepeatActionSetEnd": {},
"filterTypeRawLabel": "Raw",
"@filterTypeRawLabel": {},
"filterTypeSphericalVideoLabel": "360° Video",
"@filterTypeSphericalVideoLabel": {},
"filterTypeGeotiffLabel": "GeoTIFF",
"@filterTypeGeotiffLabel": {},
"filterMimeVideoLabel": "Video",
"@filterMimeVideoLabel": {},
"coordinateFormatDms": "DMS",
"@coordinateFormatDms": {},
"coordinateDms": "{coordinate} {direction}",
"@coordinateDms": {
"placeholders": {
"coordinate": {
"type": "String",
"example": "38° 41 47.72″"
},
"direction": {
"type": "String",
"example": "S"
}
}
},
"coordinateDmsNorth": "P",
"@coordinateDmsNorth": {},
"lengthUnitPixel": "px",
"@lengthUnitPixel": {},
"lengthUnitPercent": "%",
"@lengthUnitPercent": {},
"mapStyleGoogleNormal": "Google Maps",
"@mapStyleGoogleNormal": {},
"mapStyleHuaweiNormal": "Petal Maps",
"@mapStyleHuaweiNormal": {},
"mapStyleHuaweiTerrain": "Petal Maps (Maasto)",
"@mapStyleHuaweiTerrain": {},
"overlayHistogramRGB": "RGB",
"@overlayHistogramRGB": {},
"subtitlePositionTop": "Ylhäällä",
"@subtitlePositionTop": {},
"subtitlePositionBottom": "Alhaalla",
"@subtitlePositionBottom": {},
"themeBrightnessLight": "Vaalea",
"@themeBrightnessLight": {},
"themeBrightnessDark": "Tumma",
"@themeBrightnessDark": {},
"themeBrightnessBlack": "Musta",
"@themeBrightnessBlack": {},
"unitSystemMetric": "Metrinen",
"@unitSystemMetric": {},
"unitSystemImperial": "Brittiläinen",
"@unitSystemImperial": {},
"vaultLockTypePattern": "Kuvio",
"@vaultLockTypePattern": {},
"vaultLockTypePin": "PIN",
"@vaultLockTypePin": {},
"vaultLockTypePassword": "Salasana",
"@vaultLockTypePassword": {},
"settingsVideoEnablePip": "Kuva kuvassa",
"@settingsVideoEnablePip": {},
"videoControlsPlay": "Toista",
"@videoControlsPlay": {},
"videoControlsPlayOutside": "Avaa toisella soittimella",
"@videoControlsPlayOutside": {},
"videoControlsPlaySeek": "Toista & selaa eteen/taakse",
"@videoControlsPlaySeek": {},
"videoControlsNone": "Ei mitään",
"@videoControlsNone": {},
"videoLoopModeNever": "Ei koskaan",
"@videoLoopModeNever": {},
"videoLoopModeShortOnly": "Vain lyhyissä videoissa",
"@videoLoopModeShortOnly": {},
"videoPlaybackSkip": "Ohita",
"@videoPlaybackSkip": {},
"videoPlaybackMuted": "Toista mykistettynä",
"@videoPlaybackMuted": {},
"videoPlaybackWithSound": "Toista äänillä",
"@videoPlaybackWithSound": {},
"videoResumptionModeAlways": "Aina",
"@videoResumptionModeAlways": {},
"wallpaperTargetLock": "Lukitusnäyttö",
"@wallpaperTargetLock": {},
"wallpaperTargetHomeLock": "Koti- ja lukitusnäyttö",
"@wallpaperTargetHomeLock": {},
"widgetDisplayedItemRandom": "Satunnainen",
"@widgetDisplayedItemRandom": {},
"focalLength": "{length} mm",
"@focalLength": {
"placeholders": {
"length": {
"type": "String",
"example": "5.4"
}
}
},
"videoActionUnmute": "Poista mykistys",
"@videoActionUnmute": {},
"coordinateDmsWest": "L",
"@coordinateDmsWest": {},
"coordinateDmsSouth": "E",
"@coordinateDmsSouth": {},
"coordinateDmsEast": "I",
"@coordinateDmsEast": {},
"videoLoopModeAlways": "Aina",
"@videoLoopModeAlways": {},
"videoResumptionModeNever": "Ei koskaan",
"@videoResumptionModeNever": {},
"viewerTransitionNone": "Ei mitään",
"@viewerTransitionNone": {},
"wallpaperTargetHome": "Kotinäyttö",
"@wallpaperTargetHome": {},
"storageVolumeDescriptionFallbackPrimary": "Sisäinen tallennustila",
"@storageVolumeDescriptionFallbackPrimary": {},
"storageVolumeDescriptionFallbackNonPrimary": "SD-kortti",
"@storageVolumeDescriptionFallbackNonPrimary": {}
} }

View file

@ -1380,5 +1380,9 @@
"settingsForceWesternArabicNumeralsTile": "Toujours utiliser les chiffres arabes", "settingsForceWesternArabicNumeralsTile": "Toujours utiliser les chiffres arabes",
"@settingsForceWesternArabicNumeralsTile": {}, "@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "Afficher dans Collection", "chipActionShowCollection": "Afficher dans Collection",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"explorerPageTitle": "Explorateur",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Afficher dans Explorateur",
"@chipActionGoToExplorerPage": {}
} }

View file

@ -1380,5 +1380,9 @@
"settingsForceWesternArabicNumeralsTile": "아라비아 숫자 항상 사용", "settingsForceWesternArabicNumeralsTile": "아라비아 숫자 항상 사용",
"@settingsForceWesternArabicNumeralsTile": {}, "@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "미디어 페이지에서 보기", "chipActionShowCollection": "미디어 페이지에서 보기",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"explorerPageTitle": "탐색기",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "탐색기 페이지에서 보기",
"@chipActionGoToExplorerPage": {}
} }

View file

@ -1538,5 +1538,9 @@
"renameProcessorHash": "Skrót", "renameProcessorHash": "Skrót",
"@renameProcessorHash": {}, "@renameProcessorHash": {},
"chipActionShowCollection": "Pokaż w Kolekcji", "chipActionShowCollection": "Pokaż w Kolekcji",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "Pokaż w przeglądarce",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Przeglądarka",
"@explorerPageTitle": {}
} }

View file

@ -593,7 +593,7 @@
"@collectionCopySuccessFeedback": {}, "@collectionCopySuccessFeedback": {},
"collectionMoveSuccessFeedback": "{count, plural, =1{Перемещён 1 объект} few{Перемещено {count} объекта} other{Перемещено {count} объектов}}", "collectionMoveSuccessFeedback": "{count, plural, =1{Перемещён 1 объект} few{Перемещено {count} объекта} other{Перемещено {count} объектов}}",
"@collectionMoveSuccessFeedback": {}, "@collectionMoveSuccessFeedback": {},
"collectionRenameSuccessFeedback": "{count, plural, =1{Переименован 1 объект} few{Переименовао {count} объекта} other{Переименовано {count} объектов}}", "collectionRenameSuccessFeedback": "{count, plural, =1{Переименован 1 объект} few{Переименовано {count} объекта} other{Переименовано {count} объектов}}",
"@collectionRenameSuccessFeedback": {}, "@collectionRenameSuccessFeedback": {},
"collectionEditSuccessFeedback": "{count, plural, =1{Изменён 1 объект} few{Изменено {count} объекта} other{Изменено {count} объектов}}", "collectionEditSuccessFeedback": "{count, plural, =1{Изменён 1 объект} few{Изменено {count} объекта} other{Изменено {count} объектов}}",
"@collectionEditSuccessFeedback": {}, "@collectionEditSuccessFeedback": {},
@ -1380,5 +1380,9 @@
"settingsForceWesternArabicNumeralsTile": "Принудительные арабские цифры", "settingsForceWesternArabicNumeralsTile": "Принудительные арабские цифры",
"@settingsForceWesternArabicNumeralsTile": {}, "@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "Показать в Коллекции", "chipActionShowCollection": "Показать в Коллекции",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "Показать в проводнике",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Проводник",
"@explorerPageTitle": {}
} }

View file

@ -1526,5 +1526,17 @@
"settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR", "settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR",
"@settingsThumbnailShowHdrIcon": {}, "@settingsThumbnailShowHdrIcon": {},
"chipActionShowCollection": "Zobraziť v kolekcií", "chipActionShowCollection": "Zobraziť v kolekcií",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"videoActionABRepeat": "Opakovanie A-B",
"@videoActionABRepeat": {},
"videoRepeatActionSetStart": "Nastaviť začiatok",
"@videoRepeatActionSetStart": {},
"videoRepeatActionSetEnd": "Nastaviť koniec",
"@videoRepeatActionSetEnd": {},
"settingsForceWesternArabicNumeralsTile": "Vynútiť arabské číslice",
"@settingsForceWesternArabicNumeralsTile": {},
"stopTooltip": "Zastaviť",
"@stopTooltip": {},
"renameProcessorHash": "Hash",
"@renameProcessorHash": {}
} }

View file

@ -1380,5 +1380,9 @@
"renameProcessorHash": "Sağlama", "renameProcessorHash": "Sağlama",
"@renameProcessorHash": {}, "@renameProcessorHash": {},
"chipActionShowCollection": "Koleksiyonda göster", "chipActionShowCollection": "Koleksiyonda göster",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "Gezginde göster",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Gezgin",
"@explorerPageTitle": {}
} }

View file

@ -1035,7 +1035,7 @@
"@settingsSubtitleThemeTextAlignmentCenter": {}, "@settingsSubtitleThemeTextAlignmentCenter": {},
"settingsSubtitleThemeTextAlignmentRight": "Праворуч", "settingsSubtitleThemeTextAlignmentRight": "Праворуч",
"@settingsSubtitleThemeTextAlignmentRight": {}, "@settingsSubtitleThemeTextAlignmentRight": {},
"settingsVideoControlsTile": "Управління", "settingsVideoControlsTile": "Елементи керування",
"@settingsVideoControlsTile": {}, "@settingsVideoControlsTile": {},
"settingsVideoButtonsTile": "Кнопки", "settingsVideoButtonsTile": "Кнопки",
"@settingsVideoButtonsTile": {}, "@settingsVideoButtonsTile": {},
@ -1049,7 +1049,7 @@
"@settingsSaveSearchHistory": {}, "@settingsSaveSearchHistory": {},
"settingsEnableBin": "Використовувати кошик", "settingsEnableBin": "Використовувати кошик",
"@settingsEnableBin": {}, "@settingsEnableBin": {},
"settingsAllowMediaManagement": "Дозволити управління медіа", "settingsAllowMediaManagement": "Дозволити керування мультимедіа",
"@settingsAllowMediaManagement": {}, "@settingsAllowMediaManagement": {},
"settingsHiddenItemsTile": "Приховані елементи", "settingsHiddenItemsTile": "Приховані елементи",
"@settingsHiddenItemsTile": {}, "@settingsHiddenItemsTile": {},
@ -1297,7 +1297,7 @@
"@settingsSlideshowAnimatedZoomEffect": {}, "@settingsSlideshowAnimatedZoomEffect": {},
"settingsSubtitleThemeSample": "Це зразок.", "settingsSubtitleThemeSample": "Це зразок.",
"@settingsSubtitleThemeSample": {}, "@settingsSubtitleThemeSample": {},
"settingsVideoControlsPageTitle": "Управління", "settingsVideoControlsPageTitle": "Елементи керування",
"@settingsVideoControlsPageTitle": {}, "@settingsVideoControlsPageTitle": {},
"settingsVideoSectionTitle": "Відео", "settingsVideoSectionTitle": "Відео",
"@settingsVideoSectionTitle": {}, "@settingsVideoSectionTitle": {},
@ -1538,5 +1538,9 @@
"settingsForceWesternArabicNumeralsTile": "Примусові арабські цифри", "settingsForceWesternArabicNumeralsTile": "Примусові арабські цифри",
"@settingsForceWesternArabicNumeralsTile": {}, "@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "Показати у Колекції", "chipActionShowCollection": "Показати у Колекції",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "Показати в провіднику",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Провідник",
"@explorerPageTitle": {}
} }

View file

@ -1380,5 +1380,9 @@
"settingsForceWesternArabicNumeralsTile": "强制使用阿拉伯数字", "settingsForceWesternArabicNumeralsTile": "强制使用阿拉伯数字",
"@settingsForceWesternArabicNumeralsTile": {}, "@settingsForceWesternArabicNumeralsTile": {},
"chipActionShowCollection": "在媒体集中显示", "chipActionShowCollection": "在媒体集中显示",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"explorerPageTitle": "资源管理器",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "在资源管理器中显示",
"@chipActionGoToExplorerPage": {}
} }

View file

@ -91,11 +91,14 @@ class Contributors {
Contributor('cheese', 'deanlemans5646@gmail.com'), Contributor('cheese', 'deanlemans5646@gmail.com'),
Contributor('Owen Elderbroek', 'o.elderbroek@gmail.com'), Contributor('Owen Elderbroek', 'o.elderbroek@gmail.com'),
Contributor('Maxi', 'maxitendo01@proton.me'), Contributor('Maxi', 'maxitendo01@proton.me'),
Contributor('Jerguš Fonfer', 'caro.jf@protonmail.com'),
Contributor('elfriob', 'elfriob@ya.ru'),
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali // Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese // Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese // Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
// Contributor('Grooty12', 'Rasmus@rosendahl-kaa.name'), // Danish // Contributor('Grooty12', 'Rasmus@rosendahl-kaa.name'), // Danish
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish // Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew // Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi // Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi // Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi

View file

@ -63,14 +63,12 @@ class Device {
final auth = LocalAuthentication(); final auth = LocalAuthentication();
_canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported(); _canAuthenticateUser = await auth.canCheckBiometrics || await auth.isDeviceSupported();
final floating = Floating();
try { try {
_supportPictureInPicture = await floating.isPipAvailable; _supportPictureInPicture = await Floating().isPipAvailable;
} on PlatformException catch (_) { } on PlatformException catch (_) {
// as of floating v2.0.0, plugin assumes activity and fails when bound via service // as of floating v2.0.0, plugin assumes activity and fails when bound via service
_supportPictureInPicture = false; _supportPictureInPicture = false;
} }
floating.dispose();
final capabilities = await deviceService.getCapabilities(); final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false; _canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;

View file

@ -44,7 +44,8 @@ class AvesEntry with AvesEntryBase {
AddressDetails? _addressDetails; AddressDetails? _addressDetails;
TrashDetails? trashDetails; TrashDetails? trashDetails;
List<AvesEntry>? burstEntries; // synthetic stack of related entries, e.g. burst shots or raw/developed pairs
List<AvesEntry>? stackedEntries;
@override @override
final AChangeNotifier visualChangeNotifier = AChangeNotifier(); final AChangeNotifier visualChangeNotifier = AChangeNotifier();
@ -69,7 +70,7 @@ class AvesEntry with AvesEntryBase {
required int? durationMillis, required int? durationMillis,
required this.trashed, required this.trashed,
required this.origin, required this.origin,
this.burstEntries, this.stackedEntries,
}) : id = id ?? 0 { }) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) { if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated( FlutterMemoryAllocations.instance.dispatchObjectCreated(
@ -93,7 +94,7 @@ class AvesEntry with AvesEntryBase {
int? dateAddedSecs, int? dateAddedSecs,
int? dateModifiedSecs, int? dateModifiedSecs,
int? origin, int? origin,
List<AvesEntry>? burstEntries, List<AvesEntry>? stackedEntries,
}) { }) {
final copyEntryId = id ?? this.id; final copyEntryId = id ?? this.id;
final copied = AvesEntry( final copied = AvesEntry(
@ -114,7 +115,7 @@ class AvesEntry with AvesEntryBase {
durationMillis: durationMillis, durationMillis: durationMillis,
trashed: trashed, trashed: trashed,
origin: origin ?? this.origin, origin: origin ?? this.origin,
burstEntries: burstEntries ?? this.burstEntries, stackedEntries: stackedEntries ?? this.stackedEntries,
) )
..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId) ..catalogMetadata = _catalogMetadata?.copyWith(id: copyEntryId)
..addressDetails = _addressDetails?.copyWith(id: copyEntryId) ..addressDetails = _addressDetails?.copyWith(id: copyEntryId)

View file

@ -7,9 +7,9 @@ import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
extension ExtraAvesEntryMultipage on AvesEntry { extension ExtraAvesEntryMultipage on AvesEntry {
bool get isMultiPage => isBurst || ((catalogMetadata?.isMultiPage ?? false) && (isMotionPhoto || !isHdr)); bool get isMultiPage => isStack || ((catalogMetadata?.isMultiPage ?? false) && (isMotionPhoto || !isHdr));
bool get isBurst => burstEntries?.isNotEmpty == true; bool get isStack => stackedEntries?.isNotEmpty == true;
bool get isMotionPhoto => catalogMetadata?.isMotionPhoto ?? false; bool get isMotionPhoto => catalogMetadata?.isMotionPhoto ?? false;
@ -19,10 +19,10 @@ extension ExtraAvesEntryMultipage on AvesEntry {
} }
Future<MultiPageInfo?> getMultiPageInfo() async { Future<MultiPageInfo?> getMultiPageInfo() async {
if (isBurst) { if (isStack) {
return MultiPageInfo( return MultiPageInfo(
mainEntry: this, mainEntry: this,
pages: burstEntries! pages: stackedEntries!
.mapIndexed((index, entry) => SinglePageInfo( .mapIndexed((index, entry) => SinglePageInfo(
index: index, index: index,
pageId: entry.id, pageId: entry.id,

View file

@ -52,13 +52,13 @@ class AlbumFilter extends CoveredCollectionFilter {
String getTooltip(BuildContext context) => album; String getTooltip(BuildContext context) => album;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
return IconUtils.getAlbumIcon( return IconUtils.getAlbumIcon(
context: context, context: context,
albumPath: album, albumPath: album,
size: size, size: size,
) ?? ) ??
(showGenericIcon ? Icon(AIcons.album, size: size) : null); (allowGenericIcon ? Icon(AIcons.album, size: size) : null);
} }
@override @override

View file

@ -68,7 +68,7 @@ class AspectRatioFilter extends CollectionFilter {
} }
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.aspectRatio, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.aspectRatio, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -69,7 +69,7 @@ class CoordinateFilter extends CollectionFilter {
} }
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.geoBounds, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.geoBounds, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -122,7 +122,7 @@ class DateFilter extends CollectionFilter {
} }
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.date, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.date, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -45,7 +45,7 @@ class FavouriteFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel; String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.favourite, size: size);
@override @override
Future<Color> color(BuildContext context) { Future<Color> color(BuildContext context) {

View file

@ -133,7 +133,7 @@ abstract class CollectionFilter extends Equatable implements Comparable<Collecti
String getTooltip(BuildContext context) => getLabel(context); String getTooltip(BuildContext context) => getLabel(context);
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => null; Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => null;
Future<Color> color(BuildContext context) { Future<Color> color(BuildContext context) {
final colors = context.read<AvesColorsData>(); final colors = context.read<AvesColorsData>();

View file

@ -89,7 +89,7 @@ class LocationFilter extends CoveredCollectionFilter {
String getLabel(BuildContext context) => _isUnlocated ? context.l10n.filterNoLocationLabel : _location; String getLabel(BuildContext context) => _isUnlocated ? context.l10n.filterNoLocationLabel : _location;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
if (_isUnlocated) { if (_isUnlocated) {
return Icon(AIcons.locationUnlocated, size: size); return Icon(AIcons.locationUnlocated, size: size);
} }

View file

@ -77,7 +77,7 @@ class MimeFilter extends CollectionFilter {
} }
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size);
@override @override
Future<Color> color(BuildContext context) { Future<Color> color(BuildContext context) {

View file

@ -70,7 +70,7 @@ class MissingFilter extends CollectionFilter {
} }
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -60,8 +60,8 @@ class OrFilter extends CollectionFilter {
String getLabel(BuildContext context) => _filters.map((v) => v.getLabel(context)).join(', '); String getLabel(BuildContext context) => _filters.map((v) => v.getLabel(context)).join(', ');
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, showGenericIcon: showGenericIcon); return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, allowGenericIcon: allowGenericIcon);
} }
@override @override

View file

@ -1,5 +1,9 @@
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/view/view.dart';
import 'package:flutter/widgets.dart';
class PathFilter extends CollectionFilter { class PathFilter extends CollectionFilter {
static const type = 'path'; static const type = 'path';
@ -47,6 +51,19 @@ class PathFilter extends CollectionFilter {
@override @override
String get universalLabel => path; String get universalLabel => path;
@override
String getLabel(BuildContext context) {
final _directory = androidFileUtils.relativeDirectoryFromPath(path);
if (_directory == null) return universalLabel;
if (_directory.relativeDir.isEmpty) {
return _directory.getVolumeDescription(context);
}
return pContext.split(_directory.relativeDir).last;
}
@override
Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.explorer, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -96,7 +96,7 @@ class PlaceholderFilter extends CollectionFilter {
} }
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -82,7 +82,7 @@ class QueryFilter extends CollectionFilter {
String get universalLabel => query; String get universalLabel => query;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.text, size: size);
@override @override
Future<Color> color(BuildContext context) { Future<Color> color(BuildContext context) {

View file

@ -64,7 +64,7 @@ class RatingFilter extends CollectionFilter {
}; };
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
return switch (rating) { return switch (rating) {
-1 => Icon(AIcons.ratingRejected, size: size), -1 => Icon(AIcons.ratingRejected, size: size),
0 => Icon(AIcons.ratingUnrated, size: size), 0 => Icon(AIcons.ratingUnrated, size: size),

View file

@ -51,7 +51,7 @@ class RecentlyAddedFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterRecentlyAddedLabel; String getLabel(BuildContext context) => context.l10n.filterRecentlyAddedLabel;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.dateRecent, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.dateRecent, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -47,8 +47,8 @@ class TagFilter extends CoveredCollectionFilter {
String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterNoTagLabel : tag; String getLabel(BuildContext context) => tag.isEmpty ? context.l10n.filterNoTagLabel : tag;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) { Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
return showGenericIcon ? Icon(tag.isEmpty ? AIcons.tagUntagged : AIcons.tag, size: size) : null; return allowGenericIcon ? Icon(tag.isEmpty ? AIcons.tagUntagged : AIcons.tag, size: size) : null;
} }
@override @override

View file

@ -41,7 +41,7 @@ class TrashFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterBinLabel; String getLabel(BuildContext context) => context.l10n.filterBinLabel;
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.bin, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(AIcons.bin, size: size);
@override @override
String get category => type; String get category => type;

View file

@ -99,7 +99,7 @@ class TypeFilter extends CollectionFilter {
} }
@override @override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) => Icon(_icon, size: size);
@override @override
Future<Color> color(BuildContext context) { Future<Color> color(BuildContext context) {

View file

@ -32,10 +32,10 @@ class MultiPageInfo {
_pages.insert(0, firstPage.copyWith(isDefault: true)); _pages.insert(0, firstPage.copyWith(isDefault: true));
} }
final burstEntries = mainEntry.burstEntries; final stackedEntries = mainEntry.stackedEntries;
if (burstEntries != null) { if (stackedEntries != null) {
_pageEntries.addEntries(pages.map((pageInfo) { _pageEntries.addEntries(pages.map((pageInfo) {
final pageEntry = burstEntries.firstWhere((entry) => entry.uri == pageInfo.uri); final pageEntry = stackedEntries.firstWhere((entry) => entry.uri == pageInfo.uri);
return MapEntry(pageInfo, pageEntry); return MapEntry(pageInfo, pageEntry);
})); }));
} }

View file

@ -1,6 +1,7 @@
import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/naming_pattern.dart'; import 'package:aves/model/naming_pattern.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/countries_page.dart'; import 'package:aves/widgets/filter_grids/countries_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart';
@ -39,6 +40,7 @@ class SettingsDefaults {
AlbumListPage.routeName, AlbumListPage.routeName,
CountryListPage.routeName, CountryListPage.routeName,
TagListPage.routeName, TagListPage.routeName,
ExplorerPage.routeName,
]; ];
// collection // collection

View file

@ -1,4 +1,5 @@
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
@ -12,6 +13,8 @@ extension ExtraHomePageSetting on HomePageSetting {
return AlbumListPage.routeName; return AlbumListPage.routeName;
case HomePageSetting.tags: case HomePageSetting.tags:
return TagListPage.routeName; return TagListPage.routeName;
case HomePageSetting.explorer:
return ExplorerPage.routeName;
} }
} }
} }

View file

@ -3,11 +3,13 @@ import 'package:aves_model/aves_model.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
extension ExtraWidgetShape on WidgetShape { extension ExtraWidgetShape on WidgetShape {
Path path(Size widgetSize, double devicePixelRatio) { static const double _defaultCornerRadius = 24;
Path path(Size widgetSize, double devicePixelRatio, {double? cornerRadiusPx}) {
final rect = Offset.zero & widgetSize; final rect = Offset.zero & widgetSize;
switch (this) { switch (this) {
case WidgetShape.rrect: case WidgetShape.rrect:
return Path()..addRRect(BorderRadius.circular(24 * devicePixelRatio).toRRect(rect)); return Path()..addRRect(BorderRadius.circular(cornerRadiusPx ?? (_defaultCornerRadius * devicePixelRatio)).toRRect(rect));
case WidgetShape.circle: case WidgetShape.circle:
return Path() return Path()
..addOval(Rect.fromCircle( ..addOval(Rect.fromCircle(

View file

@ -8,6 +8,7 @@ import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/modules/app.dart'; import 'package:aves/model/settings/modules/app.dart';
import 'package:aves/model/settings/modules/collection.dart'; import 'package:aves/model/settings/modules/collection.dart';
@ -206,6 +207,8 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
AccessibilityAnimations get accessibilityAnimations => getEnumOrDefault(SettingKeys.accessibilityAnimationsKey, SettingsDefaults.accessibilityAnimations, AccessibilityAnimations.values); AccessibilityAnimations get accessibilityAnimations => getEnumOrDefault(SettingKeys.accessibilityAnimationsKey, SettingsDefaults.accessibilityAnimations, AccessibilityAnimations.values);
bool get animate => accessibilityAnimations.animate;
set accessibilityAnimations(AccessibilityAnimations newValue) => set(SettingKeys.accessibilityAnimationsKey, newValue.toString()); set accessibilityAnimations(AccessibilityAnimations newValue) => set(SettingKeys.accessibilityAnimationsKey, newValue.toString());
AccessibilityTimeout get timeToTakeAction => getEnumOrDefault(SettingKeys.timeToTakeActionKey, SettingsDefaults.timeToTakeAction, AccessibilityTimeout.values); AccessibilityTimeout get timeToTakeAction => getEnumOrDefault(SettingKeys.timeToTakeActionKey, SettingsDefaults.timeToTakeAction, AccessibilityTimeout.values);

View file

@ -3,12 +3,14 @@ import 'dart:collection';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart'; import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/entry/sort.dart'; import 'package:aves/model/entry/sort.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/location.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';
@ -18,6 +20,7 @@ import 'package:aves/model/source/events.dart';
import 'package:aves/model/source/location/location.dart'; import 'package:aves/model/source/location/location.dart';
import 'package:aves/model/source/section_keys.dart'; import 'package:aves/model/source/section_keys.dart';
import 'package:aves/model/source/tag.dart'; import 'package:aves/model/source/tag.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart'; import 'package:aves_utils/aves_utils.dart';
@ -34,7 +37,7 @@ class CollectionLens with ChangeNotifier {
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
int? id; int? id;
bool listenToSource, groupBursts, fixedSort; bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
List<AvesEntry>? fixedSelection; List<AvesEntry>? fixedSelection;
final Set<AvesEntry> _syntheticEntries = {}; final Set<AvesEntry> _syntheticEntries = {};
@ -47,7 +50,8 @@ class CollectionLens with ChangeNotifier {
Set<CollectionFilter?>? filters, Set<CollectionFilter?>? filters,
this.id, this.id,
this.listenToSource = true, this.listenToSource = true,
this.groupBursts = true, this.stackBursts = true,
this.stackDevelopedRaws = true,
this.fixedSort = false, this.fixedSort = false,
this.fixedSelection, this.fixedSelection,
}) : filters = (filters ?? {}).whereNotNull().toSet(), }) : filters = (filters ?? {}).whereNotNull().toSet(),
@ -192,30 +196,59 @@ class CollectionLens with ChangeNotifier {
_disposeSyntheticEntries(); _disposeSyntheticEntries();
_filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry)))); _filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry))));
if (groupBursts) { if (stackBursts) {
_groupBursts(); _stackBursts();
}
if (stackDevelopedRaws) {
_stackDevelopedRaws();
} }
} }
void _groupBursts() { void _stackBursts() {
final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.getBurstKey(burstPatterns)).whereNotNullKey(); final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.getBurstKey(burstPatterns)).whereNotNullKey();
byBurstKey.forEach((burstKey, entries) { byBurstKey.forEach((burstKey, entries) {
if (entries.length > 1) { if (entries.length > 1) {
entries.sort(AvesEntrySort.compareByName); entries.sort(AvesEntrySort.compareByName);
final mainEntry = entries.first; final mainEntry = entries.first;
final burstEntry = mainEntry.copyWith(burstEntries: entries); final stackEntry = mainEntry.copyWith(stackedEntries: entries);
_syntheticEntries.add(burstEntry); _syntheticEntries.add(stackEntry);
entries.skip(1).toList().forEach((subEntry) { entries.skip(1).forEach((subEntry) {
_filteredSortedEntries.remove(subEntry); _filteredSortedEntries.remove(subEntry);
}); });
final index = _filteredSortedEntries.indexOf(mainEntry); final index = _filteredSortedEntries.indexOf(mainEntry);
_filteredSortedEntries.removeAt(index); _filteredSortedEntries.removeAt(index);
_filteredSortedEntries.insert(index, burstEntry); _filteredSortedEntries.insert(index, stackEntry);
} }
}); });
} }
void _stackDevelopedRaws() {
final allRawEntries = _filteredSortedEntries.where((entry) => entry.isRaw).toSet();
if (allRawEntries.isNotEmpty) {
final allDevelopedEntries = _filteredSortedEntries.where(MimeFilter(MimeTypes.jpeg).test).toSet();
final rawEntriesByDir = groupBy<AvesEntry, String?>(allRawEntries, (entry) => entry.directory);
rawEntriesByDir.forEach((dir, dirRawEntries) {
if (dir != null) {
final dirDevelopedEntries = allDevelopedEntries.where((entry) => entry.directory == dir).toSet();
for (final rawEntry in dirRawEntries) {
final rawFilename = rawEntry.filenameWithoutExtension;
final developedEntry = dirDevelopedEntries.firstWhereOrNull((entry) => entry.filenameWithoutExtension == rawFilename);
if (developedEntry != null) {
final stackEntry = rawEntry.copyWith(stackedEntries: [rawEntry, developedEntry]);
_syntheticEntries.add(stackEntry);
_filteredSortedEntries.remove(developedEntry);
final index = _filteredSortedEntries.indexOf(rawEntry);
_filteredSortedEntries.removeAt(index);
_filteredSortedEntries.insert(0, stackEntry);
}
}
}
});
}
}
void _applySort() { void _applySort() {
if (fixedSort) return; if (fixedSort) return;
@ -322,23 +355,52 @@ class CollectionLens with ChangeNotifier {
} }
void _onEntryRemoved(Set<AvesEntry> entries) { void _onEntryRemoved(Set<AvesEntry> entries) {
if (groupBursts) { if (_syntheticEntries.isNotEmpty) {
// find impacted burst groups // find impacted stacks
final obsoleteBurstEntries = <AvesEntry>{}; final obsoleteStacks = <AvesEntry>{};
final burstKeys = entries.map((entry) => entry.getBurstKey(burstPatterns)).whereNotNull().toSet();
if (burstKeys.isNotEmpty) { void _replaceStack(AvesEntry stackEntry, AvesEntry entry) {
_filteredSortedEntries.where((entry) => entry.isBurst && burstKeys.contains(entry.getBurstKey(burstPatterns))).forEach((mainEntry) { obsoleteStacks.add(stackEntry);
final subEntries = mainEntry.burstEntries!; fixedSelection?.replace(stackEntry, entry);
_filteredSortedEntries.replace(stackEntry, entry);
_sortedEntries?.replace(stackEntry, entry);
sections.forEach((key, sectionEntries) => sectionEntries.replace(stackEntry, entry));
}
final stacks = _filteredSortedEntries.where((entry) => entry.isStack).toSet();
stacks.forEach((stackEntry) {
final subEntries = stackEntry.stackedEntries!;
if (subEntries.any(entries.contains)) {
final mainEntry = subEntries.first;
// remove the deleted sub-entries // remove the deleted sub-entries
subEntries.removeWhere(entries.contains); subEntries.removeWhere(entries.contains);
if (subEntries.isEmpty) {
// remove the burst entry itself switch (subEntries.length) {
obsoleteBurstEntries.add(mainEntry); case 0:
// remove the stack itself
obsoleteStacks.add(stackEntry);
break;
case 1:
// replace the stack by the last remaining sub-entry
_replaceStack(stackEntry, subEntries.first);
break;
default:
// keep the stack with the remaining sub-entries
if (!subEntries.contains(mainEntry)) {
// recreate the stack with the correct main entry
_replaceStack(stackEntry, subEntries.first.copyWith(stackedEntries: subEntries));
}
break;
} }
// TODO TLAD [burst] recreate the burst main entry if the first sub-entry got deleted }
}); });
entries.addAll(obsoleteBurstEntries);
} obsoleteStacks.forEach((stackEntry) {
_syntheticEntries.remove(stackEntry);
stackEntry.dispose();
});
entries.addAll(obsoleteStacks);
} }
// we should remove obsolete entries and sections // we should remove obsolete entries and sections

View file

@ -134,4 +134,15 @@ class MimeTypes {
} }
return null; return null;
} }
static const Map<String, String> _defaultExtensions = {
bmp: '.bmp',
gif: '.gif',
jpeg: '.jpg',
png: '.png',
svg: '.svg',
webp: '.webp',
};
static String? extensionFor(String mimeType) => _defaultExtensions[mimeType];
} }

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/services/app_service.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -27,7 +28,11 @@ class IntentService {
'uris': uris, 'uris': uris,
}); });
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
await reportService.recordError(e, stack); if (e.code == 'submitPickedItems-large') {
throw TooManyItemsException();
} else {
await reportService.recordError(e, stack);
}
} }
} }

View file

@ -193,15 +193,17 @@ class PlatformMediaEditService implements MediaEditService {
@immutable @immutable
class EntryConvertOptions extends Equatable { class EntryConvertOptions extends Equatable {
final EntryConvertAction action;
final String mimeType; final String mimeType;
final bool writeMetadata; final bool writeMetadata;
final LengthUnit lengthUnit; final LengthUnit lengthUnit;
final int width, height, quality; final int width, height, quality;
@override @override
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height, quality]; List<Object?> get props => [action, mimeType, writeMetadata, lengthUnit, width, height, quality];
const EntryConvertOptions({ const EntryConvertOptions({
required this.action,
required this.mimeType, required this.mimeType,
required this.writeMetadata, required this.writeMetadata,
required this.lengthUnit, required this.lengthUnit,

View file

@ -33,6 +33,8 @@ abstract class StorageService {
Future<bool> deleteTempDirectory(); Future<bool> deleteTempDirectory();
Future<bool> deleteExternalCache();
// returns whether user granted access to a directory of his choosing // returns whether user granted access to a directory of his choosing
Future<bool> requestDirectoryAccess(String path); Future<bool> requestDirectoryAccess(String path);
@ -202,6 +204,17 @@ class PlatformStorageService implements StorageService {
return false; return false;
} }
@override
Future<bool> deleteExternalCache() async {
try {
final result = await _platform.invokeMethod('deleteExternalCache');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
@override @override
Future<bool> canRequestMediaFileBulkAccess() async { Future<bool> canRequestMediaFileBulkAccess() async {
try { try {

View file

@ -60,7 +60,7 @@ class ADurations {
static const highlightJumpDelay = Duration(milliseconds: 400); static const highlightJumpDelay = Duration(milliseconds: 400);
static const highlightScrollInitDelay = Duration(milliseconds: 800); static const highlightScrollInitDelay = Duration(milliseconds: 800);
static const motionPhotoAutoPlayDelay = Duration(milliseconds: 700); static const motionPhotoAutoPlayDelay = Duration(milliseconds: 700);
static const videoPauseAppInactiveDelay = Duration(milliseconds: 300); static const appInactiveReactionDelay = Duration(milliseconds: 300);
static const videoOverlayHideDelay = Duration(milliseconds: 500); static const videoOverlayHideDelay = Duration(milliseconds: 500);
static const videoProgressTimerInterval = Duration(milliseconds: 300); static const videoProgressTimerInterval = Duration(milliseconds: 300);
static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const doubleBackTimerDelay = Duration(milliseconds: 1000);

View file

@ -29,8 +29,10 @@ class AIcons {
static const disc = Icons.fiber_manual_record; static const disc = Icons.fiber_manual_record;
static const display = Icons.light_mode_outlined; static const display = Icons.light_mode_outlined;
static const error = Icons.error_outline; static const error = Icons.error_outline;
static const explorer = Icons.account_tree_outlined;
static const folder = Icons.folder_outlined; static const folder = Icons.folder_outlined;
static const grid = Icons.grid_on_outlined; static const geoBounds = Icons.public_outlined;
static final github = MdiIcons.github;
static const home = Icons.home_outlined; static const home = Icons.home_outlined;
// as of Flutter v3.16.3, // as of Flutter v3.16.3,
@ -39,13 +41,15 @@ class AIcons {
static const important = IconData(labelImportantOutlineCodePoint, fontFamily: materialIconsFontFamily, matchTextDirection: true); static const important = IconData(labelImportantOutlineCodePoint, fontFamily: materialIconsFontFamily, matchTextDirection: true);
static const language = Icons.translate_outlined; static const language = Icons.translate_outlined;
static final legal = MdiIcons.scaleBalance;
static const location = Icons.place_outlined; static const location = Icons.place_outlined;
static const locationUnlocated = Icons.location_off_outlined; static const locationUnlocated = Icons.location_off_outlined;
static const country = Icons.flag_outlined; static const country = Icons.flag_outlined;
static const state = Icons.flag_outlined; static const state = Icons.flag_outlined;
static const place = Icons.place_outlined; static const place = Icons.place_outlined;
static const mainStorage = Icons.smartphone_outlined;
static const mimeType = Icons.code_outlined; static const mimeType = Icons.code_outlined;
static const name = Icons.abc_outlined;
static const newTier = Icons.fiber_new_outlined;
static const opacity = Icons.opacity; static const opacity = Icons.opacity;
static const palette = Icons.palette_outlined; static const palette = Icons.palette_outlined;
static final privacy = MdiIcons.shieldAccountOutline; static final privacy = MdiIcons.shieldAccountOutline;
@ -54,15 +58,20 @@ class AIcons {
static final ratingRejected = MdiIcons.starMinusOutline; static final ratingRejected = MdiIcons.starMinusOutline;
static final ratingUnrated = MdiIcons.starOffOutline; static final ratingUnrated = MdiIcons.starOffOutline;
static const raw = Icons.raw_on_outlined; static const raw = Icons.raw_on_outlined;
static const shooting = Icons.camera_outlined;
static const removableStorage = Icons.sd_storage_outlined;
static const sensorControlEnabled = Icons.explore_outlined; static const sensorControlEnabled = Icons.explore_outlined;
static const sensorControlDisabled = Icons.explore_off_outlined; static const sensorControlDisabled = Icons.explore_off_outlined;
static const settings = Icons.settings_outlined; static const settings = Icons.settings_outlined;
static const shooting = Icons.camera_outlined;
static const size = Icons.data_usage_outlined; static const size = Icons.data_usage_outlined;
static const text = Icons.format_quote_outlined; static const storageCard = Icons.sd_storage_outlined;
static const storageMain = Icons.smartphone_outlined;
static const streamVideo = Icons.movie_outlined;
static const streamAudio = Icons.audiotrack_outlined;
static const streamText = Icons.closed_caption_outlined;
static const tag = Icons.local_offer_outlined; static const tag = Icons.local_offer_outlined;
static final tagUntagged = MdiIcons.tagOffOutline; static final tagUntagged = MdiIcons.tagOffOutline;
static const text = Icons.format_quote_outlined;
static const thumbnails = Icons.grid_on_outlined;
static const volumeMin = Icons.volume_mute_outlined; static const volumeMin = Icons.volume_mute_outlined;
static const volumeMax = Icons.volume_up_outlined; static const volumeMax = Icons.volume_up_outlined;
@ -100,7 +109,6 @@ class AIcons {
static const favouriteActive = Icons.favorite; static const favouriteActive = Icons.favorite;
static final filter = MdiIcons.filterOutline; static final filter = MdiIcons.filterOutline;
static final filterOff = MdiIcons.filterOffOutline; static final filterOff = MdiIcons.filterOffOutline;
static const geoBounds = Icons.public_outlined;
static const goUp = Icons.arrow_upward_outlined; static const goUp = Icons.arrow_upward_outlined;
static const hide = Icons.visibility_off_outlined; static const hide = Icons.visibility_off_outlined;
static const info = Icons.info_outlined; static const info = Icons.info_outlined;
@ -110,8 +118,7 @@ class AIcons {
static final move = MdiIcons.fileMoveOutline; static final move = MdiIcons.fileMoveOutline;
static const mute = Icons.volume_off_outlined; static const mute = Icons.volume_off_outlined;
static const unmute = Icons.volume_up_outlined; static const unmute = Icons.volume_up_outlined;
static const name = Icons.abc_outlined; static const rename = Icons.abc_outlined;
static const newTier = Icons.fiber_new_outlined;
static const openOutside = Icons.open_in_new_outlined; static const openOutside = Icons.open_in_new_outlined;
static final openVideo = MdiIcons.moviePlayOutline; static final openVideo = MdiIcons.moviePlayOutline;
static const pin = Icons.push_pin_outlined; static const pin = Icons.push_pin_outlined;
@ -133,20 +140,17 @@ class AIcons {
static const rotateScreen = Icons.screen_rotation_outlined; static const rotateScreen = Icons.screen_rotation_outlined;
static const search = Icons.search_outlined; static const search = Icons.search_outlined;
static const select = Icons.select_all_outlined; static const select = Icons.select_all_outlined;
static const selectStreams = Icons.translate_outlined;
static const setAs = Icons.wallpaper_outlined; static const setAs = Icons.wallpaper_outlined;
static final setBoundEnd = MdiIcons.rayEnd;
static final setBoundStart = MdiIcons.rayStart;
static final setCover = MdiIcons.imageEditOutline; static final setCover = MdiIcons.imageEditOutline;
static final setEnd = MdiIcons.rayEnd;
static final setStart = MdiIcons.rayStart;
static const share = Icons.share_outlined; static const share = Icons.share_outlined;
static const show = Icons.visibility_outlined; static const show = Icons.visibility_outlined;
static final showFullscreen = MdiIcons.arrowExpand; static final showFullscreen = MdiIcons.arrowExpand;
static const slideshow = Icons.slideshow_outlined; static const slideshow = Icons.slideshow_outlined;
static const speed = Icons.speed_outlined; static const speed = Icons.speed_outlined;
static const stats = Icons.donut_small_outlined; static const stats = Icons.donut_small_outlined;
static const streams = Icons.translate_outlined;
static const streamVideo = Icons.movie_outlined;
static const streamAudio = Icons.audiotrack_outlined;
static const streamText = Icons.closed_caption_outlined;
static const vaultLock = Icons.lock_outline; static const vaultLock = Icons.lock_outline;
static const vaultAdd = Icons.enhanced_encryption_outlined; static const vaultAdd = Icons.enhanced_encryption_outlined;
static final vaultConfigure = MdiIcons.shieldLockOutline; static final vaultConfigure = MdiIcons.shieldLockOutline;
@ -190,9 +194,6 @@ class AIcons {
static const selected = Icons.check_circle_outline; static const selected = Icons.check_circle_outline;
static const unselected = Icons.radio_button_unchecked; static const unselected = Icons.radio_button_unchecked;
static final github = MdiIcons.github;
static final legal = MdiIcons.scaleBalance;
// Material Icons references to make constant instances of `IconData` // Material Icons references to make constant instances of `IconData`
// as non-constant instances of `IconData` prevent icon font tree shaking // as non-constant instances of `IconData` prevent icon font tree shaking
static const labelImportantOutlineCodePoint = 0xe362; static const labelImportantOutlineCodePoint = 0xe362;

View file

@ -1,5 +1,15 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
extension ExtraList<E> on List<E> {
bool replace(E old, E newItem) {
final index = indexOf(old);
if (index == -1) return false;
this[index] = newItem;
return true;
}
}
extension ExtraMapNullableKey<K extends Object, V> on Map<K?, V> { extension ExtraMapNullableKey<K extends Object, V> on Map<K?, V> {
Map<K, V> whereNotNullKey() => <K, V>{for (var v in keys.whereNotNull()) v: this[v] as V}; Map<K, V> whereNotNullKey() => <K, V>{for (var v in keys.whereNotNull()) v: this[v] as V};
} }

View file

@ -11,6 +11,7 @@ extension ExtraChipActionView on ChipAction {
ChipAction.goToCountryPage => l10n.chipActionGoToCountryPage, ChipAction.goToCountryPage => l10n.chipActionGoToCountryPage,
ChipAction.goToPlacePage => l10n.chipActionGoToPlacePage, ChipAction.goToPlacePage => l10n.chipActionGoToPlacePage,
ChipAction.goToTagPage => l10n.chipActionGoToTagPage, ChipAction.goToTagPage => l10n.chipActionGoToTagPage,
ChipAction.goToExplorerPage => l10n.chipActionGoToExplorerPage,
ChipAction.ratingOrGreater || ChipAction.ratingOrGreater ||
ChipAction.ratingOrLower => ChipAction.ratingOrLower =>
// different data depending on state // different data depending on state
@ -30,6 +31,7 @@ extension ExtraChipActionView on ChipAction {
ChipAction.goToCountryPage => AIcons.country, ChipAction.goToCountryPage => AIcons.country,
ChipAction.goToPlacePage => AIcons.place, ChipAction.goToPlacePage => AIcons.place,
ChipAction.goToTagPage => AIcons.tag, ChipAction.goToTagPage => AIcons.tag,
ChipAction.goToExplorerPage => AIcons.explorer,
ChipAction.ratingOrGreater || ChipAction.ratingOrLower => AIcons.rating, ChipAction.ratingOrGreater || ChipAction.ratingOrLower => AIcons.rating,
ChipAction.reverse => AIcons.reverse, ChipAction.reverse => AIcons.reverse,
ChipAction.hide => AIcons.hide, ChipAction.hide => AIcons.hide,

View file

@ -67,7 +67,7 @@ extension ExtraChipSetActionView on ChipSetAction {
ChipSetAction.showCountryStates => AIcons.state, ChipSetAction.showCountryStates => AIcons.state,
ChipSetAction.showCollection => AIcons.allCollection, ChipSetAction.showCollection => AIcons.allCollection,
// selecting (single filter) // selecting (single filter)
ChipSetAction.rename => AIcons.name, ChipSetAction.rename => AIcons.rename,
ChipSetAction.setCover => AIcons.setCover, ChipSetAction.setCover => AIcons.setCover,
ChipSetAction.configureVault => AIcons.vaultConfigure, ChipSetAction.configureVault => AIcons.vaultConfigure,
}; };

View file

@ -90,7 +90,7 @@ extension ExtraEntryActionView on EntryAction {
EntryAction.restore => AIcons.restore, EntryAction.restore => AIcons.restore,
EntryAction.convert => AIcons.convert, EntryAction.convert => AIcons.convert,
EntryAction.print => AIcons.print, EntryAction.print => AIcons.print,
EntryAction.rename => AIcons.name, EntryAction.rename => AIcons.rename,
EntryAction.copy => AIcons.copy, EntryAction.copy => AIcons.copy,
EntryAction.move => AIcons.move, EntryAction.move => AIcons.move,
EntryAction.share => AIcons.share, EntryAction.share => AIcons.share,
@ -109,7 +109,7 @@ extension ExtraEntryActionView on EntryAction {
EntryAction.videoToggleMute => EntryAction.videoToggleMute =>
// different data depending on toggle state // different data depending on toggle state
AIcons.mute, AIcons.mute,
EntryAction.videoSelectStreams => AIcons.streams, EntryAction.videoSelectStreams => AIcons.selectStreams,
EntryAction.videoSetSpeed => AIcons.speed, EntryAction.videoSetSpeed => AIcons.speed,
EntryAction.videoABRepeat => AIcons.repeat, EntryAction.videoABRepeat => AIcons.repeat,
EntryAction.videoSettings => AIcons.videoSettings, EntryAction.videoSettings => AIcons.videoSettings,

View file

@ -5,45 +5,46 @@ import 'package:flutter/material.dart';
extension ExtraEntrySetActionView on EntrySetAction { extension ExtraEntrySetActionView on EntrySetAction {
String getText(BuildContext context) { String getText(BuildContext context) {
final l10n = context.l10n;
return switch (this) { return switch (this) {
// general // general
EntrySetAction.configureView => context.l10n.menuActionConfigureView, EntrySetAction.configureView => l10n.menuActionConfigureView,
EntrySetAction.select => context.l10n.menuActionSelect, EntrySetAction.select => l10n.menuActionSelect,
EntrySetAction.selectAll => context.l10n.menuActionSelectAll, EntrySetAction.selectAll => l10n.menuActionSelectAll,
EntrySetAction.selectNone => context.l10n.menuActionSelectNone, EntrySetAction.selectNone => l10n.menuActionSelectNone,
// browsing // browsing
EntrySetAction.searchCollection => MaterialLocalizations.of(context).searchFieldLabel, EntrySetAction.searchCollection => MaterialLocalizations.of(context).searchFieldLabel,
EntrySetAction.toggleTitleSearch => EntrySetAction.toggleTitleSearch =>
// different data depending on toggle state // different data depending on toggle state
context.l10n.collectionActionShowTitleSearch, l10n.collectionActionShowTitleSearch,
EntrySetAction.addShortcut => context.l10n.collectionActionAddShortcut, EntrySetAction.addShortcut => l10n.collectionActionAddShortcut,
EntrySetAction.setHome => context.l10n.collectionActionSetHome, EntrySetAction.setHome => l10n.collectionActionSetHome,
EntrySetAction.emptyBin => context.l10n.collectionActionEmptyBin, EntrySetAction.emptyBin => l10n.collectionActionEmptyBin,
// browsing or selecting // browsing or selecting
EntrySetAction.map => context.l10n.menuActionMap, EntrySetAction.map => l10n.menuActionMap,
EntrySetAction.slideshow => context.l10n.menuActionSlideshow, EntrySetAction.slideshow => l10n.menuActionSlideshow,
EntrySetAction.stats => context.l10n.menuActionStats, EntrySetAction.stats => l10n.menuActionStats,
EntrySetAction.rescan => context.l10n.collectionActionRescan, EntrySetAction.rescan => l10n.collectionActionRescan,
// selecting // selecting
EntrySetAction.share => context.l10n.entryActionShare, EntrySetAction.share => l10n.entryActionShare,
EntrySetAction.delete => context.l10n.entryActionDelete, EntrySetAction.delete => l10n.entryActionDelete,
EntrySetAction.restore => context.l10n.entryActionRestore, EntrySetAction.restore => l10n.entryActionRestore,
EntrySetAction.copy => context.l10n.collectionActionCopy, EntrySetAction.copy => l10n.collectionActionCopy,
EntrySetAction.move => context.l10n.collectionActionMove, EntrySetAction.move => l10n.collectionActionMove,
EntrySetAction.rename => context.l10n.entryActionRename, EntrySetAction.rename => l10n.entryActionRename,
EntrySetAction.convert => context.l10n.entryActionConvert, EntrySetAction.convert => l10n.entryActionConvert,
EntrySetAction.toggleFavourite => EntrySetAction.toggleFavourite =>
// different data depending on toggle state // different data depending on toggle state
context.l10n.entryActionAddFavourite, l10n.entryActionAddFavourite,
EntrySetAction.rotateCCW => context.l10n.entryActionRotateCCW, EntrySetAction.rotateCCW => l10n.entryActionRotateCCW,
EntrySetAction.rotateCW => context.l10n.entryActionRotateCW, EntrySetAction.rotateCW => l10n.entryActionRotateCW,
EntrySetAction.flip => context.l10n.entryActionFlip, EntrySetAction.flip => l10n.entryActionFlip,
EntrySetAction.editDate => context.l10n.entryInfoActionEditDate, EntrySetAction.editDate => l10n.entryInfoActionEditDate,
EntrySetAction.editLocation => context.l10n.entryInfoActionEditLocation, EntrySetAction.editLocation => l10n.entryInfoActionEditLocation,
EntrySetAction.editTitleDescription => context.l10n.entryInfoActionEditTitleDescription, EntrySetAction.editTitleDescription => l10n.entryInfoActionEditTitleDescription,
EntrySetAction.editRating => context.l10n.entryInfoActionEditRating, EntrySetAction.editRating => l10n.entryInfoActionEditRating,
EntrySetAction.editTags => context.l10n.entryInfoActionEditTags, EntrySetAction.editTags => l10n.entryInfoActionEditTags,
EntrySetAction.removeMetadata => context.l10n.entryInfoActionRemoveMetadata, EntrySetAction.removeMetadata => l10n.entryInfoActionRemoveMetadata,
}; };
} }
@ -75,7 +76,7 @@ extension ExtraEntrySetActionView on EntrySetAction {
EntrySetAction.restore => AIcons.restore, EntrySetAction.restore => AIcons.restore,
EntrySetAction.copy => AIcons.copy, EntrySetAction.copy => AIcons.copy,
EntrySetAction.move => AIcons.move, EntrySetAction.move => AIcons.move,
EntrySetAction.rename => AIcons.name, EntrySetAction.rename => AIcons.rename,
EntrySetAction.convert => AIcons.convert, EntrySetAction.convert => AIcons.convert,
EntrySetAction.toggleFavourite => EntrySetAction.toggleFavourite =>
// different data depending on toggle state // different data depending on toggle state

View file

@ -0,0 +1,21 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/widgets.dart';
extension ExtraEntryConvertActionView on EntryConvertAction {
String getText(BuildContext context) {
final l10n = context.l10n;
return switch (this) {
EntryConvertAction.convert => l10n.entryActionConvert,
EntryConvertAction.convertMotionPhotoToStillImage => l10n.entryActionConvertMotionPhotoToStillImage,
};
}
IconData getIconData() {
return switch (this) {
EntryConvertAction.convert => AIcons.convert,
EntryConvertAction.convertMotionPhotoToStillImage => AIcons.convertToStillImage,
};
}
}

View file

@ -83,6 +83,7 @@ extension ExtraHomePageSettingView on HomePageSetting {
HomePageSetting.collection => l10n.drawerCollectionAll, HomePageSetting.collection => l10n.drawerCollectionAll,
HomePageSetting.albums => l10n.drawerAlbumPage, HomePageSetting.albums => l10n.drawerAlbumPage,
HomePageSetting.tags => l10n.drawerTagPage, HomePageSetting.tags => l10n.drawerTagPage,
HomePageSetting.explorer => l10n.explorerPageTitle,
}; };
} }
} }

View file

@ -39,7 +39,7 @@ extension ExtraAlbumChipGroupFactorView on AlbumChipGroupFactor {
return switch (this) { return switch (this) {
AlbumChipGroupFactor.importance => AIcons.important, AlbumChipGroupFactor.importance => AIcons.important,
AlbumChipGroupFactor.mimeType => AIcons.mimeType, AlbumChipGroupFactor.mimeType => AIcons.mimeType,
AlbumChipGroupFactor.volume => AIcons.removableStorage, AlbumChipGroupFactor.volume => AIcons.storageCard,
AlbumChipGroupFactor.none => AIcons.clear, AlbumChipGroupFactor.none => AIcons.clear,
}; };
} }

View file

@ -7,6 +7,7 @@ export 'src/actions/map_cluster.dart';
export 'src/actions/share.dart'; export 'src/actions/share.dart';
export 'src/actions/slideshow.dart'; export 'src/actions/slideshow.dart';
export 'src/editor/enums.dart'; export 'src/editor/enums.dart';
export 'src/metadata/convert_action.dart';
export 'src/metadata/date_edit_action.dart'; export 'src/metadata/date_edit_action.dart';
export 'src/metadata/date_field_source.dart'; export 'src/metadata/date_field_source.dart';
export 'src/metadata/fields.dart'; export 'src/metadata/fields.dart';

View file

@ -38,8 +38,10 @@ void widgetMainCommon(AppFlavor flavor) async {
Future<Map<String, dynamic>> _drawWidget(dynamic args) async { Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
final widgetId = args['widgetId'] as int; final widgetId = args['widgetId'] as int;
final widthPx = args['widthPx'] as int; final sizesDip = (args['sizesDip'] as List).cast<Map>().map((kv) {
final heightPx = args['heightPx'] as int; return Size(kv['widthDip'] as double, kv['heightDip'] as double);
}).toList();
final cornerRadiusPx = args['cornerRadiusPx'] as double?;
final devicePixelRatio = args['devicePixelRatio'] as double; final devicePixelRatio = args['devicePixelRatio'] as double;
final drawEntryImage = args['drawEntryImage'] as bool; final drawEntryImage = args['drawEntryImage'] as bool;
final reuseEntry = args['reuseEntry'] as bool; final reuseEntry = args['reuseEntry'] as bool;
@ -53,14 +55,22 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
entry: entry, entry: entry,
devicePixelRatio: devicePixelRatio, devicePixelRatio: devicePixelRatio,
); );
final bytes = await painter.drawWidget( final bytesBySizeDip = <Map<String, dynamic>>[];
widthPx: widthPx, await Future.forEach(sizesDip, (sizeDip) async {
heightPx: heightPx, final bytes = await painter.drawWidget(
outline: outline, sizeDip: sizeDip,
shape: settings.getWidgetShape(widgetId), cornerRadiusPx: cornerRadiusPx,
); outline: outline,
shape: settings.getWidgetShape(widgetId),
);
bytesBySizeDip.add({
'widthDip': sizeDip.width,
'heightDip': sizeDip.height,
'bytes': bytes,
});
});
return { return {
'bytes': bytes, 'bytesBySizeDip': bytesBySizeDip,
'updateOnTap': settings.getWidgetOpenPage(widgetId) == WidgetOpenPage.updateWidget, 'updateOnTap': settings.getWidgetOpenPage(widgetId) == WidgetOpenPage.updateWidget,
}; };
} }

View file

@ -87,6 +87,7 @@ class _AboutDataUsageState extends State<AboutDataUsage> with FeedbackMixin {
label: context.l10n.aboutDataUsageClearCache, label: context.l10n.aboutDataUsageClearCache,
onPressed: () async { onPressed: () async {
await storageService.deleteTempDirectory(); await storageService.deleteTempDirectory();
await storageService.deleteExternalCache();
await mediaFetchService.clearSizedThumbnailDiskCache(); await mediaFetchService.clearSizedThumbnailDiskCache();
imageCache.clear(); imageCache.clear();
_reload(); _reload();

View file

@ -233,13 +233,13 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_licenses.add(const Padding( _licenses.add(const Padding(
padding: EdgeInsets.all(18.0), padding: EdgeInsets.all(18),
child: Divider(), child: Divider(),
)); ));
for (final LicenseParagraph paragraph in paragraphs) { for (final LicenseParagraph paragraph in paragraphs) {
if (paragraph.indent == LicenseParagraph.centeredIndent) { if (paragraph.indent == LicenseParagraph.centeredIndent) {
_licenses.add(Padding( _licenses.add(Padding(
padding: const EdgeInsets.only(top: 16.0), padding: const EdgeInsets.only(top: 16),
child: Text( child: Text(
paragraph.text, paragraph.text,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
@ -249,7 +249,7 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
} else { } else {
assert(paragraph.indent >= 0); assert(paragraph.indent >= 0);
_licenses.add(Padding( _licenses.add(Padding(
padding: EdgeInsetsDirectional.only(top: 8.0, start: 16.0 * paragraph.indent), padding: EdgeInsetsDirectional.only(top: 8, start: 16.0 * paragraph.indent),
child: Text(paragraph.text), child: Text(paragraph.text),
)); ));
} }
@ -278,7 +278,7 @@ class _PackageLicensePageState extends State<_PackageLicensePage> {
..._licenses, ..._licenses,
if (!_loaded) if (!_loaded)
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 24.0), padding: EdgeInsets.symmetric(vertical: 24),
child: Center( child: Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),

View file

@ -8,7 +8,6 @@ import 'package:aves/model/apps.dart';
import 'package:aves/model/device.dart'; import 'package:aves/model/device.dart';
import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/settings/defaults.dart'; import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart'; import 'package:aves/model/settings/enums/display_refresh_rate_mode.dart';
import 'package:aves/model/settings/enums/screen_on.dart'; import 'package:aves/model/settings/enums/screen_on.dart';
import 'package:aves/model/settings/enums/theme_brightness.dart'; import 'package:aves/model/settings/enums/theme_brightness.dart';
@ -325,7 +324,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(Theme.of(context))); WidgetsBinding.instance.addPostFrameCallback((_) => AvesApp.setSystemUIStyle(Theme.of(context)));
} }
return Selector<Settings, bool>( return Selector<Settings, bool>(
selector: (context, s) => s.initialized ? s.accessibilityAnimations.animate : true, selector: (context, s) => s.initialized ? s.animate : true,
builder: (context, areAnimationsEnabled, child) { builder: (context, areAnimationsEnabled, child) {
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: _shouldUseBoldFontLoader, future: _shouldUseBoldFontLoader,
@ -668,7 +667,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
class AvesScrollBehavior extends MaterialScrollBehavior { class AvesScrollBehavior extends MaterialScrollBehavior {
@override @override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) { Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate); final animate = context.select<Settings, bool>((v) => v.animate);
return animate return animate
? StretchingOverscrollIndicator( ? StretchingOverscrollIndicator(
axisDirection: details.direction, axisDirection: details.direction,

View file

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
@ -171,7 +172,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
selector: (context, s) => s.collectionBrowsingQuickActions, selector: (context, s) => s.collectionBrowsingQuickActions,
builder: (context, _, child) { builder: (context, _, child) {
final useTvLayout = settings.useTvLayout; final useTvLayout = settings.useTvLayout;
final actions = _buildActions(context, selection);
final onFilterTap = canRemoveFilters ? collection.removeFilter : null; final onFilterTap = canRemoveFilters ? collection.removeFilter : null;
return AvesAppBar( return AvesAppBar(
contentHeight: appBarContentHeight, contentHeight: appBarContentHeight,
@ -181,7 +181,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
isSelecting: isSelecting, isSelecting: isSelecting,
), ),
title: _buildAppBarTitle(isSelecting), title: _buildAppBarTitle(isSelecting),
actions: useTvLayout ? [] : actions, actions: (context, maxWidth) => useTvLayout ? [] : _buildActions(context, selection, maxWidth),
bottom: Column( bottom: Column(
children: [ children: [
if (useTvLayout) if (useTvLayout)
@ -190,7 +190,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
children: actions, children: _buildActions(context, selection, double.infinity),
), ),
), ),
if (showFilterBar) if (showFilterBar)
@ -301,7 +301,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
} }
List<Widget> _buildActions(BuildContext context, Selection<AvesEntry> selection) { List<Widget> _buildActions(BuildContext context, Selection<AvesEntry> selection, double maxWidth) {
final appMode = context.watch<ValueNotifier<AppMode>>().value; final appMode = context.watch<ValueNotifier<AppMode>>().value;
final isSelecting = selection.isSelecting; final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length; final selectedItemCount = selection.selectedItems.length;
@ -333,6 +333,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
context: context, context: context,
appMode: appMode, appMode: appMode,
selection: selection, selection: selection,
maxWidth: maxWidth,
isVisible: isVisible, isVisible: isVisible,
canApply: canApply, canApply: canApply,
); );
@ -366,20 +367,29 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}).toList(); }).toList();
} }
static double _iconButtonWidth(BuildContext context) {
const defaultPadding = EdgeInsets.all(8);
const defaultIconSize = 24.0;
return defaultPadding.horizontal + MediaQuery.textScalerOf(context).scale(defaultIconSize);
}
List<Widget> _buildMobileActions({ List<Widget> _buildMobileActions({
required BuildContext context, required BuildContext context,
required AppMode appMode, required AppMode appMode,
required Selection<AvesEntry> selection, required Selection<AvesEntry> selection,
required double maxWidth,
required bool Function(EntrySetAction action) isVisible, required bool Function(EntrySetAction action) isVisible,
required bool Function(EntrySetAction action) canApply, required bool Function(EntrySetAction action) canApply,
}) { }) {
final availableCount = (maxWidth / _iconButtonWidth(context)).floor();
final isSelecting = selection.isSelecting; final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length; final selectedItemCount = selection.selectedItems.length;
final hasSelection = selectedItemCount > 0; final hasSelection = selectedItemCount > 0;
final browsingQuickActions = settings.collectionBrowsingQuickActions; final browsingQuickActions = settings.collectionBrowsingQuickActions;
final selectionQuickActions = isTrash ? [EntrySetAction.delete, EntrySetAction.restore] : settings.collectionSelectionQuickActions; final selectionQuickActions = isTrash ? [EntrySetAction.delete, EntrySetAction.restore] : settings.collectionSelectionQuickActions;
final quickActions = isSelecting ? selectionQuickActions : browsingQuickActions; final quickActions = (isSelecting ? selectionQuickActions : browsingQuickActions).take(max(0, availableCount - 1)).toList();
final quickActionButtons = quickActions.where(isVisible).map( final quickActionButtons = quickActions.where(isVisible).map(
(action) => _buildButtonIcon(context, action, enabled: canApply(action), selection: selection), (action) => _buildButtonIcon(context, action, enabled: canApply(action), selection: selection),
); );
@ -396,7 +406,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection), (action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
); );
final allContextualActions = isSelecting ? EntrySetActions.pageSelection: EntrySetActions.pageBrowsing; final allContextualActions = isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing;
final contextualMenuActions = allContextualActions.where(_isValidForMenu).fold(<EntrySetAction?>[], (prev, v) { final contextualMenuActions = allContextualActions.where(_isValidForMenu).fold(<EntrySetAction?>[], (prev, v) {
if (v == null && (prev.isEmpty || prev.last == null)) return prev; if (v == null && (prev.isEmpty || prev.last == null)) return prev;
return [...prev, v]; return [...prev, v];
@ -444,7 +454,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
} }
Set<AvesEntry> _getExpandedSelectedItems(Selection<AvesEntry> selection) { Set<AvesEntry> _getExpandedSelectedItems(Selection<AvesEntry> selection) {
return selection.selectedItems.expand((entry) => entry.burstEntries ?? {entry}).toSet(); return selection.selectedItems.expand((entry) => entry.stackedEntries ?? {entry}).toSet();
} }
// key is expected by test driver (e.g. 'menu-configureView', 'menu-map') // key is expected by test driver (e.g. 'menu-configureView', 'menu-map')

View file

@ -7,10 +7,10 @@ import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/app_service.dart';
import 'package:aves/services/intent_service.dart'; import 'package:aves/services/intent_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/collection/collection_grid.dart';
@ -24,6 +24,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_fab.dart'; import 'package:aves/widgets/common/identity/aves_fab.dart';
import 'package:aves/widgets/common/providers/query_provider.dart'; import 'package:aves/widgets/common/providers/query_provider.dart';
import 'package:aves/widgets/common/providers/selection_provider.dart'; import 'package:aves/widgets/common/providers/selection_provider.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/navigation/drawer/app_drawer.dart'; import 'package:aves/widgets/navigation/drawer/app_drawer.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart'; import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:aves/widgets/navigation/tv_rail.dart'; import 'package:aves/widgets/navigation/tv_rail.dart';
@ -186,10 +187,21 @@ class _CollectionPageState extends State<CollectionPage> {
return hasSelection return hasSelection
? AvesFab( ? AvesFab(
tooltip: context.l10n.pickTooltip, tooltip: context.l10n.pickTooltip,
onPressed: () { onPressed: () async {
final items = context.read<Selection<AvesEntry>>().selectedItems; final items = context.read<Selection<AvesEntry>>().selectedItems;
final uris = items.map((entry) => entry.uri).toList(); final uris = items.map((entry) => entry.uri).toList();
IntentService.submitPickedItems(uris); try {
await IntentService.submitPickedItems(uris);
} on TooManyItemsException catch (_) {
await showDialog(
context: context,
builder: (context) => AvesDialog(
content: Text(context.l10n.tooManyItemsErrorDialogMessage),
actions: const [OkButton()],
),
routeSettings: const RouteSettings(name: AvesDialog.warningRouteName),
);
}
}, },
) )
: null; : null;
@ -217,7 +229,7 @@ class _CollectionPageState extends State<CollectionPage> {
await Future.delayed(delayDuration + ADurations.highlightScrollInitDelay); await Future.delayed(delayDuration + ADurations.highlightScrollInitDelay);
if (!mounted) return; if (!mounted) return;
final animate = context.read<Settings>().accessibilityAnimations.animate; final animate = context.read<Settings>().animate;
context.read<HighlightInfo>().trackItem(item, animate: animate, highlightItem: item); context.read<HighlightInfo>().trackItem(item, animate: animate, highlightItem: item);
} }
} }

View file

@ -5,6 +5,7 @@ import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.dart'; import 'package:aves/model/entry/extensions/favourites.dart';
import 'package:aves/model/entry/extensions/metadata_edition.dart'; import 'package:aves/model/entry/extensions/metadata_edition.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
@ -20,6 +21,7 @@ import 'package:aves/model/vaults/vaults.dart';
import 'package:aves/services/app_service.dart'; import 'package:aves/services/app_service.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/media_edit_service.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
@ -34,6 +36,7 @@ import 'package:aves/widgets/common/search/route.dart';
import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart';
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart';
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_page.dart'; import 'package:aves/widgets/dialogs/entry_editors/rename_entry_set_page.dart';
import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/location_pick_page.dart';
import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/map/map_page.dart';
@ -237,7 +240,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
Set<AvesEntry> _getTargetItems(BuildContext context) { Set<AvesEntry> _getTargetItems(BuildContext context) {
final selection = context.read<Selection<AvesEntry>>(); final selection = context.read<Selection<AvesEntry>>();
final groupedEntries = (selection.isSelecting ? selection.selectedItems : context.read<CollectionLens>().sortedEntries); final groupedEntries = (selection.isSelecting ? selection.selectedItems : context.read<CollectionLens>().sortedEntries);
return groupedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet(); return groupedEntries.expand((entry) => entry.stackedEntries ?? {entry}).toSet();
} }
Future<void> _share(BuildContext context) async { Future<void> _share(BuildContext context) async {
@ -366,9 +369,23 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
_browse(context); _browse(context);
} }
void _convert(BuildContext context) { Future<void> _convert(BuildContext context) async {
final entries = _getTargetItems(context); final entries = _getTargetItems(context);
convert(context, entries);
final options = await showDialog<EntryConvertOptions>(
context: context,
builder: (context) => ConvertEntryDialog(entries: entries),
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
);
if (options == null) return;
switch (options.action) {
case EntryConvertAction.convert:
await doExport(context, entries, options);
case EntryConvertAction.convertMotionPhotoToStillImage:
final todoItems = entries.where((entry) => entry.isMotionPhoto).toSet();
await _edit(context, todoItems, (entry) => entry.removeTrailerVideo());
}
_browse(context); _browse(context);
} }

View file

@ -39,7 +39,7 @@ class AlbumSectionHeader extends StatelessWidget {
title: albumName ?? context.l10n.sectionUnknown, title: albumName ?? context.l10n.sectionUnknown,
trailing: _directory != null && androidFileUtils.isOnRemovableStorage(_directory) trailing: _directory != null && androidFileUtils.isOnRemovableStorage(_directory)
? const Icon( ? const Icon(
AIcons.removableStorage, AIcons.storageCard,
size: 16, size: 16,
color: Color(0xFF757575), color: Color(0xFF757575),
) )

View file

@ -80,7 +80,7 @@ class EntryListDetails extends StatelessWidget {
final date = entry.bestDate; final date = entry.bestDate;
final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable; final dateText = date != null ? formatDateTime(date, locale, use24hour) : AText.valueNotAvailable;
final size = entry.burstEntries?.map((v) => v.sizeBytes).sum ?? entry.sizeBytes; final size = entry.stackedEntries?.map((v) => v.sizeBytes).sum ?? entry.sizeBytes;
final sizeText = size != null ? formatFileSize(locale, size) : AText.valueNotAvailable; final sizeText = size != null ? formatFileSize(locale, size) : AText.valueNotAvailable;
return Wrap( return Wrap(

View file

@ -35,7 +35,7 @@ class AlbumQuickChooser extends StatelessWidget {
pointerGlobalPosition: pointerGlobalPosition, pointerGlobalPosition: pointerGlobalPosition,
itemBuilder: (context, album) => AvesFilterChip( itemBuilder: (context, album) => AvesFilterChip(
filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)), filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)),
showGenericIcon: false, allowGenericIcon: false,
), ),
); );
} }

View file

@ -32,7 +32,7 @@ class TagQuickChooser extends StatelessWidget {
pointerGlobalPosition: pointerGlobalPosition, pointerGlobalPosition: pointerGlobalPosition,
itemBuilder: (context, filter) => AvesFilterChip( itemBuilder: (context, filter) => AvesFilterChip(
filter: filter, filter: filter,
showGenericIcon: false, allowGenericIcon: false,
), ),
); );
} }

View file

@ -13,6 +13,7 @@ import 'package:aves/model/multipage.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/image_op_events.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/enums.dart';
@ -27,7 +28,6 @@ import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart'; import 'package:aves/widgets/dialogs/aves_confirmation_dialog.dart';
import 'package:aves/widgets/dialogs/convert_entry_dialog.dart';
import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart'; import 'package:aves/widgets/dialogs/pick_dialogs/album_pick_page.dart';
import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart'; import 'package:aves/widgets/dialogs/selection_dialogs/single_selection.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart';
@ -37,14 +37,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
Future<void> convert(BuildContext context, Set<AvesEntry> targetEntries) async { Future<void> doExport(BuildContext context, Set<AvesEntry> targetEntries, EntryConvertOptions options) async {
final options = await showDialog<EntryConvertOptions>(
context: context,
builder: (context) => ConvertEntryDialog(entries: targetEntries),
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
);
if (options == null) return;
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export); final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
if (destinationAlbum == null) return; if (destinationAlbum == null) return;
if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return;
@ -70,6 +63,34 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
} }
}); });
final l10n = context.l10n;
var nameConflictStrategy = NameConflictStrategy.rename;
final destinationDirectory = Directory(destinationAlbum);
final destinationExtension = MimeTypes.extensionFor(options.mimeType);
final names = [
...selection.map((v) => '${v.filenameWithoutExtension}$destinationExtension'),
// do not guard up front based on directory existence,
// as conflicts could be within moved entries scattered across multiple albums
if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)),
].map((v) => v.toLowerCase()).toList();
// case insensitive comparison
final uniqueNames = names.toSet();
if (uniqueNames.length < names.length) {
final value = await showDialog<NameConflictStrategy>(
context: context,
builder: (context) => AvesSingleSelectionDialog<NameConflictStrategy>(
initialValue: nameConflictStrategy,
options: Map.fromEntries(NameConflictStrategy.values.map((v) => MapEntry(v, v.getName(context)))),
message: l10n.nameConflictDialogSingleSourceMessage,
confirmationButtonLabel: l10n.continueButtonLabel,
),
routeSettings: const RouteSettings(name: AvesSingleSelectionDialog.routeName),
);
if (value == null) return;
nameConflictStrategy = value;
}
final selectionCount = selection.length; final selectionCount = selection.length;
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
source.pauseMonitoring(); source.pauseMonitoring();
@ -79,7 +100,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
selection, selection,
options: options, options: options,
destinationAlbum: destinationAlbum, destinationAlbum: destinationAlbum,
nameConflictStrategy: NameConflictStrategy.rename, nameConflictStrategy: nameConflictStrategy,
), ),
itemCount: selectionCount, itemCount: selectionCount,
onDone: (processed) async { onDone: (processed) async {
@ -91,7 +112,6 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
source.resumeMonitoring(); source.resumeMonitoring();
unawaited(source.refreshUris(newUris)); unawaited(source.refreshUris(newUris));
final l10n = context.l10n;
// get navigator beforehand because // get navigator beforehand because
// local context may be deactivated when action is triggered after navigation // local context may be deactivated when action is triggered after navigation
final navigator = Navigator.maybeOf(context); final navigator = Navigator.maybeOf(context);
@ -173,7 +193,8 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
// do not guard up front based on directory existence, // do not guard up front based on directory existence,
// as conflicts could be within moved entries scattered across multiple albums // as conflicts could be within moved entries scattered across multiple albums
if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)), if (await destinationDirectory.exists()) ...destinationDirectory.listSync().map((v) => pContext.basename(v.path)),
]; ].map((v) => v.toLowerCase()).toList();
// case insensitive comparison
final uniqueNames = names.toSet(); final uniqueNames = names.toSet();
if (uniqueNames.length < names.length) { if (uniqueNames.length < names.length) {
final value = await showDialog<NameConflictStrategy>( final value = await showDialog<NameConflictStrategy>(

View file

@ -1,7 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/enums/accessibility_timeout.dart'; import 'package:aves/model/settings/enums/accessibility_timeout.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/colors.dart'; import 'package:aves/theme/colors.dart';
@ -122,7 +121,7 @@ mixin FeedbackMixin {
static double snackBarHorizontalPadding(SnackBarThemeData snackBarTheme) { static double snackBarHorizontalPadding(SnackBarThemeData snackBarTheme) {
final isFloatingSnackBar = (snackBarTheme.behavior ?? SnackBarBehavior.fixed) == SnackBarBehavior.floating; final isFloatingSnackBar = (snackBarTheme.behavior ?? SnackBarBehavior.fixed) == SnackBarBehavior.floating;
final horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0; final double horizontalPadding = isFloatingSnackBar ? 16.0 : 24.0;
return horizontalPadding; return horizontalPadding;
} }
@ -182,9 +181,9 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
Stream<T> get opStream => widget.opStream; Stream<T> get opStream => widget.opStream;
static const fontSize = 18.0; static const double fontSize = 18.0;
static const diameter = 160.0; static const double diameter = 160.0;
static const strokeWidth = 8.0; static const double strokeWidth = 8.0;
@override @override
void initState() { void initState() {
@ -224,7 +223,7 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
final theme = Theme.of(context); final theme = Theme.of(context);
final colorScheme = theme.colorScheme; final colorScheme = theme.colorScheme;
final progressColor = colorScheme.primary; final progressColor = colorScheme.primary;
final animate = context.select<Settings, bool>((v) => v.accessibilityAnimations.animate); final animate = context.select<Settings, bool>((v) => v.animate);
return PopScope( return PopScope(
canPop: false, canPop: false,
child: StreamBuilder<T>( child: StreamBuilder<T>(

View file

@ -212,7 +212,7 @@ class _OverlaySnackBarState extends State<OverlaySnackBar> {
final IconButton? iconButton = showCloseIcon final IconButton? iconButton = showCloseIcon
? IconButton( ? IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
iconSize: 24.0, iconSize: 24,
color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor, color: widget.closeIconColor ?? snackBarTheme.closeIconColor ?? defaults.closeIconColor,
onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss), onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(reason: SnackBarClosedReason.dismiss),
tooltip: MaterialLocalizations.of(context).closeButtonTooltip, tooltip: MaterialLocalizations.of(context).closeButtonTooltip,

View file

@ -10,7 +10,7 @@ class ArrowClipper extends CustomClipper<Path> {
path.lineTo(0.0, 0.0); path.lineTo(0.0, 0.0);
path.close(); path.close();
const arrowWidth = 8.0; const double arrowWidth = 8.0;
final startPointX = (size.width - arrowWidth) / 2; final startPointX = (size.width - arrowWidth) / 2;
var startPointY = size.height / 2 - arrowWidth / 2; var startPointY = size.height / 2 - arrowWidth / 2;
path.moveTo(startPointX, startPointY); path.moveTo(startPointX, startPointY);

View file

@ -30,7 +30,7 @@ class LinkChip extends StatelessWidget {
borderRadius: borderRadius, borderRadius: borderRadius,
onTap: onTap ?? () => AvesApp.launchUrl(urlString), onTap: onTap ?? () => AvesApp.launchUrl(urlString),
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View file

@ -11,7 +11,7 @@ class AvesPopupMenuButton<T> extends PopupMenuButton<T> {
super.onCanceled, super.onCanceled,
super.tooltip, super.tooltip,
super.elevation, super.elevation,
super.padding = const EdgeInsets.all(8.0), super.padding = const EdgeInsets.all(8),
super.child, super.child,
super.icon, super.icon,
super.offset = Offset.zero, super.offset = Offset.zero,

View file

@ -4,6 +4,7 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/explorer/explorer_page.dart';
import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart'; import 'package:aves/widgets/filter_grids/tags_page.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
@ -32,7 +33,7 @@ class TvNavigationPopHandler {
return switch (homePage) { return switch (homePage) {
HomePageSetting.collection => context.read<CollectionLens>().filters.isEmpty, HomePageSetting.collection => context.read<CollectionLens>().filters.isEmpty,
HomePageSetting.albums || HomePageSetting.tags => true, HomePageSetting.albums || HomePageSetting.tags || HomePageSetting.explorer => true,
}; };
} }
@ -47,6 +48,7 @@ class TvNavigationPopHandler {
HomePageSetting.collection => buildRoute((context) => CollectionPage(source: context.read<CollectionSource>(), filters: null)), HomePageSetting.collection => buildRoute((context) => CollectionPage(source: context.read<CollectionSource>(), filters: null)),
HomePageSetting.albums => buildRoute((context) => const AlbumListPage()), HomePageSetting.albums => buildRoute((context) => const AlbumListPage()),
HomePageSetting.tags => buildRoute((context) => const TagListPage()), HomePageSetting.tags => buildRoute((context) => const TagListPage()),
HomePageSetting.explorer => buildRoute((context) => const ExplorerPage()),
}; };
} }
} }

View file

@ -170,7 +170,7 @@ class ExpandableFilterRow extends StatelessWidget {
// key `album-{path}` is expected by test driver // key `album-{path}` is expected by test driver
key: Key(filter.key), key: Key(filter.key),
filter: filter, filter: filter,
showGenericIcon: showGenericIcon, allowGenericIcon: showGenericIcon,
leadingOverride: leadingBuilder?.call(filter), leadingOverride: leadingBuilder?.call(filter),
heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap, heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap,
onTap: onTap, onTap: onTap,

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