Merge branch 'develop'
2
.flutter
|
@ -1 +1 @@
|
||||||
Subproject commit 41456452f29d64e8deb623a3c927524bcf9f111b
|
Subproject commit abb292a07e20d696c4568099f918f6c5f330e6b0
|
2
.github/workflows/check.yml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Clone the repository.
|
- name: Clone the repository.
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get packages for the Flutter project.
|
- name: Get packages for the Flutter project.
|
||||||
run: scripts/pub_get_all.sh
|
run: scripts/pub_get_all.sh
|
||||||
|
|
2
.github/workflows/release.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
|
|
||||||
- name: Clone the repository.
|
- name: Clone the repository.
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get packages for the Flutter project.
|
- name: Get packages for the Flutter project.
|
||||||
run: scripts/pub_get_all.sh
|
run: scripts/pub_get_all.sh
|
||||||
|
|
17
CHANGELOG.md
|
@ -4,6 +4,23 @@ 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.5"></a>[v1.10.5] - 2024-02-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Viewer: prompt to show newly edited item
|
||||||
|
- Widget: outline color options according to device theme
|
||||||
|
- Catalan translation (thanks Marc Amorós)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- upgraded Flutter to stable v3.19.1
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- untracked binned items recovery
|
||||||
|
- untracked vault items recovery
|
||||||
|
|
||||||
## <a id="v1.10.4"></a>[v1.10.4] - 2024-02-07
|
## <a id="v1.10.4"></a>[v1.10.4] - 2024-02-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -214,7 +214,6 @@ dependencies {
|
||||||
|
|
||||||
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'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.3.7'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
|
|
|
@ -319,7 +319,7 @@
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
<!-- as of Flutter v3.16.0 (stable),
|
<!-- as of Flutter v3.19.0 (stable),
|
||||||
Impeller fails to render videos & platform views, has poor performance -->
|
Impeller fails to render videos & platform views, has poor performance -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||||
|
|
|
@ -87,6 +87,8 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
|
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
|
||||||
if (widthPx == 0 || heightPx == 0) return null
|
if (widthPx == 0 || heightPx == 0) return null
|
||||||
|
|
||||||
|
val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
|
||||||
initFlutterEngine(context)
|
initFlutterEngine(context)
|
||||||
val messenger = flutterEngine!!.dartExecutor
|
val messenger = flutterEngine!!.dartExecutor
|
||||||
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
|
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
|
||||||
|
@ -101,6 +103,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
||||||
"devicePixelRatio" to getDevicePixelRatio(),
|
"devicePixelRatio" to getDevicePixelRatio(),
|
||||||
"drawEntryImage" to drawEntryImage,
|
"drawEntryImage" to drawEntryImage,
|
||||||
"reuseEntry" to reuseEntry,
|
"reuseEntry" to reuseEntry,
|
||||||
|
"isSystemThemeDark" to isNightModeOn,
|
||||||
), object : MethodChannel.Result {
|
), object : MethodChannel.Result {
|
||||||
override fun success(result: Any?) {
|
override fun success(result: Any?) {
|
||||||
cont.resume(result)
|
cont.resume(result)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import deckers.thibault.aves.channel.calls.*
|
||||||
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
|
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
|
||||||
import deckers.thibault.aves.channel.calls.window.WindowHandler
|
import deckers.thibault.aves.channel.calls.window.WindowHandler
|
||||||
import deckers.thibault.aves.channel.streams.*
|
import deckers.thibault.aves.channel.streams.*
|
||||||
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
|
||||||
import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired
|
import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
@ -218,6 +219,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
|
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
|
||||||
|
|
||||||
PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
|
PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
|
||||||
|
EDIT_REQUEST -> onEditResult(resultCode, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,6 +228,14 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
pendingCollectionFilterPickHandler?.let { it(filters) }
|
pendingCollectionFilterPickHandler?.let { it(filters) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onEditResult(resultCode: Int, intent: Intent?) {
|
||||||
|
val fields: FieldMap? = if (resultCode == RESULT_OK) hashMapOf(
|
||||||
|
"uri" to intent?.data.toString(),
|
||||||
|
"mimeType" to intent?.type,
|
||||||
|
) else null
|
||||||
|
pendingEditIntentHandler?.let { it(fields) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||||
val treeUri = intent?.data
|
val treeUri = intent?.data
|
||||||
if (resultCode != RESULT_OK || treeUri == null) {
|
if (resultCode != RESULT_OK || treeUri == null) {
|
||||||
|
@ -458,6 +468,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
const val DELETE_SINGLE_PERMISSION_REQUEST = 5
|
const val DELETE_SINGLE_PERMISSION_REQUEST = 5
|
||||||
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
|
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
|
||||||
const val PICK_COLLECTION_FILTERS_REQUEST = 7
|
const val PICK_COLLECTION_FILTERS_REQUEST = 7
|
||||||
|
const val EDIT_REQUEST = 8
|
||||||
|
|
||||||
const val INTENT_ACTION_EDIT = "edit"
|
const val INTENT_ACTION_EDIT = "edit"
|
||||||
const val INTENT_ACTION_PICK_ITEMS = "pick_items"
|
const val INTENT_ACTION_PICK_ITEMS = "pick_items"
|
||||||
|
@ -493,6 +504,8 @@ open class MainActivity : FlutterFragmentActivity() {
|
||||||
|
|
||||||
var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null
|
var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null
|
||||||
|
|
||||||
|
var pendingEditIntentHandler: ((fields: FieldMap?) -> Unit)? = null
|
||||||
|
|
||||||
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
|
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
|
||||||
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
|
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
|
||||||
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return
|
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return
|
||||||
|
|
|
@ -52,7 +52,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
"getPackages" -> ioScope.launch { safe(call, result, ::getPackages) }
|
"getPackages" -> ioScope.launch { safe(call, result, ::getPackages) }
|
||||||
"getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) }
|
"getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) }
|
||||||
"copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) }
|
"copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) }
|
||||||
"edit" -> safe(call, result, ::edit)
|
|
||||||
"open" -> safe(call, result, ::open)
|
"open" -> safe(call, result, ::open)
|
||||||
"openMap" -> safe(call, result, ::openMap)
|
"openMap" -> safe(call, result, ::openMap)
|
||||||
"setAs" -> safe(call, result, ::setAs)
|
"setAs" -> safe(call, result, ::setAs)
|
||||||
|
@ -207,22 +206,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun edit(call: MethodCall, result: MethodChannel.Result) {
|
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
|
||||||
val mimeType = call.argument<String>("mimeType")
|
|
||||||
if (uri == null) {
|
|
||||||
result.error("edit-args", "missing arguments", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_EDIT)
|
|
||||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
.setDataAndType(getShareableUri(context, uri), mimeType)
|
|
||||||
val started = safeStartActivity(intent)
|
|
||||||
|
|
||||||
result.success(started)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun open(call: MethodCall, result: MethodChannel.Result) {
|
private fun open(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val title = call.argument<String>("title")
|
val title = call.argument<String>("title")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
@ -404,6 +387,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
|
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
|
||||||
// so we use a joined `String` as fallback
|
// so we use a joined `String` as fallback
|
||||||
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
|
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
result.error("pin-intent", "failed to build intent", null)
|
result.error("pin-intent", "failed to build intent", null)
|
||||||
return
|
return
|
||||||
|
@ -434,6 +418,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
FileProvider.getUriForFile(context, authority, File(path))
|
FileProvider.getUriForFile(context, authority, File(path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> uri
|
else -> uri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) }
|
"getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) }
|
||||||
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
|
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
|
||||||
|
"getUntrackedTrashPaths" -> ioScope.launch { safe(call, result, ::getUntrackedTrashPaths) }
|
||||||
|
"getUntrackedVaultPaths" -> ioScope.launch { safe(call, result, ::getUntrackedVaultPaths) }
|
||||||
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
|
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
|
||||||
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
|
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
|
||||||
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
|
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
|
||||||
|
@ -125,6 +127,35 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(volumes)
|
result.success(volumes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getUntrackedTrashPaths(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val knownPaths = call.argument<List<String>>("knownPaths")
|
||||||
|
if (knownPaths == null) {
|
||||||
|
result.error("getUntrackedTrashPaths-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val trashDirs = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }
|
||||||
|
val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.map { file -> file.path } ?: listOf() }
|
||||||
|
val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()
|
||||||
|
|
||||||
|
result.success(untrackedPaths)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUntrackedVaultPaths(call: MethodCall, result: MethodChannel.Result) {
|
||||||
|
val vault = call.argument<String>("vault")
|
||||||
|
val knownPaths = call.argument<List<String>>("knownPaths")
|
||||||
|
if (vault == null || knownPaths == null) {
|
||||||
|
result.error("getUntrackedVaultPaths-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val vaultDir = File(StorageUtils.getVaultRoot(context), vault)
|
||||||
|
val vaultItemPaths = vaultDir.listFiles()?.map { file -> file.path } ?: listOf()
|
||||||
|
val untrackedPaths = vaultItemPaths.filterNot(knownPaths::contains).toList()
|
||||||
|
|
||||||
|
result.success(untrackedPaths)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getVaultRoot(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
private fun getVaultRoot(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||||
result.success(StorageUtils.getVaultRoot(context))
|
result.success(StorageUtils.getVaultRoot(context))
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import deckers.thibault.aves.MainActivity
|
import deckers.thibault.aves.MainActivity
|
||||||
import deckers.thibault.aves.PendingStorageAccessResultHandler
|
import deckers.thibault.aves.PendingStorageAccessResultHandler
|
||||||
|
import deckers.thibault.aves.channel.calls.AppAdapterHandler
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
import deckers.thibault.aves.utils.PermissionManager
|
import deckers.thibault.aves.utils.PermissionManager
|
||||||
|
@ -47,6 +48,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
|
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
|
||||||
"createFile" -> ioScope.launch { createFile() }
|
"createFile" -> ioScope.launch { createFile() }
|
||||||
"openFile" -> ioScope.launch { openFile() }
|
"openFile" -> ioScope.launch { openFile() }
|
||||||
|
"edit" -> edit()
|
||||||
"pickCollectionFilters" -> pickCollectionFilters()
|
"pickCollectionFilters" -> pickCollectionFilters()
|
||||||
else -> endOfStream()
|
else -> endOfStream()
|
||||||
}
|
}
|
||||||
|
@ -100,10 +102,13 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
endOfStream()
|
endOfStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun safeStartActivityForResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) {
|
private suspend fun safeStartActivityForStorageAccessResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) {
|
||||||
if (intent.resolveActivity(activity.packageManager) != null) {
|
if (intent.resolveActivity(activity.packageManager) != null) {
|
||||||
MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied)
|
MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied)
|
||||||
activity.startActivityForResult(intent, requestCode)
|
if (!safeStartActivityForResult(intent, requestCode)) {
|
||||||
|
MainActivity.notifyError("failed to start activity for intent=$intent extras=${intent.extras}")
|
||||||
|
onDenied()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
|
MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
|
||||||
onDenied()
|
onDenied()
|
||||||
|
@ -144,7 +149,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
type = mimeType
|
type = mimeType
|
||||||
putExtra(Intent.EXTRA_TITLE, name)
|
putExtra(Intent.EXTRA_TITLE, name)
|
||||||
}
|
}
|
||||||
safeStartActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
|
safeStartActivityForStorageAccessResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun openFile() {
|
private suspend fun openFile() {
|
||||||
|
@ -177,7 +182,33 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
addCategory(Intent.CATEGORY_OPENABLE)
|
addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
|
setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
|
||||||
}
|
}
|
||||||
safeStartActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
|
safeStartActivityForStorageAccessResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun edit() {
|
||||||
|
val uri = args["uri"] as String?
|
||||||
|
val mimeType = args["mimeType"] as String? // optional
|
||||||
|
if (uri == null) {
|
||||||
|
error("edit-args", "missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_EDIT)
|
||||||
|
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
.setDataAndType(AppAdapterHandler.getShareableUri(activity, Uri.parse(uri)), mimeType)
|
||||||
|
|
||||||
|
if (intent.resolveActivity(activity.packageManager) == null) {
|
||||||
|
error("edit-resolve", "cannot resolve activity for this intent", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
MainActivity.pendingEditIntentHandler = { fields ->
|
||||||
|
success(fields)
|
||||||
|
endOfStream()
|
||||||
|
}
|
||||||
|
if (!safeStartActivityForResult(intent, MainActivity.EDIT_REQUEST)) {
|
||||||
|
error("edit-start", "cannot start activity for this intent", null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickCollectionFilters() {
|
private fun pickCollectionFilters() {
|
||||||
|
@ -192,6 +223,24 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
||||||
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
|
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun safeStartActivityForResult(intent: Intent, requestCode: Int): Boolean {
|
||||||
|
return try {
|
||||||
|
activity.startActivityForResult(intent, requestCode)
|
||||||
|
true
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
if (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION != 0) {
|
||||||
|
// in some environments, providing the write flag yields a `SecurityException`:
|
||||||
|
// "UID XXXX does not have permission to content://XXXX"
|
||||||
|
// so we retry without it
|
||||||
|
Log.i(LOG_TAG, "retry intent=$intent without FLAG_GRANT_WRITE_URI_PERMISSION")
|
||||||
|
intent.flags = intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION.inv()
|
||||||
|
safeStartActivityForResult(intent, requestCode)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {
|
override fun onCancel(arguments: Any?) {
|
||||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,7 +159,7 @@ object SafePngMetadataReader {
|
||||||
// Only compression method allowed by the spec is zero: deflate
|
// Only compression method allowed by the spec is zero: deflate
|
||||||
if (compressionMethod.toInt() == 0) {
|
if (compressionMethod.toInt() == 0) {
|
||||||
// bytes left for compressed text is:
|
// bytes left for compressed text is:
|
||||||
// total bytes length - (profilenamebytes length + null byte + compression method byte)
|
// total bytes length - (profileNameBytes length + null byte + compression method byte)
|
||||||
val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1)
|
val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1)
|
||||||
val compressedProfile = reader.getBytes(bytesLeft)
|
val compressedProfile = reader.getBytes(bytesLeft)
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -16,8 +16,16 @@ internal class FileImageProvider : ImageProvider() {
|
||||||
var mimeType = sourceMimeType
|
var mimeType = sourceMimeType
|
||||||
|
|
||||||
if (mimeType == null) {
|
if (mimeType == null) {
|
||||||
val extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
var extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
||||||
if (extension != null) {
|
if (extension.isEmpty()) {
|
||||||
|
uri.path?.let { path ->
|
||||||
|
val lastDotIndex = path.lastIndexOf('.')
|
||||||
|
if (lastDotIndex >= 0) {
|
||||||
|
extension = path.substring(lastDotIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extension.isNotEmpty()) {
|
||||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
12
android/app/src/main/res/values-ca/strings.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Aves</string>
|
||||||
|
<string name="app_widget_label">Marc de foto</string>
|
||||||
|
<string name="wallpaper">Fons de pantalla</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">Mode segur</string>
|
||||||
|
<string name="search_shortcut_short_label">Buscar</string>
|
||||||
|
<string name="videos_shortcut_short_label">Vídeos</string>
|
||||||
|
<string name="analysis_channel_name">Exploració de mitjans</string>
|
||||||
|
<string name="analysis_notification_default_title">Explorant mitjans</string>
|
||||||
|
<string name="analysis_notification_action_stop">Atura</string>
|
||||||
|
</resources>
|
|
@ -8,4 +8,5 @@
|
||||||
<string name="analysis_channel_name">मीडिया जाँचे</string>
|
<string name="analysis_channel_name">मीडिया जाँचे</string>
|
||||||
<string name="app_name">ऐवीज</string>
|
<string name="app_name">ऐवीज</string>
|
||||||
<string name="videos_shortcut_short_label">वीडियो</string>
|
<string name="videos_shortcut_short_label">वीडियो</string>
|
||||||
|
<string name="safe_mode_shortcut_short_label">सेफ मोड</string>
|
||||||
</resources>
|
</resources>
|
1
android/exifinterface/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
178
android/exifinterface/LICENSE.txt
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
30
android/exifinterface/build.gradle
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
plugins {
|
||||||
|
id 'com.android.library'
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'androidx.exifinterface.media'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdk 19
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles "consumer-rules.pro"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.annotation:annotation:1.7.1'
|
||||||
|
}
|
0
android/exifinterface/consumer-rules.pro
Normal file
21
android/exifinterface/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
4
android/exifinterface/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,192 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.exifinterface.media;
|
||||||
|
|
||||||
|
import android.media.MediaDataSource;
|
||||||
|
import android.media.MediaMetadataRetriever;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.system.ErrnoException;
|
||||||
|
import android.system.Os;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.annotation.DoNotInline;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
class ExifInterfaceUtils {
|
||||||
|
private static final String TAG = "ExifInterfaceUtils";
|
||||||
|
|
||||||
|
private ExifInterfaceUtils() {
|
||||||
|
// Prevent instantiation
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
|
||||||
|
* Returns the total number of bytes transferred.
|
||||||
|
*/
|
||||||
|
static int copy(InputStream in, OutputStream out) throws IOException {
|
||||||
|
int total = 0;
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int c;
|
||||||
|
while ((c = in.read(buffer)) != -1) {
|
||||||
|
total += c;
|
||||||
|
out.write(buffer, 0, c);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
|
||||||
|
* closed.
|
||||||
|
*/
|
||||||
|
static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
|
||||||
|
int remainder = numBytes;
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
while (remainder > 0) {
|
||||||
|
int bytesToRead = Math.min(remainder, 8192);
|
||||||
|
int bytesRead = in.read(buffer, 0, bytesToRead);
|
||||||
|
if (bytesRead != bytesToRead) {
|
||||||
|
throw new IOException("Failed to copy the given amount of bytes from the input"
|
||||||
|
+ "stream to the output stream.");
|
||||||
|
}
|
||||||
|
remainder -= bytesRead;
|
||||||
|
out.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert given int[] to long[]. If long[] is given, just return it.
|
||||||
|
* Return null for other types of input.
|
||||||
|
*/
|
||||||
|
static long[] convertToLongArray(Object inputObj) {
|
||||||
|
if (inputObj instanceof int[]) {
|
||||||
|
int[] input = (int[]) inputObj;
|
||||||
|
long[] result = new long[input.length];
|
||||||
|
for (int i = 0; i < input.length; i++) {
|
||||||
|
result[i] = input[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else if (inputObj instanceof long[]) {
|
||||||
|
return (long[]) inputObj;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean startsWith(byte[] cur, byte[] val) {
|
||||||
|
if (cur == null || val == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cur.length < val.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < val.length; i++) {
|
||||||
|
if (cur[i] != val[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String byteArrayToHexString(byte[] bytes) {
|
||||||
|
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
||||||
|
for (int i = 0; i < bytes.length; i++) {
|
||||||
|
sb.append(String.format("%02x", bytes[i]));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static long parseSubSeconds(String subSec) {
|
||||||
|
try {
|
||||||
|
final int len = Math.min(subSec.length(), 3);
|
||||||
|
long sub = Long.parseLong(subSec.substring(0, len));
|
||||||
|
for (int i = len; i < 3; i++) {
|
||||||
|
sub *= 10;
|
||||||
|
}
|
||||||
|
return sub;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
// Ignored
|
||||||
|
}
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
|
||||||
|
*/
|
||||||
|
static void closeQuietly(Closeable closeable) {
|
||||||
|
if (closeable != null) {
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (RuntimeException rethrown) {
|
||||||
|
throw rethrown;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes a file descriptor that has been duplicated.
|
||||||
|
*/
|
||||||
|
static void closeFileDescriptor(FileDescriptor fd) {
|
||||||
|
// Os.dup and Os.close was introduced in API 21 so this method shouldn't be called
|
||||||
|
// in API < 21.
|
||||||
|
if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
try {
|
||||||
|
Api21Impl.close(fd);
|
||||||
|
// Catching ErrnoException will raise error in API < 21
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.e(TAG, "Error closing fd.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "closeFileDescriptor is called in API < 21, which must be wrong.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(21)
|
||||||
|
static class Api21Impl {
|
||||||
|
private Api21Impl() {}
|
||||||
|
|
||||||
|
@DoNotInline
|
||||||
|
static FileDescriptor dup(FileDescriptor fileDescriptor) throws ErrnoException {
|
||||||
|
return Os.dup(fileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DoNotInline
|
||||||
|
static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
|
||||||
|
return Os.lseek(fd, offset, whence);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DoNotInline
|
||||||
|
static void close(FileDescriptor fd) throws ErrnoException {
|
||||||
|
Os.close(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(23)
|
||||||
|
static class Api23Impl {
|
||||||
|
private Api23Impl() {}
|
||||||
|
|
||||||
|
@DoNotInline
|
||||||
|
static void setDataSource(MediaMetadataRetriever retriever, MediaDataSource dataSource) {
|
||||||
|
retriever.setDataSource(dataSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,3 +24,4 @@ assert(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
apply {
|
apply {
|
||||||
from("$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle")
|
from("$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle")
|
||||||
}
|
}
|
||||||
|
include(":exifinterface")
|
||||||
|
|
5
fastlane/metadata/android/ca/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||||
|
|
||||||
|
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||||
|
|
||||||
|
<i>Aves</i> integrates with Android (from KitKat to Android 14, including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
BIN
fastlane/metadata/android/ca/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
fastlane/metadata/android/ca/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 281 KiB |
BIN
fastlane/metadata/android/ca/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 497 KiB |
BIN
fastlane/metadata/android/ca/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 203 KiB |
BIN
fastlane/metadata/android/ca/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
fastlane/metadata/android/ca/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
fastlane/metadata/android/ca/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 327 KiB |
BIN
fastlane/metadata/android/ca/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 338 KiB |
1
fastlane/metadata/android/ca/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Gallery and metadata explorer
|
3
fastlane/metadata/android/en-US/changelogs/114.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
In v1.10.5:
|
||||||
|
- enjoy the app in Catalan
|
||||||
|
Full changelog available on GitHub
|
3
fastlane/metadata/android/en-US/changelogs/11401.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
In v1.10.5:
|
||||||
|
- enjoy the app in Catalan
|
||||||
|
Full changelog available on GitHub
|
|
@ -53,7 +53,7 @@
|
||||||
"@previousTooltip": {},
|
"@previousTooltip": {},
|
||||||
"welcomeMessage": "مرحبا بكم في Aves",
|
"welcomeMessage": "مرحبا بكم في Aves",
|
||||||
"@welcomeMessage": {},
|
"@welcomeMessage": {},
|
||||||
"applyButtonLabel": "تقديم",
|
"applyButtonLabel": "تأكيد",
|
||||||
"@applyButtonLabel": {},
|
"@applyButtonLabel": {},
|
||||||
"nextButtonLabel": "التالي",
|
"nextButtonLabel": "التالي",
|
||||||
"@nextButtonLabel": {},
|
"@nextButtonLabel": {},
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
"@pickTooltip": {},
|
"@pickTooltip": {},
|
||||||
"chipActionGoToCountryPage": "عرض في الدول",
|
"chipActionGoToCountryPage": "عرض في الدول",
|
||||||
"@chipActionGoToCountryPage": {},
|
"@chipActionGoToCountryPage": {},
|
||||||
"applyTooltip": "تقدم",
|
"applyTooltip": "تأكيد",
|
||||||
"@applyTooltip": {},
|
"@applyTooltip": {},
|
||||||
"chipActionUnpin": "إلغاء التثبيت في الأعلى",
|
"chipActionUnpin": "إلغاء التثبيت في الأعلى",
|
||||||
"@chipActionUnpin": {},
|
"@chipActionUnpin": {},
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
"@chipActionGoToTagPage": {},
|
"@chipActionGoToTagPage": {},
|
||||||
"chipActionLock": "Заблакаваць",
|
"chipActionLock": "Заблакаваць",
|
||||||
"@chipActionLock": {},
|
"@chipActionLock": {},
|
||||||
"chipActionSetCover": "Усталяваць вокладку",
|
"chipActionSetCover": "Ўсталяваць вокладку",
|
||||||
"@chipActionSetCover": {},
|
"@chipActionSetCover": {},
|
||||||
"chipActionRename": "Перайменаваць",
|
"chipActionRename": "Перайменаваць",
|
||||||
"@chipActionRename": {},
|
"@chipActionRename": {},
|
||||||
|
@ -124,11 +124,11 @@
|
||||||
"@entryActionConvertMotionPhotoToStillImage": {},
|
"@entryActionConvertMotionPhotoToStillImage": {},
|
||||||
"entryActionViewMotionPhotoVideo": "Адкрыць відэа",
|
"entryActionViewMotionPhotoVideo": "Адкрыць відэа",
|
||||||
"@entryActionViewMotionPhotoVideo": {},
|
"@entryActionViewMotionPhotoVideo": {},
|
||||||
"entryActionSetAs": "Усталяваць як",
|
"entryActionSetAs": "Ўсталяваць як",
|
||||||
"@entryActionSetAs": {},
|
"@entryActionSetAs": {},
|
||||||
"entryActionAddFavourite": "Дадаць у абранае",
|
"entryActionAddFavourite": "Дадаць у абранае",
|
||||||
"@entryActionAddFavourite": {},
|
"@entryActionAddFavourite": {},
|
||||||
"videoActionUnmute": "Уключыць гук",
|
"videoActionUnmute": "Ўключыць гук",
|
||||||
"@videoActionUnmute": {},
|
"@videoActionUnmute": {},
|
||||||
"videoActionCaptureFrame": "Захоп кадра",
|
"videoActionCaptureFrame": "Захоп кадра",
|
||||||
"@videoActionCaptureFrame": {},
|
"@videoActionCaptureFrame": {},
|
||||||
|
@ -449,7 +449,7 @@
|
||||||
"@wallpaperTargetHomeLock": {},
|
"@wallpaperTargetHomeLock": {},
|
||||||
"widgetTapUpdateWidget": "Абнавіць віджэт",
|
"widgetTapUpdateWidget": "Абнавіць віджэт",
|
||||||
"@widgetTapUpdateWidget": {},
|
"@widgetTapUpdateWidget": {},
|
||||||
"storageVolumeDescriptionFallbackPrimary": "Унутраная памяць",
|
"storageVolumeDescriptionFallbackPrimary": "Ўнутраная памяць",
|
||||||
"@storageVolumeDescriptionFallbackPrimary": {},
|
"@storageVolumeDescriptionFallbackPrimary": {},
|
||||||
"restrictedAccessDialogMessage": "Гэтай праграме забаронена змяняць файлы ў {directory} «{volume}».\n\nКаб перамясціць элементы ў іншую дырэкторыю, выкарыстоўвайце папярэдне ўсталяваны дыспетчар файлаў або праграму галерэі.",
|
"restrictedAccessDialogMessage": "Гэтай праграме забаронена змяняць файлы ў {directory} «{volume}».\n\nКаб перамясціць элементы ў іншую дырэкторыю, выкарыстоўвайце папярэдне ўсталяваны дыспетчар файлаў або праграму галерэі.",
|
||||||
"@restrictedAccessDialogMessage": {
|
"@restrictedAccessDialogMessage": {
|
||||||
|
@ -465,7 +465,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"missingSystemFilePickerDialogMessage": "Сродак выбару сістэмных файлаў адсутнічае або адключаны. Уключыце яго і паўтарыце спробу.",
|
"missingSystemFilePickerDialogMessage": "Сродак выбару сістэмных файлаў адсутнічае або адключаны. Ўключыце яго і паўтарыце спробу.",
|
||||||
"@missingSystemFilePickerDialogMessage": {},
|
"@missingSystemFilePickerDialogMessage": {},
|
||||||
"unsupportedTypeDialogMessage": "{count, plural, =1{Гэта аперацыя не падтрымліваецца для элементаў наступнага тыпу: {types}.} other{Гэта аперацыя не падтрымліваецца для элементаў наступных тыпаў: {types}.}}",
|
"unsupportedTypeDialogMessage": "{count, plural, =1{Гэта аперацыя не падтрымліваецца для элементаў наступнага тыпу: {types}.} other{Гэта аперацыя не падтрымліваецца для элементаў наступных тыпаў: {types}.}}",
|
||||||
"@unsupportedTypeDialogMessage": {
|
"@unsupportedTypeDialogMessage": {
|
||||||
|
@ -517,15 +517,15 @@
|
||||||
"@configureVaultDialogTitle": {},
|
"@configureVaultDialogTitle": {},
|
||||||
"vaultDialogLockTypeLabel": "Тып блакіроўкі",
|
"vaultDialogLockTypeLabel": "Тып блакіроўкі",
|
||||||
"@vaultDialogLockTypeLabel": {},
|
"@vaultDialogLockTypeLabel": {},
|
||||||
"pinDialogEnter": "Увядзіце PIN-код",
|
"pinDialogEnter": "Ўвядзіце PIN-код",
|
||||||
"@pinDialogEnter": {},
|
"@pinDialogEnter": {},
|
||||||
"patternDialogEnter": "Увядзіце графічны ключ",
|
"patternDialogEnter": "Ўвядзіце графічны ключ",
|
||||||
"@patternDialogEnter": {},
|
"@patternDialogEnter": {},
|
||||||
"patternDialogConfirm": "Пацвердзіце графічны ключ",
|
"patternDialogConfirm": "Пацвердзіце графічны ключ",
|
||||||
"@patternDialogConfirm": {},
|
"@patternDialogConfirm": {},
|
||||||
"pinDialogConfirm": "Пацвердзіце PIN-код",
|
"pinDialogConfirm": "Пацвердзіце PIN-код",
|
||||||
"@pinDialogConfirm": {},
|
"@pinDialogConfirm": {},
|
||||||
"passwordDialogEnter": "Увядзіце пароль",
|
"passwordDialogEnter": "Ўвядзіце пароль",
|
||||||
"@passwordDialogEnter": {},
|
"@passwordDialogEnter": {},
|
||||||
"passwordDialogConfirm": "Пацвердзіце пароль",
|
"passwordDialogConfirm": "Пацвердзіце пароль",
|
||||||
"@passwordDialogConfirm": {},
|
"@passwordDialogConfirm": {},
|
||||||
|
@ -577,7 +577,7 @@
|
||||||
"@sourceViewerPageTitle": {},
|
"@sourceViewerPageTitle": {},
|
||||||
"panoramaDisableSensorControl": "Адключыць сэнсарнае кіраванне",
|
"panoramaDisableSensorControl": "Адключыць сэнсарнае кіраванне",
|
||||||
"@panoramaDisableSensorControl": {},
|
"@panoramaDisableSensorControl": {},
|
||||||
"panoramaEnableSensorControl": "Уключыць сэнсарнае кіраванне",
|
"panoramaEnableSensorControl": "Ўключыць сэнсарнае кіраванне",
|
||||||
"@panoramaEnableSensorControl": {},
|
"@panoramaEnableSensorControl": {},
|
||||||
"tagPlaceholderPlace": "Месца",
|
"tagPlaceholderPlace": "Месца",
|
||||||
"@tagPlaceholderPlace": {},
|
"@tagPlaceholderPlace": {},
|
||||||
|
@ -601,7 +601,7 @@
|
||||||
"@videoControlsNone": {},
|
"@videoControlsNone": {},
|
||||||
"viewerErrorUnknown": "Ой!",
|
"viewerErrorUnknown": "Ой!",
|
||||||
"@viewerErrorUnknown": {},
|
"@viewerErrorUnknown": {},
|
||||||
"viewerSetWallpaperButtonLabel": "УСТАНАВІЦЬ ШПАЛЕРЫ",
|
"viewerSetWallpaperButtonLabel": "ЎСТАНАВІЦЬ ШПАЛЕРЫ",
|
||||||
"@viewerSetWallpaperButtonLabel": {},
|
"@viewerSetWallpaperButtonLabel": {},
|
||||||
"statsTopAlbumsSectionTitle": "Лепшыя альбомы",
|
"statsTopAlbumsSectionTitle": "Лепшыя альбомы",
|
||||||
"@statsTopAlbumsSectionTitle": {},
|
"@statsTopAlbumsSectionTitle": {},
|
||||||
|
@ -819,7 +819,7 @@
|
||||||
"@aboutDataUsageMisc": {},
|
"@aboutDataUsageMisc": {},
|
||||||
"albumVideoCaptures": "Відэазапісы",
|
"albumVideoCaptures": "Відэазапісы",
|
||||||
"@albumVideoCaptures": {},
|
"@albumVideoCaptures": {},
|
||||||
"editEntryDateDialogSetCustom": "Усталяваць карыстацкую дату",
|
"editEntryDateDialogSetCustom": "Ўсталяваць карыстацкую дату",
|
||||||
"@editEntryDateDialogSetCustom": {},
|
"@editEntryDateDialogSetCustom": {},
|
||||||
"settingsSearchEmpty": "Няма адпаведнай налады",
|
"settingsSearchEmpty": "Няма адпаведнай налады",
|
||||||
"@settingsSearchEmpty": {},
|
"@settingsSearchEmpty": {},
|
||||||
|
@ -845,7 +845,7 @@
|
||||||
"@collectionSelectSectionTooltip": {},
|
"@collectionSelectSectionTooltip": {},
|
||||||
"aboutLicensesBanner": "Гэта праграма выкарыстоўвае наступныя пакеты і бібліятэкі з адкрытым зыходным кодам.",
|
"aboutLicensesBanner": "Гэта праграма выкарыстоўвае наступныя пакеты і бібліятэкі з адкрытым зыходным кодам.",
|
||||||
"@aboutLicensesBanner": {},
|
"@aboutLicensesBanner": {},
|
||||||
"dateYesterday": "Учора",
|
"dateYesterday": "Ўчора",
|
||||||
"@dateYesterday": {},
|
"@dateYesterday": {},
|
||||||
"aboutDataUsageDatabase": "База дадзеных",
|
"aboutDataUsageDatabase": "База дадзеных",
|
||||||
"@aboutDataUsageDatabase": {},
|
"@aboutDataUsageDatabase": {},
|
||||||
|
@ -879,7 +879,7 @@
|
||||||
"@videoStreamSelectionDialogAudio": {},
|
"@videoStreamSelectionDialogAudio": {},
|
||||||
"videoSpeedDialogLabel": "Хуткасць прайгравання",
|
"videoSpeedDialogLabel": "Хуткасць прайгравання",
|
||||||
"@videoSpeedDialogLabel": {},
|
"@videoSpeedDialogLabel": {},
|
||||||
"editEntryLocationDialogSetCustom": "Устанавіць карыстацкае месцазнаходжанне",
|
"editEntryLocationDialogSetCustom": "Ўстанавіць карыстацкае месцазнаходжанне",
|
||||||
"@editEntryLocationDialogSetCustom": {},
|
"@editEntryLocationDialogSetCustom": {},
|
||||||
"placeEmpty": "Няма месцаў",
|
"placeEmpty": "Няма месцаў",
|
||||||
"@placeEmpty": {},
|
"@placeEmpty": {},
|
||||||
|
@ -1141,7 +1141,7 @@
|
||||||
"@genericFailureFeedback": {},
|
"@genericFailureFeedback": {},
|
||||||
"aboutDataUsageData": "Дадзеныя",
|
"aboutDataUsageData": "Дадзеныя",
|
||||||
"@aboutDataUsageData": {},
|
"@aboutDataUsageData": {},
|
||||||
"aboutDataUsageInternal": "Унутраны",
|
"aboutDataUsageInternal": "Ўнутраны",
|
||||||
"@aboutDataUsageInternal": {},
|
"@aboutDataUsageInternal": {},
|
||||||
"albumDownload": "Загрузкі",
|
"albumDownload": "Загрузкі",
|
||||||
"@albumDownload": {},
|
"@albumDownload": {},
|
||||||
|
@ -1519,7 +1519,7 @@
|
||||||
"minutes": {}
|
"minutes": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"collectionActionSetHome": "Усталяваць як галоўную",
|
"collectionActionSetHome": "Ўсталяваць як галоўную",
|
||||||
"@collectionActionSetHome": {},
|
"@collectionActionSetHome": {},
|
||||||
"setHomeCustomCollection": "Уласная калекцыя",
|
"setHomeCustomCollection": "Уласная калекцыя",
|
||||||
"@setHomeCustomCollection": {},
|
"@setHomeCustomCollection": {},
|
||||||
|
|
1528
lib/l10n/app_ca.arb
Normal file
|
@ -68,10 +68,38 @@
|
||||||
"@previousTooltip": {},
|
"@previousTooltip": {},
|
||||||
"hideTooltip": "छिपाए",
|
"hideTooltip": "छिपाए",
|
||||||
"@hideTooltip": {},
|
"@hideTooltip": {},
|
||||||
"cancelTooltip": "कैंसिल",
|
"cancelTooltip": "रद्द करें",
|
||||||
"@cancelTooltip": {},
|
"@cancelTooltip": {},
|
||||||
"changeTooltip": "बदलें",
|
"changeTooltip": "बदलें",
|
||||||
"@changeTooltip": {},
|
"@changeTooltip": {},
|
||||||
"showTooltip": "देखें",
|
"showTooltip": "देखें",
|
||||||
"@showTooltip": {}
|
"@showTooltip": {},
|
||||||
|
"applyTooltip": "लगाए",
|
||||||
|
"@applyTooltip": {},
|
||||||
|
"chipActionGoToPlacePage": "स्थानों में दिखाएं",
|
||||||
|
"@chipActionGoToPlacePage": {},
|
||||||
|
"saveCopyButtonLabel": "सेव कॉपी",
|
||||||
|
"@saveCopyButtonLabel": {},
|
||||||
|
"doubleBackExitMessage": "बाहर जाने के लिए दोबारा \"पीछे\" पर टैप करें",
|
||||||
|
"@doubleBackExitMessage": {},
|
||||||
|
"sourceStateLoading": "लोड हो रहा है",
|
||||||
|
"@sourceStateLoading": {},
|
||||||
|
"chipActionGoToTagPage": "टैग्स में दिखाएं",
|
||||||
|
"@chipActionGoToTagPage": {},
|
||||||
|
"resetTooltip": "रिसेट",
|
||||||
|
"@resetTooltip": {},
|
||||||
|
"saveTooltip": "सेव करें",
|
||||||
|
"@saveTooltip": {},
|
||||||
|
"pickTooltip": "चुनें",
|
||||||
|
"@pickTooltip": {},
|
||||||
|
"doNotAskAgain": "दोबारा मत पूछो",
|
||||||
|
"@doNotAskAgain": {},
|
||||||
|
"chipActionDelete": "मिटाएं",
|
||||||
|
"@chipActionDelete": {},
|
||||||
|
"chipActionGoToAlbumPage": "एल्बम में दिखाए",
|
||||||
|
"@chipActionGoToAlbumPage": {},
|
||||||
|
"chipActionGoToCountryPage": "देशों में दिखाएं",
|
||||||
|
"@chipActionGoToCountryPage": {},
|
||||||
|
"chipActionHide": "छिपाए",
|
||||||
|
"@chipActionHide": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1450,5 +1450,79 @@
|
||||||
"searchStatesSectionTitle": "State",
|
"searchStatesSectionTitle": "State",
|
||||||
"@searchStatesSectionTitle": {},
|
"@searchStatesSectionTitle": {},
|
||||||
"statsTopStatesSectionTitle": "Statele de top",
|
"statsTopStatesSectionTitle": "Statele de top",
|
||||||
"@statsTopStatesSectionTitle": {}
|
"@statsTopStatesSectionTitle": {},
|
||||||
|
"overlayHistogramNone": "Nimic",
|
||||||
|
"@overlayHistogramNone": {},
|
||||||
|
"overlayHistogramLuminance": "Luminanță",
|
||||||
|
"@overlayHistogramLuminance": {},
|
||||||
|
"saveCopyButtonLabel": "SALVEAZĂ COPIA",
|
||||||
|
"@saveCopyButtonLabel": {},
|
||||||
|
"applyTooltip": "Aplică",
|
||||||
|
"@applyTooltip": {},
|
||||||
|
"editorTransformCrop": "Decupare",
|
||||||
|
"@editorTransformCrop": {},
|
||||||
|
"cropAspectRatioFree": "Liber",
|
||||||
|
"@cropAspectRatioFree": {},
|
||||||
|
"cropAspectRatioOriginal": "Original",
|
||||||
|
"@cropAspectRatioOriginal": {},
|
||||||
|
"settingsVideoPlaybackPageTitle": "Redare",
|
||||||
|
"@settingsVideoPlaybackPageTitle": {},
|
||||||
|
"settingsAskEverytime": "Întreabă de fiecare dată",
|
||||||
|
"@settingsAskEverytime": {},
|
||||||
|
"aboutDataUsageCache": "Cache",
|
||||||
|
"@aboutDataUsageCache": {},
|
||||||
|
"aboutDataUsageDatabase": "Bază de date",
|
||||||
|
"@aboutDataUsageDatabase": {},
|
||||||
|
"aboutDataUsageMisc": "Diverse",
|
||||||
|
"@aboutDataUsageMisc": {},
|
||||||
|
"settingsVideoPlaybackTile": "Redare",
|
||||||
|
"@settingsVideoPlaybackTile": {},
|
||||||
|
"overlayHistogramRGB": "RGB",
|
||||||
|
"@overlayHistogramRGB": {},
|
||||||
|
"widgetTapUpdateWidget": "Actualizare widget",
|
||||||
|
"@widgetTapUpdateWidget": {},
|
||||||
|
"exportEntryDialogQuality": "Calitate",
|
||||||
|
"@exportEntryDialogQuality": {},
|
||||||
|
"aboutDataUsageSectionTitle": "Utilizare date",
|
||||||
|
"@aboutDataUsageSectionTitle": {},
|
||||||
|
"aboutDataUsageData": "Date",
|
||||||
|
"@aboutDataUsageData": {},
|
||||||
|
"aboutDataUsageInternal": "Intern",
|
||||||
|
"@aboutDataUsageInternal": {},
|
||||||
|
"aboutDataUsageExternal": "Extern",
|
||||||
|
"@aboutDataUsageExternal": {},
|
||||||
|
"collectionActionSetHome": "Setare ca principal",
|
||||||
|
"@collectionActionSetHome": {},
|
||||||
|
"aboutDataUsageClearCache": "Golește memoria cache",
|
||||||
|
"@aboutDataUsageClearCache": {},
|
||||||
|
"setHomeCustomCollection": "Colecție personalizată",
|
||||||
|
"@setHomeCustomCollection": {},
|
||||||
|
"settingsThumbnailShowHdrIcon": "Afișare pictogramă HDR",
|
||||||
|
"@settingsThumbnailShowHdrIcon": {},
|
||||||
|
"settingsViewerShowHistogram": "Afișare histogramă",
|
||||||
|
"@settingsViewerShowHistogram": {},
|
||||||
|
"settingsVideoResumptionModeTile": "Reluare redare",
|
||||||
|
"@settingsVideoResumptionModeTile": {},
|
||||||
|
"entryActionCast": "Proiectare",
|
||||||
|
"@entryActionCast": {},
|
||||||
|
"settingsVideoResumptionModeDialogTitle": "Reluare redare",
|
||||||
|
"@settingsVideoResumptionModeDialogTitle": {},
|
||||||
|
"editorActionTransform": "Transformă",
|
||||||
|
"@editorActionTransform": {},
|
||||||
|
"tagEditorDiscardDialogMessage": "Dorești să renunți la modificări?",
|
||||||
|
"@tagEditorDiscardDialogMessage": {},
|
||||||
|
"editorTransformRotate": "Rotire",
|
||||||
|
"@editorTransformRotate": {},
|
||||||
|
"cropAspectRatioSquare": "Pătrat",
|
||||||
|
"@cropAspectRatioSquare": {},
|
||||||
|
"videoResumptionModeNever": "Niciodată",
|
||||||
|
"@videoResumptionModeNever": {},
|
||||||
|
"videoResumptionModeAlways": "Mereu",
|
||||||
|
"@videoResumptionModeAlways": {},
|
||||||
|
"castDialogTitle": "Dispozitive de proiectare",
|
||||||
|
"@castDialogTitle": {},
|
||||||
|
"maxBrightnessNever": "Niciodată",
|
||||||
|
"@maxBrightnessNever": {},
|
||||||
|
"maxBrightnessAlways": "Mereu",
|
||||||
|
"@maxBrightnessAlways": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
"@chipActionGoToCountryPage": {},
|
"@chipActionGoToCountryPage": {},
|
||||||
"chipActionGoToTagPage": "在标签中显示",
|
"chipActionGoToTagPage": "在标签中显示",
|
||||||
"@chipActionGoToTagPage": {},
|
"@chipActionGoToTagPage": {},
|
||||||
"chipActionFilterOut": "滤除",
|
"chipActionFilterOut": "筛除",
|
||||||
"@chipActionFilterOut": {},
|
"@chipActionFilterOut": {},
|
||||||
"chipActionFilterIn": "筛选",
|
"chipActionFilterIn": "筛选",
|
||||||
"@chipActionFilterIn": {},
|
"@chipActionFilterIn": {},
|
||||||
|
@ -1243,7 +1243,7 @@
|
||||||
"@exportEntryDialogQuality": {},
|
"@exportEntryDialogQuality": {},
|
||||||
"placeEmpty": "没有地点",
|
"placeEmpty": "没有地点",
|
||||||
"@placeEmpty": {},
|
"@placeEmpty": {},
|
||||||
"settingsAskEverytime": "每次询问",
|
"settingsAskEverytime": "每次都询问",
|
||||||
"@settingsAskEverytime": {},
|
"@settingsAskEverytime": {},
|
||||||
"settingsModificationWarningDialogMessage": "其他设置将被修改。",
|
"settingsModificationWarningDialogMessage": "其他设置将被修改。",
|
||||||
"@settingsModificationWarningDialogMessage": {},
|
"@settingsModificationWarningDialogMessage": {},
|
||||||
|
|
|
@ -38,7 +38,7 @@ void mainCommon(AppFlavor flavor, {Map? debugIntentData}) {
|
||||||
// cf https://docs.flutter.dev/testing/errors
|
// cf https://docs.flutter.dev/testing/errors
|
||||||
|
|
||||||
LeakTracking.start();
|
LeakTracking.start();
|
||||||
MemoryAllocations.instance.addListener(
|
FlutterMemoryAllocations.instance.addListener(
|
||||||
(event) => LeakTracking.dispatchObjectEvent(event.toMap()),
|
(event) => LeakTracking.dispatchObjectEvent(event.toMap()),
|
||||||
);
|
);
|
||||||
runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData));
|
runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData));
|
||||||
|
|
|
@ -77,6 +77,7 @@ class Contributors {
|
||||||
Contributor('fuzfyy', 'egeozce35@gmail.com'),
|
Contributor('fuzfyy', 'egeozce35@gmail.com'),
|
||||||
Contributor('minh', 'teaminh@skiff.com'),
|
Contributor('minh', 'teaminh@skiff.com'),
|
||||||
Contributor('luckris25', 'lk1thebestl@gmail.com'),
|
Contributor('luckris25', 'lk1thebestl@gmail.com'),
|
||||||
|
Contributor('Marc Amorós', 'marquitus99@gmail.com'),
|
||||||
// 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
|
||||||
|
@ -84,6 +85,7 @@ class Contributors {
|
||||||
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
|
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
|
||||||
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
||||||
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
|
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
|
||||||
|
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi
|
||||||
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
|
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
|
||||||
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
|
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
|
||||||
// Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central)
|
// Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central)
|
||||||
|
|
|
@ -72,7 +72,7 @@ class AvesEntry with AvesEntryBase {
|
||||||
this.burstEntries,
|
this.burstEntries,
|
||||||
}) : id = id ?? 0 {
|
}) : id = id ?? 0 {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$AvesEntry',
|
className: '$AvesEntry',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -188,7 +188,7 @@ class AvesEntry with AvesEntryBase {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
visualChangeNotifier.dispose();
|
visualChangeNotifier.dispose();
|
||||||
metadataChangeNotifier.dispose();
|
metadataChangeNotifier.dispose();
|
||||||
|
|
|
@ -18,7 +18,7 @@ class MultiPageInfo {
|
||||||
required List<SinglePageInfo> pages,
|
required List<SinglePageInfo> pages,
|
||||||
}) : _pages = pages {
|
}) : _pages = pages {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$MultiPageInfo',
|
className: '$MultiPageInfo',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -44,7 +44,7 @@ class MultiPageInfo {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_transientEntries.forEach((entry) => entry.dispose());
|
_transientEntries.forEach((entry) => entry.dispose());
|
||||||
}
|
}
|
||||||
|
|
23
lib/model/settings/enums/widget_outline.dart
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
extension ExtraWidgetOutline on WidgetOutline {
|
||||||
|
Future<Color?> color(Brightness brightness) async {
|
||||||
|
switch (this) {
|
||||||
|
case WidgetOutline.none:
|
||||||
|
return SynchronousFuture(null);
|
||||||
|
case WidgetOutline.black:
|
||||||
|
return SynchronousFuture(Colors.black);
|
||||||
|
case WidgetOutline.white:
|
||||||
|
return SynchronousFuture(Colors.white);
|
||||||
|
case WidgetOutline.systemBlackAndWhite:
|
||||||
|
return SynchronousFuture(brightness == Brightness.dark ? Colors.black : Colors.white);
|
||||||
|
case WidgetOutline.systemDynamic:
|
||||||
|
final corePalette = await DynamicColorPlugin.getCorePalette();
|
||||||
|
final scheme = corePalette?.toColorScheme(brightness: brightness);
|
||||||
|
return scheme?.primary ?? await WidgetOutline.systemBlackAndWhite.color(brightness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -274,12 +274,9 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
|
||||||
|
|
||||||
// widget
|
// widget
|
||||||
|
|
||||||
Color? getWidgetOutline(int widgetId) {
|
WidgetOutline getWidgetOutline(int widgetId) => getEnumOrDefault('${SettingKeys.widgetOutlinePrefixKey}$widgetId', WidgetOutline.none, WidgetOutline.values);
|
||||||
final value = getInt('${SettingKeys.widgetOutlinePrefixKey}$widgetId');
|
|
||||||
return value != null ? Color(value) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void setWidgetOutline(int widgetId, Color? newValue) => set('${SettingKeys.widgetOutlinePrefixKey}$widgetId', newValue?.value);
|
void setWidgetOutline(int widgetId, WidgetOutline newValue) => set('${SettingKeys.widgetOutlinePrefixKey}$widgetId', newValue.toString());
|
||||||
|
|
||||||
WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('${SettingKeys.widgetShapePrefixKey}$widgetId', SettingsDefaults.widgetShape, WidgetShape.values);
|
WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('${SettingKeys.widgetShapePrefixKey}$widgetId', SettingsDefaults.widgetShape, WidgetShape.values);
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ class AnalysisController {
|
||||||
this.progressOffset = 0,
|
this.progressOffset = 0,
|
||||||
}) {
|
}) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$AnalysisController',
|
className: '$AnalysisController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -25,7 +25,7 @@ class AnalysisController {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_stopSignal.dispose();
|
_stopSignal.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ mixin SourceBase {
|
||||||
|
|
||||||
Map<int, AvesEntry> get entryById;
|
Map<int, AvesEntry> get entryById;
|
||||||
|
|
||||||
|
Set<AvesEntry> get allEntries;
|
||||||
|
|
||||||
Set<AvesEntry> get visibleEntries;
|
Set<AvesEntry> get visibleEntries;
|
||||||
|
|
||||||
Set<AvesEntry> get trashedEntries;
|
Set<AvesEntry> get trashedEntries;
|
||||||
|
@ -62,7 +64,7 @@ mixin SourceBase {
|
||||||
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin {
|
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin {
|
||||||
CollectionSource() {
|
CollectionSource() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$CollectionSource',
|
className: '$CollectionSource',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -86,7 +88,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_rawEntries.forEach((v) => v.dispose());
|
_rawEntries.forEach((v) => v.dispose());
|
||||||
}
|
}
|
||||||
|
@ -103,6 +105,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
||||||
|
|
||||||
final Set<AvesEntry> _rawEntries = {};
|
final Set<AvesEntry> _rawEntries = {};
|
||||||
|
|
||||||
|
@override
|
||||||
Set<AvesEntry> get allEntries => Set.unmodifiable(_rawEntries);
|
Set<AvesEntry> get allEntries => Set.unmodifiable(_rawEntries);
|
||||||
|
|
||||||
Set<AvesEntry>? _visibleEntries, _trashedEntries;
|
Set<AvesEntry>? _visibleEntries, _trashedEntries;
|
||||||
|
@ -261,8 +264,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (entry.trashed) {
|
if (entry.trashed) {
|
||||||
|
final trashPath = entry.trashDetails?.path;
|
||||||
|
if (trashPath != null) {
|
||||||
entry.contentId = null;
|
entry.contentId = null;
|
||||||
entry.uri = 'file://${entry.trashDetails?.path}';
|
entry.uri = Uri.file(trashPath).toString();
|
||||||
|
} else {
|
||||||
|
debugPrint('failed to update uri from unknown trash path for uri=${entry.uri}');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (persist) {
|
if (persist) {
|
||||||
|
|
|
@ -148,12 +148,21 @@ class MediaStoreSource extends CollectionSource {
|
||||||
knownDateByContentId[contentId] = 0;
|
knownDateByContentId[contentId] = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// items to add to the collection
|
||||||
|
final pendingNewEntries = <AvesEntry>{};
|
||||||
|
|
||||||
|
// recover untracked trash items
|
||||||
|
debugPrint('$runtimeType refresh ${stopwatch.elapsed} recover untracked entries');
|
||||||
|
if (directory == null) {
|
||||||
|
pendingNewEntries.addAll(await recoverUntrackedTrashItems());
|
||||||
|
}
|
||||||
|
|
||||||
// fetch new & modified entries
|
// fetch new & modified entries
|
||||||
debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch new entries');
|
debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch new entries');
|
||||||
// refresh after the first 10 entries, then after 100 more, then every 1000 entries
|
// refresh after the first 10 entries, then after 100 more, then every 1000 entries
|
||||||
var refreshCount = 10;
|
var refreshCount = 10;
|
||||||
const refreshCountMax = 1000;
|
const refreshCountMax = 1000;
|
||||||
final allNewEntries = <AvesEntry>{}, pendingNewEntries = <AvesEntry>{};
|
final allNewEntries = <AvesEntry>{};
|
||||||
void addPendingEntries() {
|
void addPendingEntries() {
|
||||||
allNewEntries.addAll(pendingNewEntries);
|
allNewEntries.addAll(pendingNewEntries);
|
||||||
addEntries(pendingNewEntries);
|
addEntries(pendingNewEntries);
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
|
import 'package:aves/model/metadata/trash.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/common/image_op_events.dart';
|
import 'package:aves/services/common/image_op_events.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
mixin TrashMixin on SourceBase {
|
mixin TrashMixin on SourceBase {
|
||||||
static const Duration binKeepDuration = Duration(days: 30);
|
static const Duration binKeepDuration = Duration(days: 30);
|
||||||
|
@ -32,4 +37,50 @@ mixin TrashMixin on SourceBase {
|
||||||
);
|
);
|
||||||
return await completer.future;
|
return await completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Set<AvesEntry>> recoverUntrackedTrashItems() async {
|
||||||
|
final newEntries = <AvesEntry>{};
|
||||||
|
|
||||||
|
final knownPaths = allEntries.map((v) => v.trashDetails?.path).whereNotNull().toSet();
|
||||||
|
final untrackedPaths = await storageService.getUntrackedTrashPaths(knownPaths);
|
||||||
|
if (untrackedPaths.isNotEmpty) {
|
||||||
|
debugPrint('Recovering ${untrackedPaths.length} untracked bin items');
|
||||||
|
final recoveryPath = pContext.join(androidFileUtils.picturesPath, AndroidFileUtils.recoveryDir);
|
||||||
|
await Future.forEach(untrackedPaths, (untrackedPath) async {
|
||||||
|
TrashDetails _buildTrashDetails(int id) => TrashDetails(
|
||||||
|
id: id,
|
||||||
|
path: untrackedPath,
|
||||||
|
dateMillis: DateTime.now().millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
|
||||||
|
final uri = Uri.file(untrackedPath).toString();
|
||||||
|
final entry = allEntries.firstWhereOrNull((v) => v.uri == uri);
|
||||||
|
if (entry != null) {
|
||||||
|
// there is already a matching entry
|
||||||
|
// but missing trash details, and possibly not marked as trash
|
||||||
|
final id = entry.id;
|
||||||
|
entry.contentId = null;
|
||||||
|
entry.trashed = true;
|
||||||
|
entry.trashDetails = _buildTrashDetails(id);
|
||||||
|
// persist
|
||||||
|
await metadataDb.updateEntry(id, entry);
|
||||||
|
await metadataDb.updateTrash(id, entry.trashDetails);
|
||||||
|
} else {
|
||||||
|
// there is no matching entry
|
||||||
|
final sourceEntry = await mediaFetchService.getEntry(uri, null);
|
||||||
|
if (sourceEntry != null) {
|
||||||
|
final id = metadataDb.nextId;
|
||||||
|
sourceEntry.id = id;
|
||||||
|
sourceEntry.path = pContext.join(recoveryPath, pContext.basename(untrackedPath));
|
||||||
|
sourceEntry.trashed = true;
|
||||||
|
sourceEntry.trashDetails = _buildTrashDetails(id);
|
||||||
|
newEntries.add(sourceEntry);
|
||||||
|
} else {
|
||||||
|
await reportService.recordError('Failed to recover untracked bin item at uri=$uri', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newEntries;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
import 'package:aves/model/entry/origins.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/vaults/details.dart';
|
import 'package:aves/model/vaults/details.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves_screen_state/aves_screen_state.dart';
|
import 'package:aves_screen_state/aves_screen_state.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
final Vaults vaults = Vaults._private();
|
final Vaults vaults = Vaults._private();
|
||||||
|
|
||||||
|
@ -14,6 +18,8 @@ class Vaults extends ChangeNotifier {
|
||||||
Set<VaultDetails> _rows = {};
|
Set<VaultDetails> _rows = {};
|
||||||
final Set<String> _unlockedDirPaths = {};
|
final Set<String> _unlockedDirPaths = {};
|
||||||
|
|
||||||
|
static const _fileScheme = 'file';
|
||||||
|
|
||||||
Vaults._private();
|
Vaults._private();
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
@ -118,7 +124,7 @@ class Vaults extends ChangeNotifier {
|
||||||
|
|
||||||
bool isVaultEntryUri(String uriString) {
|
bool isVaultEntryUri(String uriString) {
|
||||||
final uri = Uri.parse(uriString);
|
final uri = Uri.parse(uriString);
|
||||||
if (uri.scheme != 'file') return false;
|
if (uri.scheme != _fileScheme) return false;
|
||||||
|
|
||||||
final path = uri.pathSegments.fold('', (prev, v) => '$prev${pContext.separator}$v');
|
final path = uri.pathSegments.fold('', (prev, v) => '$prev${pContext.separator}$v');
|
||||||
return vaultDirectories.any(path.startsWith);
|
return vaultDirectories.any(path.startsWith);
|
||||||
|
@ -132,13 +138,47 @@ class Vaults extends ChangeNotifier {
|
||||||
_onLockStateChanged();
|
_onLockStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void unlock(String dirPath) {
|
Future<void> unlock(BuildContext context, String dirPath) async {
|
||||||
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return;
|
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return;
|
||||||
|
|
||||||
|
// recover untracked vault items
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
|
final newEntries = await recoverUntrackedItems(source, dirPath);
|
||||||
|
if (newEntries.isNotEmpty) {
|
||||||
|
source.addEntries(newEntries);
|
||||||
|
await metadataDb.saveEntries(newEntries);
|
||||||
|
unawaited(source.analyze(null, entries: newEntries));
|
||||||
|
}
|
||||||
|
|
||||||
_unlockedDirPaths.add(dirPath);
|
_unlockedDirPaths.add(dirPath);
|
||||||
_onLockStateChanged();
|
_onLockStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Set<AvesEntry>> recoverUntrackedItems(CollectionSource source, String dirPath) async {
|
||||||
|
final newEntries = <AvesEntry>{};
|
||||||
|
|
||||||
|
final vaultName = detailsForPath(dirPath)?.name;
|
||||||
|
if (vaultName == null) return newEntries;
|
||||||
|
|
||||||
|
final knownPaths = source.allEntries.where((v) => v.origin == EntryOrigins.vault && v.directory == dirPath).map((v) => v.path).whereNotNull().toSet();
|
||||||
|
final untrackedPaths = await storageService.getUntrackedVaultPaths(vaultName, knownPaths);
|
||||||
|
if (untrackedPaths.isNotEmpty) {
|
||||||
|
debugPrint('Recovering ${untrackedPaths.length} untracked vault items');
|
||||||
|
await Future.forEach(untrackedPaths, (untrackedPath) async {
|
||||||
|
final uri = Uri.file(untrackedPath).toString();
|
||||||
|
final sourceEntry = await mediaFetchService.getEntry(uri, null);
|
||||||
|
if (sourceEntry != null) {
|
||||||
|
sourceEntry.id = metadataDb.nextId;
|
||||||
|
sourceEntry.origin = EntryOrigins.vault;
|
||||||
|
newEntries.add(sourceEntry);
|
||||||
|
} else {
|
||||||
|
await reportService.recordError('Failed to recover untracked vault item at uri=$uri', null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newEntries;
|
||||||
|
}
|
||||||
|
|
||||||
void _onScreenOff() => lock(all.where((v) => v.autoLockScreenOff).map((v) => v.path).toSet());
|
void _onScreenOff() => lock(all.where((v) => v.autoLockScreenOff).map((v) => v.path).toSet());
|
||||||
|
|
||||||
void _onLockStateChanged() {
|
void _onLockStateChanged() {
|
||||||
|
|
|
@ -94,7 +94,7 @@ class Analyzer {
|
||||||
Analyzer() {
|
Analyzer() {
|
||||||
debugPrint('$runtimeType create');
|
debugPrint('$runtimeType create');
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$Analyzer',
|
className: '$Analyzer',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -107,7 +107,7 @@ class Analyzer {
|
||||||
void dispose() {
|
void dispose() {
|
||||||
debugPrint('$runtimeType dispose');
|
debugPrint('$runtimeType dispose');
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_stopUpdateTimer();
|
_stopUpdateTimer();
|
||||||
_controller?.dispose();
|
_controller?.dispose();
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/apps.dart';
|
import 'package:aves/model/apps.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
|
@ -7,6 +9,7 @@ import 'package:aves/utils/math_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:streams_channel/streams_channel.dart';
|
||||||
|
|
||||||
abstract class AppService {
|
abstract class AppService {
|
||||||
Future<Set<Package>> getPackages();
|
Future<Set<Package>> getPackages();
|
||||||
|
@ -15,7 +18,7 @@ abstract class AppService {
|
||||||
|
|
||||||
Future<bool> copyToClipboard(String uri, String? label);
|
Future<bool> copyToClipboard(String uri, String? label);
|
||||||
|
|
||||||
Future<bool> edit(String uri, String mimeType);
|
Future<Map<String, dynamic>> edit(String uri, String mimeType);
|
||||||
|
|
||||||
Future<bool> open(String uri, String mimeType, {required bool forceChooser});
|
Future<bool> open(String uri, String mimeType, {required bool forceChooser});
|
||||||
|
|
||||||
|
@ -32,6 +35,7 @@ abstract class AppService {
|
||||||
|
|
||||||
class PlatformAppService implements AppService {
|
class PlatformAppService implements AppService {
|
||||||
static const _platform = MethodChannel('deckers.thibault/aves/app');
|
static const _platform = MethodChannel('deckers.thibault/aves/app');
|
||||||
|
static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream');
|
||||||
|
|
||||||
static final _knownAppDirs = {
|
static final _knownAppDirs = {
|
||||||
'com.kakao.talk': {'KakaoTalkDownload'},
|
'com.kakao.talk': {'KakaoTalkDownload'},
|
||||||
|
@ -89,17 +93,29 @@ class PlatformAppService implements AppService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> edit(String uri, String mimeType) async {
|
Future<Map<String, dynamic>> edit(String uri, String mimeType) async {
|
||||||
try {
|
try {
|
||||||
final result = await _platform.invokeMethod('edit', <String, dynamic>{
|
final completer = Completer<Map?>();
|
||||||
|
_stream.receiveBroadcastStream(<String, dynamic>{
|
||||||
|
'op': 'edit',
|
||||||
'uri': uri,
|
'uri': uri,
|
||||||
'mimeType': mimeType,
|
'mimeType': mimeType,
|
||||||
});
|
}).listen(
|
||||||
if (result != null) return result as bool;
|
(data) => completer.complete(data as Map?),
|
||||||
|
onError: completer.completeError,
|
||||||
|
onDone: () {
|
||||||
|
if (!completer.isCompleted) completer.complete({'error': 'cancelled'});
|
||||||
|
},
|
||||||
|
cancelOnError: true,
|
||||||
|
);
|
||||||
|
// `await` here, so that `completeError` will be caught below
|
||||||
|
final result = await completer.future;
|
||||||
|
if (result == null) return {'error': 'cancelled'};
|
||||||
|
return result.cast<String, dynamic>();
|
||||||
} on PlatformException catch (e, stack) {
|
} on PlatformException catch (e, stack) {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
|
return {'error': e.code};
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -12,6 +12,10 @@ abstract class StorageService {
|
||||||
|
|
||||||
Future<Set<StorageVolume>> getStorageVolumes();
|
Future<Set<StorageVolume>> getStorageVolumes();
|
||||||
|
|
||||||
|
Future<Set<String>> getUntrackedTrashPaths(Iterable<String> knownPaths);
|
||||||
|
|
||||||
|
Future<Set<String>> getUntrackedVaultPaths(String vaultName, Iterable<String> knownPaths);
|
||||||
|
|
||||||
Future<String> getVaultRoot();
|
Future<String> getVaultRoot();
|
||||||
|
|
||||||
Future<int?> getFreeSpace(StorageVolume volume);
|
Future<int?> getFreeSpace(StorageVolume volume);
|
||||||
|
@ -71,6 +75,33 @@ class PlatformStorageService implements StorageService {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Set<String>> getUntrackedTrashPaths(Iterable<String> knownPaths) async {
|
||||||
|
try {
|
||||||
|
final result = await _platform.invokeMethod('getUntrackedTrashPaths', <String, dynamic>{
|
||||||
|
'knownPaths': knownPaths.toList(),
|
||||||
|
});
|
||||||
|
return (result as List).cast<String>().toSet();
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Set<String>> getUntrackedVaultPaths(String vaultName, Iterable<String> knownPaths) async {
|
||||||
|
try {
|
||||||
|
final result = await _platform.invokeMethod('getUntrackedVaultPaths', <String, dynamic>{
|
||||||
|
'vault': vaultName,
|
||||||
|
'knownPaths': knownPaths.toList(),
|
||||||
|
});
|
||||||
|
return (result as List).cast<String>().toSet();
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
await reportService.recordError(e, stack);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String> getVaultRoot() async {
|
Future<String> getVaultRoot() async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/widgets/aves_app.dart';
|
import 'package:aves/widgets/aves_app.dart';
|
||||||
import 'package:aves_utils/aves_utils.dart';
|
import 'package:aves_utils/aves_utils.dart';
|
||||||
|
@ -11,6 +10,8 @@ class Themes {
|
||||||
fontFeatures: [FontFeature.enable('smcp')],
|
fontFeatures: [FontFeature.enable('smcp')],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static String asButtonLabel(String s) => s.toUpperCase();
|
||||||
|
|
||||||
static TextStyle searchFieldStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;
|
static TextStyle searchFieldStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;
|
||||||
|
|
||||||
static Color overlayBackgroundColor({
|
static Color overlayBackgroundColor({
|
||||||
|
|
|
@ -20,7 +20,8 @@ class AndroidFileUtils {
|
||||||
static const mediaStoreUriRoot = '$contentScheme://$mediaStoreAuthority/';
|
static const mediaStoreUriRoot = '$contentScheme://$mediaStoreAuthority/';
|
||||||
static const mediaUriPathRoots = {'/$externalVolume/images/', '/$externalVolume/video/'};
|
static const mediaUriPathRoots = {'/$externalVolume/images/', '/$externalVolume/video/'};
|
||||||
|
|
||||||
static const String trashDirPath = '#trash';
|
static const recoveryDir = 'Lost & Found';
|
||||||
|
static const trashDirPath = '#trash';
|
||||||
|
|
||||||
late final String separator, vaultRoot, primaryStorage;
|
late final String separator, vaultRoot, primaryStorage;
|
||||||
late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath;
|
late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath;
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
||||||
import 'package:aves/app_flavor.dart';
|
import 'package:aves/app_flavor.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/sort.dart';
|
import 'package:aves/model/entry/sort.dart';
|
||||||
|
import 'package:aves/model/settings/enums/widget_outline.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/media_store_source.dart';
|
import 'package:aves/model/source/media_store_source.dart';
|
||||||
|
@ -20,6 +21,7 @@ void widgetMainCommon(AppFlavor flavor) async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
initPlatformServices();
|
initPlatformServices();
|
||||||
await settings.init(monitorPlatformSettings: false);
|
await settings.init(monitorPlatformSettings: false);
|
||||||
|
await reportService.init();
|
||||||
|
|
||||||
_widgetDrawChannel.setMethodCallHandler((call) async {
|
_widgetDrawChannel.setMethodCallHandler((call) async {
|
||||||
// widget settings may be modified in a different process after channel setup
|
// widget settings may be modified in a different process after channel setup
|
||||||
|
@ -41,6 +43,10 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
|
||||||
final devicePixelRatio = args['devicePixelRatio'] as double;
|
final devicePixelRatio = args['devicePixelRatio'] as double;
|
||||||
final drawEntryImage = args['drawEntryImage'] as bool;
|
final drawEntryImage = args['drawEntryImage'] as bool;
|
||||||
final reuseEntry = args['reuseEntry'] as bool;
|
final reuseEntry = args['reuseEntry'] as bool;
|
||||||
|
final isSystemThemeDark = args['isSystemThemeDark'] as bool;
|
||||||
|
|
||||||
|
final brightness = isSystemThemeDark ? Brightness.dark : Brightness.light;
|
||||||
|
final outline = await settings.getWidgetOutline(widgetId).color(brightness);
|
||||||
|
|
||||||
final entry = drawEntryImage ? await _getWidgetEntry(widgetId, reuseEntry) : null;
|
final entry = drawEntryImage ? await _getWidgetEntry(widgetId, reuseEntry) : null;
|
||||||
final painter = HomeWidgetPainter(
|
final painter = HomeWidgetPainter(
|
||||||
|
@ -50,7 +56,7 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
|
||||||
final bytes = await painter.drawWidget(
|
final bytes = await painter.drawWidget(
|
||||||
widthPx: widthPx,
|
widthPx: widthPx,
|
||||||
heightPx: heightPx,
|
heightPx: heightPx,
|
||||||
outline: settings.getWidgetOutline(widgetId),
|
outline: outline,
|
||||||
shape: settings.getWidgetShape(widgetId),
|
shape: settings.getWidgetShape(widgetId),
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
|
|
|
@ -18,6 +18,7 @@ import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/media/enums.dart';
|
import 'package:aves/services/media/enums.dart';
|
||||||
import 'package:aves/services/media/media_edit_service.dart';
|
import 'package:aves/services/media/media_edit_service.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/collection/collection_page.dart';
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
||||||
|
@ -250,8 +251,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||||
if (toBin) {
|
if (toBin) {
|
||||||
if (movedEntries.isNotEmpty) {
|
if (movedEntries.isNotEmpty) {
|
||||||
action = SnackBarAction(
|
action = SnackBarAction(
|
||||||
// TODO TLAD [l10n] key for "RESTORE"
|
label: Themes.asButtonLabel(l10n.entryActionRestore),
|
||||||
label: l10n.entryActionRestore.toUpperCase(),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (navigator != null) {
|
if (navigator != null) {
|
||||||
doMove(
|
doMove(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/view/view.dart';
|
import 'package:aves/view/view.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
@ -61,7 +62,7 @@ mixin PermissionAwareMixin {
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||||
// MD2 button labels were upper case but they are lower case in MD3
|
// MD2 button labels were upper case but they are lower case in MD3
|
||||||
child: Text(MaterialLocalizations.of(context).okButtonLabel.toUpperCase()),
|
child: Text(Themes.asButtonLabel(MaterialLocalizations.of(context).okButtonLabel)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,7 @@ import 'package:local_auth/error_codes.dart' as auth_error;
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
|
|
||||||
mixin VaultAwareMixin on FeedbackMixin {
|
mixin VaultAwareMixin on FeedbackMixin {
|
||||||
Future<bool> _tryUnlock(String dirPath, BuildContext context) async {
|
Future<bool> _tryUnlock(BuildContext context, String dirPath) async {
|
||||||
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return true;
|
if (!vaults.isVault(dirPath) || !vaults.isLocked(dirPath)) return true;
|
||||||
|
|
||||||
final details = vaults.detailsForPath(dirPath);
|
final details = vaults.detailsForPath(dirPath);
|
||||||
|
@ -67,12 +67,12 @@ mixin VaultAwareMixin on FeedbackMixin {
|
||||||
|
|
||||||
if (confirmed == null || !confirmed) return false;
|
if (confirmed == null || !confirmed) return false;
|
||||||
|
|
||||||
vaults.unlock(dirPath);
|
await vaults.unlock(context, dirPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> unlockAlbum(BuildContext context, String dirPath) async {
|
Future<bool> unlockAlbum(BuildContext context, String dirPath) async {
|
||||||
final success = await _tryUnlock(dirPath, context);
|
final success = await _tryUnlock(context, dirPath);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
|
showFeedback(context, FeedbackType.warn, context.l10n.genericFailureFeedback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/events.dart';
|
import 'package:aves/model/source/events.dart';
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class ColorIndicator extends StatelessWidget {
|
class ColorIndicator extends StatelessWidget {
|
||||||
final Color? value;
|
final Color? value;
|
||||||
|
final Color? alternate;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
static const double radius = 16;
|
static const double radius = 16;
|
||||||
|
@ -10,18 +11,33 @@ class ColorIndicator extends StatelessWidget {
|
||||||
const ColorIndicator({
|
const ColorIndicator({
|
||||||
super.key,
|
super.key,
|
||||||
required this.value,
|
required this.value,
|
||||||
|
this.alternate,
|
||||||
this.child,
|
this.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const dimension = radius * 2;
|
const dimension = radius * 2;
|
||||||
|
|
||||||
|
Gradient? gradient;
|
||||||
|
final _value = value;
|
||||||
|
final _alternate = alternate;
|
||||||
|
if (_value != null && _alternate != null && _alternate != _value) {
|
||||||
|
gradient = LinearGradient(
|
||||||
|
begin: AlignmentDirectional.topStart,
|
||||||
|
end: AlignmentDirectional.bottomEnd,
|
||||||
|
colors: [_value, _value, _alternate, _alternate],
|
||||||
|
stops: const [0, .5, .5, 1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: dimension,
|
height: dimension,
|
||||||
width: dimension,
|
width: dimension,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: value,
|
color: _value,
|
||||||
border: AvesBorder.border(context),
|
border: AvesBorder.border(context),
|
||||||
|
gradient: gradient,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
@ -13,7 +13,7 @@ class DoubleBackPopHandler {
|
||||||
|
|
||||||
DoubleBackPopHandler() {
|
DoubleBackPopHandler() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$DoubleBackPopHandler',
|
className: '$DoubleBackPopHandler',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -23,7 +23,7 @@ class DoubleBackPopHandler {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_stopBackTimer();
|
_stopBackTimer();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
|
|
|
@ -20,7 +20,7 @@ abstract class AvesSearchDelegate extends SearchDelegate {
|
||||||
required super.searchFieldStyle,
|
required super.searchFieldStyle,
|
||||||
}) {
|
}) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$AvesSearchDelegate',
|
className: '$AvesSearchDelegate',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -135,7 +135,7 @@ abstract class AvesSearchDelegate extends SearchDelegate {
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
currentBodyNotifier.dispose();
|
currentBodyNotifier.dispose();
|
||||||
queryTextController.dispose();
|
queryTextController.dispose();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
|
|
|
@ -28,7 +28,7 @@ class TileExtentController {
|
||||||
required this.horizontalPadding,
|
required this.horizontalPadding,
|
||||||
}) {
|
}) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$TileExtentController',
|
className: '$TileExtentController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -42,7 +42,7 @@ class TileExtentController {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -165,7 +166,7 @@ class CancelButton extends StatelessWidget {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: () => Navigator.maybeOf(context)?.pop(),
|
onPressed: () => Navigator.maybeOf(context)?.pop(),
|
||||||
// MD2 button labels were upper case but they are lower case in MD3
|
// MD2 button labels were upper case but they are lower case in MD3
|
||||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel.toUpperCase()),
|
child: Text(Themes.asButtonLabel(context.l10n.cancelTooltip)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -178,7 +179,7 @@ class OkButton extends StatelessWidget {
|
||||||
return TextButton(
|
return TextButton(
|
||||||
onPressed: () => Navigator.maybeOf(context)?.pop(),
|
onPressed: () => Navigator.maybeOf(context)?.pop(),
|
||||||
// MD2 button labels were upper case but they are lower case in MD3
|
// MD2 button labels were upper case but they are lower case in MD3
|
||||||
child: Text(MaterialLocalizations.of(context).okButtonLabel.toUpperCase()),
|
child: Text(Themes.asButtonLabel(MaterialLocalizations.of(context).okButtonLabel)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -301,7 +301,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
initialDate: _customDateTime,
|
initialDate: _customDateTime,
|
||||||
firstDate: DateTime(0),
|
firstDate: DateTime(0),
|
||||||
lastDate: DateTime(2100),
|
lastDate: DateTime(2100),
|
||||||
cancelText: MaterialLocalizations.of(context).cancelButtonLabel.toUpperCase(),
|
cancelText: Themes.asButtonLabel(context.l10n.cancelTooltip),
|
||||||
confirmText: context.l10n.nextButtonLabel,
|
confirmText: context.l10n.nextButtonLabel,
|
||||||
);
|
);
|
||||||
if (_date == null) return;
|
if (_date == null) return;
|
||||||
|
@ -309,7 +309,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
||||||
final _time = await showTimePicker(
|
final _time = await showTimePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialTime: TimeOfDay.fromDateTime(_customDateTime),
|
initialTime: TimeOfDay.fromDateTime(_customDateTime),
|
||||||
cancelText: MaterialLocalizations.of(context).cancelButtonLabel.toUpperCase(),
|
cancelText: Themes.asButtonLabel(context.l10n.cancelTooltip),
|
||||||
);
|
);
|
||||||
if (_time == null) return;
|
if (_time == null) return;
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
||||||
const CancelButton(),
|
const CancelButton(),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||||
child: Text(MaterialLocalizations.of(context).okButtonLabel.toUpperCase()),
|
child: Text(Themes.asButtonLabel(MaterialLocalizations.of(context).okButtonLabel)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -34,7 +34,7 @@ class TransformController {
|
||||||
|
|
||||||
TransformController(this.displaySize) {
|
TransformController(this.displaySize) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$TransformController',
|
className: '$TransformController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -46,7 +46,7 @@ class TransformController {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
aspectRatioNotifier.dispose();
|
aspectRatioNotifier.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,10 @@ class HomeWidgetPainter {
|
||||||
final AvesEntry? entry;
|
final AvesEntry? entry;
|
||||||
final double devicePixelRatio;
|
final double devicePixelRatio;
|
||||||
|
|
||||||
|
// do not use `AlignmentDirectional` as there is no `TextDirection` in context
|
||||||
static const backgroundGradient = LinearGradient(
|
static const backgroundGradient = LinearGradient(
|
||||||
begin: AlignmentDirectional.bottomStart,
|
begin: Alignment.bottomLeft,
|
||||||
end: AlignmentDirectional.topEnd,
|
end: Alignment.topRight,
|
||||||
colors: AColors.boraBoraGradient,
|
colors: AColors.boraBoraGradient,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/trash.dart';
|
import 'package:aves/model/filters/trash.dart';
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
|
|
@ -36,7 +36,7 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DragTarget<T>(
|
return DragTarget<T>(
|
||||||
onWillAccept: (data) {
|
onWillAcceptWithDetails: (details) {
|
||||||
if (draggedQuickAction.value != null) {
|
if (draggedQuickAction.value != null) {
|
||||||
_setPanelHighlight(true);
|
_setPanelHighlight(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class QuickActionButton<T extends Object> extends StatelessWidget {
|
||||||
|
|
||||||
DragTarget<T> _buildDragTarget(Widget? child) {
|
DragTarget<T> _buildDragTarget(Widget? child) {
|
||||||
return DragTarget<T>(
|
return DragTarget<T>(
|
||||||
onWillAccept: (data) {
|
onWillAcceptWithDetails: (details) {
|
||||||
if (draggedQuickAction.value != null) {
|
if (draggedQuickAction.value != null) {
|
||||||
insertAction(draggedQuickAction.value!, placement, action);
|
insertAction(draggedQuickAction.value!, placement, action);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/settings/enums/widget_outline.dart';
|
||||||
import 'package:aves/model/settings/enums/widget_shape.dart';
|
import 'package:aves/model/settings/enums/widget_shape.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/widget_service.dart';
|
import 'package:aves/services/widget_service.dart';
|
||||||
|
@ -33,10 +35,11 @@ class HomeWidgetSettingsPage extends StatefulWidget {
|
||||||
|
|
||||||
class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||||
late WidgetShape _shape;
|
late WidgetShape _shape;
|
||||||
late Color? _outline;
|
late WidgetOutline _outline;
|
||||||
late WidgetOpenPage _openPage;
|
late WidgetOpenPage _openPage;
|
||||||
late WidgetDisplayedItem _displayedItem;
|
late WidgetDisplayedItem _displayedItem;
|
||||||
late Set<CollectionFilter> _collectionFilters;
|
late Set<CollectionFilter> _collectionFilters;
|
||||||
|
Future<Map<Brightness, Map<WidgetOutline, Color?>>> _outlineColorsByBrightness = Future.value({});
|
||||||
|
|
||||||
int get widgetId => widget.widgetId;
|
int get widgetId => widget.widgetId;
|
||||||
|
|
||||||
|
@ -61,6 +64,24 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||||
_openPage = settings.getWidgetOpenPage(widgetId);
|
_openPage = settings.getWidgetOpenPage(widgetId);
|
||||||
_displayedItem = settings.getWidgetDisplayedItem(widgetId);
|
_displayedItem = settings.getWidgetDisplayedItem(widgetId);
|
||||||
_collectionFilters = settings.getWidgetCollectionFilters(widgetId);
|
_collectionFilters = settings.getWidgetCollectionFilters(widgetId);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOutlineColors());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateOutlineColors() {
|
||||||
|
_outlineColorsByBrightness = _loadOutlineColors();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<Brightness, Map<WidgetOutline, Color?>>> _loadOutlineColors() async {
|
||||||
|
final byBrightness = <Brightness, Map<WidgetOutline, Color?>>{};
|
||||||
|
await Future.forEach(Brightness.values, (brightness) async {
|
||||||
|
final byOutline = <WidgetOutline, Color?>{};
|
||||||
|
await Future.forEach(WidgetOutline.values, (outline) async {
|
||||||
|
byOutline[outline] = await outline.color(brightness);
|
||||||
|
});
|
||||||
|
byBrightness[brightness] = byOutline;
|
||||||
|
});
|
||||||
|
return byBrightness;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -71,17 +92,27 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||||
title: Text(l10n.settingsWidgetPageTitle),
|
title: Text(l10n.settingsWidgetPageTitle),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Column(
|
child: FutureBuilder<Map<Brightness, Map<WidgetOutline, Color?>>>(
|
||||||
|
future: _outlineColorsByBrightness,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final outlineColorsByBrightness = snapshot.data;
|
||||||
|
if (outlineColorsByBrightness == null) return const SizedBox();
|
||||||
|
|
||||||
|
final effectiveOutlineColors = outlineColorsByBrightness[Theme.of(context).brightness];
|
||||||
|
if (effectiveOutlineColors == null) return const SizedBox();
|
||||||
|
|
||||||
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildShapeSelector(),
|
_buildShapeSelector(effectiveOutlineColors),
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(l10n.settingsWidgetShowOutline),
|
title: Text(l10n.settingsWidgetShowOutline),
|
||||||
trailing: HomeWidgetOutlineSelector(
|
trailing: HomeWidgetOutlineSelector(
|
||||||
getter: () => _outline,
|
getter: () => _outline,
|
||||||
setter: (v) => setState(() => _outline = v),
|
setter: (v) => setState(() => _outline = v),
|
||||||
|
outlineColorsByBrightness: outlineColorsByBrightness,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SettingsSelectionListTile<WidgetOpenPage>(
|
SettingsSelectionListTile<WidgetOpenPage>(
|
||||||
|
@ -117,12 +148,14 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildShapeSelector() {
|
Widget _buildShapeSelector(Map<WidgetOutline, Color?> outlineColors) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
|
@ -143,7 +176,7 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||||
height: 124,
|
height: 124,
|
||||||
decoration: ShapeDecoration(
|
decoration: ShapeDecoration(
|
||||||
gradient: selected ? gradient : deselectedGradient,
|
gradient: selected ? gradient : deselectedGradient,
|
||||||
shape: _WidgetShapeBorder(_outline, shape),
|
shape: _WidgetShapeBorder(_outline, shape, outlineColors),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -169,12 +202,13 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WidgetShapeBorder extends ShapeBorder {
|
class _WidgetShapeBorder extends ShapeBorder {
|
||||||
final Color? outline;
|
final WidgetOutline outline;
|
||||||
final WidgetShape shape;
|
final WidgetShape shape;
|
||||||
|
final Map<WidgetOutline, Color?> outlineColors;
|
||||||
|
|
||||||
static const _devicePixelRatio = 1.0;
|
static const _devicePixelRatio = 1.0;
|
||||||
|
|
||||||
const _WidgetShapeBorder(this.outline, this.shape);
|
const _WidgetShapeBorder(this.outline, this.shape, this.outlineColors);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
|
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
|
||||||
|
@ -191,10 +225,11 @@ class _WidgetShapeBorder extends ShapeBorder {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||||
if (outline != null) {
|
final outlineColor = outlineColors[outline];
|
||||||
|
if (outlineColor != null) {
|
||||||
final path = shape.path(rect.size, _devicePixelRatio);
|
final path = shape.path(rect.size, _devicePixelRatio);
|
||||||
canvas.clipPath(path);
|
canvas.clipPath(path);
|
||||||
HomeWidgetPainter.drawOutline(canvas, path, _devicePixelRatio, outline!);
|
HomeWidgetPainter.drawOutline(canvas, path, _devicePixelRatio, outlineColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,13 +238,15 @@ class _WidgetShapeBorder extends ShapeBorder {
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeWidgetOutlineSelector extends StatefulWidget {
|
class HomeWidgetOutlineSelector extends StatefulWidget {
|
||||||
final ValueGetter<Color?> getter;
|
final ValueGetter<WidgetOutline> getter;
|
||||||
final ValueSetter<Color?> setter;
|
final ValueSetter<WidgetOutline> setter;
|
||||||
|
final Map<Brightness, Map<WidgetOutline, Color?>> outlineColorsByBrightness;
|
||||||
|
|
||||||
const HomeWidgetOutlineSelector({
|
const HomeWidgetOutlineSelector({
|
||||||
super.key,
|
super.key,
|
||||||
required this.getter,
|
required this.getter,
|
||||||
required this.setter,
|
required this.setter,
|
||||||
|
required this.outlineColorsByBrightness,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -217,35 +254,40 @@ class HomeWidgetOutlineSelector extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> {
|
class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> {
|
||||||
static const List<Color?> options = [
|
|
||||||
null,
|
|
||||||
Colors.black,
|
|
||||||
Colors.white,
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return DropdownButtonHideUnderline(
|
return DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<Color?>(
|
child: DropdownButton<WidgetOutline>(
|
||||||
items: _buildItems(context),
|
items: _buildItems(context),
|
||||||
value: widget.getter(),
|
value: widget.getter(),
|
||||||
onChanged: (selected) {
|
onChanged: (selected) {
|
||||||
widget.setter(selected);
|
widget.setter(selected ?? WidgetOutline.none);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DropdownMenuItem<Color?>> _buildItems(BuildContext context) {
|
List<DropdownMenuItem<WidgetOutline>> _buildItems(BuildContext context) {
|
||||||
return options.map((selected) {
|
return supportedWidgetOutlines.map((selected) {
|
||||||
return DropdownMenuItem<Color?>(
|
final lightColors = widget.outlineColorsByBrightness[Brightness.light];
|
||||||
|
final darkColors = widget.outlineColorsByBrightness[Brightness.dark];
|
||||||
|
return DropdownMenuItem<WidgetOutline>(
|
||||||
value: selected,
|
value: selected,
|
||||||
child: ColorIndicator(
|
child: ColorIndicator(
|
||||||
value: selected,
|
value: lightColors?[selected],
|
||||||
child: selected == null ? const Icon(AIcons.clear) : null,
|
alternate: darkColors?[selected],
|
||||||
|
child: lightColors?[selected] == null ? const Icon(AIcons.clear) : null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<WidgetOutline> get supportedWidgetOutlines => [
|
||||||
|
WidgetOutline.none,
|
||||||
|
WidgetOutline.black,
|
||||||
|
WidgetOutline.white,
|
||||||
|
WidgetOutline.systemBlackAndWhite,
|
||||||
|
if (device.isDynamicColorAvailable) WidgetOutline.systemDynamic,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ class SupportedLocales {
|
||||||
static const languagesByLanguageCode = {
|
static const languagesByLanguageCode = {
|
||||||
'ar': 'العربية',
|
'ar': 'العربية',
|
||||||
'be': 'Беларуская мова',
|
'be': 'Беларуская мова',
|
||||||
|
'ca': 'Català',
|
||||||
'cs': 'Čeština',
|
'cs': 'Čeština',
|
||||||
'de': 'Deutsch',
|
'de': 'Deutsch',
|
||||||
'el': 'Ελληνικά',
|
'el': 'Ελληνικά',
|
||||||
|
|
|
@ -16,6 +16,7 @@ import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/vaults/vaults.dart';
|
import 'package:aves/model/vaults/vaults.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/widgets/collection/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/entry_storage.dart';
|
import 'package:aves/widgets/common/action_mixins/entry_storage.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
import 'package:aves/widgets/common/action_mixins/feedback.dart';
|
||||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||||
|
@ -242,8 +243,15 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
).dispatch(context);
|
).dispatch(context);
|
||||||
}
|
}
|
||||||
case EntryAction.edit:
|
case EntryAction.edit:
|
||||||
appService.edit(targetEntry.uri, targetEntry.mimeType).then((success) {
|
appService.edit(targetEntry.uri, targetEntry.mimeType).then((fields) async {
|
||||||
if (!success) showNoMatchingAppDialog(context);
|
final error = fields['error'] as String?;
|
||||||
|
if (error == null) {
|
||||||
|
final resultUri = fields['uri'] as String?;
|
||||||
|
final mimeType = fields['mimeType'] as String?;
|
||||||
|
await _handleEditResult(context, resultUri, mimeType);
|
||||||
|
} else if (error == 'edit-resolve') {
|
||||||
|
await showNoMatchingAppDialog(context);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
case EntryAction.open:
|
case EntryAction.open:
|
||||||
appService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) {
|
appService.open(targetEntry.uri, targetEntry.mimeTypeAnySubtype, forceChooser: true).then((success) {
|
||||||
|
@ -280,6 +288,42 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleEditResult(BuildContext context, String? resultUri, String? mimeType) async {
|
||||||
|
final _collection = collection;
|
||||||
|
if (_collection == null || resultUri == null) return;
|
||||||
|
|
||||||
|
final editedEntry = await mediaFetchService.getEntry(resultUri, mimeType);
|
||||||
|
if (editedEntry == null) return;
|
||||||
|
|
||||||
|
final editedUri = editedEntry.uri;
|
||||||
|
final matchCurrentFilters = _collection.filters.every((filter) => filter.test(editedEntry));
|
||||||
|
|
||||||
|
final l10n = context.l10n;
|
||||||
|
// get navigator beforehand because
|
||||||
|
// local context may be deactivated when action is triggered after navigation
|
||||||
|
final navigator = Navigator.maybeOf(context);
|
||||||
|
final showAction = SnackBarAction(
|
||||||
|
label: l10n.showButtonLabel,
|
||||||
|
onPressed: () {
|
||||||
|
if (navigator != null) {
|
||||||
|
final source = _collection.source;
|
||||||
|
navigator.pushAndRemoveUntil(
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: const RouteSettings(name: CollectionPage.routeName),
|
||||||
|
builder: (context) => CollectionPage(
|
||||||
|
source: source,
|
||||||
|
filters: matchCurrentFilters ? _collection.filters : {},
|
||||||
|
highlightTest: (entry) => entry.uri == editedUri,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(route) => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
showFeedback(context, FeedbackType.info, l10n.genericSuccessFeedback, showAction);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> quickMove(BuildContext context, String album, {required bool copy}) async {
|
Future<void> quickMove(BuildContext context, String album, {required bool copy}) async {
|
||||||
if (!await unlockAlbum(context, album)) return;
|
if (!await unlockAlbum(context, album)) return;
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
required this.collection,
|
required this.collection,
|
||||||
}) {
|
}) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$VideoActionDelegate',
|
className: '$VideoActionDelegate',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -45,7 +45,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
stopOverlayHidingTimer();
|
stopOverlayHidingTimer();
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ class ViewerController with CastMixin {
|
||||||
this.autopilotAnimatedZoom = false,
|
this.autopilotAnimatedZoom = false,
|
||||||
}) {
|
}) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$ViewerController',
|
className: '$ViewerController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -67,7 +67,7 @@ class ViewerController with CastMixin {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
entryNotifier.removeListener(_onEntryChanged);
|
entryNotifier.removeListener(_onEntryChanged);
|
||||||
windowService.setHdrColorMode(false);
|
windowService.setHdrColorMode(false);
|
||||||
|
|
|
@ -2,11 +2,13 @@ import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/metadata/address.dart';
|
import 'package:aves/model/metadata/address.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/model/metadata/trash.dart';
|
import 'package:aves/model/metadata/trash.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/video_playback.dart';
|
import 'package:aves/model/video_playback.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class DbTab extends StatefulWidget {
|
class DbTab extends StatefulWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
|
@ -83,7 +85,14 @@ class _DbTabState extends State<DbTab> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('DB entry:${data == null ? ' no row' : ''}'),
|
Text('DB entry:${data == null ? ' no row' : ''}'),
|
||||||
if (data != null)
|
if (data != null) ...[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final source = context.read<CollectionSource>();
|
||||||
|
await source.removeEntries({entry.uri}, includeTrash: true);
|
||||||
|
},
|
||||||
|
child: const Text('Untrack entry'),
|
||||||
|
),
|
||||||
InfoRowGroup(
|
InfoRowGroup(
|
||||||
info: {
|
info: {
|
||||||
'uri': data.uri,
|
'uri': data.uri,
|
||||||
|
@ -102,6 +111,7 @@ class _DbTabState extends State<DbTab> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -170,7 +180,15 @@ class _DbTabState extends State<DbTab> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('DB trash details:${data == null ? ' no row' : ''}'),
|
Text('DB trash details:${data == null ? ' no row' : ''}'),
|
||||||
if (data != null)
|
if (data != null) ...[
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
entry.trashDetails = null;
|
||||||
|
await metadataDb.updateTrash(entry.id, entry.trashDetails);
|
||||||
|
_loadDatabase();
|
||||||
|
},
|
||||||
|
child: const Text('Remove details'),
|
||||||
|
),
|
||||||
InfoRowGroup(
|
InfoRowGroup(
|
||||||
info: {
|
info: {
|
||||||
'dateMillis': '${data.dateMillis}',
|
'dateMillis': '${data.dateMillis}',
|
||||||
|
@ -178,6 +196,7 @@ class _DbTabState extends State<DbTab> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -12,7 +12,7 @@ class MultiPageConductor {
|
||||||
|
|
||||||
MultiPageConductor() {
|
MultiPageConductor() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$MultiPageConductor',
|
className: '$MultiPageConductor',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -22,7 +22,7 @@ class MultiPageConductor {
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
await _disposeAll();
|
await _disposeAll();
|
||||||
_controllers.clear();
|
_controllers.clear();
|
||||||
|
|
|
@ -25,7 +25,7 @@ class MultiPageController {
|
||||||
|
|
||||||
MultiPageController(this.entry) {
|
MultiPageController(this.entry) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$MultiPageController',
|
className: '$MultiPageController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -48,7 +48,7 @@ class MultiPageController {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_info?.dispose();
|
_info?.dispose();
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|
|
@ -21,7 +21,7 @@ class VideoConductor {
|
||||||
|
|
||||||
VideoConductor({CollectionLens? collection}) : _collection = collection {
|
VideoConductor({CollectionLens? collection}) : _collection = collection {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$VideoConductor',
|
className: '$VideoConductor',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -31,7 +31,7 @@ class VideoConductor {
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_subscriptions
|
_subscriptions
|
||||||
..forEach((sub) => sub.cancel())
|
..forEach((sub) => sub.cancel())
|
||||||
|
|
|
@ -14,7 +14,7 @@ class ViewStateConductor {
|
||||||
|
|
||||||
ViewStateConductor() {
|
ViewStateConductor() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$ViewStateConductor',
|
className: '$ViewStateConductor',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -24,7 +24,7 @@ class ViewStateConductor {
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_controllers.forEach((v) => v.dispose());
|
_controllers.forEach((v) => v.dispose());
|
||||||
_controllers.clear();
|
_controllers.clear();
|
||||||
|
|
|
@ -16,7 +16,7 @@ class ViewStateController with HistogramMixin {
|
||||||
required this.viewStateNotifier,
|
required this.viewStateNotifier,
|
||||||
}) {
|
}) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$ViewStateController',
|
className: '$ViewStateController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -26,7 +26,7 @@ class ViewStateController with HistogramMixin {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
viewStateNotifier.dispose();
|
viewStateNotifier.dispose();
|
||||||
fullImageNotifier.dispose();
|
fullImageNotifier.dispose();
|
||||||
|
|
|
@ -21,7 +21,7 @@ class AvesMagnifierController {
|
||||||
MagnifierState? initialState,
|
MagnifierState? initialState,
|
||||||
}) : super() {
|
}) : super() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$AvesMagnifierController',
|
className: '$AvesMagnifierController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -41,7 +41,7 @@ class AvesMagnifierController {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_stateStreamController.close();
|
_stateStreamController.close();
|
||||||
|
|
|
@ -57,18 +57,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.8.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.11.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -98,14 +98,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: web
|
|
||||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.0"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
flutter: ">=1.16.0"
|
flutter: ">=1.16.0"
|
||||||
|
|
|
@ -3,7 +3,7 @@ version: 0.0.1
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
@ -19,7 +19,7 @@ class AvesMapController {
|
||||||
|
|
||||||
AvesMapController() {
|
AvesMapController() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectCreated(
|
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||||
library: 'aves',
|
library: 'aves',
|
||||||
className: '$AvesMapController',
|
className: '$AvesMapController',
|
||||||
object: this,
|
object: this,
|
||||||
|
@ -29,7 +29,7 @@ class AvesMapController {
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) {
|
if (kFlutterMemoryAllocationsEnabled) {
|
||||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||||
}
|
}
|
||||||
_streamController.close();
|
_streamController.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,10 +89,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
|
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.1"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -145,18 +145,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.8.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.11.0"
|
||||||
mgrs_dart:
|
mgrs_dart:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -262,10 +262,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.5.0"
|
||||||
wkt_parser:
|
wkt_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -275,5 +275,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
flutter: ">=3.10.0"
|
flutter: ">=3.10.0"
|
||||||
|
|
|
@ -3,7 +3,7 @@ version: 0.0.1
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
@ -48,4 +48,6 @@ enum WidgetDisplayedItem { random, mostRecent }
|
||||||
|
|
||||||
enum WidgetOpenPage { home, collection, viewer, updateWidget }
|
enum WidgetOpenPage { home, collection, viewer, updateWidget }
|
||||||
|
|
||||||
|
enum WidgetOutline { none, black, white, systemBlackAndWhite, systemDynamic }
|
||||||
|
|
||||||
enum WidgetShape { rrect, circle, heart }
|
enum WidgetShape { rrect, circle, heart }
|
||||||
|
|
|
@ -50,18 +50,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.8.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.11.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -75,13 +75,5 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: web
|
|
||||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.3.0"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.2.0 <4.0.0"
|
dart: ">=3.3.0 <4.0.0"
|
||||||
|
|
|
@ -3,7 +3,7 @@ version: 0.0.1
|
||||||
publish_to: none
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.2.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|