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="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
|
## <a id="v1.10.7"></a>[v1.10.7] - 2024-03-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -2,9 +2,10 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'com.google.devtools.ksp' version "$ksp_version"
|
id 'com.google.devtools.ksp'
|
||||||
id 'kotlin-android'
|
id 'kotlin-android'
|
||||||
id 'kotlin-kapt'
|
id 'kotlin-kapt'
|
||||||
|
id 'dev.flutter.flutter-gradle-plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
def packageName = "deckers.thibault.aves"
|
def packageName = "deckers.thibault.aves"
|
||||||
|
@ -20,11 +21,6 @@ if (localPropertiesFile.exists()) {
|
||||||
}
|
}
|
||||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
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
|
// Keys
|
||||||
|
|
||||||
|
@ -54,8 +50,8 @@ android {
|
||||||
ndkVersion '25.1.8937393'
|
ndkVersion '25.1.8937393'
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
|
@ -181,12 +177,12 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(KotlinCompile).configureEach {
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
jvmToolchain(8)
|
jvmToolchain(17)
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
@ -210,7 +206,7 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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.appcompat:appcompat:1.6.1"
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
|
@ -226,7 +222,7 @@ dependencies {
|
||||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
// SLF4J implementation for `mp4parser`
|
// SLF4J implementation for `mp4parser`
|
||||||
implementation 'org.slf4j:slf4j-simple:2.0.11'
|
implementation 'org.slf4j:slf4j-simple:2.0.12'
|
||||||
|
|
||||||
// forked, built by JitPack:
|
// forked, built by JitPack:
|
||||||
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||||
|
@ -240,7 +236,7 @@ dependencies {
|
||||||
// huawei flavor only
|
// huawei flavor only
|
||||||
huaweiImplementation "com.huawei.agconnect:agconnect-core:$huawei_agconnect_version"
|
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'
|
kapt 'androidx.annotation:annotation:1.7.1'
|
||||||
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
<!-- to access media with original metadata with scoped storage (API >=29) -->
|
<!-- to access media with original metadata with scoped storage (API >=29) -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
<!-- to provide a foreground service type, as required by Android 14 (API 34) -->
|
<!-- 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
|
<uses-permission
|
||||||
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
|
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
|
||||||
tools:ignore="SystemPermissionTypo" />
|
tools:ignore="SystemPermissionTypo" />
|
||||||
|
@ -72,11 +73,11 @@
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
allow install on API 19, despite the `minSdk` declared in dependencies:
|
allow install on API 19, despite the `minSdk` declared in dependencies:
|
||||||
- Google Maps is from API 20
|
|
||||||
- the Security library is from API 21
|
- the Security library is from API 21
|
||||||
- FFmpegKit for Flutter is from API 24 (when not LTS)
|
- 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 -->
|
<!-- from Android 11, we should define <queries> to make other apps visible to this app -->
|
||||||
<queries>
|
<queries>
|
||||||
|
@ -258,6 +259,7 @@
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- anonymous service for analysis worker is specified here to provide service type -->
|
<!-- anonymous service for analysis worker is specified here to provide service type -->
|
||||||
|
<!-- TODO TLAD [Android 15 (API 35)] use `mediaProcessing` -->
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
|
|
|
@ -176,6 +176,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
||||||
// from Android 14 (API 34), foreground service type is mandatory
|
// from Android 14 (API 34), foreground service type is mandatory
|
||||||
// despite the sample code omitting it at:
|
// despite the sample code omitting it at:
|
||||||
// https://developer.android.com/guide/background/persistent/how-to/long-running
|
// 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
|
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||||
ForegroundInfo(NOTIFICATION_ID, notification, type)
|
ForegroundInfo(NOTIFICATION_ID, notification, type)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.FlutterUtils
|
import deckers.thibault.aves.utils.FlutterUtils
|
||||||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
@ -56,7 +57,7 @@ class HomeWidgetSettingsActivity : MainActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
override fun extractIntentData(intent: Intent?): FieldMap {
|
||||||
return hashMapOf(
|
return hashMapOf(
|
||||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS,
|
INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS,
|
||||||
INTENT_DATA_KEY_WIDGET_ID to appWidgetId,
|
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() {
|
override fun onStop() {
|
||||||
Log.i(LOG_TAG, "onStop")
|
Log.i(LOG_TAG, "onStop")
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
@ -242,7 +230,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
|
|
||||||
private fun onEditResult(resultCode: Int, intent: Intent?) {
|
private fun onEditResult(resultCode: Int, intent: Intent?) {
|
||||||
val fields: FieldMap? = if (resultCode == RESULT_OK) hashMapOf(
|
val fields: FieldMap? = if (resultCode == RESULT_OK) hashMapOf(
|
||||||
"uri" to intent?.data.toString(),
|
"uri" to intent?.data?.toString(),
|
||||||
"mimeType" to intent?.type,
|
"mimeType" to intent?.type,
|
||||||
) else null
|
) else null
|
||||||
pendingEditIntentHandler?.let { it(fields) }
|
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) {
|
when (val action = intent?.action) {
|
||||||
Intent.ACTION_MAIN -> {
|
Intent.ACTION_MAIN -> {
|
||||||
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) {
|
val fields = hashMapOf<String, Any?>(
|
||||||
return hashMapOf(
|
INTENT_DATA_KEY_LAUNCHER to intent.hasCategory(Intent.CATEGORY_LAUNCHER),
|
||||||
INTENT_DATA_KEY_SAFE_MODE to true,
|
INTENT_DATA_KEY_SAFE_MODE to intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
|
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
|
||||||
val filters = extractFiltersFromIntent(intent)
|
val filters = extractFiltersFromIntent(intent)
|
||||||
return hashMapOf(
|
fields[INTENT_DATA_KEY_PAGE] = page
|
||||||
INTENT_DATA_KEY_PAGE to page,
|
fields[INTENT_DATA_KEY_FILTERS] = filters
|
||||||
INTENT_DATA_KEY_FILTERS to filters,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return fields
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
|
@ -496,6 +482,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
const val INTENT_DATA_KEY_ACTION = "action"
|
const val INTENT_DATA_KEY_ACTION = "action"
|
||||||
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
|
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
|
||||||
const val INTENT_DATA_KEY_FILTERS = "filters"
|
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_MIME_TYPE = "mimeType"
|
||||||
const val INTENT_DATA_KEY_PAGE = "page"
|
const val INTENT_DATA_KEY_PAGE = "page"
|
||||||
const val INTENT_DATA_KEY_QUERY = "query"
|
const val INTENT_DATA_KEY_QUERY = "query"
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package deckers.thibault.aves
|
package deckers.thibault.aves
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import deckers.thibault.aves.model.FieldMap
|
||||||
|
|
||||||
class ScreenSaverSettingsActivity : MainActivity() {
|
class ScreenSaverSettingsActivity : MainActivity() {
|
||||||
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
|
override fun extractIntentData(intent: Intent?): FieldMap {
|
||||||
return hashMapOf(
|
return hashMapOf(
|
||||||
INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS,
|
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.calls.window.WindowHandler
|
||||||
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
|
||||||
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
|
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
|
||||||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
@ -25,7 +26,7 @@ import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class WallpaperActivity : FlutterFragmentActivity() {
|
class WallpaperActivity : FlutterFragmentActivity() {
|
||||||
private lateinit var intentDataMap: MutableMap<String, Any?>
|
private lateinit var intentDataMap: FieldMap
|
||||||
private lateinit var mediaSessionHandler: MediaSessionHandler
|
private lateinit var mediaSessionHandler: MediaSessionHandler
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
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) {
|
when (intent?.action) {
|
||||||
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
|
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
|
||||||
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
|
(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)
|
val provider = getProvider(context, uri)
|
||||||
if (provider == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback {
|
provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback {
|
||||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
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 com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
import deckers.thibault.aves.metadata.ExifGeoTiffTags
|
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.describeAll
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
|
||||||
|
@ -110,7 +111,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
"getAllMetadata" -> ioScope.launch { safe(call, result, ::getAllMetadata) }
|
||||||
"getCatalogMetadata" -> ioScope.launch { safe(call, result, ::getCatalogMetadata) }
|
"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) }
|
"getGeoTiffInfo" -> ioScope.launch { safe(call, result, ::getGeoTiffInfo) }
|
||||||
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
"getMultiPageInfo" -> ioScope.launch { safe(call, result, ::getMultiPageInfo) }
|
||||||
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
|
"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) }
|
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
|
||||||
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
|
||||||
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
"getDate" -> ioScope.launch { safe(call, result, ::getDate) }
|
||||||
|
"getFields" -> ioScope.launch { safe(call, result, ::getFields) }
|
||||||
else -> result.notImplemented()
|
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 mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
|
@ -1250,6 +1252,71 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(dateMillis)
|
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 {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
private val LOG_TAG = LogUtils.createTag<MetadataFetchHandler>()
|
||||||
const val CHANNEL = "deckers.thibault/aves/metadata_fetch"
|
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 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()
|
val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()
|
||||||
|
|
||||||
result.success(untrackedPaths)
|
result.success(untrackedPaths)
|
||||||
|
|
|
@ -12,7 +12,9 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import deckers.thibault.aves.decoder.MultiPageImage
|
import deckers.thibault.aves.decoder.MultiPageImage
|
||||||
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
|
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.BitmapUtils.getBytes
|
||||||
|
import deckers.thibault.aves.utils.MemoryUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
@ -73,7 +75,7 @@ class RegionFetcher internal constructor(
|
||||||
BitmapRegionDecoderCompat.newInstance(input)
|
BitmapRegionDecoderCompat.newInstance(input)
|
||||||
}
|
}
|
||||||
if (newDecoder == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
currentDecoderRef = LastDecoderRef(uri, newDecoder)
|
currentDecoderRef = LastDecoderRef(uri, newDecoder)
|
||||||
|
@ -96,14 +98,22 @@ class RegionFetcher internal constructor(
|
||||||
regionRect
|
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)
|
val bitmap = decoder.decodeRegion(effectiveRect, options)
|
||||||
if (bitmap != null) {
|
if (bitmap != null) {
|
||||||
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
|
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
|
||||||
} else {
|
} 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) {
|
} 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.SVG
|
||||||
import com.caverock.androidsvg.SVGParseException
|
import com.caverock.androidsvg.SVGParseException
|
||||||
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
|
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.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.MemoryUtils
|
import deckers.thibault.aves.utils.MemoryUtils
|
||||||
import deckers.thibault.aves.utils.StorageUtils
|
import deckers.thibault.aves.utils.StorageUtils
|
||||||
|
@ -116,8 +117,4 @@ class SvgRegionFetcher internal constructor(
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
val svg: SVG,
|
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)
|
.setDataAndType(AppAdapterHandler.getShareableUri(activity, Uri.parse(uri)), mimeType)
|
||||||
|
|
||||||
if (intent.resolveActivity(activity.packageManager) == null) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +207,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
endOfStream()
|
endOfStream()
|
||||||
}
|
}
|
||||||
if (!safeStartActivityForResult(intent, MainActivity.EDIT_REQUEST)) {
|
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 {
|
init {
|
||||||
onAppResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispose() {
|
|
||||||
onAppPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAppResume() {
|
|
||||||
Log.i(LOG_TAG, "start listening to Media Store")
|
Log.i(LOG_TAG, "start listening to Media Store")
|
||||||
context.contentResolver.apply {
|
context.contentResolver.apply {
|
||||||
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
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")
|
Log.i(LOG_TAG, "stop listening to Media Store")
|
||||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,19 +62,11 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
onAppResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun dispose() {
|
|
||||||
onAppPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAppResume() {
|
|
||||||
Log.i(LOG_TAG, "start listening to system settings")
|
Log.i(LOG_TAG, "start listening to system settings")
|
||||||
context.contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, contentObserver)
|
context.contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, contentObserver)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAppPause() {
|
fun dispose() {
|
||||||
Log.i(LOG_TAG, "stop listening to system settings")
|
Log.i(LOG_TAG, "stop listening to system settings")
|
||||||
context.contentResolver.unregisterContentObserver(contentObserver)
|
context.contentResolver.unregisterContentObserver(contentObserver)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ object BitmapUtils {
|
||||||
private val freeBaos = ArrayList<ByteArrayOutputStream>()
|
private val freeBaos = ArrayList<ByteArrayOutputStream>()
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
const val ARGB_8888_BYTE_SIZE = 4
|
||||||
|
|
||||||
suspend fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? {
|
suspend fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? {
|
||||||
val stream: ByteArrayOutputStream
|
val stream: ByteArrayOutputStream
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
kotlin_version = '1.9.21'
|
|
||||||
ksp_version = "$kotlin_version-1.0.15"
|
|
||||||
agp_version = '8.2.2'
|
|
||||||
glide_version = '4.16.0'
|
glide_version = '4.16.0'
|
||||||
// AppGallery Connect plugin versions: https://developer.huawei.com/consumer/en/doc/development/AppGallery-connect-Guides/agc-sdk-changenotes-0000001058732550
|
// 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'
|
huawei_agconnect_version = '1.9.1.300'
|
||||||
|
@ -22,9 +19,6 @@ buildscript {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:$agp_version"
|
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
||||||
|
|
||||||
if (useCrashlytics) {
|
if (useCrashlytics) {
|
||||||
// GMS & Firebase Crashlytics (used by some flavors only)
|
// GMS & Firebase Crashlytics (used by some flavors only)
|
||||||
classpath 'com.google.gms:google-services:4.4.0'
|
classpath 'com.google.gms:google-services:4.4.0'
|
||||||
|
|
|
@ -20,8 +20,8 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx4G -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
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
|
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
|
|
||||||
android.defaults.buildfeatures.buildconfig=true
|
android.defaults.buildfeatures.buildconfig=true
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.nonFinalResIds=false
|
android.nonFinalResIds=false
|
||||||
|
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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.exifGpsTrackRef:
|
||||||
case MetadataField.exifGpsVersionId:
|
case MetadataField.exifGpsVersionId:
|
||||||
case MetadataField.exifImageDescription:
|
case MetadataField.exifImageDescription:
|
||||||
|
case MetadataField.exifMake:
|
||||||
|
case MetadataField.exifModel:
|
||||||
case MetadataField.exifUserComment:
|
case MetadataField.exifUserComment:
|
||||||
return MetadataType.exif;
|
return MetadataType.exif;
|
||||||
case MetadataField.mp4GpsCoordinates:
|
case MetadataField.mp4GpsCoordinates:
|
||||||
|
@ -145,6 +147,10 @@ extension ExtraMetadataFieldConvert on MetadataField {
|
||||||
return 'GPSVersionID';
|
return 'GPSVersionID';
|
||||||
case MetadataField.exifImageDescription:
|
case MetadataField.exifImageDescription:
|
||||||
return 'ImageDescription';
|
return 'ImageDescription';
|
||||||
|
case MetadataField.exifMake:
|
||||||
|
return 'Make';
|
||||||
|
case MetadataField.exifModel:
|
||||||
|
return 'Model';
|
||||||
case MetadataField.exifUserComment:
|
case MetadataField.exifUserComment:
|
||||||
return 'UserComment';
|
return 'UserComment';
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1524,5 +1524,13 @@
|
||||||
"collectionActionSetHome": "تعيين كخلفية",
|
"collectionActionSetHome": "تعيين كخلفية",
|
||||||
"@collectionActionSetHome": {},
|
"@collectionActionSetHome": {},
|
||||||
"setHomeCustomCollection": "مجموعة مخصصة",
|
"setHomeCustomCollection": "مجموعة مخصصة",
|
||||||
"@setHomeCustomCollection": {}
|
"@setHomeCustomCollection": {},
|
||||||
|
"videoActionABRepeat": "تكرار A-B",
|
||||||
|
"@videoActionABRepeat": {},
|
||||||
|
"videoRepeatActionSetEnd": "تعيين نهاية التشغيل",
|
||||||
|
"@videoRepeatActionSetEnd": {},
|
||||||
|
"stopTooltip": "توقف",
|
||||||
|
"@stopTooltip": {},
|
||||||
|
"videoRepeatActionSetStart": "تعيين بداية التشغيل",
|
||||||
|
"@videoRepeatActionSetStart": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"@welcomeTermsToggle": {},
|
"@welcomeTermsToggle": {},
|
||||||
"welcomeOptional": "Неабавязковыя",
|
"welcomeOptional": "Неабавязковыя",
|
||||||
"@welcomeOptional": {},
|
"@welcomeOptional": {},
|
||||||
"welcomeMessage": "Сардэчна запрашаем у Aves",
|
"welcomeMessage": "Сардэчна запрашаем ў Aves",
|
||||||
"@welcomeMessage": {},
|
"@welcomeMessage": {},
|
||||||
"itemCount": "{count, plural, =1{1 элемент} other{{count} элементаў}}",
|
"itemCount": "{count, plural, =1{1 элемент} other{{count} элементаў}}",
|
||||||
"@itemCount": {
|
"@itemCount": {
|
||||||
|
@ -38,7 +38,7 @@
|
||||||
"@saveTooltip": {},
|
"@saveTooltip": {},
|
||||||
"doNotAskAgain": "Больш не пытайся",
|
"doNotAskAgain": "Больш не пытайся",
|
||||||
"@doNotAskAgain": {},
|
"@doNotAskAgain": {},
|
||||||
"chipActionGoToCountryPage": "Паказаць у краінах",
|
"chipActionGoToCountryPage": "Паказаць ў Краінах",
|
||||||
"@chipActionGoToCountryPage": {},
|
"@chipActionGoToCountryPage": {},
|
||||||
"chipActionFilterOut": "Адфільтраваць",
|
"chipActionFilterOut": "Адфільтраваць",
|
||||||
"@chipActionFilterOut": {},
|
"@chipActionFilterOut": {},
|
||||||
|
@ -56,17 +56,17 @@
|
||||||
"@sourceStateCataloguing": {},
|
"@sourceStateCataloguing": {},
|
||||||
"chipActionDelete": "Выдаліць",
|
"chipActionDelete": "Выдаліць",
|
||||||
"@chipActionDelete": {},
|
"@chipActionDelete": {},
|
||||||
"chipActionGoToAlbumPage": "Паказаць у альбомах",
|
"chipActionGoToAlbumPage": "Паказаць ў Альбомах",
|
||||||
"@chipActionGoToAlbumPage": {},
|
"@chipActionGoToAlbumPage": {},
|
||||||
"chipActionHide": "Схаваць",
|
"chipActionHide": "Схаваць",
|
||||||
"@chipActionHide": {},
|
"@chipActionHide": {},
|
||||||
"chipActionCreateVault": "Стварыце сховішча",
|
"chipActionCreateVault": "Стварыце сховішча",
|
||||||
"@chipActionCreateVault": {},
|
"@chipActionCreateVault": {},
|
||||||
"chipActionGoToPlacePage": "Паказаць у месцах",
|
"chipActionGoToPlacePage": "Паказаць ў Лакацыях",
|
||||||
"@chipActionGoToPlacePage": {},
|
"@chipActionGoToPlacePage": {},
|
||||||
"chipActionUnpin": "Адмацаваць зверху",
|
"chipActionUnpin": "Адмацаваць зверху",
|
||||||
"@chipActionUnpin": {},
|
"@chipActionUnpin": {},
|
||||||
"chipActionGoToTagPage": "Паказаць у тэгах",
|
"chipActionGoToTagPage": "Паказаць ў Тэгах",
|
||||||
"@chipActionGoToTagPage": {},
|
"@chipActionGoToTagPage": {},
|
||||||
"chipActionLock": "Заблакаваць",
|
"chipActionLock": "Заблакаваць",
|
||||||
"@chipActionLock": {},
|
"@chipActionLock": {},
|
||||||
|
@ -76,7 +76,7 @@
|
||||||
"@chipActionRename": {},
|
"@chipActionRename": {},
|
||||||
"chipActionConfigureVault": "Наладзіць сховішча",
|
"chipActionConfigureVault": "Наладзіць сховішча",
|
||||||
"@chipActionConfigureVault": {},
|
"@chipActionConfigureVault": {},
|
||||||
"entryActionCopyToClipboard": "Скапіраваць у буфер абмену",
|
"entryActionCopyToClipboard": "Скапіяваць ў буфер абмену",
|
||||||
"@entryActionCopyToClipboard": {},
|
"@entryActionCopyToClipboard": {},
|
||||||
"entryActionDelete": "Выдаліць",
|
"entryActionDelete": "Выдаліць",
|
||||||
"@entryActionDelete": {},
|
"@entryActionDelete": {},
|
||||||
|
@ -120,13 +120,13 @@
|
||||||
"@entryActionRotateScreen": {},
|
"@entryActionRotateScreen": {},
|
||||||
"entryActionViewSource": "Паглядзець крыніцу",
|
"entryActionViewSource": "Паглядзець крыніцу",
|
||||||
"@entryActionViewSource": {},
|
"@entryActionViewSource": {},
|
||||||
"entryActionConvertMotionPhotoToStillImage": "Пераўтварыць у нерухомую выяву",
|
"entryActionConvertMotionPhotoToStillImage": "Пераўтварыць ў нерухомую выяву",
|
||||||
"@entryActionConvertMotionPhotoToStillImage": {},
|
"@entryActionConvertMotionPhotoToStillImage": {},
|
||||||
"entryActionViewMotionPhotoVideo": "Адкрыць відэа",
|
"entryActionViewMotionPhotoVideo": "Адкрыць відэа",
|
||||||
"@entryActionViewMotionPhotoVideo": {},
|
"@entryActionViewMotionPhotoVideo": {},
|
||||||
"entryActionSetAs": "Ўсталяваць як",
|
"entryActionSetAs": "Ўсталяваць як",
|
||||||
"@entryActionSetAs": {},
|
"@entryActionSetAs": {},
|
||||||
"entryActionAddFavourite": "Дадаць у абранае",
|
"entryActionAddFavourite": "Дадаць ў абранае",
|
||||||
"@entryActionAddFavourite": {},
|
"@entryActionAddFavourite": {},
|
||||||
"videoActionUnmute": "Ўключыць гук",
|
"videoActionUnmute": "Ўключыць гук",
|
||||||
"@videoActionUnmute": {},
|
"@videoActionUnmute": {},
|
||||||
|
@ -188,11 +188,11 @@
|
||||||
"@entryActionEdit": {},
|
"@entryActionEdit": {},
|
||||||
"entryActionOpen": "Адкрыць з дапамогай",
|
"entryActionOpen": "Адкрыць з дапамогай",
|
||||||
"@entryActionOpen": {},
|
"@entryActionOpen": {},
|
||||||
"entryActionOpenMap": "Паказаць у праграме карты",
|
"entryActionOpenMap": "Паказаць ў праграме карты",
|
||||||
"@entryActionOpenMap": {},
|
"@entryActionOpenMap": {},
|
||||||
"videoActionMute": "Адключыць гук",
|
"videoActionMute": "Адключыць гук",
|
||||||
"@videoActionMute": {},
|
"@videoActionMute": {},
|
||||||
"slideshowActionShowInCollection": "Паказаць у калекцыі",
|
"slideshowActionShowInCollection": "Паказаць ў Калекцыі",
|
||||||
"@slideshowActionShowInCollection": {},
|
"@slideshowActionShowInCollection": {},
|
||||||
"entryInfoActionEditDate": "Рэдагаваць дату і час",
|
"entryInfoActionEditDate": "Рэдагаваць дату і час",
|
||||||
"@entryInfoActionEditDate": {},
|
"@entryInfoActionEditDate": {},
|
||||||
|
@ -363,7 +363,7 @@
|
||||||
"@vaultLockTypePassword": {},
|
"@vaultLockTypePassword": {},
|
||||||
"settingsVideoEnablePip": "Карцінка ў карцінцы",
|
"settingsVideoEnablePip": "Карцінка ў карцінцы",
|
||||||
"@settingsVideoEnablePip": {},
|
"@settingsVideoEnablePip": {},
|
||||||
"videoControlsPlayOutside": "Адкрыць у іншым прайгравальніку",
|
"videoControlsPlayOutside": "Адкрыць ў іншым прайгравальніку",
|
||||||
"@videoControlsPlayOutside": {},
|
"@videoControlsPlayOutside": {},
|
||||||
"videoControlsPlay": "Прайграванне",
|
"videoControlsPlay": "Прайграванне",
|
||||||
"@videoControlsPlay": {},
|
"@videoControlsPlay": {},
|
||||||
|
@ -478,7 +478,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"addShortcutDialogLabel": "Ярлык хуткага доступу",
|
"addShortcutDialogLabel": "Назва ярлыка",
|
||||||
"@addShortcutDialogLabel": {},
|
"@addShortcutDialogLabel": {},
|
||||||
"addShortcutButtonLabel": "ДАДАЦЬ",
|
"addShortcutButtonLabel": "ДАДАЦЬ",
|
||||||
"@addShortcutButtonLabel": {},
|
"@addShortcutButtonLabel": {},
|
||||||
|
@ -507,7 +507,7 @@
|
||||||
"@newAlbumDialogTitle": {},
|
"@newAlbumDialogTitle": {},
|
||||||
"newAlbumDialogNameLabel": "Назва альбома",
|
"newAlbumDialogNameLabel": "Назва альбома",
|
||||||
"@newAlbumDialogNameLabel": {},
|
"@newAlbumDialogNameLabel": {},
|
||||||
"newAlbumDialogNameLabelAlreadyExistsHelper": "Каталог ужо існуе",
|
"newAlbumDialogNameLabelAlreadyExistsHelper": "Каталог ўжо існуе",
|
||||||
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
"@newAlbumDialogNameLabelAlreadyExistsHelper": {},
|
||||||
"newAlbumDialogStorageLabel": "Захоўванне:",
|
"newAlbumDialogStorageLabel": "Захоўванне:",
|
||||||
"@newAlbumDialogStorageLabel": {},
|
"@newAlbumDialogStorageLabel": {},
|
||||||
|
@ -691,13 +691,13 @@
|
||||||
"@aboutBugReportInstruction": {},
|
"@aboutBugReportInstruction": {},
|
||||||
"entryActionCast": "Трансляцыя",
|
"entryActionCast": "Трансляцыя",
|
||||||
"@entryActionCast": {},
|
"@entryActionCast": {},
|
||||||
"hideFilterConfirmationDialogMessage": "Адпаведныя фота і відэа будуць схаваны з вашай калекцыі. Вы можаце убачыць іх зноў у наладах «Прыватнасць».\n\nВы ўпэўнены, што хочаце іх схаваць?",
|
"hideFilterConfirmationDialogMessage": "Адпаведныя фота і відэа будуць схаваны з вашай калекцыі. Вы можаце убачыць іх зноў ў наладах «Прыватнасць».\n\nВы ўпэўнены, што хочаце іх схаваць?",
|
||||||
"@hideFilterConfirmationDialogMessage": {},
|
"@hideFilterConfirmationDialogMessage": {},
|
||||||
"renameEntrySetPagePatternFieldLabel": "Шаблон наймення",
|
"renameEntrySetPagePatternFieldLabel": "Шаблон наймення",
|
||||||
"@renameEntrySetPagePatternFieldLabel": {},
|
"@renameEntrySetPagePatternFieldLabel": {},
|
||||||
"renameAlbumDialogLabel": "Новая назва",
|
"renameAlbumDialogLabel": "Новая назва",
|
||||||
"@renameAlbumDialogLabel": {},
|
"@renameAlbumDialogLabel": {},
|
||||||
"renameAlbumDialogLabelAlreadyExistsHelper": "Каталог ужо ёсць",
|
"renameAlbumDialogLabelAlreadyExistsHelper": "Каталог ўжо ёсць",
|
||||||
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
"@renameAlbumDialogLabelAlreadyExistsHelper": {},
|
||||||
"aboutBugReportButton": "Адправіць справаздачу",
|
"aboutBugReportButton": "Адправіць справаздачу",
|
||||||
"@aboutBugReportButton": {},
|
"@aboutBugReportButton": {},
|
||||||
|
@ -781,7 +781,7 @@
|
||||||
"@statsPageTitle": {},
|
"@statsPageTitle": {},
|
||||||
"settingsUnitSystemDialogTitle": "Адзінкі вымярэння",
|
"settingsUnitSystemDialogTitle": "Адзінкі вымярэння",
|
||||||
"@settingsUnitSystemDialogTitle": {},
|
"@settingsUnitSystemDialogTitle": {},
|
||||||
"editEntryDialogCopyFromItem": "Скапіюваць з іншага элемента",
|
"editEntryDialogCopyFromItem": "Скапіяваць з іншага элемента",
|
||||||
"@editEntryDialogCopyFromItem": {},
|
"@editEntryDialogCopyFromItem": {},
|
||||||
"settingsThemeEnableDynamicColor": "Дынамічны колер",
|
"settingsThemeEnableDynamicColor": "Дынамічны колер",
|
||||||
"@settingsThemeEnableDynamicColor": {},
|
"@settingsThemeEnableDynamicColor": {},
|
||||||
|
@ -791,13 +791,13 @@
|
||||||
"@renameEntrySetPageInsertTooltip": {},
|
"@renameEntrySetPageInsertTooltip": {},
|
||||||
"settingsThemeBrightnessTile": "Тэма",
|
"settingsThemeBrightnessTile": "Тэма",
|
||||||
"@settingsThemeBrightnessTile": {},
|
"@settingsThemeBrightnessTile": {},
|
||||||
"settingsSystemDefault": "Як у сістэме",
|
"settingsSystemDefault": "Як ў сістэме",
|
||||||
"@settingsSystemDefault": {},
|
"@settingsSystemDefault": {},
|
||||||
"settingsCollectionTile": "Калекцыя",
|
"settingsCollectionTile": "Калекцыя",
|
||||||
"@settingsCollectionTile": {},
|
"@settingsCollectionTile": {},
|
||||||
"settingsThemeBrightnessDialogTitle": "Тэма",
|
"settingsThemeBrightnessDialogTitle": "Тэма",
|
||||||
"@settingsThemeBrightnessDialogTitle": {},
|
"@settingsThemeBrightnessDialogTitle": {},
|
||||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэты альбом і элемент у ім?} few{Выдаліць гэты альбом і {count} элементы у ім?} other{Выдаліць гэты альбом і {count} элементаў у ім?}}",
|
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэты альбом і элемент ў ім?} few{Выдаліць гэты альбом і {count} элементы ў ім?} other{Выдаліць гэты альбом і {count} элементаў ў ім?}}",
|
||||||
"@deleteSingleAlbumConfirmationDialogMessage": {
|
"@deleteSingleAlbumConfirmationDialogMessage": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
|
@ -853,7 +853,7 @@
|
||||||
"@tileLayoutMosaic": {},
|
"@tileLayoutMosaic": {},
|
||||||
"collectionDeselectSectionTooltip": "Адмяніць выбар раздзела",
|
"collectionDeselectSectionTooltip": "Адмяніць выбар раздзела",
|
||||||
"@collectionDeselectSectionTooltip": {},
|
"@collectionDeselectSectionTooltip": {},
|
||||||
"settingsKeepScreenOnTile": "Трымаць экран уключаным",
|
"settingsKeepScreenOnTile": "Трымаць экран ўключаным",
|
||||||
"@settingsKeepScreenOnTile": {},
|
"@settingsKeepScreenOnTile": {},
|
||||||
"tileLayoutGrid": "Сетка",
|
"tileLayoutGrid": "Сетка",
|
||||||
"@tileLayoutGrid": {},
|
"@tileLayoutGrid": {},
|
||||||
|
@ -895,7 +895,7 @@
|
||||||
"@searchCountriesSectionTitle": {},
|
"@searchCountriesSectionTitle": {},
|
||||||
"settingsAskEverytime": "Пытацца кожны раз",
|
"settingsAskEverytime": "Пытацца кожны раз",
|
||||||
"@settingsAskEverytime": {},
|
"@settingsAskEverytime": {},
|
||||||
"editEntryDateDialogCopyField": "Капіяваць з іншай даты",
|
"editEntryDateDialogCopyField": "Скапіяваць з іншай даты",
|
||||||
"@editEntryDateDialogCopyField": {},
|
"@editEntryDateDialogCopyField": {},
|
||||||
"searchTagsSectionTitle": "Тэгі",
|
"searchTagsSectionTitle": "Тэгі",
|
||||||
"@searchTagsSectionTitle": {},
|
"@searchTagsSectionTitle": {},
|
||||||
|
@ -953,7 +953,7 @@
|
||||||
"@albumPickPageTitlePick": {},
|
"@albumPickPageTitlePick": {},
|
||||||
"menuActionMap": "Карта",
|
"menuActionMap": "Карта",
|
||||||
"@menuActionMap": {},
|
"@menuActionMap": {},
|
||||||
"collectionActionMove": "Перамясціць у альбом",
|
"collectionActionMove": "Перамясціць ў альбом",
|
||||||
"@collectionActionMove": {},
|
"@collectionActionMove": {},
|
||||||
"searchAlbumsSectionTitle": "Альбомы",
|
"searchAlbumsSectionTitle": "Альбомы",
|
||||||
"@searchAlbumsSectionTitle": {},
|
"@searchAlbumsSectionTitle": {},
|
||||||
|
@ -1013,9 +1013,9 @@
|
||||||
"@albumPageTitle": {},
|
"@albumPageTitle": {},
|
||||||
"editEntryLocationDialogTitle": "Месцазнаходжанне",
|
"editEntryLocationDialogTitle": "Месцазнаходжанне",
|
||||||
"@editEntryLocationDialogTitle": {},
|
"@editEntryLocationDialogTitle": {},
|
||||||
"albumPickPageTitleCopy": "Капіюваць у альбом",
|
"albumPickPageTitleCopy": "Скапіяваць ў альбом",
|
||||||
"@albumPickPageTitleCopy": {},
|
"@albumPickPageTitleCopy": {},
|
||||||
"collectionActionCopy": "Скапіюваць у альбом",
|
"collectionActionCopy": "Скапіяваць ў альбом",
|
||||||
"@collectionActionCopy": {},
|
"@collectionActionCopy": {},
|
||||||
"viewDialogReverseSortOrder": "Адваротны парадак сартавання",
|
"viewDialogReverseSortOrder": "Адваротны парадак сартавання",
|
||||||
"@viewDialogReverseSortOrder": {},
|
"@viewDialogReverseSortOrder": {},
|
||||||
|
@ -1033,7 +1033,7 @@
|
||||||
"@tagEmpty": {},
|
"@tagEmpty": {},
|
||||||
"collectionActionShowTitleSearch": "Паказаць фільтр загалоўка",
|
"collectionActionShowTitleSearch": "Паказаць фільтр загалоўка",
|
||||||
"@collectionActionShowTitleSearch": {},
|
"@collectionActionShowTitleSearch": {},
|
||||||
"menuActionSelectAll": "Выбраць усё",
|
"menuActionSelectAll": "Выбраць ўсё",
|
||||||
"@menuActionSelectAll": {},
|
"@menuActionSelectAll": {},
|
||||||
"settingsConfirmationTile": "Дыялогі пацверджання",
|
"settingsConfirmationTile": "Дыялогі пацверджання",
|
||||||
"@settingsConfirmationTile": {},
|
"@settingsConfirmationTile": {},
|
||||||
|
@ -1059,7 +1059,7 @@
|
||||||
"@drawerCollectionAnimated": {},
|
"@drawerCollectionAnimated": {},
|
||||||
"durationDialogHours": "Гадзіны",
|
"durationDialogHours": "Гадзіны",
|
||||||
"@durationDialogHours": {},
|
"@durationDialogHours": {},
|
||||||
"settingsKeepScreenOnDialogTitle": "Трымаць экран уключаным",
|
"settingsKeepScreenOnDialogTitle": "Трымаць экран ўключаным",
|
||||||
"@settingsKeepScreenOnDialogTitle": {},
|
"@settingsKeepScreenOnDialogTitle": {},
|
||||||
"drawerPlacePage": "Месцы",
|
"drawerPlacePage": "Месцы",
|
||||||
"@drawerPlacePage": {},
|
"@drawerPlacePage": {},
|
||||||
|
@ -1077,7 +1077,7 @@
|
||||||
"@appExportFavourites": {},
|
"@appExportFavourites": {},
|
||||||
"collectionEmptyImages": "Няма выяў",
|
"collectionEmptyImages": "Няма выяў",
|
||||||
"@collectionEmptyImages": {},
|
"@collectionEmptyImages": {},
|
||||||
"albumPickPageTitleExport": "Экспартаваць у альбом",
|
"albumPickPageTitleExport": "Экспартаваць ў альбом",
|
||||||
"@albumPickPageTitleExport": {},
|
"@albumPickPageTitleExport": {},
|
||||||
"settingsActionExportDialogTitle": "Экспарт",
|
"settingsActionExportDialogTitle": "Экспарт",
|
||||||
"@settingsActionExportDialogTitle": {},
|
"@settingsActionExportDialogTitle": {},
|
||||||
|
@ -1149,7 +1149,7 @@
|
||||||
"@coverDialogTabColor": {},
|
"@coverDialogTabColor": {},
|
||||||
"genericSuccessFeedback": "Гатова!",
|
"genericSuccessFeedback": "Гатова!",
|
||||||
"@genericSuccessFeedback": {},
|
"@genericSuccessFeedback": {},
|
||||||
"aboutLicensesShowAllButtonLabel": "Паказаць усе ліцэнзіі",
|
"aboutLicensesShowAllButtonLabel": "Паказаць ўсе ліцэнзіі",
|
||||||
"@aboutLicensesShowAllButtonLabel": {},
|
"@aboutLicensesShowAllButtonLabel": {},
|
||||||
"sortOrderNewestFirst": "Спачатку самае новае",
|
"sortOrderNewestFirst": "Спачатку самае новае",
|
||||||
"@sortOrderNewestFirst": {},
|
"@sortOrderNewestFirst": {},
|
||||||
|
@ -1175,7 +1175,7 @@
|
||||||
"@menuActionStats": {},
|
"@menuActionStats": {},
|
||||||
"appPickDialogTitle": "Выбраць праграму",
|
"appPickDialogTitle": "Выбраць праграму",
|
||||||
"@appPickDialogTitle": {},
|
"@appPickDialogTitle": {},
|
||||||
"albumPickPageTitleMove": "Перамясціць у альбом",
|
"albumPickPageTitleMove": "Перамясціць ў альбом",
|
||||||
"@albumPickPageTitleMove": {},
|
"@albumPickPageTitleMove": {},
|
||||||
"coverDialogTabCover": "Вокладка",
|
"coverDialogTabCover": "Вокладка",
|
||||||
"@coverDialogTabCover": {},
|
"@coverDialogTabCover": {},
|
||||||
|
@ -1405,7 +1405,7 @@
|
||||||
"@settingsStorageAccessEmpty": {},
|
"@settingsStorageAccessEmpty": {},
|
||||||
"settingsRemoveAnimationsTile": "Выдаліць анімацыі",
|
"settingsRemoveAnimationsTile": "Выдаліць анімацыі",
|
||||||
"@settingsRemoveAnimationsTile": {},
|
"@settingsRemoveAnimationsTile": {},
|
||||||
"settingsStorageAccessBanner": "Некаторыя каталогі патрабуюць відавочнага дазволу на змяненне файлаў у іх. Тут вы можаце прагледзець каталогі, да якіх вы раней далі доступ.",
|
"settingsStorageAccessBanner": "Некаторыя каталогі патрабуюць відавочнага дазволу на змяненне файлаў ў іх. Тут вы можаце прагледзець каталогі, да якіх вы раней далі доступ.",
|
||||||
"@settingsStorageAccessBanner": {},
|
"@settingsStorageAccessBanner": {},
|
||||||
"collectionCopySuccessFeedback": "{count, plural, =1{1 элемент скапіяваны} few{{count} элементы скапіявана} other{{count} элементаў скапіявана}}",
|
"collectionCopySuccessFeedback": "{count, plural, =1{1 элемент скапіяваны} few{{count} элементы скапіявана} other{{count} элементаў скапіявана}}",
|
||||||
"@collectionCopySuccessFeedback": {
|
"@collectionCopySuccessFeedback": {
|
||||||
|
@ -1467,7 +1467,7 @@
|
||||||
"@settingsSubtitleThemeTextPositionTile": {},
|
"@settingsSubtitleThemeTextPositionTile": {},
|
||||||
"settingsVideoBackgroundModeDialogTitle": "Фонавы рэжым",
|
"settingsVideoBackgroundModeDialogTitle": "Фонавы рэжым",
|
||||||
"@settingsVideoBackgroundModeDialogTitle": {},
|
"@settingsVideoBackgroundModeDialogTitle": {},
|
||||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэтыя альбомы і элемент у іх?} few{Выдаліць гэтыя альбомы і {count} элементы у іх?} other{Выдаліць гэтыя альбомы і {count} элементаў у іх?}}",
|
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Выдаліць гэтыя альбомы і элемент ў іх?} few{Выдаліць гэтыя альбомы і {count} элементы ў іх?} other{Выдаліць гэтыя альбомы і {count} элементаў ў іх?}}",
|
||||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
|
@ -1524,5 +1524,13 @@
|
||||||
"setHomeCustomCollection": "Ўласная калекцыя",
|
"setHomeCustomCollection": "Ўласная калекцыя",
|
||||||
"@setHomeCustomCollection": {},
|
"@setHomeCustomCollection": {},
|
||||||
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
|
"settingsThumbnailShowHdrIcon": "Паказаць значок HDR",
|
||||||
"@settingsThumbnailShowHdrIcon": {}
|
"@settingsThumbnailShowHdrIcon": {},
|
||||||
|
"videoRepeatActionSetEnd": "Ўсталяваць канец",
|
||||||
|
"@videoRepeatActionSetEnd": {},
|
||||||
|
"stopTooltip": "Спыніць",
|
||||||
|
"@stopTooltip": {},
|
||||||
|
"videoActionABRepeat": "Паўтарыць ад А да Б",
|
||||||
|
"@videoActionABRepeat": {},
|
||||||
|
"videoRepeatActionSetStart": "Ўсталяваць пачатак",
|
||||||
|
"@videoRepeatActionSetStart": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
"actionRemove": "Remove",
|
"actionRemove": "Remove",
|
||||||
"resetTooltip": "Reset",
|
"resetTooltip": "Reset",
|
||||||
"saveTooltip": "Save",
|
"saveTooltip": "Save",
|
||||||
|
"stopTooltip": "Stop",
|
||||||
"pickTooltip": "Pick",
|
"pickTooltip": "Pick",
|
||||||
|
|
||||||
"doubleBackExitMessage": "Tap “back” again to exit.",
|
"doubleBackExitMessage": "Tap “back” again to exit.",
|
||||||
|
@ -127,6 +128,10 @@
|
||||||
"videoActionSkip10": "Seek forward 10 seconds",
|
"videoActionSkip10": "Seek forward 10 seconds",
|
||||||
"videoActionSelectStreams": "Select tracks",
|
"videoActionSelectStreams": "Select tracks",
|
||||||
"videoActionSetSpeed": "Playback speed",
|
"videoActionSetSpeed": "Playback speed",
|
||||||
|
"videoActionABRepeat": "A-B repeat",
|
||||||
|
|
||||||
|
"videoRepeatActionSetStart": "Set start",
|
||||||
|
"videoRepeatActionSetEnd": "Set end",
|
||||||
|
|
||||||
"viewerActionSettings": "Settings",
|
"viewerActionSettings": "Settings",
|
||||||
"viewerActionLock": "Lock viewer",
|
"viewerActionLock": "Lock viewer",
|
||||||
|
|
|
@ -1366,5 +1366,13 @@
|
||||||
"collectionActionSetHome": "Fijar como inicio",
|
"collectionActionSetHome": "Fijar como inicio",
|
||||||
"@collectionActionSetHome": {},
|
"@collectionActionSetHome": {},
|
||||||
"setHomeCustomCollection": "Colección personalizada",
|
"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": "Collection personnalisée",
|
||||||
"@setHomeCustomCollection": {},
|
"@setHomeCustomCollection": {},
|
||||||
"settingsThumbnailShowHdrIcon": "Afficher l’icône HDR",
|
"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": "Tampilkan ikon HDR",
|
||||||
"@settingsThumbnailShowHdrIcon": {},
|
"@settingsThumbnailShowHdrIcon": {},
|
||||||
"castDialogTitle": "Siarkan Perangkat",
|
"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": {},
|
"@lengthUnitPercent": {},
|
||||||
"saveCopyButtonLabel": "コピーを保存",
|
"saveCopyButtonLabel": "コピーを保存",
|
||||||
"@saveCopyButtonLabel": {},
|
"@saveCopyButtonLabel": {},
|
||||||
"columnCount": "{count, plural, =1{1 列} other{{count} 列}}",
|
"columnCount": "{count, plural, other{{count} 列}}",
|
||||||
"@columnCount": {
|
"@columnCount": {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"count": {}
|
"count": {}
|
||||||
|
@ -1246,5 +1246,23 @@
|
||||||
"entryActionCast": "キャスト",
|
"entryActionCast": "キャスト",
|
||||||
"@entryActionCast": {},
|
"@entryActionCast": {},
|
||||||
"editorTransformRotate": "回転",
|
"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": "홈으로 설정",
|
||||||
"@collectionActionSetHome": {},
|
"@collectionActionSetHome": {},
|
||||||
"setHomeCustomCollection": "지정 미디어",
|
"setHomeCustomCollection": "지정 미디어",
|
||||||
"@setHomeCustomCollection": {}
|
"@setHomeCustomCollection": {},
|
||||||
|
"videoRepeatActionSetStart": "시작 지점 설정",
|
||||||
|
"@videoRepeatActionSetStart": {},
|
||||||
|
"videoRepeatActionSetEnd": "종료 지점 설정",
|
||||||
|
"@videoRepeatActionSetEnd": {},
|
||||||
|
"stopTooltip": "취소",
|
||||||
|
"@stopTooltip": {},
|
||||||
|
"videoActionABRepeat": "A-B 반복",
|
||||||
|
"@videoActionABRepeat": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1524,5 +1524,13 @@
|
||||||
"setHomeCustomCollection": "Własna kolekcja",
|
"setHomeCustomCollection": "Własna kolekcja",
|
||||||
"@setHomeCustomCollection": {},
|
"@setHomeCustomCollection": {},
|
||||||
"collectionActionSetHome": "Ustaw jako stronę główną",
|
"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": "Transmitir",
|
||||||
"@entryActionCast": {},
|
"@entryActionCast": {},
|
||||||
"castDialogTitle": "Dispositivos para Transmitir",
|
"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": "Собственная коллекция",
|
||||||
"@setHomeCustomCollection": {},
|
"@setHomeCustomCollection": {},
|
||||||
"collectionActionSetHome": "Установить как главную",
|
"collectionActionSetHome": "Установить как главную",
|
||||||
"@collectionActionSetHome": {}
|
"@collectionActionSetHome": {},
|
||||||
|
"videoRepeatActionSetStart": "Установить начало",
|
||||||
|
"@videoRepeatActionSetStart": {},
|
||||||
|
"stopTooltip": "Остановить",
|
||||||
|
"@stopTooltip": {},
|
||||||
|
"videoActionABRepeat": "Повторить от А до Б",
|
||||||
|
"@videoActionABRepeat": {},
|
||||||
|
"videoRepeatActionSetEnd": "Установить конец",
|
||||||
|
"@videoRepeatActionSetEnd": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1524,5 +1524,13 @@
|
||||||
"setHomeCustomCollection": "Власна колекція",
|
"setHomeCustomCollection": "Власна колекція",
|
||||||
"@setHomeCustomCollection": {},
|
"@setHomeCustomCollection": {},
|
||||||
"collectionActionSetHome": "Встановити як головну",
|
"collectionActionSetHome": "Встановити як головну",
|
||||||
"@collectionActionSetHome": {}
|
"@collectionActionSetHome": {},
|
||||||
|
"videoRepeatActionSetStart": "Змінити початок",
|
||||||
|
"@videoRepeatActionSetStart": {},
|
||||||
|
"videoRepeatActionSetEnd": "Змінити кінець",
|
||||||
|
"@videoRepeatActionSetEnd": {},
|
||||||
|
"stopTooltip": "Зупинити",
|
||||||
|
"@stopTooltip": {},
|
||||||
|
"videoActionABRepeat": "Повторити від А до Б",
|
||||||
|
"@videoActionABRepeat": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ class Contributors {
|
||||||
Contributor('luckris25', 'lk1thebestl@gmail.com'),
|
Contributor('luckris25', 'lk1thebestl@gmail.com'),
|
||||||
Contributor('Marc Amorós', 'marquitus99@gmail.com'),
|
Contributor('Marc Amorós', 'marquitus99@gmail.com'),
|
||||||
Contributor('elea11', 'p.manuel.warnecke@gmail.com'),
|
Contributor('elea11', 'p.manuel.warnecke@gmail.com'),
|
||||||
|
Contributor('しいたけ', 'Shiitake@users.noreply.hosted.weblate.org'),
|
||||||
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
||||||
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
||||||
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
|
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
|
||||||
|
|
|
@ -33,8 +33,8 @@ class LiveAvesAvailability implements AvesAvailability {
|
||||||
return _isConnected!;
|
return _isConnected!;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateConnectivityFromResult(ConnectivityResult result) {
|
void _updateConnectivityFromResult(List<ConnectivityResult> result) {
|
||||||
final newValue = result != ConnectivityResult.none;
|
final newValue = result.isNotEmpty && !result.contains(ConnectivityResult.none);
|
||||||
if (_isConnected != newValue) {
|
if (_isConnected != newValue) {
|
||||||
_isConnected = newValue;
|
_isConnected = newValue;
|
||||||
debugPrint('Device is connected=$_isConnected');
|
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/catalog.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.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/exif.dart';
|
||||||
import 'package:aves/ref/metadata/iptc.dart';
|
import 'package:aves/ref/metadata/iptc.dart';
|
||||||
import 'package:aves/ref/metadata/xmp.dart';
|
import 'package:aves/ref/metadata/xmp.dart';
|
||||||
|
@ -121,7 +122,7 @@ extension ExtraAvesEntryMetadataEdition on AvesEntry {
|
||||||
if (latLng != null && latLng != removalLocation) {
|
if (latLng != null && latLng != removalLocation) {
|
||||||
final latitude = latLng.latitude;
|
final latitude = latLng.latitude;
|
||||||
final longitude = latLng.longitude;
|
final longitude = latLng.longitude;
|
||||||
const locale = 'en_US';
|
const locale = asciiLocale;
|
||||||
final isoLat = '${latitude >= 0 ? '+' : '-'}${NumberFormat('00.0000', locale).format(latitude.abs())}';
|
final isoLat = '${latitude >= 0 ? '+' : '-'}${NumberFormat('00.0000', locale).format(latitude.abs())}';
|
||||||
final isoLon = '${longitude >= 0 ? '+' : '-'}${NumberFormat('000.0000', locale).format(longitude.abs())}';
|
final isoLon = '${longitude >= 0 ? '+' : '-'}${NumberFormat('000.0000', locale).format(longitude.abs())}';
|
||||||
iso6709String = '$isoLat$isoLon/';
|
iso6709String = '$isoLat$isoLon/';
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
import 'package:aves/convert/metadata/fields.dart';
|
||||||
import 'package:aves/model/entry/entry.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:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
@ -16,6 +20,7 @@ class NamingPattern {
|
||||||
factory NamingPattern.from({
|
factory NamingPattern.from({
|
||||||
required String userPattern,
|
required String userPattern,
|
||||||
required int entryCount,
|
required int entryCount,
|
||||||
|
required String locale,
|
||||||
}) {
|
}) {
|
||||||
final processors = <NamingProcessor>[];
|
final processors = <NamingProcessor>[];
|
||||||
|
|
||||||
|
@ -36,7 +41,13 @@ class NamingPattern {
|
||||||
switch (processorKey) {
|
switch (processorKey) {
|
||||||
case DateNamingProcessor.key:
|
case DateNamingProcessor.key:
|
||||||
if (processorOptions != null) {
|
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:
|
case NameNamingProcessor.key:
|
||||||
processors.add(const NameNamingProcessor());
|
processors.add(const NameNamingProcessor());
|
||||||
|
@ -95,21 +106,33 @@ class NamingPattern {
|
||||||
switch (processorKey) {
|
switch (processorKey) {
|
||||||
case DateNamingProcessor.key:
|
case DateNamingProcessor.key:
|
||||||
return '<$processorKey, yyyyMMdd-HHmmss>';
|
return '<$processorKey, yyyyMMdd-HHmmss>';
|
||||||
|
case TagsNamingProcessor.key:
|
||||||
|
return '<$processorKey, ->';
|
||||||
case CounterNamingProcessor.key:
|
case CounterNamingProcessor.key:
|
||||||
case NameNamingProcessor.key:
|
case NameNamingProcessor.key:
|
||||||
default:
|
default:
|
||||||
|
if (processorKey.startsWith(MetadataFieldNamingProcessor.key)) {
|
||||||
|
final field = MetadataFieldNamingProcessor.fieldFromKey(processorKey);
|
||||||
|
return '<${MetadataFieldNamingProcessor.key}, $field>';
|
||||||
|
}
|
||||||
return '<$processorKey>';
|
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
|
@immutable
|
||||||
abstract class NamingProcessor extends Equatable {
|
abstract class NamingProcessor extends Equatable {
|
||||||
const NamingProcessor();
|
const NamingProcessor();
|
||||||
|
|
||||||
String? process(AvesEntry entry, int index);
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues);
|
||||||
|
|
||||||
|
Set<MetadataField> getRequiredFields() => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -122,7 +145,7 @@ class LiteralNamingProcessor extends NamingProcessor {
|
||||||
const LiteralNamingProcessor(this.text);
|
const LiteralNamingProcessor(this.text);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? process(AvesEntry entry, int index) => text;
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -134,15 +157,63 @@ class DateNamingProcessor extends NamingProcessor {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [format.pattern];
|
List<Object?> get props => [format.pattern];
|
||||||
|
|
||||||
DateNamingProcessor(String pattern) : format = DateFormat(pattern);
|
DateNamingProcessor(String pattern, String locale) : format = DateFormat(pattern, locale);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? process(AvesEntry entry, int index) {
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
|
||||||
final date = entry.bestDate;
|
final date = entry.bestDate;
|
||||||
return date != null ? format.format(date) : null;
|
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
|
@immutable
|
||||||
class NameNamingProcessor extends NamingProcessor {
|
class NameNamingProcessor extends NamingProcessor {
|
||||||
static const key = 'name';
|
static const key = 'name';
|
||||||
|
@ -153,7 +224,7 @@ class NameNamingProcessor extends NamingProcessor {
|
||||||
const NameNamingProcessor();
|
const NameNamingProcessor();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? process(AvesEntry entry, int index) => entry.filenameWithoutExtension;
|
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => entry.filenameWithoutExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
|
@ -174,5 +245,5 @@ class CounterNamingProcessor extends NamingProcessor {
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@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);
|
].join(localeSeparator);
|
||||||
}
|
}
|
||||||
set(SettingKeys.localeKey, tag);
|
set(SettingKeys.localeKey, tag);
|
||||||
_appliedLocale = null;
|
resetAppliedLocale();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Locale> _systemLocalesFallback = [];
|
List<Locale> _systemLocalesFallback = [];
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/settings/defaults.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:aves_model/aves_model.dart';
|
||||||
import 'package:collection/collection.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);
|
AlbumChipGroupFactor get albumGroupFactor => getEnumOrDefault(SettingKeys.albumGroupFactorKey, SettingsDefaults.albumGroupFactor, AlbumChipGroupFactor.values);
|
||||||
|
|
||||||
set albumGroupFactor(AlbumChipGroupFactor newValue) => set(SettingKeys.albumGroupFactorKey, newValue.toString());
|
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 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;
|
bool get showAlbumPickQuery => getBool(SettingKeys.showAlbumPickQueryKey) ?? false;
|
||||||
|
|
||||||
set showAlbumPickQuery(bool newValue) => set(SettingKeys.showAlbumPickQueryKey, newValue);
|
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/filter_grids.dart';
|
||||||
import 'package:aves/model/settings/modules/info.dart';
|
import 'package:aves/model/settings/modules/info.dart';
|
||||||
import 'package:aves/model/settings/modules/navigation.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/search.dart';
|
||||||
import 'package:aves/model/settings/modules/viewer.dart';
|
import 'package:aves/model/settings/modules/viewer.dart';
|
||||||
import 'package:aves/ref/bursts.dart';
|
import 'package:aves/ref/bursts.dart';
|
||||||
|
@ -37,7 +38,7 @@ import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
final Settings settings = Settings._private();
|
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 List<StreamSubscription> _subscriptions = [];
|
||||||
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
|
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
|
||||||
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
|
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
|
||||||
|
@ -83,7 +84,8 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
|
||||||
enableBlurEffect = performanceClass >= 29;
|
enableBlurEffect = performanceClass >= 29;
|
||||||
|
|
||||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
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] : [];
|
collectionBurstPatterns = pattern != null ? [pattern] : [];
|
||||||
|
|
||||||
// availability
|
// availability
|
||||||
|
|
|
@ -196,19 +196,19 @@ mixin AlbumMixin on SourceBase {
|
||||||
final parts = pContext.split(dirPath);
|
final parts = pContext.split(dirPath);
|
||||||
for (var i = parts.length - 1; i > 0; i--) {
|
for (var i = parts.length - 1; i > 0; i--) {
|
||||||
final name = pContext.joinAll(['', ...parts.skip(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;
|
if (others.every((item) => !item.endsWith(testName))) return name;
|
||||||
}
|
}
|
||||||
return dirPath;
|
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);
|
final uniqueNameInDevice = unique(dirPath, otherAlbumsOnDevice);
|
||||||
if (uniqueNameInDevice.length <= relativeDir.length) {
|
if (uniqueNameInDevice.length <= relativeDir.length) {
|
||||||
return uniqueNameInDevice;
|
return uniqueNameInDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
final volumePath = dir.volumePath;
|
final volumePath = dir.volumePath.toLowerCase();
|
||||||
String trimVolumePath(String? path) => path!.substring(dir.volumePath.length);
|
String trimVolumePath(String? path) => path!.substring(dir.volumePath.length);
|
||||||
final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path.startsWith(volumePath)).map(trimVolumePath).toSet();
|
final otherAlbumsOnVolume = otherAlbumsOnDevice.where((path) => path.startsWith(volumePath)).map(trimVolumePath).toSet();
|
||||||
final uniqueNameInVolume = unique(trimVolumePath(dirPath), otherAlbumsOnVolume);
|
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/h264.dart';
|
||||||
import 'package:aves/model/video/profiles/hevc.dart';
|
import 'package:aves/model/video/profiles/hevc.dart';
|
||||||
import 'package:aves/ref/languages.dart';
|
import 'package:aves/ref/languages.dart';
|
||||||
|
import 'package:aves/ref/locales.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/ref/mp4.dart';
|
import 'package:aves/ref/mp4.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
@ -453,7 +454,7 @@ class VideoMetadataFormatter {
|
||||||
|
|
||||||
static String _formatFilesize(dynamic value) {
|
static String _formatFilesize(dynamic value) {
|
||||||
final size = value is int ? value : int.tryParse(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) {
|
static String _formatLanguage(String value) {
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
class BurstPatterns {
|
class BurstPatterns {
|
||||||
static const _keyGroupName = 'key';
|
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 samsung = r'^(?<key>\d{8}_\d{6})_(\d+)$';
|
||||||
static const sony = r'^DSC(PDC)?_\d+_BURST(?<key>\d{17})(_COVER)?$';
|
static const sony = r'^DSC(PDC)?_\d+_BURST(?<key>\d{17})(_COVER)?$';
|
||||||
|
|
||||||
static final options = [
|
static final options = [
|
||||||
|
BurstPatterns.fairphone,
|
||||||
BurstPatterns.samsung,
|
BurstPatterns.samsung,
|
||||||
BurstPatterns.sony,
|
BurstPatterns.sony,
|
||||||
];
|
];
|
||||||
|
|
||||||
static String getName(String pattern) {
|
static String getName(String pattern) {
|
||||||
return switch (pattern) {
|
return switch (pattern) {
|
||||||
|
fairphone => 'Fairphone',
|
||||||
samsung => 'Samsung',
|
samsung => 'Samsung',
|
||||||
sony => 'Sony',
|
sony => 'Sony',
|
||||||
_ => pattern,
|
_ => pattern,
|
||||||
|
@ -19,6 +22,7 @@ class BurstPatterns {
|
||||||
|
|
||||||
static String getExample(String pattern) {
|
static String getExample(String pattern) {
|
||||||
return switch (pattern) {
|
return switch (pattern) {
|
||||||
|
fairphone => 'IMG_20151021_072800_BURST007',
|
||||||
samsung => '20151021_072800_007',
|
samsung => '20151021_072800_007',
|
||||||
sony => 'DSC_0007_BURST20151021072800123',
|
sony => 'DSC_0007_BURST20151021072800123',
|
||||||
_ => '?',
|
_ => '?',
|
||||||
|
@ -26,6 +30,7 @@ class BurstPatterns {
|
||||||
}
|
}
|
||||||
|
|
||||||
static const byManufacturer = {
|
static const byManufacturer = {
|
||||||
|
_Manufacturers.fairphone: fairphone,
|
||||||
_Manufacturers.samsung: samsung,
|
_Manufacturers.samsung: samsung,
|
||||||
_Manufacturers.sony: sony,
|
_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 {
|
class _Manufacturers {
|
||||||
|
static const fairphone = 'fairphone';
|
||||||
static const samsung = 'samsung';
|
static const samsung = 'samsung';
|
||||||
static const sony = 'sony';
|
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 }
|
enum AnalyzerState { running, stopping, stopped }
|
||||||
|
|
||||||
class Analyzer {
|
class Analyzer with WidgetsBindingObserver {
|
||||||
late AppLocalizations _l10n;
|
late AppLocalizations _l10n;
|
||||||
final ValueNotifier<AnalyzerState> _serviceStateNotifier = ValueNotifier<AnalyzerState>(AnalyzerState.stopped);
|
final ValueNotifier<AnalyzerState> _serviceStateNotifier = ValueNotifier<AnalyzerState>(AnalyzerState.stopped);
|
||||||
AnalysisController? _controller;
|
AnalysisController? _controller;
|
||||||
|
@ -102,6 +102,7 @@ class Analyzer {
|
||||||
}
|
}
|
||||||
_serviceStateNotifier.addListener(_onServiceStateChanged);
|
_serviceStateNotifier.addListener(_onServiceStateChanged);
|
||||||
_source.stateNotifier.addListener(_onSourceStateChanged);
|
_source.stateNotifier.addListener(_onSourceStateChanged);
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
@ -111,11 +112,18 @@ class Analyzer {
|
||||||
}
|
}
|
||||||
_stopUpdateTimer();
|
_stopUpdateTimer();
|
||||||
_controller?.dispose();
|
_controller?.dispose();
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_serviceStateNotifier.removeListener(_onServiceStateChanged);
|
_serviceStateNotifier.removeListener(_onServiceStateChanged);
|
||||||
_source.stateNotifier.removeListener(_onSourceStateChanged);
|
_source.stateNotifier.removeListener(_onSourceStateChanged);
|
||||||
_source.dispose();
|
_source.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didHaveMemoryPressure() {
|
||||||
|
super.didHaveMemoryPressure();
|
||||||
|
reportService.log('Analyzer memory pressure');
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> start(dynamic args) async {
|
Future<void> start(dynamic args) async {
|
||||||
List<int>? entryIds;
|
List<int>? entryIds;
|
||||||
var force = false;
|
var force = false;
|
||||||
|
@ -126,7 +134,7 @@ class Analyzer {
|
||||||
progressTotal = args['progressTotal'];
|
progressTotal = args['progressTotal'];
|
||||||
progressOffset = args['progressOffset'];
|
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?.dispose();
|
||||||
_controller = AnalysisController(
|
_controller = AnalysisController(
|
||||||
canStartService: false,
|
canStartService: false,
|
||||||
|
@ -147,8 +155,8 @@ class Analyzer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
Future<void> stop() async {
|
||||||
debugPrint('$runtimeType stop');
|
await reportService.log('Analyzer stop');
|
||||||
_serviceStateNotifier.value = AnalyzerState.stopped;
|
_serviceStateNotifier.value = AnalyzerState.stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ abstract class MetadataFetchService {
|
||||||
|
|
||||||
Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});
|
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);
|
Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);
|
||||||
|
|
||||||
|
@ -39,6 +39,8 @@ abstract class MetadataFetchService {
|
||||||
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
Future<String?> getContentResolverProp(AvesEntry entry, String prop);
|
||||||
|
|
||||||
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
Future<DateTime?> getDate(AvesEntry entry, MetadataField field);
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getFields(AvesEntry entry, Set<MetadataField> fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformMetadataFetchService implements MetadataFetchService {
|
class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
|
@ -110,7 +112,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OverlayMetadata> getFields(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
|
||||||
if (fields.isNotEmpty && !entry.isSvg) {
|
if (fields.isNotEmpty && !entry.isSvg) {
|
||||||
try {
|
try {
|
||||||
// returns fields on demand, with various value types:
|
// returns fields on demand, with various value types:
|
||||||
|
@ -119,7 +121,7 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
// 'exposureTime' (string),
|
// 'exposureTime' (string),
|
||||||
// 'focalLength' (double),
|
// 'focalLength' (double),
|
||||||
// 'iso' (int),
|
// 'iso' (int),
|
||||||
final result = await _platform.invokeMethod('getFields', <String, dynamic>{
|
final result = await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
|
||||||
'mimeType': entry.mimeType,
|
'mimeType': entry.mimeType,
|
||||||
'uri': entry.uri,
|
'uri': entry.uri,
|
||||||
'sizeBytes': entry.sizeBytes,
|
'sizeBytes': entry.sizeBytes,
|
||||||
|
@ -284,4 +286,24 @@ class PlatformMetadataFetchService implements MetadataFetchService {
|
||||||
}
|
}
|
||||||
return null;
|
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 info = Icons.info_outlined;
|
||||||
static const layers = Icons.layers_outlined;
|
static const layers = Icons.layers_outlined;
|
||||||
static const map = Icons.map_outlined;
|
static const map = Icons.map_outlined;
|
||||||
|
static const more = Icons.more_horiz_outlined;
|
||||||
static final move = MdiIcons.fileMoveOutline;
|
static final move = MdiIcons.fileMoveOutline;
|
||||||
static const mute = Icons.volume_off_outlined;
|
static const mute = Icons.volume_off_outlined;
|
||||||
static const unmute = Icons.volume_up_outlined;
|
static const unmute = Icons.volume_up_outlined;
|
||||||
|
@ -118,7 +119,10 @@ class AIcons {
|
||||||
static const pause = Icons.pause;
|
static const pause = Icons.pause;
|
||||||
static const print = Icons.print_outlined;
|
static const print = Icons.print_outlined;
|
||||||
static const refresh = Icons.refresh_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 const replay10 = Icons.replay_10_outlined;
|
||||||
|
static final resetBounds = MdiIcons.rayStartEnd;
|
||||||
static const reverse = Icons.invert_colors_outlined;
|
static const reverse = Icons.invert_colors_outlined;
|
||||||
static const skip10 = Icons.forward_10_outlined;
|
static const skip10 = Icons.forward_10_outlined;
|
||||||
static const reset = Icons.restart_alt_outlined;
|
static const reset = Icons.restart_alt_outlined;
|
||||||
|
@ -130,6 +134,8 @@ class AIcons {
|
||||||
static const select = Icons.select_all_outlined;
|
static const select = Icons.select_all_outlined;
|
||||||
static const setAs = Icons.wallpaper_outlined;
|
static const setAs = Icons.wallpaper_outlined;
|
||||||
static final setCover = MdiIcons.imageEditOutline;
|
static final setCover = MdiIcons.imageEditOutline;
|
||||||
|
static final setEnd = MdiIcons.rayEnd;
|
||||||
|
static final setStart = MdiIcons.rayStart;
|
||||||
static const share = Icons.share_outlined;
|
static const share = Icons.share_outlined;
|
||||||
static const show = Icons.visibility_outlined;
|
static const show = Icons.visibility_outlined;
|
||||||
static final showFullscreen = MdiIcons.arrowExpand;
|
static final showFullscreen = MdiIcons.arrowExpand;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/ref/locales.dart';
|
||||||
import 'package:aves/ref/metadata/xmp.dart';
|
import 'package:aves/ref/metadata/xmp.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:xml/xml.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')}';
|
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(
|
static String? getString(
|
||||||
List<XmlNode> nodes,
|
List<XmlNode> nodes,
|
||||||
|
|
|
@ -36,6 +36,7 @@ extension ExtraEntryActionView on EntryAction {
|
||||||
l10n.videoActionMute,
|
l10n.videoActionMute,
|
||||||
EntryAction.videoSelectStreams => l10n.videoActionSelectStreams,
|
EntryAction.videoSelectStreams => l10n.videoActionSelectStreams,
|
||||||
EntryAction.videoSetSpeed => l10n.videoActionSetSpeed,
|
EntryAction.videoSetSpeed => l10n.videoActionSetSpeed,
|
||||||
|
EntryAction.videoABRepeat => l10n.videoActionABRepeat,
|
||||||
EntryAction.videoSettings => l10n.viewerActionSettings,
|
EntryAction.videoSettings => l10n.viewerActionSettings,
|
||||||
EntryAction.videoTogglePlay =>
|
EntryAction.videoTogglePlay =>
|
||||||
// different data depending on toggle state
|
// different data depending on toggle state
|
||||||
|
@ -110,6 +111,7 @@ extension ExtraEntryActionView on EntryAction {
|
||||||
AIcons.mute,
|
AIcons.mute,
|
||||||
EntryAction.videoSelectStreams => AIcons.streams,
|
EntryAction.videoSelectStreams => AIcons.streams,
|
||||||
EntryAction.videoSetSpeed => AIcons.speed,
|
EntryAction.videoSetSpeed => AIcons.speed,
|
||||||
|
EntryAction.videoABRepeat => AIcons.repeat,
|
||||||
EntryAction.videoSettings => AIcons.videoSettings,
|
EntryAction.videoSettings => AIcons.videoSettings,
|
||||||
EntryAction.videoTogglePlay =>
|
EntryAction.videoTogglePlay =>
|
||||||
// different data depending on toggle state
|
// different data depending on toggle state
|
||||||
|
|
|
@ -11,6 +11,10 @@ extension ExtraMetadataFieldView on MetadataField {
|
||||||
return 'Exif digitized date';
|
return 'Exif digitized date';
|
||||||
case MetadataField.exifGpsDatestamp:
|
case MetadataField.exifGpsDatestamp:
|
||||||
return 'Exif GPS date';
|
return 'Exif GPS date';
|
||||||
|
case MetadataField.exifMake:
|
||||||
|
return 'Exif make';
|
||||||
|
case MetadataField.exifModel:
|
||||||
|
return 'Exif model';
|
||||||
case MetadataField.xmpXmpCreateDate:
|
case MetadataField.xmpXmpCreateDate:
|
||||||
return 'XMP xmp:CreateDate';
|
return 'XMP xmp:CreateDate';
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -6,6 +6,7 @@ import 'package:aves/app_flavor.dart';
|
||||||
import 'package:aves/flutter_version.dart';
|
import 'package:aves/flutter_version.dart';
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/ref/locales.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/colors.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 result = await Process.run('logcat', ['-d']);
|
||||||
final logs = result.stdout;
|
final logs = result.stdout;
|
||||||
final success = await storageService.createFile(
|
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,
|
MimeTypes.plainText,
|
||||||
Uint8List.fromList(utf8.encode(logs)),
|
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_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/media_store_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/accessibility_service.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/colors.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/widgets/welcome_page.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
import 'package:aves_utils/aves_utils.dart';
|
import 'package:aves_utils/aves_utils.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_localization_nn/flutter_localization_nn.dart';
|
import 'package:flutter_localization_nn/flutter_localization_nn.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:overlay_support/overlay_support.dart';
|
import 'package:overlay_support/overlay_support.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.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<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder);
|
||||||
final ValueNotifier<TvMediaQueryModifier?> _tvMediaQueryModifierNotifier = ValueNotifier(null);
|
final ValueNotifier<TvMediaQueryModifier?> _tvMediaQueryModifierNotifier = ValueNotifier(null);
|
||||||
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.main);
|
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
|
// observers are not registered when using the same list object with different items
|
||||||
// the list itself needs to be reassigned
|
// the list itself needs to be reassigned
|
||||||
|
@ -217,6 +221,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
Provider<TvRailController>.value(value: _tvRailController),
|
Provider<TvRailController>.value(value: _tvRailController),
|
||||||
DurationsProvider(),
|
DurationsProvider(),
|
||||||
HighlightInfoProvider(),
|
HighlightInfoProvider(),
|
||||||
|
ListenableProvider<ValueNotifier<LocaleOverrides>>.value(value: _localeOverridesNotifier),
|
||||||
],
|
],
|
||||||
child: OverlaySupport(
|
child: OverlaySupport(
|
||||||
child: FutureBuilder<void>(
|
child: FutureBuilder<void>(
|
||||||
|
@ -239,9 +244,6 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
),
|
),
|
||||||
builder: (context, s, child) {
|
builder: (context, s, child) {
|
||||||
final (settingsLocale, themeBrightness, enableDynamicColor) = s;
|
final (settingsLocale, themeBrightness, enableDynamicColor) = s;
|
||||||
|
|
||||||
AStyles.updateStylesForLocale(settings.appliedLocale);
|
|
||||||
|
|
||||||
return DynamicColorBuilder(
|
return DynamicColorBuilder(
|
||||||
builder: (lightScheme, darkScheme) {
|
builder: (lightScheme, darkScheme) {
|
||||||
const defaultAccent = AvesColorsData.defaultAccent;
|
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
|
@override
|
||||||
void didChangeMetrics() => _updateCutoutInsets();
|
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();
|
Widget _getFirstPage({Map? intentData}) => settings.hasAcceptedTerms ? HomePage(intentData: intentData) : const WelcomePage();
|
||||||
|
|
||||||
Size? _getScreenSize(BuildContext context) {
|
Size? _getScreenSize(BuildContext context) {
|
||||||
|
@ -504,7 +539,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _monitorSettings() {
|
void _monitorSettings() {
|
||||||
void applyIsInstalledAppAccessAllowed() {
|
void _applyIsInstalledAppAccessAllowed() {
|
||||||
if (settings.isInstalledAppAccessAllowed) {
|
if (settings.isInstalledAppAccessAllowed) {
|
||||||
appInventory.initAppNames();
|
appInventory.initAppNames();
|
||||||
} else {
|
} 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) {
|
switch (settings.maxBrightness) {
|
||||||
case MaxBrightness.never:
|
case MaxBrightness.never:
|
||||||
case MaxBrightness.viewerOnly:
|
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) {
|
if (!settings.isRotationLocked && !settings.useTvLayout) {
|
||||||
windowService.requestOrientation();
|
windowService.requestOrientation();
|
||||||
}
|
}
|
||||||
|
@ -545,20 +580,22 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
|
|
||||||
final settingStream = settings.updateStream;
|
final settingStream = settings.updateStream;
|
||||||
// app
|
// 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
|
// display
|
||||||
settingStream.where((event) => event.key == SettingKeys.displayRefreshRateModeKey).listen((_) => applyDisplayRefreshRateMode());
|
settingStream.where((event) => event.key == SettingKeys.displayRefreshRateModeKey).listen((_) => _applyDisplayRefreshRateMode());
|
||||||
settingStream.where((event) => event.key == SettingKeys.maxBrightnessKey).listen((_) => applyMaxBrightness());
|
settingStream.where((event) => event.key == SettingKeys.maxBrightnessKey).listen((_) => _applyMaxBrightness());
|
||||||
settingStream.where((event) => event.key == SettingKeys.forceTvLayoutKey).listen((_) => applyForceTvLayout());
|
settingStream.where((event) => event.key == SettingKeys.forceTvLayoutKey).listen((_) => applyForceTvLayout());
|
||||||
// navigation
|
// navigation
|
||||||
settingStream.where((event) => event.key == SettingKeys.keepScreenOnKey).listen((_) => applyKeepScreenOn());
|
settingStream.where((event) => event.key == SettingKeys.keepScreenOnKey).listen((_) => _applyKeepScreenOn());
|
||||||
// platform settings
|
// platform settings
|
||||||
settingStream.where((event) => event.key == SettingKeys.platformAccelerometerRotationKey).listen((_) => applyIsRotationLocked());
|
settingStream.where((event) => event.key == SettingKeys.platformAccelerometerRotationKey).listen((_) => _applyIsRotationLocked());
|
||||||
|
|
||||||
applyDisplayRefreshRateMode();
|
_applyLocale();
|
||||||
applyMaxBrightness();
|
_applyDisplayRefreshRateMode();
|
||||||
applyKeepScreenOn();
|
_applyMaxBrightness();
|
||||||
applyIsRotationLocked();
|
_applyKeepScreenOn();
|
||||||
|
_applyIsRotationLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setupErrorReporting() async {
|
Future<void> _setupErrorReporting() async {
|
||||||
|
@ -632,3 +669,18 @@ class AvesScrollBehavior extends MaterialScrollBehavior {
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef TvMediaQueryModifier = MediaQueryData Function(MediaQueryData);
|
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 newest = firstKey.date;
|
||||||
final oldest = lastKey.date;
|
final oldest = lastKey.date;
|
||||||
if (newest != null && oldest != null) {
|
if (newest != null && oldest != null) {
|
||||||
final localeName = context.l10n.localeName;
|
final locale = context.l10n.localeName;
|
||||||
final dateFormat = (newest.difference(oldest).inDays).abs() > 365 ? DateFormat.y(localeName) : DateFormat.MMM(localeName);
|
final dateFormat = (newest.difference(oldest).inDays).abs() > 365 ? DateFormat.y(locale) : DateFormat.MMM(locale);
|
||||||
String? lastLabel;
|
String? lastLabel;
|
||||||
sectionLayouts.forEach((section) {
|
sectionLayouts.forEach((section) {
|
||||||
final date = (section.sectionKey as EntryDateSectionKey).date;
|
final date = (section.sectionKey as EntryDateSectionKey).date;
|
||||||
|
|
|
@ -356,10 +356,11 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
);
|
);
|
||||||
if (pattern == null) return;
|
if (pattern == null) return;
|
||||||
|
|
||||||
final entriesToNewName = Map.fromEntries(entries.mapIndexed((index, entry) {
|
final namingFutures = entries.mapIndexed((index, entry) async {
|
||||||
final newName = pattern.apply(entry, index);
|
final newName = await pattern.apply(entry, index);
|
||||||
return MapEntry(entry, '$newName${entry.extension}');
|
return MapEntry(entry, '$newName${entry.extension}');
|
||||||
})).whereNotNullValue();
|
});
|
||||||
|
final entriesToNewName = Map.fromEntries(await Future.wait(namingFutures)).whereNotNullValue();
|
||||||
await rename(context, entriesToNewName: entriesToNewName, persist: true);
|
await rename(context, entriesToNewName: entriesToNewName, persist: true);
|
||||||
|
|
||||||
_browse(context);
|
_browse(context);
|
||||||
|
@ -555,15 +556,16 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> removeLocation(BuildContext context, Set<AvesEntry> entries) async {
|
Future<void> removeLocation(BuildContext context, Set<AvesEntry> entries) async {
|
||||||
|
final l10n = context.l10n;
|
||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AvesDialog(
|
builder: (context) => AvesDialog(
|
||||||
content: Text(context.l10n.genericDangerWarningDialogMessage),
|
content: Text(l10n.genericDangerWarningDialogMessage),
|
||||||
actions: [
|
actions: [
|
||||||
const CancelButton(),
|
const CancelButton(),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
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 entry = entries.first;
|
||||||
final initialTitle = entry.catalogMetadata?.xmpTitle ?? '';
|
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 ?? '';
|
final initialDescription = fields.description ?? '';
|
||||||
|
|
||||||
return showDialog<Map<DescriptionField, String?>>(
|
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/circle.dart';
|
||||||
import 'package:aves/widgets/common/basic/text/change_highlight.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/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
import 'package:aves/widgets/common/extensions/theme.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:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:overlay_support/overlay_support.dart';
|
import 'package:overlay_support/overlay_support.dart';
|
||||||
|
@ -22,6 +22,13 @@ import 'package:provider/provider.dart';
|
||||||
enum FeedbackType { info, warn }
|
enum FeedbackType { info, warn }
|
||||||
|
|
||||||
mixin FeedbackMixin {
|
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 dismissFeedback(BuildContext context) => ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
|
|
||||||
void showFeedback(BuildContext context, FeedbackType type, String message, [SnackBarAction? action]) {
|
void showFeedback(BuildContext context, FeedbackType type, String message, [SnackBarAction? action]) {
|
||||||
|
@ -53,10 +60,7 @@ mixin FeedbackMixin {
|
||||||
stop: action != null ? start.add(duration) : null,
|
stop: action != null ? start.add(duration) : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.currentRouteName == EntryViewerPage.routeName) {
|
if (snackBarMarginOverrideNotifier.value != null) {
|
||||||
// avoid interactive widgets at the bottom of the page
|
|
||||||
final margin = EntryViewerPage.snackBarMargin(context);
|
|
||||||
|
|
||||||
// as of Flutter v2.10.4, `SnackBar` can only be positioned at the bottom,
|
// 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
|
// and space under the snack bar `margin` does not receive gestures
|
||||||
// (because it is used by the `Dismissible` wrapping the snack bar)
|
// (because it is used by the `Dismissible` wrapping the snack bar)
|
||||||
|
@ -65,8 +69,15 @@ mixin FeedbackMixin {
|
||||||
notificationOverlayEntry = showOverlayNotification(
|
notificationOverlayEntry = showOverlayNotification(
|
||||||
(context) => SafeArea(
|
(context) => SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: Padding(
|
child: ValueListenableBuilder<EdgeInsets?>(
|
||||||
padding: margin,
|
valueListenable: snackBarMarginOverrideNotifier,
|
||||||
|
builder: (context, margin, child) {
|
||||||
|
return AnimatedPadding(
|
||||||
|
padding: margin ?? snackBarMarginDefault(context),
|
||||||
|
duration: ADurations.pageTransitionAnimation,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
child: OverlaySnackBar(
|
child: OverlaySnackBar(
|
||||||
content: snackBarContent,
|
content: snackBarContent,
|
||||||
action: action != null
|
action: action != null
|
||||||
|
@ -346,6 +357,7 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
|
||||||
final contentTextFontSize = contentTextStyle.fontSize ?? theme.textTheme.bodyMedium!.fontSize!;
|
final contentTextFontSize = contentTextStyle.fontSize ?? theme.textTheme.bodyMedium!.fontSize!;
|
||||||
final timerChangeShadowColor = colorScheme.primary;
|
final timerChangeShadowColor = colorScheme.primary;
|
||||||
|
|
||||||
|
final remainingDurationAnimation = _remainingDurationMillis;
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
if (widget.type == FeedbackType.warn) ...[
|
if (widget.type == FeedbackType.warn) ...[
|
||||||
|
@ -356,16 +368,17 @@ class _FeedbackMessageState extends State<_FeedbackMessage> with SingleTickerPro
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
],
|
],
|
||||||
Expanded(child: Text(widget.message)),
|
Expanded(child: Text(widget.message)),
|
||||||
if (_remainingDurationMillis != null) ...[
|
if (remainingDurationAnimation != null) ...[
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
AnimatedBuilder(
|
AnimatedBuilder(
|
||||||
animation: _remainingDurationMillis!,
|
animation: remainingDurationAnimation,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final remainingDurationMillis = _remainingDurationMillis!.value;
|
final remainingDurationMillis = remainingDurationAnimation.value;
|
||||||
|
final totalDurationMillis = _totalDurationMillis;
|
||||||
return CircularIndicator(
|
return CircularIndicator(
|
||||||
radius: 16,
|
radius: 16,
|
||||||
lineWidth: 2,
|
lineWidth: 2,
|
||||||
percent: remainingDurationMillis / _totalDurationMillis!,
|
percent: totalDurationMillis != null && totalDurationMillis > 0 ? remainingDurationMillis / totalDurationMillis : 0,
|
||||||
background: Colors.grey,
|
background: Colors.grey,
|
||||||
// progress color is provided by the caller,
|
// progress color is provided by the caller,
|
||||||
// because we cannot use the app context theme here
|
// 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/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ class AvesPopScope extends StatelessWidget {
|
||||||
Navigator.maybeOf(context)?.pop();
|
Navigator.maybeOf(context)?.pop();
|
||||||
} else {
|
} else {
|
||||||
// exit
|
// exit
|
||||||
|
reportService.log('Exit by pop');
|
||||||
SystemNavigator.pop();
|
SystemNavigator.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/ref/locales.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.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) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
|
||||||
|
final currentSizeBytes = formatFileSize(asciiLocale, imageCache.currentSizeBytes);
|
||||||
|
final maxSizeBytes = formatFileSize(asciiLocale, imageCache.maximumSizeBytes);
|
||||||
return AvesExpansionTile(
|
return AvesExpansionTile(
|
||||||
title: 'Cache',
|
title: 'Cache',
|
||||||
children: [
|
children: [
|
||||||
|
@ -25,7 +28,7 @@ class _DebugCacheSectionState extends State<DebugCacheSection> with AutomaticKee
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
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),
|
const SizedBox(width: 8),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/model/metadata/trash.dart';
|
||||||
import 'package:aves/model/vaults/details.dart';
|
import 'package:aves/model/vaults/details.dart';
|
||||||
import 'package:aves/model/vaults/vaults.dart';
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
import 'package:aves/model/video_playback.dart';
|
import 'package:aves/model/video_playback.dart';
|
||||||
|
import 'package:aves/ref/locales.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||||
|
@ -65,7 +66,7 @@ class _DebugAppDatabaseSectionState extends State<DebugAppDatabaseSection> with
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text('DB file size: ${formatFileSize('en_US', snapshot.data!)}'),
|
child: Text('DB file size: ${formatFileSize(asciiLocale, snapshot.data!)}'),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
|
|
@ -69,6 +69,7 @@ class _DebugSettingsSectionState extends State<DebugSettingsSection> with Automa
|
||||||
'drawerPageBookmarks': toMultiline(settings.drawerPageBookmarks),
|
'drawerPageBookmarks': toMultiline(settings.drawerPageBookmarks),
|
||||||
'pinnedFilters': toMultiline(settings.pinnedFilters),
|
'pinnedFilters': toMultiline(settings.pinnedFilters),
|
||||||
'hiddenFilters': toMultiline(settings.hiddenFilters),
|
'hiddenFilters': toMultiline(settings.hiddenFilters),
|
||||||
|
'deactivatedHiddenFilters': toMultiline(settings.deactivatedHiddenFilters),
|
||||||
'searchHistory': toMultiline(settings.searchHistory),
|
'searchHistory': toMultiline(settings.searchHistory),
|
||||||
'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums),
|
'recentDestinationAlbums': toMultiline(settings.recentDestinationAlbums),
|
||||||
'recentTags': toMultiline(settings.recentTags),
|
'recentTags': toMultiline(settings.recentTags),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/ref/locales.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
|
@ -47,7 +48,7 @@ class _DebugStorageSectionState extends State<DebugStorageSection> with Automati
|
||||||
'isPrimary': '${v.isPrimary}',
|
'isPrimary': '${v.isPrimary}',
|
||||||
'isRemovable': '${v.isRemovable}',
|
'isRemovable': '${v.isRemovable}',
|
||||||
'state': v.state,
|
'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/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/theme/styles.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/collection/collection_grid.dart';
|
||||||
import 'package:aves/widgets/common/basic/font_size_icon_theme.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/popup/menu_row.dart';
|
||||||
import 'package:aves/widgets/common/basic/scaffold.dart';
|
import 'package:aves/widgets/common/basic/scaffold.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -36,6 +38,7 @@ class RenameEntrySetPage extends StatefulWidget {
|
||||||
class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
final TextEditingController _patternTextController = TextEditingController();
|
final TextEditingController _patternTextController = TextEditingController();
|
||||||
final ValueNotifier<NamingPattern> _namingPatternNotifier = ValueNotifier<NamingPattern>(const NamingPattern([]));
|
final ValueNotifier<NamingPattern> _namingPatternNotifier = ValueNotifier<NamingPattern>(const NamingPattern([]));
|
||||||
|
late final String locale;
|
||||||
|
|
||||||
static const int previewMax = 10;
|
static const int previewMax = 10;
|
||||||
static const double thumbnailExtent = 48;
|
static const double thumbnailExtent = 48;
|
||||||
|
@ -49,7 +52,11 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_patternTextController.text = settings.entryRenamingPattern;
|
_patternTextController.text = settings.entryRenamingPattern;
|
||||||
_patternTextController.addListener(_onUserPatternChanged);
|
_patternTextController.addListener(_onUserPatternChanged);
|
||||||
_onUserPatternChanged();
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
locale = context.l10n.localeName;
|
||||||
|
_onUserPatternChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -91,10 +98,6 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
child: PopupMenuButton<String>(
|
child: PopupMenuButton<String>(
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
|
||||||
value: DateNamingProcessor.key,
|
|
||||||
child: MenuRow(text: l10n.viewerInfoLabelDate, icon: const Icon(AIcons.date)),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: NameNamingProcessor.key,
|
value: NameNamingProcessor.key,
|
||||||
child: MenuRow(text: l10n.renameProcessorName, icon: const Icon(AIcons.name)),
|
child: MenuRow(text: l10n.renameProcessorName, icon: const Icon(AIcons.name)),
|
||||||
|
@ -103,6 +106,28 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
value: CounterNamingProcessor.key,
|
value: CounterNamingProcessor.key,
|
||||||
child: MenuRow(text: l10n.renameProcessorCounter, icon: const Icon(AIcons.counter)),
|
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 {
|
onSelected: (key) async {
|
||||||
|
@ -159,11 +184,17 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
ValueListenableBuilder<NamingPattern>(
|
ValueListenableBuilder<NamingPattern>(
|
||||||
valueListenable: _namingPatternNotifier,
|
valueListenable: _namingPatternNotifier,
|
||||||
builder: (context, pattern, child) {
|
builder: (context, pattern, child) {
|
||||||
return Text(
|
return FutureBuilder<String>(
|
||||||
pattern.apply(entry, index),
|
future: pattern.apply(entry, index),
|
||||||
softWrap: false,
|
builder: (context, snapshot) {
|
||||||
overflow: TextOverflow.fade,
|
final info = snapshot.data;
|
||||||
maxLines: 1,
|
return Text(
|
||||||
|
info ?? '…',
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -203,6 +234,7 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
|
||||||
_namingPatternNotifier.value = NamingPattern.from(
|
_namingPatternNotifier.value = NamingPattern.from(
|
||||||
userPattern: _patternTextController.text,
|
userPattern: _patternTextController.text,
|
||||||
entryCount: entryCount,
|
entryCount: entryCount,
|
||||||
|
locale: locale,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,50 +58,77 @@ class _HiddenFilters extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
bool filterPredicate(CollectionFilter v) => v is! PathFilter;
|
||||||
return Selector<Settings, Set<CollectionFilter>>(
|
return Selector<Settings, Set<CollectionFilter>>(
|
||||||
selector: (context, s) => settings.hiddenFilters.where((v) => v is! PathFilter).toSet(),
|
selector: (context, s) => settings.hiddenFilters.where(filterPredicate).toSet(),
|
||||||
builder: (context, hiddenFilters, child) {
|
builder: (context, activatedHiddenFilters, child) {
|
||||||
if (hiddenFilters.isEmpty) {
|
return Selector<Settings, Set<CollectionFilter>>(
|
||||||
return Column(
|
selector: (context, s) => settings.deactivatedHiddenFilters.where(filterPredicate).toSet(),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
builder: (context, deactivatedHiddenFilters, child) {
|
||||||
children: [
|
final allHiddenFilters = {
|
||||||
_Banner(bannerText: context.l10n.settingsHiddenFiltersBanner),
|
...activatedHiddenFilters,
|
||||||
const Divider(height: 0),
|
...deactivatedHiddenFilters,
|
||||||
Expanded(
|
};
|
||||||
child: Padding(
|
if (allHiddenFilters.isEmpty) {
|
||||||
padding: const EdgeInsets.all(8),
|
return Column(
|
||||||
child: EmptyContent(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
icon: AIcons.hide,
|
children: [
|
||||||
text: context.l10n.settingsHiddenFiltersEmpty,
|
_Banner(bannerText: context.l10n.settingsHiddenFiltersBanner),
|
||||||
|
const Divider(height: 0),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: EmptyContent(
|
||||||
|
icon: AIcons.hide,
|
||||||
|
text: context.l10n.settingsHiddenFiltersEmpty,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
);
|
||||||
],
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final filterList = hiddenFilters.toList()..sort();
|
final filterList = allHiddenFilters.toList()..sort();
|
||||||
return ListView(
|
return ListView(
|
||||||
children: [
|
children: [
|
||||||
_Banner(bannerText: context.l10n.settingsHiddenFiltersBanner),
|
_Banner(bannerText: context.l10n.settingsHiddenFiltersBanner),
|
||||||
const Divider(height: 0),
|
const Divider(height: 0),
|
||||||
Padding(
|
const SizedBox(height: 8),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
...filterList.map((filter) {
|
||||||
child: Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: filterList.map((filter) {
|
|
||||||
void onRemove(CollectionFilter filter) => settings.changeFilterVisibility({filter}, true);
|
void onRemove(CollectionFilter filter) => settings.changeFilterVisibility({filter}, true);
|
||||||
return AvesFilterChip(
|
return Padding(
|
||||||
filter: filter,
|
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
onTap: onRemove,
|
child: Row(
|
||||||
onRemove: onRemove,
|
children: [
|
||||||
onLongPress: null,
|
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,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Switch(
|
||||||
|
value: activatedHiddenFilters.contains(filter),
|
||||||
|
onChanged: (v) => settings.activateHiddenFilter(filter, v),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
),
|
],
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -114,7 +141,10 @@ class _HiddenPaths extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Selector<Settings, Set<PathFilter>>(
|
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) {
|
builder: (context, hiddenPaths, child) {
|
||||||
final pathList = hiddenPaths.toList()..sort();
|
final pathList = hiddenPaths.toList()..sort();
|
||||||
return Column(
|
return Column(
|
||||||
|
|
|
@ -12,10 +12,10 @@ abstract class SettingsSection {
|
||||||
|
|
||||||
FutureOr<List<SettingsTile>> tiles(BuildContext context);
|
FutureOr<List<SettingsTile>> tiles(BuildContext context);
|
||||||
|
|
||||||
Widget build(BuildContext context, ValueNotifier<String?> expandedNotifier) {
|
Widget build(BuildContext sectionContext, ValueNotifier<String?> expandedNotifier) {
|
||||||
return FutureBuilder<List<SettingsTile>>(
|
return FutureBuilder<List<SettingsTile>>(
|
||||||
future: Future.value(tiles(context)),
|
future: Future.value(tiles(sectionContext)),
|
||||||
builder: (context, snapshot) {
|
builder: (tileContext, snapshot) {
|
||||||
final tiles = snapshot.data;
|
final tiles = snapshot.data;
|
||||||
if (tiles == null) return const SizedBox();
|
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
|
// 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
|
// so that the tile state is kept when the language is modified
|
||||||
value: key,
|
value: key,
|
||||||
leading: icon(context),
|
leading: icon(tileContext),
|
||||||
title: title(context),
|
title: title(tileContext),
|
||||||
expandedNotifier: expandedNotifier,
|
expandedNotifier: expandedNotifier,
|
||||||
showHighlight: false,
|
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/enums/accessibility_animations.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.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/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
@ -117,7 +118,7 @@ class _SettingsMobilePageState extends State<SettingsMobilePage> with FeedbackMi
|
||||||
final allJsonString = jsonEncode(allMap);
|
final allJsonString = jsonEncode(allMap);
|
||||||
|
|
||||||
final success = await storageService.createFile(
|
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,
|
MimeTypes.json,
|
||||||
Uint8List.fromList(utf8.encode(allJsonString)),
|
Uint8List.fromList(utf8.encode(allJsonString)),
|
||||||
);
|
);
|
||||||
|
|
|
@ -94,6 +94,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
return !settings.useTvLayout && targetEntry.isPureVideo;
|
return !settings.useTvLayout && targetEntry.isPureVideo;
|
||||||
case EntryAction.videoSelectStreams:
|
case EntryAction.videoSelectStreams:
|
||||||
case EntryAction.videoSetSpeed:
|
case EntryAction.videoSetSpeed:
|
||||||
|
case EntryAction.videoABRepeat:
|
||||||
case EntryAction.videoSettings:
|
case EntryAction.videoSettings:
|
||||||
case EntryAction.videoTogglePlay:
|
case EntryAction.videoTogglePlay:
|
||||||
case EntryAction.videoReplay10:
|
case EntryAction.videoReplay10:
|
||||||
|
@ -229,6 +230,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
case EntryAction.videoToggleMute:
|
case EntryAction.videoToggleMute:
|
||||||
case EntryAction.videoSelectStreams:
|
case EntryAction.videoSelectStreams:
|
||||||
case EntryAction.videoSetSpeed:
|
case EntryAction.videoSetSpeed:
|
||||||
|
case EntryAction.videoABRepeat:
|
||||||
case EntryAction.videoSettings:
|
case EntryAction.videoSettings:
|
||||||
case EntryAction.videoTogglePlay:
|
case EntryAction.videoTogglePlay:
|
||||||
case EntryAction.videoReplay10:
|
case EntryAction.videoReplay10:
|
||||||
|
|
|
@ -64,6 +64,8 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
await _showStreamSelectionDialog(context, controller);
|
await _showStreamSelectionDialog(context, controller);
|
||||||
case EntryAction.videoSetSpeed:
|
case EntryAction.videoSetSpeed:
|
||||||
await _showSpeedDialog(context, controller);
|
await _showSpeedDialog(context, controller);
|
||||||
|
case EntryAction.videoABRepeat:
|
||||||
|
controller.toggleABRepeat();
|
||||||
case EntryAction.videoSettings:
|
case EntryAction.videoSettings:
|
||||||
await _showSettings(context, controller);
|
await _showSettings(context, controller);
|
||||||
case EntryAction.videoTogglePlay:
|
case EntryAction.videoTogglePlay:
|
||||||
|
|
|
@ -63,7 +63,7 @@ class EntryViewerStack extends StatefulWidget {
|
||||||
State<EntryViewerStack> createState() => _EntryViewerStackState();
|
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();
|
final Floating _floating = Floating();
|
||||||
late int _currentEntryIndex;
|
late int _currentEntryIndex;
|
||||||
late ValueNotifier<int> _currentVerticalPage;
|
late ValueNotifier<int> _currentVerticalPage;
|
||||||
|
@ -184,6 +184,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
AvesApp.pageRouteObserver.unsubscribe(this);
|
||||||
_floating.dispose();
|
_floating.dispose();
|
||||||
cleanEntryControllers(entryNotifier.value);
|
cleanEntryControllers(entryNotifier.value);
|
||||||
_videoActionDelegate.dispose();
|
_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() {
|
void _onAppLifecycleStateChanged() {
|
||||||
switch (AvesApp.lifecycleStateNotifier.value) {
|
switch (AvesApp.lifecycleStateNotifier.value) {
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
|
@ -662,6 +698,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
|
|
||||||
void _onVerticalPageChanged(int page) {
|
void _onVerticalPageChanged(int page) {
|
||||||
_currentVerticalPage.value = page;
|
_currentVerticalPage.value = page;
|
||||||
|
_overrideSnackBarMargin();
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case transitionPage:
|
case transitionPage:
|
||||||
dismissFeedback(context);
|
dismissFeedback(context);
|
||||||
|
|
|
@ -74,7 +74,7 @@ class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
|
||||||
if (requestEntry == null) {
|
if (requestEntry == null) {
|
||||||
_detailLoader = SynchronousFuture(const OverlayMetadata());
|
_detailLoader = SynchronousFuture(const OverlayMetadata());
|
||||||
} else {
|
} else {
|
||||||
_detailLoader = metadataFetchService.getFields(requestEntry, {
|
_detailLoader = metadataFetchService.getOverlayMetadata(requestEntry, {
|
||||||
if (settings.showOverlayShootingDetails) ...{
|
if (settings.showOverlayShootingDetails) ...{
|
||||||
MetadataSyntheticField.aperture,
|
MetadataSyntheticField.aperture,
|
||||||
MetadataSyntheticField.exposureTime,
|
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;
|
bool get isPlaying => controller?.isPlaying ?? false;
|
||||||
|
|
||||||
|
ValueNotifier<ABRepeat?> get abRepeatNotifier => controller?.abRepeatNotifier ?? ValueNotifier(null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final blurred = settings.enableBlurEffect;
|
final blurred = settings.enableBlurEffect;
|
||||||
|
@ -69,8 +71,7 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
|
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
|
||||||
child: Container(
|
child: Container(
|
||||||
alignment: Alignment.center,
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred),
|
color: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred),
|
||||||
border: AvesBorder.border(context),
|
border: AvesBorder.border(context),
|
||||||
|
@ -80,62 +81,80 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
||||||
data: MediaQuery.of(context).copyWith(
|
data: MediaQuery.of(context).copyWith(
|
||||||
textScaler: TextScaler.noScaling,
|
textScaler: TextScaler.noScaling,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: ValueListenableBuilder<ABRepeat?>(
|
||||||
key: _progressBarKey,
|
valueListenable: abRepeatNotifier,
|
||||||
mainAxisSize: MainAxisSize.min,
|
builder: (context, abRepeat, child) {
|
||||||
children: [
|
return Stack(
|
||||||
Row(
|
fit: StackFit.passthrough,
|
||||||
children: [
|
children: [
|
||||||
StreamBuilder<int>(
|
if (abRepeat != null) ...[
|
||||||
stream: positionStream,
|
_buildABRepeatMark(context, abRepeat.start),
|
||||||
builder: (context, snapshot) {
|
_buildABRepeatMark(context, abRepeat.end),
|
||||||
// do not use stream snapshot because it is obsolete when switching between videos
|
],
|
||||||
final position = controller?.currentPosition.floor() ?? 0;
|
Container(
|
||||||
return Text(
|
key: _progressBarKey,
|
||||||
formatFriendlyDuration(Duration(milliseconds: position)),
|
alignment: Alignment.center,
|
||||||
style: textStyle,
|
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||||
strutStyle: strutStyle,
|
child: Column(
|
||||||
);
|
mainAxisSize: MainAxisSize.min,
|
||||||
}),
|
children: [
|
||||||
const Spacer(),
|
Row(
|
||||||
Text(
|
children: [
|
||||||
formatFriendlyDuration(Duration(milliseconds: controller?.duration ?? 0)),
|
StreamBuilder<int>(
|
||||||
style: textStyle,
|
stream: positionStream,
|
||||||
strutStyle: strutStyle,
|
builder: (context, snapshot) {
|
||||||
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
|
final position = controller?.currentPosition.floor() ?? 0;
|
||||||
|
return Text(
|
||||||
|
formatFriendlyDuration(Duration(milliseconds: position)),
|
||||||
|
style: textStyle,
|
||||||
|
strutStyle: strutStyle,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
formatFriendlyDuration(Duration(milliseconds: controller?.duration ?? 0)),
|
||||||
|
style: textStyle,
|
||||||
|
strutStyle: strutStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||||
|
child: Directionality(
|
||||||
|
// force directionality for `LinearProgressIndicator`
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: StreamBuilder<int>(
|
||||||
|
stream: positionStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
// do not use stream snapshot because it is obsolete when switching between videos
|
||||||
|
var progress = controller?.progress ?? 0.0;
|
||||||
|
if (!progress.isFinite) progress = 0.0;
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: progress,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildSpeedIndicator(),
|
||||||
|
_buildMuteIndicator(),
|
||||||
|
Text(
|
||||||
|
// fake text below to match the height of the text above and center the whole thing
|
||||||
|
'',
|
||||||
|
style: textStyle,
|
||||||
|
strutStyle: strutStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
ClipRRect(
|
},
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
|
||||||
child: Directionality(
|
|
||||||
// force directionality for `LinearProgressIndicator`
|
|
||||||
textDirection: TextDirection.ltr,
|
|
||||||
child: StreamBuilder<int>(
|
|
||||||
stream: positionStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
// do not use stream snapshot because it is obsolete when switching between videos
|
|
||||||
var progress = controller?.progress ?? 0.0;
|
|
||||||
if (!progress.isFinite) progress = 0.0;
|
|
||||||
return LinearProgressIndicator(
|
|
||||||
value: progress,
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildSpeedIndicator(),
|
|
||||||
_buildMuteIndicator(),
|
|
||||||
Text(
|
|
||||||
// fake text below to match the height of the text above and center the whole thing
|
|
||||||
'',
|
|
||||||
style: textStyle,
|
|
||||||
strutStyle: strutStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -145,6 +164,20 @@ 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)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSpeedIndicator() => StreamBuilder<double>(
|
Widget _buildSpeedIndicator() => StreamBuilder<double>(
|
||||||
stream: controller?.speedStream ?? Stream.value(1.0),
|
stream: controller?.speedStream ?? Stream.value(1.0),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
|
@ -175,11 +208,20 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
RenderBox? _getProgressBarRenderBox() {
|
||||||
|
return _progressBarKey.currentContext?.findRenderObject() as RenderBox?;
|
||||||
|
}
|
||||||
|
|
||||||
void _seekFromTap(Offset globalPosition) async {
|
void _seekFromTap(Offset globalPosition) async {
|
||||||
if (controller == null) return;
|
final box = _getProgressBarRenderBox();
|
||||||
final keyContext = _progressBarKey.currentContext!;
|
if (controller == null || box == null) return;
|
||||||
final box = keyContext.findRenderObject() as RenderBox;
|
|
||||||
final localPosition = box.globalToLocal(globalPosition);
|
final dx = box.globalToLocal(globalPosition).dx;
|
||||||
await controller!.seekToProgress(localPosition.dx / box.size.width);
|
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/model/entry/entry.dart';
|
||||||
import 'package:aves/view/view.dart';
|
import 'package:aves/view/view.dart';
|
||||||
import 'package:aves/widgets/common/identity/buttons/overlay_button.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/controls.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart';
|
import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
@ -64,19 +65,28 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Row(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
VideoABRepeatOverlay(
|
||||||
child: VideoProgressBar(
|
|
||||||
controller: controller,
|
|
||||||
scale: scale,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
VideoControlRow(
|
|
||||||
entry: entry,
|
|
||||||
controller: controller,
|
controller: controller,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
onActionSelected: widget.onActionSelected,
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: VideoProgressBar(
|
||||||
|
controller: controller,
|
||||||
|
scale: scale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
VideoControlRow(
|
||||||
|
entry: entry,
|
||||||
|
controller: controller,
|
||||||
|
scale: scale,
|
||||||
|
onActionSelected: widget.onActionSelected,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -405,6 +405,7 @@ class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateM
|
||||||
controller: controller ?? _magnifierController,
|
controller: controller ?? _magnifierController,
|
||||||
contentSize: displaySize ?? entry.displaySize,
|
contentSize: displaySize ?? entry.displaySize,
|
||||||
allowOriginalScaleBeyondRange: !isWallpaperMode,
|
allowOriginalScaleBeyondRange: !isWallpaperMode,
|
||||||
|
allowDoubleTap: _allowDoubleTap,
|
||||||
minScale: minScale,
|
minScale: minScale,
|
||||||
maxScale: maxScale,
|
maxScale: maxScale,
|
||||||
initialScale: viewerController.initialScale,
|
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) {
|
if (settings.viewerGestureSideTapNext && alignment != null) {
|
||||||
final x = alignment.x;
|
final x = alignment.x;
|
||||||
final sideRatio = _getSideRatio();
|
final sideRatio = _getSideRatio();
|
||||||
if (sideRatio != null) {
|
if (sideRatio != null) {
|
||||||
const animate = false;
|
const animate = false;
|
||||||
if (x < sideRatio) {
|
if (x < sideRatio) {
|
||||||
(context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate)).dispatch(context);
|
return context.isRtl ? const ShowNextEntryNotification(animate: animate) : const ShowPreviousEntryNotification(animate: animate);
|
||||||
return;
|
|
||||||
} else if (x > 1 - sideRatio) {
|
} else if (x > 1 - sideRatio) {
|
||||||
(context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate)).dispatch(context);
|
return context.isRtl ? const ShowPreviousEntryNotification(animate: animate) : const ShowNextEntryNotification(animate: animate);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
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