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="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
### Added

View file

@ -196,9 +196,9 @@ repositories {
dependencies {
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.lifecycle:lifecycle-process:2.8.0'
implementation 'androidx.lifecycle:lifecycle-process:2.8.2'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'

View file

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

View file

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

View file

@ -12,6 +12,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.util.SizeF
import android.widget.RemoteViews
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
@ -40,12 +41,16 @@ class HomeWidgetProvider : AppWidgetProvider() {
for (widgetId in appWidgetIds) {
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
defaultScope.launch {
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundProps)
goAsync().run {
defaultScope.launch {
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps)
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
updateWidgetImage(context, appWidgetManager, widgetId, imageProps)
finish()
}
}
}
}
@ -61,20 +66,32 @@ class HomeWidgetProvider : AppWidgetProvider() {
imageByteFetchJob = defaultScope.launch {
delay(500)
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 getWidgetSizePx(context: Context, widgetInfo: Bundle): Pair<Int, Int> {
val devicePixelRatio = getDevicePixelRatio()
val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
val widthPx = (widgetInfo.getInt(widthKey) * devicePixelRatio).roundToInt()
val heightPx = (widgetInfo.getInt(heightKey) * devicePixelRatio).roundToInt()
return Pair(widthPx, heightPx)
private fun getWidgetSizesDip(context: Context, widgetInfo: Bundle): List<FieldMap> {
var sizes: List<SizeF>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF::class.java)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@Suppress("DEPRECATION")
widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES)
} else {
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(
@ -84,8 +101,11 @@ class HomeWidgetProvider : AppWidgetProvider() {
drawEntryImage: Boolean,
reuseEntry: Boolean = false,
): FieldMap? {
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
if (widthPx == 0 || heightPx == 0) return null
val sizesDip = getWidgetSizesDip(context, widgetInfo)
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
@ -98,13 +118,16 @@ class HomeWidgetProvider : AppWidgetProvider() {
FlutterUtils.runOnUiThread {
channel.invokeMethod("drawWidget", hashMapOf(
"widgetId" to widgetId,
"widthPx" to widthPx,
"heightPx" to heightPx,
"sizesDip" to sizesDip,
"devicePixelRatio" to getDevicePixelRatio(),
"drawEntryImage" to drawEntryImage,
"reuseEntry" to reuseEntry,
"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?) {
cont.resume(result)
}
@ -123,7 +146,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
@Suppress("unchecked_cast")
return props as FieldMap?
} 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
}
@ -132,36 +155,83 @@ class HomeWidgetProvider : AppWidgetProvider() {
context: Context,
appWidgetManager: AppWidgetManager,
widgetId: Int,
widgetInfo: Bundle,
props: FieldMap?,
) {
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?
if (bytes == null || updateOnTap == null) {
if (bytesBySizeDip == null || updateOnTap == null) {
Log.e(LOG_TAG, "missing arguments")
return
}
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
if (widthPx == 0 || heightPx == 0) return
if (bytesBySizeDip.isEmpty()) {
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 {
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId)
val views = RemoteViews(context.packageName, R.layout.app_widget).apply {
setImageViewBitmap(R.id.widget_img, bitmap)
setOnClickPendingIntent(R.id.widget_img, pendingIntent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// multiple rendering for all possible sizes
val views = RemoteViews(
bytesBySizeDip.associateBy(
{ (sizeDip, _) -> sizeDip },
{ (sizeDip, bytes) -> createRemoteViewsForSize(context, widgetId, sizeDip, bytes, updateOnTap) },
).filterValues { it != null }.mapValues { (_, view) -> view!! }
)
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) {
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.Handler
import android.os.Looper
import android.os.TransactionTooLargeException
import android.provider.MediaStore
import android.util.Log
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.AnalysisHandler
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.DeviceHandler
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.SecurityHandler
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.ActivityResultStreamHandler
@ -135,6 +138,7 @@ open class MainActivity : FlutterFragmentActivity() {
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
@ -168,7 +172,7 @@ open class MainActivity : FlutterFragmentActivity() {
intentDataMap.clear()
}
"submitPickedItems" -> submitPickedItems(call)
"submitPickedItems" -> safe(call, result, ::submitPickedItems)
"submitPickedCollectionFilters" -> submitPickedCollectionFilters(call)
}
}
@ -301,16 +305,32 @@ open class MainActivity : FlutterFragmentActivity() {
Intent.ACTION_VIEW,
Intent.ACTION_SEND,
MediaStore.ACTION_REVIEW,
MediaStore.ACTION_REVIEW_SECURE,
"com.android.camera.action.REVIEW",
"com.android.camera.action.SPLIT_SCREEN_REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
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_MIME_TYPE to type,
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
}
private fun submitPickedItems(call: MethodCall) {
open fun submitPickedItems(call: MethodCall, result: MethodChannel.Result) {
val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
val intent = Intent().apply {
val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) {
data = firstUri
} else {
clipData = ClipData.newUri(contentResolver, null, firstUri).apply {
pickedUris.drop(1).forEach {
addItem(ClipData.Item(toUri(it)))
try {
if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
val intent = Intent().apply {
val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) {
data = firstUri
} else {
clipData = ClipData.newUri(contentResolver, null, firstUri).apply {
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) {
@ -498,11 +526,13 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_DATA_KEY_ACTION = "action"
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_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query"
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_WIDGET_ID = "widgetId"

View file

@ -2,132 +2,54 @@ package deckers.thibault.aves
import android.content.Intent
import android.net.Uri
import android.os.Bundle
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.channel.calls.AppAdapterHandler
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
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.MethodChannel
class WallpaperActivity : FlutterFragmentActivity() {
private lateinit var intentDataMap: FieldMap
private lateinit var mediaSessionHandler: MediaSessionHandler
class WallpaperActivity : MainActivity() {
private var originalIntent: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
override fun extractIntentData(intent: Intent?): FieldMap {
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")
intent.extras?.takeUnless { it.isEmpty }?.let {
Log.i(LOG_TAG, "onCreate intent extras=$it")
}
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(),
)
// if the media URI is not provided we need to pick one first
originalIntent = intent.action
intent.action = Intent.ACTION_PICK
}
}
Intent.ACTION_RUN -> {
// flutter run
}
else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
}
}
return HashMap()
return super.extractIntentData(intent)
}
companion object {
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>()
override fun submitPickedItems(call: MethodCall, result: MethodChannel.Result) {
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)
"deleteEmptyDirectories" -> ioScope.launch { safe(call, result, ::deleteEmptyDirectories) }
"deleteTempDirectory" -> ioScope.launch { safe(call, result, ::deleteTempDirectory) }
"deleteExternalCache" -> ioScope.launch { safe(call, result, ::deleteExternalCache) }
"canRequestMediaFileBulkAccess" -> safe(call, result, ::canRequestMediaFileBulkAccess)
"canInsertMedia" -> safe(call, result, ::canInsertMedia)
else -> result.notImplemented()
@ -49,16 +50,17 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
var internalCache = getFolderSize(context.cacheDir)
internalCache += getFolderSize(context.codeCacheDir)
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 database = getFolderSize(File(dataDir, "databases"))
val flutter = getFolderSize(File(PathUtils.getDataDirectory(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 externalData = context.getExternalFilesDirs(null).map(::getFolderSize).sum()
val externalData = externalFilesDirs.map(::getFolderSize).sum()
val miscData = internalData + externalData - (database + flutter + vaults + trash)
result.success(
@ -224,6 +226,11 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
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) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
}

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.model
import java.io.File
enum class NameConflictStrategy {
RENAME, REPLACE, SKIP;
@ -9,4 +11,6 @@ enum class NameConflictStrategy {
return valueOf(name.uppercase())
}
}
}
}
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.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictResolution
import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.BitmapUtils
@ -147,13 +148,14 @@ abstract class ImageProvider {
val oldFile = File(sourcePath)
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
oldFile.parent?.let { dir ->
resolveTargetFileNameWithoutExtension(
val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity,
dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
conflictStrategy = NameConflictStrategy.RENAME,
)?.let { targetNameWithoutExtension ->
)
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
val newFile = File(dir, targetFileName)
if (oldFile != newFile) {
@ -266,7 +268,7 @@ abstract class ImageProvider {
exportMimeType: String,
): FieldMap {
val sourceMimeType = sourceEntry.mimeType
val sourceUri = sourceEntry.uri
var sourceUri = sourceEntry.uri
val pageId = sourceEntry.pageId
var desiredNameWithoutExtension = if (sourceEntry.path != null) {
@ -279,13 +281,17 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
}
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity,
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType,
conflictStrategy = nameConflictStrategy,
) ?: return skippedFieldMap
)
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
resolution.replacementFile?.let { file ->
sourceUri = Uri.fromFile(file)
}
val targetMimeType: String
val write: (OutputStream) -> Unit
@ -391,6 +397,8 @@ abstract class ImageProvider {
} finally {
// clearing Glide target should happen after effectively writing the bitmap
Glide.with(activity).clear(target)
resolution.replacementFile?.delete()
}
}
@ -470,7 +478,7 @@ abstract class ImageProvider {
}
val captureMimeType = MimeTypes.JPEG
val targetNameWithoutExtension = try {
val resolution = try {
resolveTargetFileNameWithoutExtension(
contextWrapper = contextWrapper,
dir = targetDir,
@ -483,6 +491,7 @@ abstract class ImageProvider {
return
}
val targetNameWithoutExtension = resolution.nameWithoutExtension
if (targetNameWithoutExtension == null) {
// skip it
callback.onSuccess(skippedFieldMap)
@ -568,10 +577,13 @@ abstract class ImageProvider {
desiredNameWithoutExtension: String,
mimeType: String,
conflictStrategy: NameConflictStrategy,
): String? {
): NameConflictResolution {
var resolvedName: String? = desiredNameWithoutExtension
var replacementFile: File? = null
val extension = extensionFor(mimeType)
val targetFile = File(dir, "$desiredNameWithoutExtension$extension")
return when (conflictStrategy) {
when (conflictStrategy) {
NameConflictStrategy.RENAME -> {
var nameWithoutExtension = desiredNameWithoutExtension
var i = 0
@ -579,24 +591,28 @@ abstract class ImageProvider {
i++
nameWithoutExtension = "$desiredNameWithoutExtension ($i)"
}
nameWithoutExtension
resolvedName = nameWithoutExtension
}
NameConflictStrategy.REPLACE -> {
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)
}
desiredNameWithoutExtension
}
NameConflictStrategy.SKIP -> {
if (targetFile.exists()) {
null
} else {
desiredNameWithoutExtension
resolvedName = null
}
}
}
return NameConflictResolution(resolvedName, replacementFile)
}
// cf `MetadataFetchHandler.getCatalogMetadataByMetadataExtractor()` for a more thorough check

View file

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

View file

@ -6,32 +6,32 @@
<path
android:fillColor="#ef435a"
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:strokeWidth="1.61863"
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.53903"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#e0e0e0"
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:strokeWidth="1.61862"
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.53902"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#ffc11f"
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:strokeWidth="1.61862"
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.53902"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="@color/ic_launcher_flavour"
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:strokeWidth="1.61863"
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.53903"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />

View file

@ -6,32 +6,32 @@
<path
android:fillColor="#000000"
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:strokeWidth="1.61863"
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.53871"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000000"
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:strokeWidth="1.61862"
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.5387"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000000"
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:strokeWidth="1.61862"
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.5387"
android:strokeColor="#00000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:fillColor="#000000"
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:strokeWidth="1.61863"
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.53871"
android:strokeColor="#00000000"
android:strokeLineCap="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": {},
"settingsViewerShowMinimap": "إظهار الخريطة المصغرة",
"@settingsViewerShowMinimap": {},
"settingsCollectionBurstPatternsTile": "أنماط الانفجار",
"settingsCollectionBurstPatternsTile": "أنماط الصور المتتابعة",
"@settingsCollectionBurstPatternsTile": {},
"viewerInfoLabelPath": "المسار",
"@viewerInfoLabelPath": {},
@ -1538,5 +1538,9 @@
"renameProcessorHash": "تجزئة",
"@renameProcessorHash": {},
"chipActionShowCollection": "عرض في المجموعة",
"@chipActionShowCollection": {}
"@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "عرض في المستكشف",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "المستكشف",
"@explorerPageTitle": {}
}

View file

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

View file

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

View file

@ -1380,5 +1380,9 @@
"renameProcessorHash": "Hash",
"@renameProcessorHash": {},
"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": {},
"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": {},
"chipActionShowCollection": "Afficher dans Collection",
"@chipActionShowCollection": {}
"@chipActionShowCollection": {},
"explorerPageTitle": "Explorateur",
"@explorerPageTitle": {},
"chipActionGoToExplorerPage": "Afficher dans Explorateur",
"@chipActionGoToExplorerPage": {}
}

View file

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

View file

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

View file

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

View file

@ -1526,5 +1526,17 @@
"settingsThumbnailShowHdrIcon": "Zobraziť ikonu HDR",
"@settingsThumbnailShowHdrIcon": {},
"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": {},
"chipActionShowCollection": "Koleksiyonda göster",
"@chipActionShowCollection": {}
"@chipActionShowCollection": {},
"chipActionGoToExplorerPage": "Gezginde göster",
"@chipActionGoToExplorerPage": {},
"explorerPageTitle": "Gezgin",
"@explorerPageTitle": {}
}

View file

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

View file

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

View file

@ -91,11 +91,14 @@ class Contributors {
Contributor('cheese', 'deanlemans5646@gmail.com'),
Contributor('Owen Elderbroek', 'o.elderbroek@gmail.com'),
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('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
// Contributor('Grooty12', 'Rasmus@rosendahl-kaa.name'), // Danish
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi

View file

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

View file

@ -44,7 +44,8 @@ class AvesEntry with AvesEntryBase {
AddressDetails? _addressDetails;
TrashDetails? trashDetails;
List<AvesEntry>? burstEntries;
// synthetic stack of related entries, e.g. burst shots or raw/developed pairs
List<AvesEntry>? stackedEntries;
@override
final AChangeNotifier visualChangeNotifier = AChangeNotifier();
@ -69,7 +70,7 @@ class AvesEntry with AvesEntryBase {
required int? durationMillis,
required this.trashed,
required this.origin,
this.burstEntries,
this.stackedEntries,
}) : id = id ?? 0 {
if (kFlutterMemoryAllocationsEnabled) {
FlutterMemoryAllocations.instance.dispatchObjectCreated(
@ -93,7 +94,7 @@ class AvesEntry with AvesEntryBase {
int? dateAddedSecs,
int? dateModifiedSecs,
int? origin,
List<AvesEntry>? burstEntries,
List<AvesEntry>? stackedEntries,
}) {
final copyEntryId = id ?? this.id;
final copied = AvesEntry(
@ -114,7 +115,7 @@ class AvesEntry with AvesEntryBase {
durationMillis: durationMillis,
trashed: trashed,
origin: origin ?? this.origin,
burstEntries: burstEntries ?? this.burstEntries,
stackedEntries: stackedEntries ?? this.stackedEntries,
)
..catalogMetadata = _catalogMetadata?.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';
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;
@ -19,10 +19,10 @@ extension ExtraAvesEntryMultipage on AvesEntry {
}
Future<MultiPageInfo?> getMultiPageInfo() async {
if (isBurst) {
if (isStack) {
return MultiPageInfo(
mainEntry: this,
pages: burstEntries!
pages: stackedEntries!
.mapIndexed((index, entry) => SinglePageInfo(
index: index,
pageId: entry.id,

View file

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

View file

@ -68,7 +68,7 @@ class AspectRatioFilter extends CollectionFilter {
}
@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
String get category => type;

View file

@ -69,7 +69,7 @@ class CoordinateFilter extends CollectionFilter {
}
@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
String get category => type;

View file

@ -122,7 +122,7 @@ class DateFilter extends CollectionFilter {
}
@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
String get category => type;

View file

@ -45,7 +45,7 @@ class FavouriteFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterFavouriteLabel;
@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
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);
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) {
final colors = context.read<AvesColorsData>();

View file

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

View file

@ -77,7 +77,7 @@ class MimeFilter extends CollectionFilter {
}
@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
Future<Color> color(BuildContext context) {

View file

@ -70,7 +70,7 @@ class MissingFilter extends CollectionFilter {
}
@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
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(', ');
@override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, showGenericIcon: showGenericIcon);
Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
return _genericIcon != null ? Icon(_genericIcon, size: size) : _first.iconBuilder(context, size, allowGenericIcon: allowGenericIcon);
}
@override

View file

@ -1,5 +1,9 @@
import 'package:aves/model/filters/filters.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 {
static const type = 'path';
@ -47,6 +51,19 @@ class PathFilter extends CollectionFilter {
@override
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
String get category => type;

View file

@ -96,7 +96,7 @@ class PlaceholderFilter extends CollectionFilter {
}
@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
String get category => type;

View file

@ -82,7 +82,7 @@ class QueryFilter extends CollectionFilter {
String get universalLabel => query;
@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
Future<Color> color(BuildContext context) {

View file

@ -64,7 +64,7 @@ class RatingFilter extends CollectionFilter {
};
@override
Widget? iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) {
Widget? iconBuilder(BuildContext context, double size, {bool allowGenericIcon = true}) {
return switch (rating) {
-1 => Icon(AIcons.ratingRejected, 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;
@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
String get category => type;

View file

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

View file

@ -41,7 +41,7 @@ class TrashFilter extends CollectionFilter {
String getLabel(BuildContext context) => context.l10n.filterBinLabel;
@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
String get category => type;

View file

@ -99,7 +99,7 @@ class TypeFilter extends CollectionFilter {
}
@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
Future<Color> color(BuildContext context) {

View file

@ -32,10 +32,10 @@ class MultiPageInfo {
_pages.insert(0, firstPage.copyWith(isDefault: true));
}
final burstEntries = mainEntry.burstEntries;
if (burstEntries != null) {
final stackedEntries = mainEntry.stackedEntries;
if (stackedEntries != null) {
_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);
}));
}

View file

@ -1,6 +1,7 @@
import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/naming_pattern.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/countries_page.dart';
import 'package:aves/widgets/filter_grids/tags_page.dart';
@ -39,6 +40,7 @@ class SettingsDefaults {
AlbumListPage.routeName,
CountryListPage.routeName,
TagListPage.routeName,
ExplorerPage.routeName,
];
// collection

View file

@ -1,4 +1,5 @@
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/tags_page.dart';
import 'package:aves_model/aves_model.dart';
@ -12,6 +13,8 @@ extension ExtraHomePageSetting on HomePageSetting {
return AlbumListPage.routeName;
case HomePageSetting.tags:
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';
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;
switch (this) {
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:
return Path()
..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/mime.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/modules/app.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);
bool get animate => accessibilityAnimations.animate;
set accessibilityAnimations(AccessibilityAnimations newValue) => set(SettingKeys.accessibilityAnimationsKey, newValue.toString());
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/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/entry/sort.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/filters.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/rating.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/section_keys.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_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart';
@ -34,7 +37,7 @@ class CollectionLens with ChangeNotifier {
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = [];
int? id;
bool listenToSource, groupBursts, fixedSort;
bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
List<AvesEntry>? fixedSelection;
final Set<AvesEntry> _syntheticEntries = {};
@ -47,7 +50,8 @@ class CollectionLens with ChangeNotifier {
Set<CollectionFilter?>? filters,
this.id,
this.listenToSource = true,
this.groupBursts = true,
this.stackBursts = true,
this.stackDevelopedRaws = true,
this.fixedSort = false,
this.fixedSelection,
}) : filters = (filters ?? {}).whereNotNull().toSet(),
@ -192,30 +196,59 @@ class CollectionLens with ChangeNotifier {
_disposeSyntheticEntries();
_filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry))));
if (groupBursts) {
_groupBursts();
if (stackBursts) {
_stackBursts();
}
if (stackDevelopedRaws) {
_stackDevelopedRaws();
}
}
void _groupBursts() {
void _stackBursts() {
final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.getBurstKey(burstPatterns)).whereNotNullKey();
byBurstKey.forEach((burstKey, entries) {
if (entries.length > 1) {
entries.sort(AvesEntrySort.compareByName);
final mainEntry = entries.first;
final burstEntry = mainEntry.copyWith(burstEntries: entries);
_syntheticEntries.add(burstEntry);
final stackEntry = mainEntry.copyWith(stackedEntries: entries);
_syntheticEntries.add(stackEntry);
entries.skip(1).toList().forEach((subEntry) {
entries.skip(1).forEach((subEntry) {
_filteredSortedEntries.remove(subEntry);
});
final index = _filteredSortedEntries.indexOf(mainEntry);
_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() {
if (fixedSort) return;
@ -322,23 +355,52 @@ class CollectionLens with ChangeNotifier {
}
void _onEntryRemoved(Set<AvesEntry> entries) {
if (groupBursts) {
// find impacted burst groups
final obsoleteBurstEntries = <AvesEntry>{};
final burstKeys = entries.map((entry) => entry.getBurstKey(burstPatterns)).whereNotNull().toSet();
if (burstKeys.isNotEmpty) {
_filteredSortedEntries.where((entry) => entry.isBurst && burstKeys.contains(entry.getBurstKey(burstPatterns))).forEach((mainEntry) {
final subEntries = mainEntry.burstEntries!;
if (_syntheticEntries.isNotEmpty) {
// find impacted stacks
final obsoleteStacks = <AvesEntry>{};
void _replaceStack(AvesEntry stackEntry, AvesEntry entry) {
obsoleteStacks.add(stackEntry);
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
subEntries.removeWhere(entries.contains);
if (subEntries.isEmpty) {
// remove the burst entry itself
obsoleteBurstEntries.add(mainEntry);
switch (subEntries.length) {
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

View file

@ -134,4 +134,15 @@ class MimeTypes {
}
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 'package:aves/model/filters/filters.dart';
import 'package:aves/services/app_service.dart';
import 'package:aves/services/common/services.dart';
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
@ -27,7 +28,11 @@ class IntentService {
'uris': uris,
});
} 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
class EntryConvertOptions extends Equatable {
final EntryConvertAction action;
final String mimeType;
final bool writeMetadata;
final LengthUnit lengthUnit;
final int width, height, quality;
@override
List<Object?> get props => [mimeType, writeMetadata, lengthUnit, width, height, quality];
List<Object?> get props => [action, mimeType, writeMetadata, lengthUnit, width, height, quality];
const EntryConvertOptions({
required this.action,
required this.mimeType,
required this.writeMetadata,
required this.lengthUnit,

View file

@ -33,6 +33,8 @@ abstract class StorageService {
Future<bool> deleteTempDirectory();
Future<bool> deleteExternalCache();
// returns whether user granted access to a directory of his choosing
Future<bool> requestDirectoryAccess(String path);
@ -202,6 +204,17 @@ class PlatformStorageService implements StorageService {
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
Future<bool> canRequestMediaFileBulkAccess() async {
try {

View file

@ -60,7 +60,7 @@ class ADurations {
static const highlightJumpDelay = Duration(milliseconds: 400);
static const highlightScrollInitDelay = Duration(milliseconds: 800);
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 videoProgressTimerInterval = Duration(milliseconds: 300);
static const doubleBackTimerDelay = Duration(milliseconds: 1000);

View file

@ -29,8 +29,10 @@ class AIcons {
static const disc = Icons.fiber_manual_record;
static const display = Icons.light_mode_outlined;
static const error = Icons.error_outline;
static const explorer = Icons.account_tree_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;
// as of Flutter v3.16.3,
@ -39,13 +41,15 @@ class AIcons {
static const important = IconData(labelImportantOutlineCodePoint, fontFamily: materialIconsFontFamily, matchTextDirection: true);
static const language = Icons.translate_outlined;
static final legal = MdiIcons.scaleBalance;
static const location = Icons.place_outlined;
static const locationUnlocated = Icons.location_off_outlined;
static const country = Icons.flag_outlined;
static const state = Icons.flag_outlined;
static const place = Icons.place_outlined;
static const mainStorage = Icons.smartphone_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 palette = Icons.palette_outlined;
static final privacy = MdiIcons.shieldAccountOutline;
@ -54,15 +58,20 @@ class AIcons {
static final ratingRejected = MdiIcons.starMinusOutline;
static final ratingUnrated = MdiIcons.starOffOutline;
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 sensorControlDisabled = Icons.explore_off_outlined;
static const settings = Icons.settings_outlined;
static const shooting = Icons.camera_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 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 volumeMax = Icons.volume_up_outlined;
@ -100,7 +109,6 @@ class AIcons {
static const favouriteActive = Icons.favorite;
static final filter = MdiIcons.filterOutline;
static final filterOff = MdiIcons.filterOffOutline;
static const geoBounds = Icons.public_outlined;
static const goUp = Icons.arrow_upward_outlined;
static const hide = Icons.visibility_off_outlined;
static const info = Icons.info_outlined;
@ -110,8 +118,7 @@ class AIcons {
static final move = MdiIcons.fileMoveOutline;
static const mute = Icons.volume_off_outlined;
static const unmute = Icons.volume_up_outlined;
static const name = Icons.abc_outlined;
static const newTier = Icons.fiber_new_outlined;
static const rename = Icons.abc_outlined;
static const openOutside = Icons.open_in_new_outlined;
static final openVideo = MdiIcons.moviePlayOutline;
static const pin = Icons.push_pin_outlined;
@ -133,20 +140,17 @@ class AIcons {
static const rotateScreen = Icons.screen_rotation_outlined;
static const search = Icons.search_outlined;
static const select = Icons.select_all_outlined;
static const selectStreams = Icons.translate_outlined;
static const setAs = Icons.wallpaper_outlined;
static final setBoundEnd = MdiIcons.rayEnd;
static final setBoundStart = MdiIcons.rayStart;
static final setCover = MdiIcons.imageEditOutline;
static final setEnd = MdiIcons.rayEnd;
static final setStart = MdiIcons.rayStart;
static const share = Icons.share_outlined;
static const show = Icons.visibility_outlined;
static final showFullscreen = MdiIcons.arrowExpand;
static const slideshow = Icons.slideshow_outlined;
static const speed = Icons.speed_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 vaultAdd = Icons.enhanced_encryption_outlined;
static final vaultConfigure = MdiIcons.shieldLockOutline;
@ -190,9 +194,6 @@ class AIcons {
static const selected = Icons.check_circle_outline;
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`
// as non-constant instances of `IconData` prevent icon font tree shaking
static const labelImportantOutlineCodePoint = 0xe362;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ export 'src/actions/map_cluster.dart';
export 'src/actions/share.dart';
export 'src/actions/slideshow.dart';
export 'src/editor/enums.dart';
export 'src/metadata/convert_action.dart';
export 'src/metadata/date_edit_action.dart';
export 'src/metadata/date_field_source.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 {
final widgetId = args['widgetId'] as int;
final widthPx = args['widthPx'] as int;
final heightPx = args['heightPx'] as int;
final sizesDip = (args['sizesDip'] as List).cast<Map>().map((kv) {
return Size(kv['widthDip'] as double, kv['heightDip'] as double);
}).toList();
final cornerRadiusPx = args['cornerRadiusPx'] as double?;
final devicePixelRatio = args['devicePixelRatio'] as double;
final drawEntryImage = args['drawEntryImage'] as bool;
final reuseEntry = args['reuseEntry'] as bool;
@ -53,14 +55,22 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
entry: entry,
devicePixelRatio: devicePixelRatio,
);
final bytes = await painter.drawWidget(
widthPx: widthPx,
heightPx: heightPx,
outline: outline,
shape: settings.getWidgetShape(widgetId),
);
final bytesBySizeDip = <Map<String, dynamic>>[];
await Future.forEach(sizesDip, (sizeDip) async {
final bytes = await painter.drawWidget(
sizeDip: sizeDip,
cornerRadiusPx: cornerRadiusPx,
outline: outline,
shape: settings.getWidgetShape(widgetId),
);
bytesBySizeDip.add({
'widthDip': sizeDip.width,
'heightDip': sizeDip.height,
'bytes': bytes,
});
});
return {
'bytes': bytes,
'bytesBySizeDip': bytesBySizeDip,
'updateOnTap': settings.getWidgetOpenPage(widgetId) == WidgetOpenPage.updateWidget,
};
}

View file

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

View file

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

View file

@ -8,7 +8,6 @@ import 'package:aves/model/apps.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/recent.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/screen_on.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)));
}
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) {
return FutureBuilder<bool>(
future: _shouldUseBoldFontLoader,
@ -668,7 +667,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
class AvesScrollBehavior extends MaterialScrollBehavior {
@override
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
? StretchingOverscrollIndicator(
axisDirection: details.direction,

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
@ -171,7 +172,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
selector: (context, s) => s.collectionBrowsingQuickActions,
builder: (context, _, child) {
final useTvLayout = settings.useTvLayout;
final actions = _buildActions(context, selection);
final onFilterTap = canRemoveFilters ? collection.removeFilter : null;
return AvesAppBar(
contentHeight: appBarContentHeight,
@ -181,7 +181,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
isSelecting: isSelecting,
),
title: _buildAppBarTitle(isSelecting),
actions: useTvLayout ? [] : actions,
actions: (context, maxWidth) => useTvLayout ? [] : _buildActions(context, selection, maxWidth),
bottom: Column(
children: [
if (useTvLayout)
@ -190,7 +190,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8),
scrollDirection: Axis.horizontal,
children: actions,
children: _buildActions(context, selection, double.infinity),
),
),
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 isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length;
@ -333,6 +333,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
context: context,
appMode: appMode,
selection: selection,
maxWidth: maxWidth,
isVisible: isVisible,
canApply: canApply,
);
@ -366,20 +367,29 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}).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({
required BuildContext context,
required AppMode appMode,
required Selection<AvesEntry> selection,
required double maxWidth,
required bool Function(EntrySetAction action) isVisible,
required bool Function(EntrySetAction action) canApply,
}) {
final availableCount = (maxWidth / _iconButtonWidth(context)).floor();
final isSelecting = selection.isSelecting;
final selectedItemCount = selection.selectedItems.length;
final hasSelection = selectedItemCount > 0;
final browsingQuickActions = settings.collectionBrowsingQuickActions;
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(
(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),
);
final allContextualActions = isSelecting ? EntrySetActions.pageSelection: EntrySetActions.pageBrowsing;
final allContextualActions = isSelecting ? EntrySetActions.pageSelection : EntrySetActions.pageBrowsing;
final contextualMenuActions = allContextualActions.where(_isValidForMenu).fold(<EntrySetAction?>[], (prev, v) {
if (v == null && (prev.isEmpty || prev.last == null)) return prev;
return [...prev, v];
@ -444,7 +454,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
}
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')

View file

@ -7,10 +7,10 @@ import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/highlight.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/source/collection_lens.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/theme/durations.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/providers/query_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/nav_bar/nav_bar.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
@ -186,10 +187,21 @@ class _CollectionPageState extends State<CollectionPage> {
return hasSelection
? AvesFab(
tooltip: context.l10n.pickTooltip,
onPressed: () {
onPressed: () async {
final items = context.read<Selection<AvesEntry>>().selectedItems;
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;
@ -217,7 +229,7 @@ class _CollectionPageState extends State<CollectionPage> {
await Future.delayed(delayDuration + ADurations.highlightScrollInitDelay);
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);
}
}

View file

@ -5,6 +5,7 @@ import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/favourites.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/favourites.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/common/image_op_events.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/themes.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/aves_confirmation_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/pick_dialogs/location_pick_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) {
final selection = context.read<Selection<AvesEntry>>();
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 {
@ -366,9 +369,23 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
_browse(context);
}
void _convert(BuildContext context) {
Future<void> _convert(BuildContext context) async {
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);
}

View file

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

View file

@ -80,7 +80,7 @@ class EntryListDetails extends StatelessWidget {
final date = entry.bestDate;
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;
return Wrap(

View file

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

View file

@ -32,7 +32,7 @@ class TagQuickChooser extends StatelessWidget {
pointerGlobalPosition: pointerGlobalPosition,
itemBuilder: (context, filter) => AvesFilterChip(
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/source/collection_lens.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/services.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/extensions/build_context.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/selection_dialogs/single_selection.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
@ -37,14 +37,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
Future<void> convert(BuildContext context, Set<AvesEntry> targetEntries) async {
final options = await showDialog<EntryConvertOptions>(
context: context,
builder: (context) => ConvertEntryDialog(entries: targetEntries),
routeSettings: const RouteSettings(name: ConvertEntryDialog.routeName),
);
if (options == null) return;
Future<void> doExport(BuildContext context, Set<AvesEntry> targetEntries, EntryConvertOptions options) async {
final destinationAlbum = await pickAlbum(context: context, moveType: MoveType.export);
if (destinationAlbum == null) 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 source = context.read<CollectionSource>();
source.pauseMonitoring();
@ -79,7 +100,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
selection,
options: options,
destinationAlbum: destinationAlbum,
nameConflictStrategy: NameConflictStrategy.rename,
nameConflictStrategy: nameConflictStrategy,
),
itemCount: selectionCount,
onDone: (processed) async {
@ -91,7 +112,6 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
source.resumeMonitoring();
unawaited(source.refreshUris(newUris));
final l10n = context.l10n;
// get navigator beforehand because
// local context may be deactivated when action is triggered after navigation
final navigator = Navigator.maybeOf(context);
@ -173,7 +193,8 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
// 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>(

View file

@ -1,7 +1,6 @@
import 'dart:async';
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/settings.dart';
import 'package:aves/theme/colors.dart';
@ -122,7 +121,7 @@ mixin FeedbackMixin {
static double snackBarHorizontalPadding(SnackBarThemeData snackBarTheme) {
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;
}
@ -182,9 +181,9 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
Stream<T> get opStream => widget.opStream;
static const fontSize = 18.0;
static const diameter = 160.0;
static const strokeWidth = 8.0;
static const double fontSize = 18.0;
static const double diameter = 160.0;
static const double strokeWidth = 8.0;
@override
void initState() {
@ -224,7 +223,7 @@ class _ReportOverlayState<T> extends State<ReportOverlay<T>> with SingleTickerPr
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
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(
canPop: false,
child: StreamBuilder<T>(

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ class AvesPopupMenuButton<T> extends PopupMenuButton<T> {
super.onCanceled,
super.tooltip,
super.elevation,
super.padding = const EdgeInsets.all(8.0),
super.padding = const EdgeInsets.all(8),
super.child,
super.icon,
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/widgets/collection/collection_page.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/tags_page.dart';
import 'package:aves_model/aves_model.dart';
@ -32,7 +33,7 @@ class TvNavigationPopHandler {
return switch (homePage) {
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.albums => buildRoute((context) => const AlbumListPage()),
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: Key(filter.key),
filter: filter,
showGenericIcon: showGenericIcon,
allowGenericIcon: showGenericIcon,
leadingOverride: leadingBuilder?.call(filter),
heroType: heroTypeBuilder?.call(filter) ?? HeroType.onTap,
onTap: onTap,

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