Merge branch 'develop'
This commit is contained in:
commit
82f070f8a1
141 changed files with 1617 additions and 706 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit ba393198430278b6595976de84fe170f553cc728
|
||||
Subproject commit 300451adae589accbece3490f4396f10bdf15e6e
|
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -4,6 +4,28 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
## <a id="v1.10.8"></a>[v1.10.8] - 2024-04-01
|
||||
|
||||
### Added
|
||||
|
||||
- Collection: support for Fairphone burst pattern
|
||||
- Collection: allow using tags/make/model when bulk renaming
|
||||
- Video: A-B repeat
|
||||
- Settings: hidden items can be toggled
|
||||
|
||||
### Changed
|
||||
|
||||
- opening app from launcher always show home page
|
||||
- use dates with western arabic numerals for maghreb arabic locales
|
||||
- album unique names are case insensitive
|
||||
- upgraded Flutter to stable v3.19.5
|
||||
|
||||
### Fixed
|
||||
|
||||
- crash when decoding large region
|
||||
- viewer position drift during scale
|
||||
- viewer side gesture precedence (next entry by single tap vs zoom by double tap)
|
||||
|
||||
## <a id="v1.10.7"></a>[v1.10.7] - 2024-03-12
|
||||
|
||||
### Added
|
||||
|
|
|
@ -2,9 +2,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|||
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'com.google.devtools.ksp' version "$ksp_version"
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'dev.flutter.flutter-gradle-plugin'
|
||||
}
|
||||
|
||||
def packageName = "deckers.thibault.aves"
|
||||
|
@ -20,11 +21,6 @@ if (localPropertiesFile.exists()) {
|
|||
}
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
|
||||
// Keys
|
||||
|
||||
|
@ -54,8 +50,8 @@ android {
|
|||
ndkVersion '25.1.8937393'
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
lint {
|
||||
|
@ -181,12 +177,12 @@ android {
|
|||
}
|
||||
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(8)
|
||||
jvmToolchain(17)
|
||||
}
|
||||
|
||||
flutter {
|
||||
|
@ -210,7 +206,7 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
|
@ -226,7 +222,7 @@ dependencies {
|
|||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
// SLF4J implementation for `mp4parser`
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.11'
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.12'
|
||||
|
||||
// forked, built by JitPack:
|
||||
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||
|
@ -240,7 +236,7 @@ dependencies {
|
|||
// huawei flavor only
|
||||
huaweiImplementation "com.huawei.agconnect:agconnect-core:$huawei_agconnect_version"
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.1'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
|
||||
|
||||
kapt 'androidx.annotation:annotation:1.7.1'
|
||||
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
<!-- to access media with original metadata with scoped storage (API >=29) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||
<!-- to provide a foreground service type, as required by Android 14 (API 34) -->
|
||||
<!-- TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_MEDIA_PROCESSING` -->
|
||||
<uses-permission
|
||||
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
|
||||
tools:ignore="SystemPermissionTypo" />
|
||||
|
@ -72,11 +73,11 @@
|
|||
|
||||
<!--
|
||||
allow install on API 19, despite the `minSdk` declared in dependencies:
|
||||
- Google Maps is from API 20
|
||||
- the Security library is from API 21
|
||||
- FFmpegKit for Flutter is from API 24 (when not LTS)
|
||||
- Google Maps is from API 20
|
||||
-->
|
||||
<uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps, androidx.security:security-crypto, com.arthenica.ffmpegkit.flutter" />
|
||||
<uses-sdk tools:overrideLibrary="androidx.security, com.arthenica.ffmpegkit.flutter, io.flutter.plugins.googlemaps" />
|
||||
|
||||
<!-- from Android 11, we should define <queries> to make other apps visible to this app -->
|
||||
<queries>
|
||||
|
@ -258,6 +259,7 @@
|
|||
</receiver>
|
||||
|
||||
<!-- anonymous service for analysis worker is specified here to provide service type -->
|
||||
<!-- TODO TLAD [Android 15 (API 35)] use `mediaProcessing` -->
|
||||
<service
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
|
|
|
@ -176,6 +176,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
// from Android 14 (API 34), foreground service type is mandatory
|
||||
// despite the sample code omitting it at:
|
||||
// https://developer.android.com/guide/background/persistent/how-to/long-running
|
||||
// TODO TLAD [Android 15 (API 35)] use `FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING`
|
||||
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
ForegroundInfo(NOTIFICATION_ID, notification, type)
|
||||
} else {
|
||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves
|
|||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
|
@ -56,7 +57,7 @@ class HomeWidgetSettingsActivity : MainActivity() {
|
|||
finish()
|
||||
}
|
||||
|
||||
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
override fun extractIntentData(intent: Intent?): FieldMap {
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS,
|
||||
INTENT_DATA_KEY_WIDGET_ID to appWidgetId,
|
||||
|
|
|
@ -175,18 +175,6 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mediaStoreChangeStreamHandler.onAppResume()
|
||||
settingsChangeStreamHandler.onAppResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
mediaStoreChangeStreamHandler.onAppPause()
|
||||
settingsChangeStreamHandler.onAppPause()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log.i(LOG_TAG, "onStop")
|
||||
super.onStop()
|
||||
|
@ -242,7 +230,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
|
||||
private fun onEditResult(resultCode: Int, intent: Intent?) {
|
||||
val fields: FieldMap? = if (resultCode == RESULT_OK) hashMapOf(
|
||||
"uri" to intent?.data.toString(),
|
||||
"uri" to intent?.data?.toString(),
|
||||
"mimeType" to intent?.type,
|
||||
) else null
|
||||
pendingEditIntentHandler?.let { it(fields) }
|
||||
|
@ -279,21 +267,19 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
open fun extractIntentData(intent: Intent?): FieldMap {
|
||||
when (val action = intent?.action) {
|
||||
Intent.ACTION_MAIN -> {
|
||||
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_SAFE_MODE to true,
|
||||
val fields = hashMapOf<String, Any?>(
|
||||
INTENT_DATA_KEY_LAUNCHER to intent.hasCategory(Intent.CATEGORY_LAUNCHER),
|
||||
INTENT_DATA_KEY_SAFE_MODE to intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false),
|
||||
)
|
||||
}
|
||||
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
|
||||
val filters = extractFiltersFromIntent(intent)
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_PAGE to page,
|
||||
INTENT_DATA_KEY_FILTERS to filters,
|
||||
)
|
||||
fields[INTENT_DATA_KEY_PAGE] = page
|
||||
fields[INTENT_DATA_KEY_FILTERS] = filters
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
Intent.ACTION_VIEW,
|
||||
|
@ -496,6 +482,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
const val INTENT_DATA_KEY_ACTION = "action"
|
||||
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
|
||||
const val INTENT_DATA_KEY_FILTERS = "filters"
|
||||
const val INTENT_DATA_KEY_LAUNCHER = "launcher"
|
||||
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
|
||||
const val INTENT_DATA_KEY_PAGE = "page"
|
||||
const val INTENT_DATA_KEY_QUERY = "query"
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
import android.content.Intent
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
|
||||
class ScreenSaverSettingsActivity : MainActivity() {
|
||||
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
override fun extractIntentData(intent: Intent?): FieldMap {
|
||||
return hashMapOf(
|
||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS,
|
||||
)
|
||||
|
|
|
@ -14,6 +14,7 @@ import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
|
|||
import deckers.thibault.aves.channel.calls.window.WindowHandler
|
||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
||||
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
|
||||
import deckers.thibault.aves.model.FieldMap
|
||||
import deckers.thibault.aves.utils.FlutterUtils
|
||||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -25,7 +26,7 @@ import io.flutter.plugin.common.MethodCall
|
|||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class WallpaperActivity : FlutterFragmentActivity() {
|
||||
private lateinit var intentDataMap: MutableMap<String, Any?>
|
||||
private lateinit var intentDataMap: FieldMap
|
||||
private lateinit var mediaSessionHandler: MediaSessionHandler
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -103,7 +104,7 @@ class WallpaperActivity : FlutterFragmentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
||||
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 ->
|
||||
|
|
|
@ -36,13 +36,13 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
|
|||
|
||||
val provider = getProvider(context, uri)
|
||||
if (provider == null) {
|
||||
result.error("getEntry-provider", "failed to find provider for uri=$uri", null)
|
||||
result.error("getEntry-provider", "failed to find provider for uri=$uri mimeType=$mimeType", null)
|
||||
return
|
||||
}
|
||||
|
||||
provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback {
|
||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
||||
override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message)
|
||||
override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri mimeType=$mimeType", throwable.message)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import com.drew.metadata.webp.WebpDirectory
|
|||
import com.drew.metadata.xmp.XmpDirectory
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
import deckers.thibault.aves.metadata.ExifGeoTiffTags
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
|
||||
|
@ -110,7 +111,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
when (call.method) {
|
||||
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
||||
"getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) }
|
||||
"getFields" -> ioScope.launch { safe(call, result, ::getFields) }
|
||||
"getOverlayMetadata" -> ioScope.launch { safe(call, result, ::getOverlayMetadata) }
|
||||
"getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) }
|
||||
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
||||
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
|
||||
|
@ -119,6 +120,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
|
||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
||||
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
||||
"getFields" -> ioScope.launch { safe(call, result, ::getFields) }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
@ -815,7 +817,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getFields(call: MethodCall, result: MethodChannel.Result) {
|
||||
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
|
@ -1250,6 +1252,71 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(dateMillis)
|
||||
}
|
||||
|
||||
private fun getFields(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||
val fields = call.argument<List<String>>("fields")
|
||||
if (mimeType == null || uri == null || fields == null) {
|
||||
result.error("getFields-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val metadataMap = HashMap<String, Any>()
|
||||
if (fields.isEmpty() || isVideo(mimeType)) {
|
||||
result.success(metadataMap)
|
||||
return
|
||||
}
|
||||
|
||||
var foundExif = false
|
||||
if (canReadWithMetadataExtractor(mimeType)) {
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||
val metadata = Helper.safeRead(input)
|
||||
for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) {
|
||||
foundExif = true
|
||||
val allTags = ExifInterfaceHelper.allTags
|
||||
fields.forEach { tag ->
|
||||
allTags[tag]?.let { mapper ->
|
||||
val tagType = mapper.type
|
||||
dir.getDescription(tagType)?.let { value -> metadataMap[tag] = value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||
} catch (e: AssertionError) {
|
||||
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundExif && canReadWithExifInterface(mimeType)) {
|
||||
// fallback to read EXIF via ExifInterface
|
||||
try {
|
||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||
val exif = ExifInterface(input)
|
||||
fields.forEach { tag ->
|
||||
if (exif.hasAttribute(tag)) {
|
||||
val value = exif.getAttribute(tag)
|
||||
if (value != null) {
|
||||
metadataMap[tag] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ExifInterface initialization can fail with a RuntimeException
|
||||
// caused by an internal MediaMetadataRetriever failure
|
||||
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for mimeType=$mimeType uri=$uri", e)
|
||||
}
|
||||
}
|
||||
|
||||
result.success(metadataMap)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
||||
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
||||
|
|
|
@ -135,7 +135,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
|
||||
val trashDirs = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }
|
||||
val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.mapNotNull { file -> file?.path } ?: listOf() }
|
||||
val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.filterNotNull()?.mapNotNull { file -> file.path } ?: listOf() }
|
||||
val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()
|
||||
|
||||
result.success(untrackedPaths)
|
||||
|
|
|
@ -12,7 +12,9 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|||
import com.bumptech.glide.request.RequestOptions
|
||||
import deckers.thibault.aves.decoder.MultiPageImage
|
||||
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
|
||||
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.MemoryUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
@ -73,7 +75,7 @@ class RegionFetcher internal constructor(
|
|||
BitmapRegionDecoderCompat.newInstance(input)
|
||||
}
|
||||
if (newDecoder == null) {
|
||||
result.error("getRegion-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
|
||||
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
currentDecoderRef = LastDecoderRef(uri, newDecoder)
|
||||
|
@ -96,14 +98,22 @@ class RegionFetcher internal constructor(
|
|||
regionRect
|
||||
}
|
||||
|
||||
// use `Long` as rect size could be unexpectedly large and go beyond `Int` max
|
||||
val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * effectiveRect.width() * effectiveRect.height() / sampleSize
|
||||
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
|
||||
// decoding a region that large would yield an OOM when creating the bitmap
|
||||
result.error("fetch-large-region", "Region too large for uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
|
||||
val bitmap = decoder.decodeRegion(effectiveRect, options)
|
||||
if (bitmap != null) {
|
||||
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
|
||||
} else {
|
||||
result.error("getRegion-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
|
||||
result.error("fetch-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
|
||||
result.error("fetch-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.caverock.androidsvg.RenderOptions
|
|||
import com.caverock.androidsvg.SVG
|
||||
import com.caverock.androidsvg.SVGParseException
|
||||
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
|
||||
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
|
||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||
import deckers.thibault.aves.utils.MemoryUtils
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
|
@ -116,8 +117,4 @@ class SvgRegionFetcher internal constructor(
|
|||
val uri: Uri,
|
||||
val svg: SVG,
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val ARGB_8888_BYTE_SIZE = 4
|
||||
}
|
||||
}
|
||||
|
|
|
@ -198,7 +198,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
.setDataAndType(AppAdapterHandler.getShareableUri(activity, Uri.parse(uri)), mimeType)
|
||||
|
||||
if (intent.resolveActivity(activity.packageManager) == null) {
|
||||
error("edit-resolve", "cannot resolve activity for this intent", null)
|
||||
error("edit-resolve", "cannot resolve activity for this intent for uri=$uri mimeType=$mimeType", null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
endOfStream()
|
||||
}
|
||||
if (!safeStartActivityForResult(intent, MainActivity.EDIT_REQUEST)) {
|
||||
error("edit-start", "cannot start activity for this intent", null)
|
||||
error("edit-start", "cannot start activity for this intent for uri=$uri mimeType=$mimeType", null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,14 +30,6 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
|
|||
}
|
||||
|
||||
init {
|
||||
onAppResume()
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
onAppPause()
|
||||
}
|
||||
|
||||
fun onAppResume() {
|
||||
Log.i(LOG_TAG, "start listening to Media Store")
|
||||
context.contentResolver.apply {
|
||||
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
||||
|
@ -45,7 +37,7 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
|
|||
}
|
||||
}
|
||||
|
||||
fun onAppPause() {
|
||||
fun dispose() {
|
||||
Log.i(LOG_TAG, "stop listening to Media Store")
|
||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||
}
|
||||
|
|
|
@ -62,19 +62,11 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
|
|||
}
|
||||
|
||||
init {
|
||||
onAppResume()
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
onAppPause()
|
||||
}
|
||||
|
||||
fun onAppResume() {
|
||||
Log.i(LOG_TAG, "start listening to system settings")
|
||||
context.contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, contentObserver)
|
||||
}
|
||||
|
||||
fun onAppPause() {
|
||||
fun dispose() {
|
||||
Log.i(LOG_TAG, "stop listening to system settings")
|
||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ object BitmapUtils {
|
|||
private val freeBaos = ArrayList<ByteArrayOutputStream>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
const val ARGB_8888_BYTE_SIZE = 4
|
||||
|
||||
suspend fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? {
|
||||
val stream: ByteArrayOutputStream
|
||||
mutex.withLock {
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
buildscript {
|
||||
ext {
|
||||
kotlin_version = '1.9.21'
|
||||
ksp_version = "$kotlin_version-1.0.15"
|
||||
agp_version = '8.2.2'
|
||||
glide_version = '4.16.0'
|
||||
// AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550
|
||||
huawei_agconnect_version = '1.9.1.300'
|
||||
|
@ -22,9 +19,6 @@ buildscript {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:$agp_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
|
||||
if (useCrashlytics) {
|
||||
// GMS & Firebase Crashlytics (used by some flavors only)
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
|
|
|
@ -20,8 +20,8 @@ android {
|
|||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app"s APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
|
|
33
android/settings.gradle
Normal file
33
android/settings.gradle
Normal file
|
@ -0,0 +1,33 @@
|
|||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}
|
||||
settings.ext.flutterSdkPath = flutterSdkPath()
|
||||
|
||||
settings.ext.kotlin_version = '1.9.21'
|
||||
settings.ext.ksp_version = "$kotlin_version-1.0.15"
|
||||
settings.ext.agp_version = '8.3.1'
|
||||
|
||||
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version("1.0.0")
|
||||
id("com.android.application") version("$agp_version") apply(false)
|
||||
id("org.jetbrains.kotlin.android") version("$kotlin_version") apply(false)
|
||||
id("com.google.devtools.ksp") version("$ksp_version") apply(false)
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
|
||||
}
|
||||
|
||||
include(":app")
|
||||
include(":exifinterface")
|
|
@ -1,27 +0,0 @@
|
|||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version ("0.4.0")
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
val localPropertiesFile = File(rootProject.projectDir, "local.properties")
|
||||
val properties = java.util.Properties()
|
||||
|
||||
assert(localPropertiesFile.exists())
|
||||
localPropertiesFile.reader(Charsets.UTF_8).also { reader -> properties.load(reader) }
|
||||
|
||||
val flutterSdkPath: String? = properties.getProperty("flutter.sdk")
|
||||
assert(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
|
||||
apply {
|
||||
from("$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle")
|
||||
}
|
||||
include(":exifinterface")
|
|
@ -1,5 +0,0 @@
|
|||
In v1.9.4:
|
||||
- play your animated AVIF, AV1, and HDR videos
|
||||
- filter by rating ranges
|
||||
- judge tonal distributions with the viewer histogram
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.9.4:
|
||||
- play your animated AVIF, AV1, and HDR videos
|
||||
- filter by rating ranges
|
||||
- judge tonal distributions with the viewer histogram
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.9.5:
|
||||
- play your animated AVIF, AV1, and HDR videos
|
||||
- filter by rating ranges
|
||||
- judge tonal distributions with the viewer histogram
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.9.5:
|
||||
- play your animated AVIF, AV1, and HDR videos
|
||||
- filter by rating ranges
|
||||
- judge tonal distributions with the viewer histogram
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.9.6:
|
||||
- play your animated AVIF, AV1, and HDR videos
|
||||
- filter by rating ranges
|
||||
- judge tonal distributions with the viewer histogram
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +0,0 @@
|
|||
In v1.9.6:
|
||||
- play your animated AVIF, AV1, and HDR videos
|
||||
- filter by rating ranges
|
||||
- judge tonal distributions with the viewer histogram
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.9.7:
|
||||
- enjoy the app in Slovak & Vietnamese
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.9.7:
|
||||
- enjoy the app in Slovak & Vietnamese
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.0:
|
||||
- cast images via DLNA/UPnP
|
||||
- enjoy the app in Icelandic
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.0:
|
||||
- cast images via DLNA/UPnP
|
||||
- enjoy the app in Icelandic
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.1:
|
||||
- JPEG MPF support
|
||||
- enjoy the app in Arabic & Belarusian
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.1:
|
||||
- JPEG MPF support
|
||||
- enjoy the app in Arabic & Belarusian
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.2:
|
||||
- JPEG MPF support
|
||||
- enjoy the app in Arabic & Belarusian
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.2:
|
||||
- JPEG MPF support
|
||||
- enjoy the app in Arabic & Belarusian
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.3:
|
||||
- customize your home page
|
||||
- analyze your images with the histogram (for real this time)
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.3:
|
||||
- customize your home page
|
||||
- analyze your images with the histogram (for real this time)
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.4:
|
||||
- customize your home page
|
||||
- analyze your images with the histogram (for real this time)
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.10.4:
|
||||
- customize your home page
|
||||
- analyze your images with the histogram (for real this time)
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.10.5:
|
||||
- enjoy the app in Catalan
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.10.5:
|
||||
- enjoy the app in Catalan
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/117.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/117.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.10.8:
|
||||
- rename in bulk using tags
|
||||
- repeat a section section section of a video
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/11701.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/11701.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.10.8:
|
||||
- rename in bulk using tags
|
||||
- repeat a section section section of a video
|
||||
Full changelog available on GitHub
|
|
@ -43,6 +43,8 @@ extension ExtraMetadataFieldConvert on MetadataField {
|
|||
case MetadataField.exifGpsTrackRef:
|
||||
case MetadataField.exifGpsVersionId:
|
||||
case MetadataField.exifImageDescription:
|
||||
case MetadataField.exifMake:
|
||||
case MetadataField.exifModel:
|
||||
case MetadataField.exifUserComment:
|
||||
return MetadataType.exif;
|
||||
case MetadataField.mp4GpsCoordinates:
|
||||
|
@ -145,6 +147,10 @@ extension ExtraMetadataFieldConvert on MetadataField {
|
|||
return 'GPSVersionID';
|
||||
case MetadataField.exifImageDescription:
|
||||
return 'ImageDescription';
|
||||
case MetadataField.exifMake:
|
||||
return 'Make';
|
||||
case MetadataField.exifModel:
|
||||
return 'Model';
|
||||
case MetadataField.exifUserComment:
|
||||
return 'UserComment';
|
||||
default:
|
||||
|
|
|
@ -1524,5 +1524,13 @@
|
|||
"collectionActionSetHome": "تعيين كخلفية",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "مجموعة مخصصة",
|
||||
"@setHomeCustomCollection": {}
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoActionABRepeat": "تكرار A-B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل",
|
||||
"@videoRepeatActionSetEnd": {},
|
||||
"stopTooltip": "توقف",
|
||||
"@stopTooltip": {},
|
||||
"videoRepeatActionSetStart": "تعيين بداية التشغيل",
|
||||
"@videoRepeatActionSetStart": {}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"@welcomeTermsToggle": {},
|
||||
"welcomeOptional": "Неабавязковыя",
|
||||
"@welcomeOptional": {},
|
||||
"welcomeMessage": "Сардэчна запрашаем у Aves",
|
||||
"welcomeMessage": "Сардэчна запрашаем ў Aves",
|
||||
"@welcomeMessage": {},
|
||||
"itemCount": "{count, plural, =1{1 элемент} other{{count} элементаў}}",
|
||||
"@itemCount": {
|
||||
|
@ -38,7 +38,7 @@
|
|||
"@saveTooltip": {},
|
||||
"doNotAskAgain": "Больш не пытайся",
|
||||
"@doNotAskAgain": {},
|
||||
"chipActionGoToCountryPage": "Паказаць у краінах",
|
||||
"chipActionGoToCountryPage": "Паказаць ў Краінах",
|
||||
"@chipActionGoToCountryPage": {},
|
||||
"chipActionFilterOut": "Адфільтраваць",
|
||||
"@chipActionFilterOut": {},
|
||||
|
@ -56,17 +56,17 @@
|
|||
"@sourceStateCataloguing": {},
|
||||
"chipActionDelete": "Выдаліць",
|
||||
"@chipActionDelete": {},
|
||||
"chipActionGoToAlbumPage": "Паказаць у альбомах",
|
||||
"chipActionGoToAlbumPage": "Паказаць ў Альбомах",
|
||||
"@chipActionGoToAlbumPage": {},
|
||||
"chipActionHide": "Схаваць",
|
||||
"@chipActionHide": {},
|
||||
"chipActionCreateVault": "Стварыце сховішча",
|
||||
"@chipActionCreateVault": {},
|
||||
"chipActionGoToPlacePage": "Паказаць у месцах",
|
||||
"chipActionGoToPlacePage": "Паказаць ў Лакацыях",
|
||||
"@chipActionGoToPlacePage": {},
|
||||
"chipActionUnpin": "Адмацаваць зверху",
|
||||
"@chipActionUnpin": {},
|
||||
"chipActionGoToTagPage": "Паказаць у тэгах",
|
||||
"chipActionGoToTagPage": "Паказаць ў Тэгах",
|
||||
"@chipActionGoToTagPage": {},
|
||||
"chipActionLock": "Заблакаваць",
|
||||
"@chipActionLock": {},
|
||||
|
@ -76,7 +76,7 @@
|
|||
"@chipActionRename": {},
|
||||
"chipActionConfigureVault": "Наладзіць сховішча",
|
||||
"@chipActionConfigureVault": {},
|
||||
"entryActionCopyToClipboard": "Скапіраваць у буфер абмену",
|
||||
"entryActionCopyToClipboard": "Скапіяваць ў буфер абмену",
|
||||
"@entryActionCopyToClipboard": {},
|
||||
"entryActionDelete": "Выдаліць",
|
||||
"@entryActionDelete": {},
|
||||
|
@ -120,13 +120,13 @@
|
|||
"@entryActionRotateScreen": {},
|
||||
"entryActionViewSource": "Паглядзець крыніцу",
|
||||
"@entryActionViewSource": {},
|
||||
"entryActionConvertMotionPhotoToStillImage": "Пераўтварыць у нерухомую выяву",
|
||||
"entryActionConvertMotionPhotoToStillImage": "Пераўтварыць ў нерухомую выяву",
|
||||
"@entryActionConvertMotionPhotoToStillImage": {},
|
||||
"entryActionViewMotionPhotoVideo": "Адкрыць відэа",
|
||||
"@entryActionViewMotionPhotoVideo": {},
|
||||
"entryActionSetAs": "Ўсталяваць як",
|
||||
"@entryActionSetAs": {},
|
||||
"entryActionAddFavourite": "Дадаць у абранае",
|
||||
"entryActionAddFavourite": "Дадаць ў абранае",
|
||||
"@entryActionAddFavourite": {},
|
||||
"videoActionUnmute": "Ўключыць гук",
|
||||
"@videoActionUnmute": {},
|
||||
|
@ -188,11 +188,11 @@
|
|||
"@entryActionEdit": {},
|
||||
"entryActionOpen": "Адкрыць з дапамогай",
|
||||
"@entryActionOpen": {},
|
||||
"entryActionOpenMap": "Паказаць у праграме карты",
|
||||
"entryActionOpenMap": "Паказаць ў праграме карты",
|
||||
"@entryActionOpenMap": {},
|
||||
"videoActionMute": "Адключыць гук",
|
||||
"@videoActionMute": {},
|
||||
"slideshowActionShowInCollection": "Паказаць у калекцыі",
|
||||
"slideshowActionShowInCollection": "Паказаць ў Калекцыі",
|
||||
"@slideshowActionShowInCollection": {},
|
||||
"entryInfoActionEditDate": "Рэдагаваць дату і час",
|
||||
"@entryInfoActionEditDate": {},
|
||||
|
@ -363,7 +363,7 @@
|
|||
"@vaultLockTypePassword": {},
|
||||
"settingsVideoEnablePip": "Карцінка ў карцінцы",
|
||||
"@settingsVideoEnablePip": {},
|
||||
"videoControlsPlayOutside": "Адкрыць у іншым прайгравальніку",
|
||||
"videoControlsPlayOutside": "Адкрыць ў іншым прайгравальніку",
|
||||
"@videoControlsPlayOutside": {},
|
||||
"videoControlsPlay": "Прайграванне",
|
||||
"@videoControlsPlay": {},
|
||||
|
@ -478,7 +478,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"addShortcutDialogLabel": "Ярлык хуткага доступу",
|
||||
"addShortcutDialogLabel": "Назва ярлыка",
|
||||
"@addShortcutDialogLabel": {},
|
||||
"addShortcutButtonLabel": "ДАДАЦЬ",
|
||||
"@addShortcutButtonLabel": {},
|
||||
|
@ -507,7 +507,7 @@
|
|||
"@newAlbumDialogTitle": {},
|
||||
"newAlbumDialogNameLabel": "Назва альбома",
|
||||
"@newAlbumDialogNameLabel": {},
|
||||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Каталог ужо існуе",
|
||||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Каталог ўжо існуе",
|
||||
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
||||
"newAlbumDialogStorageLabel": "Захоўванне:",
|
||||
"@newAlbumDialogStorageLabel": {},
|
||||
|
@ -691,13 +691,13 @@
|
|||
"@aboutBugReportInstruction": {},
|
||||
"entryActionCast": "Трансляцыя",
|
||||
"@entryActionCast": {},
|
||||
"hideFilterConfirmationDialogMessage": "Адпаведныя фота і відэа будуць схаваны з вашай калекцыі. Вы можаце убачыць іх зноў у наладах «Прыватнасць».\n\nВы ўпэўнены, што хочаце іх схаваць?",
|
||||
"hideFilterConfirmationDialogMessage": "Адпаведныя фота і відэа будуць схаваны з вашай калекцыі. Вы можаце убачыць іх зноў ў наладах «Прыватнасць».\n\nВы ўпэўнены, што хочаце іх схаваць?",
|
||||
"@hideFilterConfirmationDialogMessage": {},
|
||||
"renameEntrySetPagePatternFieldLabel": "Шаблон наймення",
|
||||
"@renameEntrySetPagePatternFieldLabel": {},
|
||||
"renameAlbumDialogLabel": "Новая назва",
|
||||
"@renameAlbumDialogLabel": {},
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "Каталог ужо ёсць",
|
||||
"renameAlbumDialogLabelAlreadyExistsHelper": "Каталог ўжо ёсць",
|
||||
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
||||
"aboutBugReportButton": "Адправіць справаздачу",
|
||||
"@aboutBugReportButton": {},
|
||||
|
@ -781,7 +781,7 @@
|
|||
"@statsPageTitle": {},
|
||||
"settingsUnitSystemDialogTitle": "Адзінкі вымярэння",
|
||||
"@settingsUnitSystemDialogTitle": {},
|
||||
"editEntryDialogCopyFromItem": "Скапіюваць з іншага элемента",
|
||||
"editEntryDialogCopyFromItem": "Скапіяваць з іншага элемента",
|
||||
"@editEntryDialogCopyFromItem": {},
|
||||
"settingsThemeEnableDynamicColor": "Дынамічны колер",
|
||||
"@settingsThemeEnableDynamicColor": {},
|
||||
|
@ -791,13 +791,13 @@
|
|||
"@renameEntrySetPageInsertTooltip": {},
|
||||
"settingsThemeBrightnessTile": "Тэма",
|
||||
"@settingsThemeBrightnessTile": {},
|
||||
"settingsSystemDefault": "Як у сістэме",
|
||||
"settingsSystemDefault": "Як ў сістэме",
|
||||
"@settingsSystemDefault": {},
|
||||
"settingsCollectionTile": "Калекцыя",
|
||||
"@settingsCollectionTile": {},
|
||||
"settingsThemeBrightnessDialogTitle": "Тэма",
|
||||
"@settingsThemeBrightnessDialogTitle": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэты альбом і элемент у ім?} few{Выдаліць гэты альбом і {count} элементы у ім?} other{Выдаліць гэты альбом і {count} элементаў у ім?}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэты альбом і элемент ў ім?} few{Выдаліць гэты альбом і {count} элементы ў ім?} other{Выдаліць гэты альбом і {count} элементаў ў ім?}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -853,7 +853,7 @@
|
|||
"@tileLayoutMosaic": {},
|
||||
"collectionDeselectSectionTooltip": "Адмяніць выбар раздзела",
|
||||
"@collectionDeselectSectionTooltip": {},
|
||||
"settingsKeepScreenOnTile": "Трымаць экран уключаным",
|
||||
"settingsKeepScreenOnTile": "Трымаць экран ўключаным",
|
||||
"@settingsKeepScreenOnTile": {},
|
||||
"tileLayoutGrid": "Сетка",
|
||||
"@tileLayoutGrid": {},
|
||||
|
@ -895,7 +895,7 @@
|
|||
"@searchCountriesSectionTitle": {},
|
||||
"settingsAskEverytime": "Пытацца кожны раз",
|
||||
"@settingsAskEverytime": {},
|
||||
"editEntryDateDialogCopyField": "Капіяваць з іншай даты",
|
||||
"editEntryDateDialogCopyField": "Скапіяваць з іншай даты",
|
||||
"@editEntryDateDialogCopyField": {},
|
||||
"searchTagsSectionTitle": "Тэгі",
|
||||
"@searchTagsSectionTitle": {},
|
||||
|
@ -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": {},
|
||||
|
@ -1149,7 +1149,7 @@
|
|||
"@coverDialogTabColor": {},
|
||||
"genericSuccessFeedback": "Гатова!",
|
||||
"@genericSuccessFeedback": {},
|
||||
"aboutLicensesShowAllButtonLabel": "Паказаць усе ліцэнзіі",
|
||||
"aboutLicensesShowAllButtonLabel": "Паказаць ўсе ліцэнзіі",
|
||||
"@aboutLicensesShowAllButtonLabel": {},
|
||||
"sortOrderNewestFirst": "Спачатку самае новае",
|
||||
"@sortOrderNewestFirst": {},
|
||||
|
@ -1175,7 +1175,7 @@
|
|||
"@menuActionStats": {},
|
||||
"appPickDialogTitle": "Выбраць праграму",
|
||||
"@appPickDialogTitle": {},
|
||||
"albumPickPageTitleMove": "Перамясціць у альбом",
|
||||
"albumPickPageTitleMove": "Перамясціць ў альбом",
|
||||
"@albumPickPageTitleMove": {},
|
||||
"coverDialogTabCover": "Вокладка",
|
||||
"@coverDialogTabCover": {},
|
||||
|
@ -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": {}
|
||||
|
@ -1524,5 +1524,13 @@
|
|||
"setHomeCustomCollection": "Ўласная калекцыя",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {}
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"videoRepeatActionSetEnd": "Ўсталяваць канец",
|
||||
"@videoRepeatActionSetEnd": {},
|
||||
"stopTooltip": "Спыніць",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "Паўтарыць ад А да Б",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetStart": "Ўсталяваць пачатак",
|
||||
"@videoRepeatActionSetStart": {}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
"actionRemove": "Remove",
|
||||
"resetTooltip": "Reset",
|
||||
"saveTooltip": "Save",
|
||||
"stopTooltip": "Stop",
|
||||
"pickTooltip": "Pick",
|
||||
|
||||
"doubleBackExitMessage": "Tap “back” again to exit.",
|
||||
|
@ -127,6 +128,10 @@
|
|||
"videoActionSkip10": "Seek forward 10 seconds",
|
||||
"videoActionSelectStreams": "Select tracks",
|
||||
"videoActionSetSpeed": "Playback speed",
|
||||
"videoActionABRepeat": "A-B repeat",
|
||||
|
||||
"videoRepeatActionSetStart": "Set start",
|
||||
"videoRepeatActionSetEnd": "Set end",
|
||||
|
||||
"viewerActionSettings": "Settings",
|
||||
"viewerActionLock": "Lock viewer",
|
||||
|
|
|
@ -1366,5 +1366,13 @@
|
|||
"collectionActionSetHome": "Fijar como inicio",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Colección personalizada",
|
||||
"@setHomeCustomCollection": {}
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoRepeatActionSetStart": "Fijar el inicio",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"stopTooltip": "Parar",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "Repetir de A a B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetEnd": "Fijar el fin",
|
||||
"@videoRepeatActionSetEnd": {}
|
||||
}
|
||||
|
|
|
@ -1366,5 +1366,13 @@
|
|||
"setHomeCustomCollection": "Collection personnalisée",
|
||||
"@setHomeCustomCollection": {},
|
||||
"settingsThumbnailShowHdrIcon": "Afficher l’icône HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {}
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"videoRepeatActionSetEnd": "Définir la fin",
|
||||
"@videoRepeatActionSetEnd": {},
|
||||
"stopTooltip": "Arrêter",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "Lecture répétée A-B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetStart": "Définir le début",
|
||||
"@videoRepeatActionSetStart": {}
|
||||
}
|
||||
|
|
|
@ -1366,5 +1366,13 @@
|
|||
"settingsThumbnailShowHdrIcon": "Tampilkan ikon HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"castDialogTitle": "Siarkan Perangkat",
|
||||
"@castDialogTitle": {}
|
||||
"@castDialogTitle": {},
|
||||
"stopTooltip": "Berhenti",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "Ulang A-B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetStart": "Tetapkan awal",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"videoRepeatActionSetEnd": "Tetapkan akhir",
|
||||
"@videoRepeatActionSetEnd": {}
|
||||
}
|
||||
|
|
|
@ -1235,7 +1235,7 @@
|
|||
"@lengthUnitPercent": {},
|
||||
"saveCopyButtonLabel": "コピーを保存",
|
||||
"@saveCopyButtonLabel": {},
|
||||
"columnCount": "{count, plural, =1{1 列} other{{count} 列}}",
|
||||
"columnCount": "{count, plural, other{{count} 列}}",
|
||||
"@columnCount": {
|
||||
"placeholders": {
|
||||
"count": {}
|
||||
|
@ -1246,5 +1246,23 @@
|
|||
"entryActionCast": "キャスト",
|
||||
"@entryActionCast": {},
|
||||
"editorTransformRotate": "回転",
|
||||
"@editorTransformRotate": {}
|
||||
"@editorTransformRotate": {},
|
||||
"settingsVideoResumptionModeTile": "前回の位置からの再生",
|
||||
"@settingsVideoResumptionModeTile": {},
|
||||
"settingsAskEverytime": "都度選択",
|
||||
"@settingsAskEverytime": {},
|
||||
"maxBrightnessNever": "無効",
|
||||
"@maxBrightnessNever": {},
|
||||
"settingsVideoResumptionModeDialogTitle": "前回の位置からの再生",
|
||||
"@settingsVideoResumptionModeDialogTitle": {},
|
||||
"settingsVideoBackgroundMode": "バックグラウンド再生",
|
||||
"@settingsVideoBackgroundMode": {},
|
||||
"settingsVideoGestureVerticalDragBrightnessVolume": "左右で上下にスワイプして輝度と音量を調節",
|
||||
"@settingsVideoGestureVerticalDragBrightnessVolume": {},
|
||||
"settingsViewerShowHistogram": "ヒストグラムを表示",
|
||||
"@settingsViewerShowHistogram": {},
|
||||
"settingsVideoBackgroundModeDialogTitle": "バックグラウンド再生",
|
||||
"@settingsVideoBackgroundModeDialogTitle": {},
|
||||
"maxBrightnessAlways": "常時有効",
|
||||
"@maxBrightnessAlways": {}
|
||||
}
|
||||
|
|
|
@ -1366,5 +1366,13 @@
|
|||
"collectionActionSetHome": "홈으로 설정",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "지정 미디어",
|
||||
"@setHomeCustomCollection": {}
|
||||
"@setHomeCustomCollection": {},
|
||||
"videoRepeatActionSetStart": "시작 지점 설정",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"videoRepeatActionSetEnd": "종료 지점 설정",
|
||||
"@videoRepeatActionSetEnd": {},
|
||||
"stopTooltip": "취소",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "A-B 반복",
|
||||
"@videoActionABRepeat": {}
|
||||
}
|
||||
|
|
|
@ -1524,5 +1524,13 @@
|
|||
"setHomeCustomCollection": "Własna kolekcja",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Ustaw jako stronę główną",
|
||||
"@collectionActionSetHome": {}
|
||||
"@collectionActionSetHome": {},
|
||||
"videoRepeatActionSetStart": "Ustaw początek",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"stopTooltip": "Zatrzymaj",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "Powtarzanie A-B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetEnd": "Ustaw koniec",
|
||||
"@videoRepeatActionSetEnd": {}
|
||||
}
|
||||
|
|
|
@ -1360,5 +1360,11 @@
|
|||
"entryActionCast": "Transmitir",
|
||||
"@entryActionCast": {},
|
||||
"castDialogTitle": "Dispositivos para Transmitir",
|
||||
"@castDialogTitle": {}
|
||||
"@castDialogTitle": {},
|
||||
"settingsThumbnailShowHdrIcon": "Mostrar ícone HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"collectionActionSetHome": "Definir como início",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Coleção personalizada",
|
||||
"@setHomeCustomCollection": {}
|
||||
}
|
||||
|
|
|
@ -1366,5 +1366,13 @@
|
|||
"setHomeCustomCollection": "Собственная коллекция",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Установить как главную",
|
||||
"@collectionActionSetHome": {}
|
||||
"@collectionActionSetHome": {},
|
||||
"videoRepeatActionSetStart": "Установить начало",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"stopTooltip": "Остановить",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "Повторить от А до Б",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetEnd": "Установить конец",
|
||||
"@videoRepeatActionSetEnd": {}
|
||||
}
|
||||
|
|
|
@ -1524,5 +1524,13 @@
|
|||
"setHomeCustomCollection": "Власна колекція",
|
||||
"@setHomeCustomCollection": {},
|
||||
"collectionActionSetHome": "Встановити як головну",
|
||||
"@collectionActionSetHome": {}
|
||||
"@collectionActionSetHome": {},
|
||||
"videoRepeatActionSetStart": "Змінити початок",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"videoRepeatActionSetEnd": "Змінити кінець",
|
||||
"@videoRepeatActionSetEnd": {},
|
||||
"stopTooltip": "Зупинити",
|
||||
"@stopTooltip": {},
|
||||
"videoActionABRepeat": "Повторити від А до Б",
|
||||
"@videoActionABRepeat": {}
|
||||
}
|
||||
|
|
|
@ -79,6 +79,7 @@ class Contributors {
|
|||
Contributor('luckris25', 'lk1thebestl@gmail.com'),
|
||||
Contributor('Marc Amorós', 'marquitus99@gmail.com'),
|
||||
Contributor('elea11', 'p.manuel.warnecke@gmail.com'),
|
||||
Contributor('しいたけ', 'Shiitake@users.noreply.hosted.weblate.org'),
|
||||
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
||||
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
||||
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
|
||||
|
|
|
@ -33,8 +33,8 @@ class LiveAvesAvailability implements AvesAvailability {
|
|||
return _isConnected!;
|
||||
}
|
||||
|
||||
void _updateConnectivityFromResult(ConnectivityResult result) {
|
||||
final newValue = result != ConnectivityResult.none;
|
||||
void _updateConnectivityFromResult(List<ConnectivityResult> result) {
|
||||
final newValue = result.isNotEmpty && !result.contains(ConnectivityResult.none);
|
||||
if (_isConnected != newValue) {
|
||||
_isConnected = newValue;
|
||||
debugPrint('Device is connected=$_isConnected');
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/entry/entry.dart';
|
|||
import 'package:aves/model/entry/extensions/catalog.dart';
|
||||
import 'package:aves/model/entry/extensions/props.dart';
|
||||
import 'package:aves/model/metadata/date_modifier.dart';
|
||||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/ref/metadata/exif.dart';
|
||||
import 'package:aves/ref/metadata/iptc.dart';
|
||||
import 'package:aves/ref/metadata/xmp.dart';
|
||||
|
@ -121,7 +122,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
|||
if (latLng != null && latLng != removalLocation) {
|
||||
final latitude = latLng.latitude;
|
||||
final longitude = latLng.longitude;
|
||||
const locale = 'en_US';
|
||||
const locale = asciiLocale;
|
||||
final isoLat = '${latitude >= 0 ? '+' : '-'}${NumberFormat('00.0000', locale).format(latitude.abs())}';
|
||||
final isoLon = '${longitude >= 0 ? '+' : '-'}${NumberFormat('000.0000', locale).format(longitude.abs())}';
|
||||
iso6709String = '$isoLat$isoLon/';
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import 'package:aves/convert/metadata/fields.dart';
|
||||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
@ -16,6 +20,7 @@ class NamingPattern {
|
|||
factory NamingPattern.from({
|
||||
required String userPattern,
|
||||
required int entryCount,
|
||||
required String locale,
|
||||
}) {
|
||||
final processors = <NamingProcessor>[];
|
||||
|
||||
|
@ -36,7 +41,13 @@ class NamingPattern {
|
|||
switch (processorKey) {
|
||||
case DateNamingProcessor.key:
|
||||
if (processorOptions != null) {
|
||||
processors.add(DateNamingProcessor(processorOptions.trim()));
|
||||
processors.add(DateNamingProcessor(processorOptions.trim(), locale));
|
||||
}
|
||||
case TagsNamingProcessor.key:
|
||||
processors.add(TagsNamingProcessor(processorOptions?.trim() ?? ''));
|
||||
case MetadataFieldNamingProcessor.key:
|
||||
if (processorOptions != null) {
|
||||
processors.add(MetadataFieldNamingProcessor(processorOptions.trim()));
|
||||
}
|
||||
case NameNamingProcessor.key:
|
||||
processors.add(const NameNamingProcessor());
|
||||
|
@ -95,21 +106,33 @@ class NamingPattern {
|
|||
switch (processorKey) {
|
||||
case DateNamingProcessor.key:
|
||||
return '<$processorKey, yyyyMMdd-HHmmss>';
|
||||
case TagsNamingProcessor.key:
|
||||
return '<$processorKey, ->';
|
||||
case CounterNamingProcessor.key:
|
||||
case NameNamingProcessor.key:
|
||||
default:
|
||||
if (processorKey.startsWith(MetadataFieldNamingProcessor.key)) {
|
||||
final field = MetadataFieldNamingProcessor.fieldFromKey(processorKey);
|
||||
return '<${MetadataFieldNamingProcessor.key}, $field>';
|
||||
}
|
||||
return '<$processorKey>';
|
||||
}
|
||||
}
|
||||
|
||||
String apply(AvesEntry entry, int index) => processors.map((v) => v.process(entry, index) ?? '').join().trimLeft();
|
||||
Future<String> apply(AvesEntry entry, int index) async {
|
||||
final fields = processors.expand((v) => v.getRequiredFields()).toSet();
|
||||
final fieldValues = await metadataFetchService.getFields(entry, fields);
|
||||
return processors.map((v) => v.process(entry, index, fieldValues) ?? '').join().trim();
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
abstract class NamingProcessor extends Equatable {
|
||||
const NamingProcessor();
|
||||
|
||||
String? process(AvesEntry entry, int index);
|
||||
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues);
|
||||
|
||||
Set<MetadataField> getRequiredFields() => {};
|
||||
}
|
||||
|
||||
@immutable
|
||||
|
@ -122,7 +145,7 @@ class LiteralNamingProcessor extends NamingProcessor {
|
|||
const LiteralNamingProcessor(this.text);
|
||||
|
||||
@override
|
||||
String? process(AvesEntry entry, int index) => text;
|
||||
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => text;
|
||||
}
|
||||
|
||||
@immutable
|
||||
|
@ -134,15 +157,63 @@ class DateNamingProcessor extends NamingProcessor {
|
|||
@override
|
||||
List<Object?> get props => [format.pattern];
|
||||
|
||||
DateNamingProcessor(String pattern) : format = DateFormat(pattern);
|
||||
DateNamingProcessor(String pattern, String locale) : format = DateFormat(pattern, locale);
|
||||
|
||||
@override
|
||||
String? process(AvesEntry entry, int index) {
|
||||
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
|
||||
final date = entry.bestDate;
|
||||
return date != null ? format.format(date) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class TagsNamingProcessor extends NamingProcessor {
|
||||
static const key = 'tags';
|
||||
static const defaultSeparator = ' ';
|
||||
|
||||
final String separator;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [separator];
|
||||
|
||||
TagsNamingProcessor(String separator) : separator = separator.isEmpty ? defaultSeparator : separator;
|
||||
|
||||
@override
|
||||
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
|
||||
return entry.tags.join(separator);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class MetadataFieldNamingProcessor extends NamingProcessor {
|
||||
static const key = 'field';
|
||||
|
||||
static String keyWithField(MetadataField field) => '$key-${field.name}';
|
||||
|
||||
// loose, for user to see and later parse
|
||||
static String fieldFromKey(String keyWithField) => keyWithField.substring(key.length + 1);
|
||||
|
||||
late final MetadataField? field;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [field];
|
||||
|
||||
MetadataFieldNamingProcessor(String field) {
|
||||
final lowerField = field.toLowerCase();
|
||||
this.field = MetadataField.values.firstWhereOrNull((v) => v.name.toLowerCase() == lowerField);
|
||||
}
|
||||
|
||||
@override
|
||||
Set<MetadataField> getRequiredFields() {
|
||||
return {field}.whereNotNull().toSet();
|
||||
}
|
||||
|
||||
@override
|
||||
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
|
||||
return fieldValues[field?.toPlatform]?.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class NameNamingProcessor extends NamingProcessor {
|
||||
static const key = 'name';
|
||||
|
@ -153,7 +224,7 @@ class NameNamingProcessor extends NamingProcessor {
|
|||
const NameNamingProcessor();
|
||||
|
||||
@override
|
||||
String? process(AvesEntry entry, int index) => entry.filenameWithoutExtension;
|
||||
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => entry.filenameWithoutExtension;
|
||||
}
|
||||
|
||||
@immutable
|
||||
|
@ -174,5 +245,5 @@ class CounterNamingProcessor extends NamingProcessor {
|
|||
});
|
||||
|
||||
@override
|
||||
String? process(AvesEntry entry, int index) => '${index + start}'.padLeft(padding, '0');
|
||||
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => '${index + start}'.padLeft(padding, '0');
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ mixin AppSettings on SettingsAccess {
|
|||
].join(localeSeparator);
|
||||
}
|
||||
set(SettingKeys.localeKey, tag);
|
||||
_appliedLocale = null;
|
||||
resetAppliedLocale();
|
||||
}
|
||||
|
||||
List<Locale> _systemLocalesFallback = [];
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/defaults.dart';
|
||||
import 'package:aves/model/settings/modules/search.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
mixin FilterGridsSettings on SettingsAccess, SearchSettings {
|
||||
mixin FilterGridsSettings on SettingsAccess {
|
||||
AlbumChipGroupFactor get albumGroupFactor => getEnumOrDefault(SettingKeys.albumGroupFactorKey, SettingsDefaults.albumGroupFactor, AlbumChipGroupFactor.values);
|
||||
|
||||
set albumGroupFactor(AlbumChipGroupFactor newValue) => set(SettingKeys.albumGroupFactorKey, newValue.toString());
|
||||
|
@ -53,21 +52,6 @@ mixin FilterGridsSettings on SettingsAccess, SearchSettings {
|
|||
|
||||
set pinnedFilters(Set<CollectionFilter> newValue) => set(SettingKeys.pinnedFiltersKey, newValue.map((filter) => filter.toJson()).toList());
|
||||
|
||||
Set<CollectionFilter> get hiddenFilters => (getStringList(SettingKeys.hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
|
||||
|
||||
set hiddenFilters(Set<CollectionFilter> newValue) => set(SettingKeys.hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList());
|
||||
|
||||
void changeFilterVisibility(Set<CollectionFilter> filters, bool visible) {
|
||||
final _hiddenFilters = hiddenFilters;
|
||||
if (visible) {
|
||||
_hiddenFilters.removeAll(filters);
|
||||
} else {
|
||||
_hiddenFilters.addAll(filters);
|
||||
searchHistory = searchHistory..removeWhere(filters.contains);
|
||||
}
|
||||
hiddenFilters = _hiddenFilters;
|
||||
}
|
||||
|
||||
bool get showAlbumPickQuery => getBool(SettingKeys.showAlbumPickQueryKey) ?? false;
|
||||
|
||||
set showAlbumPickQuery(bool newValue) => set(SettingKeys.showAlbumPickQueryKey, newValue);
|
||||
|
|
42
lib/model/settings/modules/privacy.dart
Normal file
42
lib/model/settings/modules/privacy.dart
Normal file
|
@ -0,0 +1,42 @@
|
|||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/modules/search.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
mixin PrivacySettings on SettingsAccess, SearchSettings {
|
||||
Set<CollectionFilter> get hiddenFilters => (getStringList(SettingKeys.hiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
|
||||
|
||||
set hiddenFilters(Set<CollectionFilter> newValue) => set(SettingKeys.hiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList());
|
||||
|
||||
void changeFilterVisibility(Set<CollectionFilter> filters, bool visible) {
|
||||
final _hiddenFilters = hiddenFilters;
|
||||
if (visible) {
|
||||
_hiddenFilters.removeAll(filters);
|
||||
} else {
|
||||
_hiddenFilters.addAll(filters);
|
||||
searchHistory = searchHistory..removeWhere(filters.contains);
|
||||
|
||||
final _deactivatedHiddenFilters = deactivatedHiddenFilters;
|
||||
_deactivatedHiddenFilters.removeAll(filters);
|
||||
deactivatedHiddenFilters = _deactivatedHiddenFilters;
|
||||
}
|
||||
hiddenFilters = _hiddenFilters;
|
||||
}
|
||||
|
||||
Set<CollectionFilter> get deactivatedHiddenFilters => (getStringList(SettingKeys.deactivatedHiddenFiltersKey) ?? []).map(CollectionFilter.fromJson).whereNotNull().toSet();
|
||||
|
||||
set deactivatedHiddenFilters(Set<CollectionFilter> newValue) => set(SettingKeys.deactivatedHiddenFiltersKey, newValue.map((filter) => filter.toJson()).toList());
|
||||
|
||||
void activateHiddenFilter(CollectionFilter filter, bool active) {
|
||||
final _deactivatedHiddenFilters = deactivatedHiddenFilters;
|
||||
if (active) {
|
||||
_deactivatedHiddenFilters.remove(filter);
|
||||
} else {
|
||||
_deactivatedHiddenFilters.add(filter);
|
||||
}
|
||||
deactivatedHiddenFilters = _deactivatedHiddenFilters;
|
||||
|
||||
final visible = !active;
|
||||
changeFilterVisibility({filter}, visible);
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import 'package:aves/model/settings/modules/display.dart';
|
|||
import 'package:aves/model/settings/modules/filter_grids.dart';
|
||||
import 'package:aves/model/settings/modules/info.dart';
|
||||
import 'package:aves/model/settings/modules/navigation.dart';
|
||||
import 'package:aves/model/settings/modules/privacy.dart';
|
||||
import 'package:aves/model/settings/modules/search.dart';
|
||||
import 'package:aves/model/settings/modules/viewer.dart';
|
||||
import 'package:aves/ref/bursts.dart';
|
||||
|
@ -37,7 +38,7 @@ import 'package:latlong2/latlong.dart';
|
|||
|
||||
final Settings settings = Settings._private();
|
||||
|
||||
class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings, NavigationSettings, SearchSettings, CollectionSettings, FilterGridsSettings, ViewerSettings, VideoSettings, SubtitlesSettings, InfoSettings {
|
||||
class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings, NavigationSettings, SearchSettings, CollectionSettings, FilterGridsSettings, PrivacySettings, ViewerSettings, VideoSettings, SubtitlesSettings, InfoSettings {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
|
||||
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
|
||||
|
@ -83,7 +84,8 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
|
|||
enableBlurEffect = performanceClass >= 29;
|
||||
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
final pattern = BurstPatterns.byManufacturer[androidInfo.manufacturer];
|
||||
final manufacturer = androidInfo.manufacturer.toLowerCase();
|
||||
final pattern = BurstPatterns.byManufacturer[manufacturer];
|
||||
collectionBurstPatterns = pattern != null ? [pattern] : [];
|
||||
|
||||
// availability
|
||||
|
|
|
@ -196,19 +196,19 @@ mixin AlbumMixin on SourceBase {
|
|||
final parts = pContext.split(dirPath);
|
||||
for (var i = parts.length - 1; i > 0; i--) {
|
||||
final name = pContext.joinAll(['', ...parts.skip(i)]);
|
||||
final testName = '$separator$name';
|
||||
final testName = '$separator${name.toLowerCase()}';
|
||||
if (others.every((item) => !item.endsWith(testName))) return name;
|
||||
}
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
final otherAlbumsOnDevice = _directories.whereNotNull().where((item) => item != dirPath).toSet();
|
||||
final otherAlbumsOnDevice = _directories.whereNotNull().where((item) => item != dirPath).map((v) => v.toLowerCase()).toSet();
|
||||
final uniqueNameInDevice = unique(dirPath, otherAlbumsOnDevice);
|
||||
if (uniqueNameInDevice.length <= relativeDir.length) {
|
||||
return uniqueNameInDevice;
|
||||
}
|
||||
|
||||
final volumePath = dir.volumePath;
|
||||
final volumePath = dir.volumePath.toLowerCase();
|
||||
String trimVolumePath(String? path) => path!.substring(dir.volumePath.length);
|
||||
final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path.startsWith(volumePath)).map(trimVolumePath).toSet();
|
||||
final uniqueNameInVolume = unique(trimVolumePath(dirPath), otherAlbumsOnVolume);
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/video/profiles/aac.dart';
|
|||
import 'package:aves/model/video/profiles/h264.dart';
|
||||
import 'package:aves/model/video/profiles/hevc.dart';
|
||||
import 'package:aves/ref/languages.dart';
|
||||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/ref/mp4.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
|
@ -453,7 +454,7 @@ class VideoMetadataFormatter {
|
|||
|
||||
static String _formatFilesize(dynamic value) {
|
||||
final size = value is int ? value : int.tryParse(value);
|
||||
return size != null ? formatFileSize('en_US', size) : value;
|
||||
return size != null ? formatFileSize(asciiLocale, size) : value;
|
||||
}
|
||||
|
||||
static String _formatLanguage(String value) {
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
class BurstPatterns {
|
||||
static const _keyGroupName = 'key';
|
||||
|
||||
static const fairphone = r'^IMG_(?<key>\d{8}_\d{6})_BURST(\d+)$';
|
||||
static const samsung = r'^(?<key>\d{8}_\d{6})_(\d+)$';
|
||||
static const sony = r'^DSC(PDC)?_\d+_BURST(?<key>\d{17})(_COVER)?$';
|
||||
|
||||
static final options = [
|
||||
BurstPatterns.fairphone,
|
||||
BurstPatterns.samsung,
|
||||
BurstPatterns.sony,
|
||||
];
|
||||
|
||||
static String getName(String pattern) {
|
||||
return switch (pattern) {
|
||||
fairphone => 'Fairphone',
|
||||
samsung => 'Samsung',
|
||||
sony => 'Sony',
|
||||
_ => pattern,
|
||||
|
@ -19,6 +22,7 @@ class BurstPatterns {
|
|||
|
||||
static String getExample(String pattern) {
|
||||
return switch (pattern) {
|
||||
fairphone => 'IMG_20151021_072800_BURST007',
|
||||
samsung => '20151021_072800_007',
|
||||
sony => 'DSC_0007_BURST20151021072800123',
|
||||
_ => '?',
|
||||
|
@ -26,6 +30,7 @@ class BurstPatterns {
|
|||
}
|
||||
|
||||
static const byManufacturer = {
|
||||
_Manufacturers.fairphone: fairphone,
|
||||
_Manufacturers.samsung: samsung,
|
||||
_Manufacturers.sony: sony,
|
||||
};
|
||||
|
@ -47,8 +52,9 @@ class BurstPatterns {
|
|||
}
|
||||
}
|
||||
|
||||
// values as returned by `DeviceInfoPlugin().androidInfo`
|
||||
// values as returned by `DeviceInfoPlugin().androidInfo` (lower-cased)
|
||||
class _Manufacturers {
|
||||
static const fairphone = 'fairphone';
|
||||
static const samsung = 'samsung';
|
||||
static const sony = 'sony';
|
||||
}
|
||||
|
|
47
lib/ref/locales.dart
Normal file
47
lib/ref/locales.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'dart:ui';
|
||||
|
||||
const String asciiLocale = 'en_US';
|
||||
|
||||
// cf https://en.wikipedia.org/wiki/Eastern_Arabic_numerals
|
||||
bool shouldUseNativeDigits(Locale? countrifiedLocale) {
|
||||
switch (countrifiedLocale?.toString()) {
|
||||
// Maghreb
|
||||
case 'ar_DZ': // Algeria
|
||||
case 'ar_EH': // Western Sahara
|
||||
case 'ar_LY': // Libya
|
||||
case 'ar_MA': // Morocco
|
||||
case 'ar_MR': // Mauritania
|
||||
case 'ar_TN': // Tunisia
|
||||
return false;
|
||||
// Mashriq
|
||||
case 'ar_AE': // United Arab Emirates
|
||||
case 'ar_BH': // Bahrain
|
||||
case 'ar_EG': // Egypt
|
||||
case 'ar_IQ': // Iraq
|
||||
case 'ar_JO': // Jordan
|
||||
case 'ar_KW': // Kuwait
|
||||
case 'ar_LB': // Lebanon
|
||||
case 'ar_OM': // Oman
|
||||
case 'ar_PS': // Palestinian Territories
|
||||
case 'ar_QA': // Qatar
|
||||
case 'ar_SA': // Saudi Arabia
|
||||
case 'ar_SD': // Sudan
|
||||
case 'ar_SS': // South Sudan
|
||||
case 'ar_SY': // Syria
|
||||
case 'ar_YE': // Yemen
|
||||
return true;
|
||||
// Horn of Africa
|
||||
case 'ar_DJ': // Djibouti
|
||||
case 'ar_ER': // Eritrea
|
||||
case 'ar_KM': // Comoros
|
||||
case 'ar_SO': // Somalia
|
||||
return true;
|
||||
// others
|
||||
case 'ar_IL': // Israel
|
||||
case 'ar_TD': // Chad
|
||||
return true;
|
||||
case null:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -76,7 +76,7 @@ Future<void> _init() async {
|
|||
|
||||
enum AnalyzerState { running, stopping, stopped }
|
||||
|
||||
class Analyzer {
|
||||
class Analyzer with WidgetsBindingObserver {
|
||||
late AppLocalizations _l10n;
|
||||
final ValueNotifier<AnalyzerState> _serviceStateNotifier = ValueNotifier<AnalyzerState>(AnalyzerState.stopped);
|
||||
AnalysisController? _controller;
|
||||
|
@ -102,6 +102,7 @@ class Analyzer {
|
|||
}
|
||||
_serviceStateNotifier.addListener(_onServiceStateChanged);
|
||||
_source.stateNotifier.addListener(_onSourceStateChanged);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
@ -111,11 +112,18 @@ class Analyzer {
|
|||
}
|
||||
_stopUpdateTimer();
|
||||
_controller?.dispose();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_serviceStateNotifier.removeListener(_onServiceStateChanged);
|
||||
_source.stateNotifier.removeListener(_onSourceStateChanged);
|
||||
_source.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didHaveMemoryPressure() {
|
||||
super.didHaveMemoryPressure();
|
||||
reportService.log('Analyzer memory pressure');
|
||||
}
|
||||
|
||||
Future<void> start(dynamic args) async {
|
||||
List<int>? entryIds;
|
||||
var force = false;
|
||||
|
@ -126,7 +134,7 @@ class Analyzer {
|
|||
progressTotal = args['progressTotal'];
|
||||
progressOffset = args['progressOffset'];
|
||||
}
|
||||
debugPrint('$runtimeType start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal');
|
||||
await reportService.log('Analyzer start for ${entryIds?.length ?? 'all'} entries, at $progressOffset/$progressTotal');
|
||||
_controller?.dispose();
|
||||
_controller = AnalysisController(
|
||||
canStartService: false,
|
||||
|
@ -147,8 +155,8 @@ class Analyzer {
|
|||
});
|
||||
}
|
||||
|
||||
void stop() {
|
||||
debugPrint('$runtimeType stop');
|
||||
Future<void> stop() async {
|
||||
await reportService.log('Analyzer stop');
|
||||
_serviceStateNotifier.value = AnalyzerState.stopped;
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ abstract class MetadataFetchService {
|
|||
|
||||
Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});
|
||||
|
||||
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields);
|
||||
Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry, Set<MetadataSyntheticField> fields);
|
||||
|
||||
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);
|
||||
|
||||
|
@ -39,6 +39,8 @@ abstract class MetadataFetchService {
|
|||
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
||||
|
||||
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
||||
|
||||
Future<Map<String, dynamic>> getFields(AvesEntry entry, Set<MetadataField> fields);
|
||||
}
|
||||
|
||||
class PlatformMetadataFetchService implements MetadataFetchService {
|
||||
|
@ -110,7 +112,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
||||
Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
||||
if (fields.isNotEmpty && !entry.isSvg) {
|
||||
try {
|
||||
// returns fields on demand, with various value types:
|
||||
|
@ -119,7 +121,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
// 'exposureTime' (string),
|
||||
// 'focalLength' (double),
|
||||
// 'iso' (int),
|
||||
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
|
||||
final result = await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
|
@ -284,4 +286,24 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> getFields(AvesEntry entry, Set<MetadataField> fields) async {
|
||||
if (fields.isNotEmpty && !entry.isSvg) {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
|
||||
'mimeType': entry.mimeType,
|
||||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
'fields': fields.map((v) => v.toPlatform).toList(),
|
||||
});
|
||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
||||
} on PlatformException catch (e, stack) {
|
||||
if (entry.isValid) {
|
||||
await reportService.recordError(e, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,6 +105,7 @@ class AIcons {
|
|||
static const info = Icons.info_outlined;
|
||||
static const layers = Icons.layers_outlined;
|
||||
static const map = Icons.map_outlined;
|
||||
static const more = Icons.more_horiz_outlined;
|
||||
static final move = MdiIcons.fileMoveOutline;
|
||||
static const mute = Icons.volume_off_outlined;
|
||||
static const unmute = Icons.volume_up_outlined;
|
||||
|
@ -118,7 +119,10 @@ class AIcons {
|
|||
static const pause = Icons.pause;
|
||||
static const print = Icons.print_outlined;
|
||||
static const refresh = Icons.refresh_outlined;
|
||||
static const repeat = Icons.repeat_outlined;
|
||||
static final repeatOff = MdiIcons.repeatOff;
|
||||
static const replay10 = Icons.replay_10_outlined;
|
||||
static final resetBounds = MdiIcons.rayStartEnd;
|
||||
static const reverse = Icons.invert_colors_outlined;
|
||||
static const skip10 = Icons.forward_10_outlined;
|
||||
static const reset = Icons.restart_alt_outlined;
|
||||
|
@ -130,6 +134,8 @@ class AIcons {
|
|||
static const select = Icons.select_all_outlined;
|
||||
static const setAs = Icons.wallpaper_outlined;
|
||||
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;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/ref/metadata/xmp.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
|
@ -60,7 +61,7 @@ class XMP {
|
|||
return '${offsetMinutes.isNegative ? '-' : '+'}${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
static String toXmpDate(DateTime date) => '${DateFormat('yyyy-MM-ddTHH:mm:ss').format(date)}${_xmpTimeZoneDesignator(date)}';
|
||||
static String toXmpDate(DateTime date) => '${DateFormat('yyyy-MM-ddTHH:mm:ss', asciiLocale).format(date)}${_xmpTimeZoneDesignator(date)}';
|
||||
|
||||
static String? getString(
|
||||
List<XmlNode> nodes,
|
||||
|
|
|
@ -36,6 +36,7 @@ extension ExtraEntryActionView on EntryAction {
|
|||
l10n.videoActionMute,
|
||||
EntryAction.videoSelectStreams => l10n.videoActionSelectStreams,
|
||||
EntryAction.videoSetSpeed => l10n.videoActionSetSpeed,
|
||||
EntryAction.videoABRepeat => l10n.videoActionABRepeat,
|
||||
EntryAction.videoSettings => l10n.viewerActionSettings,
|
||||
EntryAction.videoTogglePlay =>
|
||||
// different data depending on toggle state
|
||||
|
@ -110,6 +111,7 @@ extension ExtraEntryActionView on EntryAction {
|
|||
AIcons.mute,
|
||||
EntryAction.videoSelectStreams => AIcons.streams,
|
||||
EntryAction.videoSetSpeed => AIcons.speed,
|
||||
EntryAction.videoABRepeat => AIcons.repeat,
|
||||
EntryAction.videoSettings => AIcons.videoSettings,
|
||||
EntryAction.videoTogglePlay =>
|
||||
// different data depending on toggle state
|
||||
|
|
|
@ -11,6 +11,10 @@ extension ExtraMetadataFieldView on MetadataField {
|
|||
return 'Exif digitized date';
|
||||
case MetadataField.exifGpsDatestamp:
|
||||
return 'Exif GPS date';
|
||||
case MetadataField.exifMake:
|
||||
return 'Exif make';
|
||||
case MetadataField.exifModel:
|
||||
return 'Exif model';
|
||||
case MetadataField.xmpXmpCreateDate:
|
||||
return 'XMP xmp:CreateDate';
|
||||
default:
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/app_flavor.dart';
|
|||
import 'package:aves/flutter_version.dart';
|
||||
import 'package:aves/model/device.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
|
@ -176,7 +177,7 @@ class _BugReportState extends State<BugReport> with FeedbackMixin {
|
|||
final result = await Process.run('logcat', ['-d']);
|
||||
final logs = result.stdout;
|
||||
final success = await storageService.createFile(
|
||||
'aves-logs-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.txt',
|
||||
'aves-logs-${DateFormat('yyyyMMdd_HHmmss', asciiLocale).format(DateTime.now())}.txt',
|
||||
MimeTypes.plainText,
|
||||
Uint8List.fromList(utf8.encode(logs)),
|
||||
);
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:aves/model/settings/settings.dart';
|
|||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/model/source/media_store_source.dart';
|
||||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/services/accessibility_service.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
|
@ -37,12 +38,14 @@ import 'package:aves/widgets/navigation/tv_rail.dart';
|
|||
import 'package:aves/widgets/welcome_page.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
import 'package:aves_utils/aves_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localization_nn/flutter_localization_nn.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:overlay_support/overlay_support.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:screen_brightness/screen_brightness.dart';
|
||||
|
@ -158,6 +161,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder);
|
||||
final ValueNotifier<TvMediaQueryModifier?> _tvMediaQueryModifierNotifier = ValueNotifier(null);
|
||||
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.main);
|
||||
final ValueNotifier<LocaleOverrides> _localeOverridesNotifier = ValueNotifier(LocaleOverrides.none);
|
||||
|
||||
// observers are not registered when using the same list object with different items
|
||||
// the list itself needs to be reassigned
|
||||
|
@ -217,6 +221,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
Provider<TvRailController>.value(value: _tvRailController),
|
||||
DurationsProvider(),
|
||||
HighlightInfoProvider(),
|
||||
ListenableProvider<ValueNotifier<LocaleOverrides>>.value(value: _localeOverridesNotifier),
|
||||
],
|
||||
child: OverlaySupport(
|
||||
child: FutureBuilder<void>(
|
||||
|
@ -239,9 +244,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
),
|
||||
builder: (context, s, child) {
|
||||
final (settingsLocale, themeBrightness, enableDynamicColor) = s;
|
||||
|
||||
AStyles.updateStylesForLocale(settings.appliedLocale);
|
||||
|
||||
return DynamicColorBuilder(
|
||||
builder: (lightScheme, darkScheme) {
|
||||
const defaultAccent = AvesColorsData.defaultAccent;
|
||||
|
@ -402,6 +404,12 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didHaveMemoryPressure() {
|
||||
super.didHaveMemoryPressure();
|
||||
reportService.log('App memory pressure');
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() => _updateCutoutInsets();
|
||||
|
||||
|
@ -411,6 +419,33 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeLocales(List<Locale>? locales) {
|
||||
_applyLocale();
|
||||
}
|
||||
|
||||
void _applyLocale() {
|
||||
settings.resetAppliedLocale();
|
||||
|
||||
final appliedLocale = settings.appliedLocale;
|
||||
AStyles.updateStylesForLocale(appliedLocale);
|
||||
|
||||
Locale? countrifiedLocale;
|
||||
if (appliedLocale.countryCode == null) {
|
||||
final languageCode = appliedLocale.languageCode;
|
||||
countrifiedLocale = WidgetsBinding.instance.platformDispatcher.locales.firstWhereOrNull((v) => v.languageCode == languageCode);
|
||||
}
|
||||
|
||||
if (appliedLocale.languageCode == 'ar') {
|
||||
final useNativeDigits = shouldUseNativeDigits(countrifiedLocale);
|
||||
DateFormat.useNativeDigitsByDefaultFor(appliedLocale.toString(), useNativeDigits);
|
||||
DateFormat.useNativeDigitsByDefaultFor(countrifiedLocale.toString(), useNativeDigits);
|
||||
}
|
||||
_localeOverridesNotifier.value = LocaleOverrides(
|
||||
countrifiedLocale: countrifiedLocale,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
|
||||
|
||||
Size? _getScreenSize(BuildContext context) {
|
||||
|
@ -504,7 +539,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
void _monitorSettings() {
|
||||
void applyIsInstalledAppAccessAllowed() {
|
||||
void _applyIsInstalledAppAccessAllowed() {
|
||||
if (settings.isInstalledAppAccessAllowed) {
|
||||
appInventory.initAppNames();
|
||||
} else {
|
||||
|
@ -512,9 +547,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
void applyDisplayRefreshRateMode() => settings.displayRefreshRateMode.apply();
|
||||
void _applyDisplayRefreshRateMode() => settings.displayRefreshRateMode.apply();
|
||||
|
||||
void applyMaxBrightness() {
|
||||
void _applyMaxBrightness() {
|
||||
switch (settings.maxBrightness) {
|
||||
case MaxBrightness.never:
|
||||
case MaxBrightness.viewerOnly:
|
||||
|
@ -524,9 +559,9 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
}
|
||||
}
|
||||
|
||||
void applyKeepScreenOn() => settings.keepScreenOn.apply();
|
||||
void _applyKeepScreenOn() => settings.keepScreenOn.apply();
|
||||
|
||||
void applyIsRotationLocked() {
|
||||
void _applyIsRotationLocked() {
|
||||
if (!settings.isRotationLocked && !settings.useTvLayout) {
|
||||
windowService.requestOrientation();
|
||||
}
|
||||
|
@ -545,20 +580,22 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
|||
|
||||
final settingStream = settings.updateStream;
|
||||
// app
|
||||
settingStream.where((event) => event.key == SettingKeys.isInstalledAppAccessAllowedKey).listen((_) => applyIsInstalledAppAccessAllowed());
|
||||
settingStream.where((event) => event.key == SettingKeys.isInstalledAppAccessAllowedKey).listen((_) => _applyIsInstalledAppAccessAllowed());
|
||||
settingStream.where((event) => event.key == SettingKeys.localeKey).listen((_) => _applyLocale());
|
||||
// display
|
||||
settingStream.where((event) => event.key == SettingKeys.displayRefreshRateModeKey).listen((_) => applyDisplayRefreshRateMode());
|
||||
settingStream.where((event) => event.key == SettingKeys.maxBrightnessKey).listen((_) => applyMaxBrightness());
|
||||
settingStream.where((event) => event.key == SettingKeys.displayRefreshRateModeKey).listen((_) => _applyDisplayRefreshRateMode());
|
||||
settingStream.where((event) => event.key == SettingKeys.maxBrightnessKey).listen((_) => _applyMaxBrightness());
|
||||
settingStream.where((event) => event.key == SettingKeys.forceTvLayoutKey).listen((_) => applyForceTvLayout());
|
||||
// navigation
|
||||
settingStream.where((event) => event.key == SettingKeys.keepScreenOnKey).listen((_) => applyKeepScreenOn());
|
||||
settingStream.where((event) => event.key == SettingKeys.keepScreenOnKey).listen((_) => _applyKeepScreenOn());
|
||||
// platform settings
|
||||
settingStream.where((event) => event.key == SettingKeys.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked());
|
||||
settingStream.where((event) => event.key == SettingKeys.platformAccelerometerRotationKey).listen((_) => _applyIsRotationLocked());
|
||||
|
||||
applyDisplayRefreshRateMode();
|
||||
applyMaxBrightness();
|
||||
applyKeepScreenOn();
|
||||
applyIsRotationLocked();
|
||||
_applyLocale();
|
||||
_applyDisplayRefreshRateMode();
|
||||
_applyMaxBrightness();
|
||||
_applyKeepScreenOn();
|
||||
_applyIsRotationLocked();
|
||||
}
|
||||
|
||||
Future<void> _setupErrorReporting() async {
|
||||
|
@ -632,3 +669,18 @@ class AvesScrollBehavior extends MaterialScrollBehavior {
|
|||
}
|
||||
|
||||
typedef TvMediaQueryModifier = MediaQueryData Function(MediaQueryData);
|
||||
|
||||
class LocaleOverrides extends Equatable {
|
||||
final Locale? countrifiedLocale;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [countrifiedLocale];
|
||||
|
||||
const LocaleOverrides({
|
||||
required this.countrifiedLocale,
|
||||
});
|
||||
|
||||
static const LocaleOverrides none = LocaleOverrides(
|
||||
countrifiedLocale: null,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -672,8 +672,8 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
|
|||
final newest = firstKey.date;
|
||||
final oldest = lastKey.date;
|
||||
if (newest != null && oldest != null) {
|
||||
final localeName = context.l10n.localeName;
|
||||
final dateFormat = (newest.difference(oldest).inDays).abs() > 365 ? DateFormat.y(localeName) : DateFormat.MMM(localeName);
|
||||
final locale = context.l10n.localeName;
|
||||
final dateFormat = (newest.difference(oldest).inDays).abs() > 365 ? DateFormat.y(locale) : DateFormat.MMM(locale);
|
||||
String? lastLabel;
|
||||
sectionLayouts.forEach((section) {
|
||||
final date = (section.sectionKey as EntryDateSectionKey).date;
|
||||
|
|
|
@ -356,10 +356,11 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
);
|
||||
if (pattern == null) return;
|
||||
|
||||
final entriesToNewName = Map.fromEntries(entries.mapIndexed((index, entry) {
|
||||
final newName = pattern.apply(entry, index);
|
||||
final namingFutures = entries.mapIndexed((index, entry) async {
|
||||
final newName = await pattern.apply(entry, index);
|
||||
return MapEntry(entry, '$newName${entry.extension}');
|
||||
})).whereNotNullValue();
|
||||
});
|
||||
final entriesToNewName = Map.fromEntries(await Future.wait(namingFutures)).whereNotNullValue();
|
||||
await rename(context, entriesToNewName: entriesToNewName, persist: true);
|
||||
|
||||
_browse(context);
|
||||
|
@ -555,15 +556,16 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
}
|
||||
|
||||
Future<void> removeLocation(BuildContext context, Set<AvesEntry> entries) async {
|
||||
final l10n = context.l10n;
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AvesDialog(
|
||||
content: Text(context.l10n.genericDangerWarningDialogMessage),
|
||||
content: Text(l10n.genericDangerWarningDialogMessage),
|
||||
actions: [
|
||||
const CancelButton(),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||
child: Text(context.l10n.applyButtonLabel),
|
||||
child: Text(l10n.applyButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -55,7 +55,7 @@ mixin EntryEditorMixin {
|
|||
|
||||
final entry = entries.first;
|
||||
final initialTitle = entry.catalogMetadata?.xmpTitle ?? '';
|
||||
final fields = await metadataFetchService.getFields(entry, {MetadataSyntheticField.description});
|
||||
final fields = await metadataFetchService.getOverlayMetadata(entry, {MetadataSyntheticField.description});
|
||||
final initialDescription = fields.description ?? '';
|
||||
|
||||
return showDialog<Map<DescriptionField, String?>>(
|
||||
|
|
|
@ -11,8 +11,8 @@ import 'package:aves/widgets/common/action_mixins/overlay_snack_bar.dart';
|
|||
import 'package:aves/widgets/common/basic/circle.dart';
|
||||
import 'package:aves/widgets/common/basic/text/change_highlight.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||
import 'package:aves/widgets/common/extensions/theme.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:overlay_support/overlay_support.dart';
|
||||
|
@ -22,6 +22,13 @@ import 'package:provider/provider.dart';
|
|||
enum FeedbackType { info, warn }
|
||||
|
||||
mixin FeedbackMixin {
|
||||
static final ValueNotifier<EdgeInsets?> snackBarMarginOverrideNotifier = ValueNotifier(null);
|
||||
|
||||
static EdgeInsets snackBarMarginDefault(BuildContext context) {
|
||||
final mq = context.read<MediaQueryData>();
|
||||
return EdgeInsets.only(bottom: max(mq.effectiveBottomPadding, mq.systemGestureInsets.bottom));
|
||||
}
|
||||
|
||||
void dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
void showFeedback(BuildContext context, FeedbackType type, String message, [SnackBarAction? action]) {
|
||||
|
@ -53,10 +60,7 @@ mixin FeedbackMixin {
|
|||
stop: action != null ? start.add(duration) : null,
|
||||
);
|
||||
|
||||
if (context.currentRouteName == EntryViewerPage.routeName) {
|
||||
// avoid interactive widgets at the bottom of the page
|
||||
final margin = EntryViewerPage.snackBarMargin(context);
|
||||
|
||||
if (snackBarMarginOverrideNotifier.value != null) {
|
||||
// as of Flutter v2.10.4, `SnackBar` can only be positioned at the bottom,
|
||||
// and space under the snack bar `margin` does not receive gestures
|
||||
// (because it is used by the `Dismissible` wrapping the snack bar)
|
||||
|
@ -65,8 +69,15 @@ mixin FeedbackMixin {
|
|||
notificationOverlayEntry = showOverlayNotification(
|
||||
(context) => SafeArea(
|
||||
bottom: false,
|
||||
child: Padding(
|
||||
padding: margin,
|
||||
child: ValueListenableBuilder<EdgeInsets?>(
|
||||
valueListenable: snackBarMarginOverrideNotifier,
|
||||
builder: (context, margin, child) {
|
||||
return AnimatedPadding(
|
||||
padding: margin ?? snackBarMarginDefault(context),
|
||||
duration: ADurations.pageTransitionAnimation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: OverlaySnackBar(
|
||||
content: snackBarContent,
|
||||
action: action != null
|
||||
|
@ -346,6 +357,7 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
|
|||
final contentTextFontSize = contentTextStyle.fontSize ?? theme.textTheme.bodyMedium!.fontSize!;
|
||||
final timerChangeShadowColor = colorScheme.primary;
|
||||
|
||||
final remainingDurationAnimation = _remainingDurationMillis;
|
||||
return Row(
|
||||
children: [
|
||||
if (widget.type == FeedbackType.warn) ...[
|
||||
|
@ -356,16 +368,17 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
|
|||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(child: Text(widget.message)),
|
||||
if (_remainingDurationMillis != null) ...[
|
||||
if (remainingDurationAnimation != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
AnimatedBuilder(
|
||||
animation: _remainingDurationMillis!,
|
||||
animation: remainingDurationAnimation,
|
||||
builder: (context, child) {
|
||||
final remainingDurationMillis = _remainingDurationMillis!.value;
|
||||
final remainingDurationMillis = remainingDurationAnimation.value;
|
||||
final totalDurationMillis = _totalDurationMillis;
|
||||
return CircularIndicator(
|
||||
radius: 16,
|
||||
lineWidth: 2,
|
||||
percent: remainingDurationMillis / _totalDurationMillis!,
|
||||
percent: totalDurationMillis != null && totalDurationMillis > 0 ? remainingDurationMillis / totalDurationMillis : 0,
|
||||
background: Colors.grey,
|
||||
// progress color is provided by the caller,
|
||||
// because we cannot use the app context theme here
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
@ -26,6 +27,7 @@ class AvesPopScope extends StatelessWidget {
|
|||
Navigator.maybeOf(context)?.pop();
|
||||
} else {
|
||||
// exit
|
||||
reportService.log('Exit by pop');
|
||||
SystemNavigator.pop();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
|
@ -15,6 +16,8 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
|
|||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
final currentSizeBytes = formatFileSize(asciiLocale, imageCache.currentSizeBytes);
|
||||
final maxSizeBytes = formatFileSize(asciiLocale, imageCache.maximumSizeBytes);
|
||||
return AvesExpansionTile(
|
||||
title: 'Cache',
|
||||
children: [
|
||||
|
@ -25,7 +28,7 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
|
|||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t${formatFileSize('en_US', imageCache.currentSizeBytes)}/${formatFileSize('en_US', imageCache.maximumSizeBytes)}'),
|
||||
child: Text('Image cache:\n\t${imageCache.currentSize}/${imageCache.maximumSize} items\n\t$currentSizeBytes/$maxSizeBytes'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/metadata/trash.dart';
|
|||
import 'package:aves/model/vaults/details.dart';
|
||||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/model/video_playback.dart';
|
||||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||
|
@ -65,7 +66,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
|
|||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text('DB file size: ${formatFileSize('en_US', snapshot.data!)}'),
|
||||
child: Text('DB file size: ${formatFileSize(asciiLocale, snapshot.data!)}'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
|
|
|
@ -69,6 +69,7 @@ class _DebugSettingsSectionState extends State<DebugSettingsSection> with Automa
|
|||
'drawerPageBookmarks': toMultiline(settings.drawerPageBookmarks),
|
||||
'pinnedFilters': toMultiline(settings.pinnedFilters),
|
||||
'hiddenFilters': toMultiline(settings.hiddenFilters),
|
||||
'deactivatedHiddenFilters': toMultiline(settings.deactivatedHiddenFilters),
|
||||
'searchHistory': toMultiline(settings.searchHistory),
|
||||
'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums),
|
||||
'recentTags': toMultiline(settings.recentTags),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/file_utils.dart';
|
||||
|
@ -47,7 +48,7 @@ class _DebugStorageSectionState extends State<DebugStorageSection> with Automati
|
|||
'isPrimary': '${v.isPrimary}',
|
||||
'isRemovable': '${v.isRemovable}',
|
||||
'state': v.state,
|
||||
if (freeSpace != null) 'freeSpace': formatFileSize('en_US', freeSpace),
|
||||
if (freeSpace != null) 'freeSpace': formatFileSize(asciiLocale, freeSpace),
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
@ -6,8 +6,10 @@ import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/theme/styles.dart';
|
||||
import 'package:aves/view/src/metadata/fields.dart';
|
||||
import 'package:aves/widgets/collection/collection_grid.dart';
|
||||
import 'package:aves/widgets/common/basic/font_size_icon_theme.dart';
|
||||
import 'package:aves/widgets/common/basic/popup/expansion_panel.dart';
|
||||
import 'package:aves/widgets/common/basic/popup/menu_row.dart';
|
||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
@ -36,6 +38,7 @@ class RenameEntrySetPage extends StatefulWidget {
|
|||
class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||
final TextEditingController _patternTextController = TextEditingController();
|
||||
final ValueNotifier<NamingPattern> _namingPatternNotifier = ValueNotifier<NamingPattern>(const NamingPattern([]));
|
||||
late final String locale;
|
||||
|
||||
static const int previewMax = 10;
|
||||
static const double thumbnailExtent = 48;
|
||||
|
@ -49,7 +52,11 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
|||
super.initState();
|
||||
_patternTextController.text = settings.entryRenamingPattern;
|
||||
_patternTextController.addListener(_onUserPatternChanged);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
locale = context.l10n.localeName;
|
||||
_onUserPatternChanged();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -91,10 +98,6 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
|||
child: PopupMenuButton<String>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: DateNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.viewerInfoLabelDate, icon: const Icon(AIcons.date)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: NameNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.renameProcessorName, icon: const Icon(AIcons.name)),
|
||||
|
@ -103,6 +106,28 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
|||
value: CounterNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.renameProcessorCounter, icon: const Icon(AIcons.counter)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: DateNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.viewerInfoLabelDate, icon: const Icon(AIcons.date)),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: TagsNamingProcessor.key,
|
||||
child: MenuRow(text: l10n.tagPageTitle, icon: const Icon(AIcons.tag)),
|
||||
),
|
||||
PopupMenuExpansionPanel<String>(
|
||||
value: MetadataFieldNamingProcessor.key,
|
||||
icon: AIcons.more,
|
||||
title: MaterialLocalizations.of(context).moreButtonTooltip,
|
||||
items: [
|
||||
MetadataField.exifMake,
|
||||
MetadataField.exifModel,
|
||||
]
|
||||
.map((field) => PopupMenuItem(
|
||||
value: MetadataFieldNamingProcessor.keyWithField(field),
|
||||
child: MenuRow(text: field.title),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: (key) async {
|
||||
|
@ -159,13 +184,19 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
|||
ValueListenableBuilder<NamingPattern>(
|
||||
valueListenable: _namingPatternNotifier,
|
||||
builder: (context, pattern, child) {
|
||||
return FutureBuilder<String>(
|
||||
future: pattern.apply(entry, index),
|
||||
builder: (context, snapshot) {
|
||||
final info = snapshot.data;
|
||||
return Text(
|
||||
pattern.apply(entry, index),
|
||||
info ?? '…',
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -203,6 +234,7 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
|||
_namingPatternNotifier.value = NamingPattern.from(
|
||||
userPattern: _patternTextController.text,
|
||||
entryCount: entryCount,
|
||||
locale: locale,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -58,10 +58,18 @@ class _HiddenFilters extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
bool filterPredicate(CollectionFilter v) => v is! PathFilter;
|
||||
return Selector<Settings, Set<CollectionFilter>>(
|
||||
selector: (context, s) => settings.hiddenFilters.where((v) => v is! PathFilter).toSet(),
|
||||
builder: (context, hiddenFilters, child) {
|
||||
if (hiddenFilters.isEmpty) {
|
||||
selector: (context, s) => settings.hiddenFilters.where(filterPredicate).toSet(),
|
||||
builder: (context, activatedHiddenFilters, child) {
|
||||
return Selector<Settings, Set<CollectionFilter>>(
|
||||
selector: (context, s) => settings.deactivatedHiddenFilters.where(filterPredicate).toSet(),
|
||||
builder: (context, deactivatedHiddenFilters, child) {
|
||||
final allHiddenFilters = {
|
||||
...activatedHiddenFilters,
|
||||
...deactivatedHiddenFilters,
|
||||
};
|
||||
if (allHiddenFilters.isEmpty) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -80,28 +88,47 @@ class _HiddenFilters extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
final filterList = hiddenFilters.toList()..sort();
|
||||
final filterList = allHiddenFilters.toList()..sort();
|
||||
return ListView(
|
||||
children: [
|
||||
_Banner(bannerText: context.l10n.settingsHiddenFiltersBanner),
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: filterList.map((filter) {
|
||||
const SizedBox(height: 8),
|
||||
...filterList.map((filter) {
|
||||
void onRemove(CollectionFilter filter) => settings.changeFilterVisibility({filter}, true);
|
||||
return AvesFilterChip(
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LayoutBuilder(builder: (context, constraints) {
|
||||
debugPrint('TLAD constraints=$constraints');
|
||||
return Row(
|
||||
children: [
|
||||
AvesFilterChip(
|
||||
filter: filter,
|
||||
maxWidth: constraints.maxWidth,
|
||||
onTap: onRemove,
|
||||
onRemove: onRemove,
|
||||
onLongPress: null,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const Spacer(),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Switch(
|
||||
value: activatedHiddenFilters.contains(filter),
|
||||
onChanged: (v) => settings.activateHiddenFilter(filter, v),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -114,7 +141,10 @@ class _HiddenPaths extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Selector<Settings, Set<PathFilter>>(
|
||||
selector: (context, s) => settings.hiddenFilters.whereType<PathFilter>().toSet(),
|
||||
selector: (context, s) => {
|
||||
...settings.hiddenFilters,
|
||||
...settings.deactivatedHiddenFilters,
|
||||
}.whereType<PathFilter>().toSet(),
|
||||
builder: (context, hiddenPaths, child) {
|
||||
final pathList = hiddenPaths.toList()..sort();
|
||||
return Column(
|
||||
|
|
|
@ -12,10 +12,10 @@ abstract class SettingsSection {
|
|||
|
||||
FutureOr<List<SettingsTile>> tiles(BuildContext context);
|
||||
|
||||
Widget build(BuildContext context, ValueNotifier<String?> expandedNotifier) {
|
||||
Widget build(BuildContext sectionContext, ValueNotifier<String?> expandedNotifier) {
|
||||
return FutureBuilder<List<SettingsTile>>(
|
||||
future: Future.value(tiles(context)),
|
||||
builder: (context, snapshot) {
|
||||
future: Future.value(tiles(sectionContext)),
|
||||
builder: (tileContext, snapshot) {
|
||||
final tiles = snapshot.data;
|
||||
if (tiles == null) return const SizedBox();
|
||||
|
||||
|
@ -25,11 +25,12 @@ abstract class SettingsSection {
|
|||
// use a fixed value instead of the title to identify this expansion tile
|
||||
// so that the tile state is kept when the language is modified
|
||||
value: key,
|
||||
leading: icon(context),
|
||||
title: title(context),
|
||||
leading: icon(tileContext),
|
||||
title: title(tileContext),
|
||||
expandedNotifier: expandedNotifier,
|
||||
showHighlight: false,
|
||||
children: tiles.map((v) => v.build(context)).toList(),
|
||||
// reuse section context so that dialogs opened from tiles have the right text theme
|
||||
children: tiles.map((v) => v.build(sectionContext)).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||
import 'package:aves/model/settings/enums/accessibility_animations.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/ref/locales.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
import 'package:aves/services/common/services.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -117,7 +118,7 @@ class _SettingsMobilePageState extends State<SettingsMobilePage> with FeedbackMi
|
|||
final allJsonString = jsonEncode(allMap);
|
||||
|
||||
final success = await storageService.createFile(
|
||||
'aves-settings-${DateFormat('yyyyMMdd_HHmmss').format(DateTime.now())}.json',
|
||||
'aves-settings-${DateFormat('yyyyMMdd_HHmmss', asciiLocale).format(DateTime.now())}.json',
|
||||
MimeTypes.json,
|
||||
Uint8List.fromList(utf8.encode(allJsonString)),
|
||||
);
|
||||
|
|
|
@ -94,6 +94,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
return !settings.useTvLayout && targetEntry.isPureVideo;
|
||||
case EntryAction.videoSelectStreams:
|
||||
case EntryAction.videoSetSpeed:
|
||||
case EntryAction.videoABRepeat:
|
||||
case EntryAction.videoSettings:
|
||||
case EntryAction.videoTogglePlay:
|
||||
case EntryAction.videoReplay10:
|
||||
|
@ -229,6 +230,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
case EntryAction.videoToggleMute:
|
||||
case EntryAction.videoSelectStreams:
|
||||
case EntryAction.videoSetSpeed:
|
||||
case EntryAction.videoABRepeat:
|
||||
case EntryAction.videoSettings:
|
||||
case EntryAction.videoTogglePlay:
|
||||
case EntryAction.videoReplay10:
|
||||
|
|
|
@ -64,6 +64,8 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
await _showStreamSelectionDialog(context, controller);
|
||||
case EntryAction.videoSetSpeed:
|
||||
await _showSpeedDialog(context, controller);
|
||||
case EntryAction.videoABRepeat:
|
||||
controller.toggleABRepeat();
|
||||
case EntryAction.videoSettings:
|
||||
await _showSettings(context, controller);
|
||||
case EntryAction.videoTogglePlay:
|
||||
|
|
|
@ -63,7 +63,7 @@ class EntryViewerStack extends StatefulWidget {
|
|||
State<EntryViewerStack> createState() => _EntryViewerStackState();
|
||||
}
|
||||
|
||||
class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, TickerProviderStateMixin {
|
||||
class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewControllerMixin, FeedbackMixin, TickerProviderStateMixin, RouteAware {
|
||||
final Floating _floating = Floating();
|
||||
late int _currentEntryIndex;
|
||||
late ValueNotifier<int> _currentVerticalPage;
|
||||
|
@ -184,6 +184,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
|
||||
@override
|
||||
void dispose() {
|
||||
AvesApp.pageRouteObserver.unsubscribe(this);
|
||||
_floating.dispose();
|
||||
cleanEntryControllers(entryNotifier.value);
|
||||
_videoActionDelegate.dispose();
|
||||
|
@ -287,6 +288,41 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
);
|
||||
}
|
||||
|
||||
// route aware
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final route = ModalRoute.of(context);
|
||||
if (route is PageRoute) {
|
||||
AvesApp.pageRouteObserver.subscribe(this, route);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() => _overrideSnackBarMargin();
|
||||
|
||||
@override
|
||||
void didPush() => _overrideSnackBarMargin();
|
||||
|
||||
@override
|
||||
void didPop() => _resetSnackBarMargin();
|
||||
|
||||
@override
|
||||
void didPushNext() => _resetSnackBarMargin();
|
||||
|
||||
void _overrideSnackBarMargin() {
|
||||
if (isViewingImage) {
|
||||
FeedbackMixin.snackBarMarginOverrideNotifier.value = EdgeInsets.only(bottom: ViewerBottomOverlay.actionSafeHeight(context));
|
||||
} else {
|
||||
FeedbackMixin.snackBarMarginOverrideNotifier.value = FeedbackMixin.snackBarMarginDefault(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _resetSnackBarMargin() => FeedbackMixin.snackBarMarginOverrideNotifier.value = null;
|
||||
|
||||
// lifecycle
|
||||
|
||||
void _onAppLifecycleStateChanged() {
|
||||
switch (AvesApp.lifecycleStateNotifier.value) {
|
||||
case AppLifecycleState.inactive:
|
||||
|
@ -662,6 +698,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
|||
|
||||
void _onVerticalPageChanged(int page) {
|
||||
_currentVerticalPage.value = page;
|
||||
_overrideSnackBarMargin();
|
||||
switch (page) {
|
||||
case transitionPage:
|
||||
dismissFeedback(context);
|
||||
|
|
|
@ -74,7 +74,7 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
|||
if (requestEntry == null) {
|
||||
_detailLoader = SynchronousFuture(const OverlayMetadata());
|
||||
} else {
|
||||
_detailLoader = metadataFetchService.getFields(requestEntry, {
|
||||
_detailLoader = metadataFetchService.getOverlayMetadata(requestEntry, {
|
||||
if (settings.showOverlayShootingDetails) ...{
|
||||
MetadataSyntheticField.aperture,
|
||||
MetadataSyntheticField.exposureTime,
|
||||
|
|
78
lib/widgets/viewer/overlay/video/ab_repeat.dart
Normal file
78
lib/widgets/viewer/overlay/video/ab_repeat.dart
Normal file
|
@ -0,0 +1,78 @@
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
|
||||
import 'package:aves_video/aves_video.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class VideoABRepeatOverlay extends StatefulWidget {
|
||||
final AvesVideoController? controller;
|
||||
final Animation<double> scale;
|
||||
|
||||
const VideoABRepeatOverlay({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.scale,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _VideoABRepeatOverlayState();
|
||||
}
|
||||
|
||||
class _VideoABRepeatOverlayState extends State<VideoABRepeatOverlay> {
|
||||
Animation<double> get scale => widget.scale;
|
||||
|
||||
AvesVideoController? get controller => widget.controller;
|
||||
|
||||
ValueNotifier<ABRepeat?> get abRepeatNotifier => controller?.abRepeatNotifier ?? ValueNotifier(null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return ValueListenableBuilder<ABRepeat?>(
|
||||
valueListenable: abRepeatNotifier,
|
||||
builder: (context, abRepeat, child) {
|
||||
if (abRepeat == null) return const SizedBox();
|
||||
|
||||
Widget boundButton;
|
||||
if (abRepeat.start == null) {
|
||||
boundButton = IconButton(
|
||||
icon: Icon(AIcons.setStart),
|
||||
onPressed: controller?.setABRepeatStart,
|
||||
tooltip: l10n.videoRepeatActionSetStart,
|
||||
);
|
||||
} else if (abRepeat.end == null) {
|
||||
boundButton = IconButton(
|
||||
icon: Icon(AIcons.setEnd),
|
||||
onPressed: controller?.setABRepeatEnd,
|
||||
tooltip: l10n.videoRepeatActionSetEnd,
|
||||
);
|
||||
} else {
|
||||
boundButton = IconButton(
|
||||
icon: Icon(AIcons.resetBounds),
|
||||
onPressed: controller?.resetABRepeat,
|
||||
tooltip: l10n.resetTooltip,
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
const Spacer(),
|
||||
OverlayButton(
|
||||
scale: scale,
|
||||
child: boundButton,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
OverlayButton(
|
||||
scale: scale,
|
||||
child: IconButton(
|
||||
icon: Icon(AIcons.repeatOff),
|
||||
onPressed: () => controller?.toggleABRepeat(),
|
||||
tooltip: l10n.stopTooltip,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -37,6 +37,8 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
|||
|
||||
bool get isPlaying => controller?.isPlaying ?? false;
|
||||
|
||||
ValueNotifier<ABRepeat?> get abRepeatNotifier => controller?.abRepeatNotifier ?? ValueNotifier(null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final blurred = settings.enableBlurEffect;
|
||||
|
@ -69,8 +71,7 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
|||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred),
|
||||
border: AvesBorder.border(context),
|
||||
|
@ -80,8 +81,21 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
|||
data: MediaQuery.of(context).copyWith(
|
||||
textScaler: TextScaler.noScaling,
|
||||
),
|
||||
child: Column(
|
||||
child: ValueListenableBuilder<ABRepeat?>(
|
||||
valueListenable: abRepeatNotifier,
|
||||
builder: (context, abRepeat, child) {
|
||||
return Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
if (abRepeat != null) ...[
|
||||
_buildABRepeatMark(context, abRepeat.start),
|
||||
_buildABRepeatMark(context, abRepeat.end),
|
||||
],
|
||||
Container(
|
||||
key: _progressBarKey,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
|
@ -138,10 +152,29 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
|||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildABRepeatMark(BuildContext context, int? position) {
|
||||
if (controller == null || position == null) return const SizedBox();
|
||||
return Positioned(
|
||||
left: _progressToDx(position / controller!.duration),
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(left: AvesBorder.straightSide(context, width: 2)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -175,11 +208,20 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
|||
},
|
||||
);
|
||||
|
||||
RenderBox? _getProgressBarRenderBox() {
|
||||
return _progressBarKey.currentContext?.findRenderObject() as RenderBox?;
|
||||
}
|
||||
|
||||
void _seekFromTap(Offset globalPosition) async {
|
||||
if (controller == null) return;
|
||||
final keyContext = _progressBarKey.currentContext!;
|
||||
final box = keyContext.findRenderObject() as RenderBox;
|
||||
final localPosition = box.globalToLocal(globalPosition);
|
||||
await controller!.seekToProgress(localPosition.dx / box.size.width);
|
||||
final box = _getProgressBarRenderBox();
|
||||
if (controller == null || box == null) return;
|
||||
|
||||
final dx = box.globalToLocal(globalPosition).dx;
|
||||
await controller!.seekToProgress(dx / box.size.width);
|
||||
}
|
||||
|
||||
double? _progressToDx(double progress) {
|
||||
final box = _getProgressBarRenderBox();
|
||||
return box == null ? null : progress * box.size.width;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/view/view.dart';
|
||||
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/ab_repeat.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/controls.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart';
|
||||
import 'package:aves_model/aves_model.dart';
|
||||
|
@ -64,7 +65,14 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
|||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
return Column(
|
||||
children: [
|
||||
VideoABRepeatOverlay(
|
||||
controller: controller,
|
||||
scale: scale,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: VideoProgressBar(
|
||||
|
@ -79,6 +87,8 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
|||
onActionSelected: widget.onActionSelected,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
|
@ -405,6 +405,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
|
|||
controller: controller ?? _magnifierController,
|
||||
contentSize: displaySize ?? entry.displaySize,
|
||||
allowOriginalScaleBeyondRange: !isWallpaperMode,
|
||||
allowDoubleTap: _allowDoubleTap,
|
||||
minScale: minScale,
|
||||
maxScale: maxScale,
|
||||
initialScale: viewerController.initialScale,
|
||||
|
@ -434,22 +435,34 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
|
|||
}
|
||||
}
|
||||
|
||||
void _onTap({Alignment? alignment}) {
|
||||
Notification? _handleSideSingleTap(Alignment? alignment) {
|
||||
if (settings.viewerGestureSideTapNext && alignment != null) {
|
||||
final x = alignment.x;
|
||||
final sideRatio = _getSideRatio();
|
||||
if (sideRatio != null) {
|
||||
const animate = false;
|
||||
if (x < sideRatio) {
|
||||
(context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
|
||||
return;
|
||||
return context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate);
|
||||
} else if (x > 1 - sideRatio) {
|
||||
(context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
|
||||
return;
|
||||
return context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate);
|
||||
}
|
||||
}
|
||||
}
|
||||
const ToggleOverlayNotification().dispatch(context);
|
||||
return null;
|
||||
}
|
||||
|
||||
void _onTap({Alignment? alignment}) => (_handleSideSingleTap(alignment) ?? const ToggleOverlayNotification()).dispatch(context);
|
||||
|
||||
// side gesture handling by precedence:
|
||||
// - seek in video by side double tap (if enabled)
|
||||
// - go to previous/next entry by side single tap (if enabled)
|
||||
// - zoom in/out by double tap
|
||||
bool _allowDoubleTap(Alignment alignment) {
|
||||
if (entry.isVideo && settings.videoGestureSideDoubleTapSeek) {
|
||||
return true;
|
||||
}
|
||||
final actionNotification = _handleSideSingleTap(alignment);
|
||||
return actionNotification == null;
|
||||
}
|
||||
|
||||
void _onMediaCommand(MediaCommandEvent event) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue