Merge branch 'develop'
This commit is contained in:
commit
a396635639
161 changed files with 2024 additions and 962 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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?)
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
In v1.11.0:
|
||||
- watch videos with SRT subtitle files
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.11.0:
|
||||
- watch videos with SRT subtitle files
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/123.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/123.txt
Normal 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
|
4
fastlane/metadata/android/en-US/changelogs/12301.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/12301.txt
Normal 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
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1380,5 +1380,9 @@
|
|||
"renameProcessorHash": "Hash",
|
||||
"@renameProcessorHash": {},
|
||||
"chipActionShowCollection": "Mostrar en Colección",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"explorerPageTitle": "Explorar",
|
||||
"@explorerPageTitle": {},
|
||||
"chipActionGoToExplorerPage": "Mostrar en el explorador",
|
||||
"@chipActionGoToExplorerPage": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -1380,5 +1380,9 @@
|
|||
"settingsForceWesternArabicNumeralsTile": "아라비아 숫자 항상 사용",
|
||||
"@settingsForceWesternArabicNumeralsTile": {},
|
||||
"chipActionShowCollection": "미디어 페이지에서 보기",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"explorerPageTitle": "탐색기",
|
||||
"@explorerPageTitle": {},
|
||||
"chipActionGoToExplorerPage": "탐색기 페이지에서 보기",
|
||||
"@chipActionGoToExplorerPage": {}
|
||||
}
|
||||
|
|
|
@ -1538,5 +1538,9 @@
|
|||
"renameProcessorHash": "Skrót",
|
||||
"@renameProcessorHash": {},
|
||||
"chipActionShowCollection": "Pokaż w Kolekcji",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"chipActionGoToExplorerPage": "Pokaż w przeglądarce",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"explorerPageTitle": "Przeglądarka",
|
||||
"@explorerPageTitle": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -1380,5 +1380,9 @@
|
|||
"renameProcessorHash": "Sağlama",
|
||||
"@renameProcessorHash": {},
|
||||
"chipActionShowCollection": "Koleksiyonda göster",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"chipActionGoToExplorerPage": "Gezginde göster",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"explorerPageTitle": "Gezgin",
|
||||
"@explorerPageTitle": {}
|
||||
}
|
||||
|
|
|
@ -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": {}
|
||||
}
|
||||
|
|
|
@ -1380,5 +1380,9 @@
|
|||
"settingsForceWesternArabicNumeralsTile": "强制使用阿拉伯数字",
|
||||
"@settingsForceWesternArabicNumeralsTile": {},
|
||||
"chipActionShowCollection": "在媒体集中显示",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"explorerPageTitle": "资源管理器",
|
||||
"@explorerPageTitle": {},
|
||||
"chipActionGoToExplorerPage": "在资源管理器中显示",
|
||||
"@chipActionGoToExplorerPage": {}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
21
lib/view/src/metadata/convert_action.dart
Normal file
21
lib/view/src/metadata/convert_action.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -83,6 +83,7 @@ extension ExtraHomePageSettingView on HomePageSetting {
|
|||
HomePageSetting.collection => l10n.drawerCollectionAll,
|
||||
HomePageSetting.albums => l10n.drawerAlbumPage,
|
||||
HomePageSetting.tags => l10n.drawerTagPage,
|
||||
HomePageSetting.explorer => l10n.explorerPageTitle,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ class TagQuickChooser extends StatelessWidget {
|
|||
pointerGlobalPosition: pointerGlobalPosition,
|
||||
itemBuilder: (context, filter) => AvesFilterChip(
|
||||
filter: filter,
|
||||
showGenericIcon: false,
|
||||
allowGenericIcon: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue