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
|
||||
steps:
|
||||
- name: Clone the repository.
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get packages for the Flutter project.
|
||||
run: scripts/pub_get_all.sh
|
||||
|
|
2
.github/workflows/release.yml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
java-version: '17'
|
||||
|
||||
- name: Clone the repository.
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get packages for the Flutter project.
|
||||
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="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
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -214,7 +214,6 @@ dependencies {
|
|||
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
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.media:media:1.7.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
|
|
|
@ -319,7 +319,7 @@
|
|||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
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 -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
|
|
|
@ -87,6 +87,8 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
|
||||
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)
|
||||
val messenger = flutterEngine!!.dartExecutor
|
||||
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
|
||||
|
@ -101,6 +103,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
|
|||
"devicePixelRatio" to getDevicePixelRatio(),
|
||||
"drawEntryImage" to drawEntryImage,
|
||||
"reuseEntry" to reuseEntry,
|
||||
"isSystemThemeDark" to isNightModeOn,
|
||||
), object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
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.WindowHandler
|
||||
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.isSoftwareRenderingRequired
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
|
@ -218,6 +219,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.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) }
|
||||
}
|
||||
|
||||
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?) {
|
||||
val treeUri = intent?.data
|
||||
if (resultCode != RESULT_OK || treeUri == null) {
|
||||
|
@ -458,6 +468,7 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
const val DELETE_SINGLE_PERMISSION_REQUEST = 5
|
||||
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
|
||||
const val PICK_COLLECTION_FILTERS_REQUEST = 7
|
||||
const val EDIT_REQUEST = 8
|
||||
|
||||
const val INTENT_ACTION_EDIT = "edit"
|
||||
const val INTENT_ACTION_PICK_ITEMS = "pick_items"
|
||||
|
@ -493,6 +504,8 @@ open class MainActivity : FlutterFragmentActivity() {
|
|||
|
||||
var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null
|
||||
|
||||
var pendingEditIntentHandler: ((fields: FieldMap?) -> Unit)? = null
|
||||
|
||||
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
|
||||
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
|
||||
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return
|
||||
|
|
|
@ -52,7 +52,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
"getPackages" -> ioScope.launch { safe(call, result, ::getPackages) }
|
||||
"getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) }
|
||||
"copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) }
|
||||
"edit" -> safe(call, result, ::edit)
|
||||
"open" -> safe(call, result, ::open)
|
||||
"openMap" -> safe(call, result, ::openMap)
|
||||
"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) {
|
||||
val title = call.argument<String>("title")
|
||||
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
|
||||
// so we use a joined `String` as fallback
|
||||
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
|
||||
|
||||
else -> {
|
||||
result.error("pin-intent", "failed to build intent", null)
|
||||
return
|
||||
|
@ -434,6 +418,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
|||
FileProvider.getUriForFile(context, authority, File(path))
|
||||
}
|
||||
}
|
||||
|
||||
else -> uri
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,8 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
when (call.method) {
|
||||
"getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) }
|
||||
"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) }
|
||||
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
|
||||
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
|
||||
|
@ -125,6 +127,35 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
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) {
|
||||
result.success(StorageUtils.getVaultRoot(context))
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.os.Looper
|
|||
import android.util.Log
|
||||
import deckers.thibault.aves.MainActivity
|
||||
import deckers.thibault.aves.PendingStorageAccessResultHandler
|
||||
import deckers.thibault.aves.channel.calls.AppAdapterHandler
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.PermissionManager
|
||||
|
@ -47,6 +48,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
|
||||
"createFile" -> ioScope.launch { createFile() }
|
||||
"openFile" -> ioScope.launch { openFile() }
|
||||
"edit" -> edit()
|
||||
"pickCollectionFilters" -> pickCollectionFilters()
|
||||
else -> endOfStream()
|
||||
}
|
||||
|
@ -100,10 +102,13 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
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) {
|
||||
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 {
|
||||
MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
|
||||
onDenied()
|
||||
|
@ -144,7 +149,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
type = mimeType
|
||||
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() {
|
||||
|
@ -177,7 +182,33 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
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() {
|
||||
|
@ -192,6 +223,24 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
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?) {
|
||||
Log.i(LOG_TAG, "onCancel arguments=$arguments")
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ object SafePngMetadataReader {
|
|||
// Only compression method allowed by the spec is zero: deflate
|
||||
if (compressionMethod.toInt() == 0) {
|
||||
// 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 compressedProfile = reader.getBytes(bytesLeft)
|
||||
try {
|
||||
|
|
|
@ -16,8 +16,16 @@ internal class FileImageProvider : ImageProvider() {
|
|||
var mimeType = sourceMimeType
|
||||
|
||||
if (mimeType == null) {
|
||||
val extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
||||
if (extension != null) {
|
||||
var extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
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="app_name">ऐवीज</string>
|
||||
<string name="videos_shortcut_short_label">वीडियो</string>
|
||||
<string name="safe_mode_shortcut_short_label">सेफ मोड</string>
|
||||
</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 {
|
||||
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": {},
|
||||
"welcomeMessage": "مرحبا بكم في Aves",
|
||||
"@welcomeMessage": {},
|
||||
"applyButtonLabel": "تقديم",
|
||||
"applyButtonLabel": "تأكيد",
|
||||
"@applyButtonLabel": {},
|
||||
"nextButtonLabel": "التالي",
|
||||
"@nextButtonLabel": {},
|
||||
|
@ -103,7 +103,7 @@
|
|||
"@pickTooltip": {},
|
||||
"chipActionGoToCountryPage": "عرض في الدول",
|
||||
"@chipActionGoToCountryPage": {},
|
||||
"applyTooltip": "تقدم",
|
||||
"applyTooltip": "تأكيد",
|
||||
"@applyTooltip": {},
|
||||
"chipActionUnpin": "إلغاء التثبيت في الأعلى",
|
||||
"@chipActionUnpin": {},
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"@chipActionGoToTagPage": {},
|
||||
"chipActionLock": "Заблакаваць",
|
||||
"@chipActionLock": {},
|
||||
"chipActionSetCover": "Усталяваць вокладку",
|
||||
"chipActionSetCover": "Ўсталяваць вокладку",
|
||||
"@chipActionSetCover": {},
|
||||
"chipActionRename": "Перайменаваць",
|
||||
"@chipActionRename": {},
|
||||
|
@ -124,11 +124,11 @@
|
|||
"@entryActionConvertMotionPhotoToStillImage": {},
|
||||
"entryActionViewMotionPhotoVideo": "Адкрыць відэа",
|
||||
"@entryActionViewMotionPhotoVideo": {},
|
||||
"entryActionSetAs": "Усталяваць як",
|
||||
"entryActionSetAs": "Ўсталяваць як",
|
||||
"@entryActionSetAs": {},
|
||||
"entryActionAddFavourite": "Дадаць у абранае",
|
||||
"@entryActionAddFavourite": {},
|
||||
"videoActionUnmute": "Уключыць гук",
|
||||
"videoActionUnmute": "Ўключыць гук",
|
||||
"@videoActionUnmute": {},
|
||||
"videoActionCaptureFrame": "Захоп кадра",
|
||||
"@videoActionCaptureFrame": {},
|
||||
|
@ -449,7 +449,7 @@
|
|||
"@wallpaperTargetHomeLock": {},
|
||||
"widgetTapUpdateWidget": "Абнавіць віджэт",
|
||||
"@widgetTapUpdateWidget": {},
|
||||
"storageVolumeDescriptionFallbackPrimary": "Унутраная памяць",
|
||||
"storageVolumeDescriptionFallbackPrimary": "Ўнутраная памяць",
|
||||
"@storageVolumeDescriptionFallbackPrimary": {},
|
||||
"restrictedAccessDialogMessage": "Гэтай праграме забаронена змяняць файлы ў {directory} «{volume}».\n\nКаб перамясціць элементы ў іншую дырэкторыю, выкарыстоўвайце папярэдне ўсталяваны дыспетчар файлаў або праграму галерэі.",
|
||||
"@restrictedAccessDialogMessage": {
|
||||
|
@ -465,7 +465,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"missingSystemFilePickerDialogMessage": "Сродак выбару сістэмных файлаў адсутнічае або адключаны. Уключыце яго і паўтарыце спробу.",
|
||||
"missingSystemFilePickerDialogMessage": "Сродак выбару сістэмных файлаў адсутнічае або адключаны. Ўключыце яго і паўтарыце спробу.",
|
||||
"@missingSystemFilePickerDialogMessage": {},
|
||||
"unsupportedTypeDialogMessage": "{count, plural, =1{Гэта аперацыя не падтрымліваецца для элементаў наступнага тыпу: {types}.} other{Гэта аперацыя не падтрымліваецца для элементаў наступных тыпаў: {types}.}}",
|
||||
"@unsupportedTypeDialogMessage": {
|
||||
|
@ -517,15 +517,15 @@
|
|||
"@configureVaultDialogTitle": {},
|
||||
"vaultDialogLockTypeLabel": "Тып блакіроўкі",
|
||||
"@vaultDialogLockTypeLabel": {},
|
||||
"pinDialogEnter": "Увядзіце PIN-код",
|
||||
"pinDialogEnter": "Ўвядзіце PIN-код",
|
||||
"@pinDialogEnter": {},
|
||||
"patternDialogEnter": "Увядзіце графічны ключ",
|
||||
"patternDialogEnter": "Ўвядзіце графічны ключ",
|
||||
"@patternDialogEnter": {},
|
||||
"patternDialogConfirm": "Пацвердзіце графічны ключ",
|
||||
"@patternDialogConfirm": {},
|
||||
"pinDialogConfirm": "Пацвердзіце PIN-код",
|
||||
"@pinDialogConfirm": {},
|
||||
"passwordDialogEnter": "Увядзіце пароль",
|
||||
"passwordDialogEnter": "Ўвядзіце пароль",
|
||||
"@passwordDialogEnter": {},
|
||||
"passwordDialogConfirm": "Пацвердзіце пароль",
|
||||
"@passwordDialogConfirm": {},
|
||||
|
@ -577,7 +577,7 @@
|
|||
"@sourceViewerPageTitle": {},
|
||||
"panoramaDisableSensorControl": "Адключыць сэнсарнае кіраванне",
|
||||
"@panoramaDisableSensorControl": {},
|
||||
"panoramaEnableSensorControl": "Уключыць сэнсарнае кіраванне",
|
||||
"panoramaEnableSensorControl": "Ўключыць сэнсарнае кіраванне",
|
||||
"@panoramaEnableSensorControl": {},
|
||||
"tagPlaceholderPlace": "Месца",
|
||||
"@tagPlaceholderPlace": {},
|
||||
|
@ -601,7 +601,7 @@
|
|||
"@videoControlsNone": {},
|
||||
"viewerErrorUnknown": "Ой!",
|
||||
"@viewerErrorUnknown": {},
|
||||
"viewerSetWallpaperButtonLabel": "УСТАНАВІЦЬ ШПАЛЕРЫ",
|
||||
"viewerSetWallpaperButtonLabel": "ЎСТАНАВІЦЬ ШПАЛЕРЫ",
|
||||
"@viewerSetWallpaperButtonLabel": {},
|
||||
"statsTopAlbumsSectionTitle": "Лепшыя альбомы",
|
||||
"@statsTopAlbumsSectionTitle": {},
|
||||
|
@ -819,7 +819,7 @@
|
|||
"@aboutDataUsageMisc": {},
|
||||
"albumVideoCaptures": "Відэазапісы",
|
||||
"@albumVideoCaptures": {},
|
||||
"editEntryDateDialogSetCustom": "Усталяваць карыстацкую дату",
|
||||
"editEntryDateDialogSetCustom": "Ўсталяваць карыстацкую дату",
|
||||
"@editEntryDateDialogSetCustom": {},
|
||||
"settingsSearchEmpty": "Няма адпаведнай налады",
|
||||
"@settingsSearchEmpty": {},
|
||||
|
@ -845,7 +845,7 @@
|
|||
"@collectionSelectSectionTooltip": {},
|
||||
"aboutLicensesBanner": "Гэта праграма выкарыстоўвае наступныя пакеты і бібліятэкі з адкрытым зыходным кодам.",
|
||||
"@aboutLicensesBanner": {},
|
||||
"dateYesterday": "Учора",
|
||||
"dateYesterday": "Ўчора",
|
||||
"@dateYesterday": {},
|
||||
"aboutDataUsageDatabase": "База дадзеных",
|
||||
"@aboutDataUsageDatabase": {},
|
||||
|
@ -879,7 +879,7 @@
|
|||
"@videoStreamSelectionDialogAudio": {},
|
||||
"videoSpeedDialogLabel": "Хуткасць прайгравання",
|
||||
"@videoSpeedDialogLabel": {},
|
||||
"editEntryLocationDialogSetCustom": "Устанавіць карыстацкае месцазнаходжанне",
|
||||
"editEntryLocationDialogSetCustom": "Ўстанавіць карыстацкае месцазнаходжанне",
|
||||
"@editEntryLocationDialogSetCustom": {},
|
||||
"placeEmpty": "Няма месцаў",
|
||||
"@placeEmpty": {},
|
||||
|
@ -1141,7 +1141,7 @@
|
|||
"@genericFailureFeedback": {},
|
||||
"aboutDataUsageData": "Дадзеныя",
|
||||
"@aboutDataUsageData": {},
|
||||
"aboutDataUsageInternal": "Унутраны",
|
||||
"aboutDataUsageInternal": "Ўнутраны",
|
||||
"@aboutDataUsageInternal": {},
|
||||
"albumDownload": "Загрузкі",
|
||||
"@albumDownload": {},
|
||||
|
@ -1519,7 +1519,7 @@
|
|||
"minutes": {}
|
||||
}
|
||||
},
|
||||
"collectionActionSetHome": "Усталяваць як галоўную",
|
||||
"collectionActionSetHome": "Ўсталяваць як галоўную",
|
||||
"@collectionActionSetHome": {},
|
||||
"setHomeCustomCollection": "Уласная калекцыя",
|
||||
"@setHomeCustomCollection": {},
|
||||
|
|
1528
lib/l10n/app_ca.arb
Normal file
|
@ -68,10 +68,38 @@
|
|||
"@previousTooltip": {},
|
||||
"hideTooltip": "छिपाए",
|
||||
"@hideTooltip": {},
|
||||
"cancelTooltip": "कैंसिल",
|
||||
"cancelTooltip": "रद्द करें",
|
||||
"@cancelTooltip": {},
|
||||
"changeTooltip": "बदलें",
|
||||
"@changeTooltip": {},
|
||||
"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": {},
|
||||
"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": {},
|
||||
"chipActionGoToTagPage": "在标签中显示",
|
||||
"@chipActionGoToTagPage": {},
|
||||
"chipActionFilterOut": "滤除",
|
||||
"chipActionFilterOut": "筛除",
|
||||
"@chipActionFilterOut": {},
|
||||
"chipActionFilterIn": "筛选",
|
||||
"@chipActionFilterIn": {},
|
||||
|
@ -1243,7 +1243,7 @@
|
|||
"@exportEntryDialogQuality": {},
|
||||
"placeEmpty": "没有地点",
|
||||
"@placeEmpty": {},
|
||||
"settingsAskEverytime": "每次询问",
|
||||
"settingsAskEverytime": "每次都询问",
|
||||
"@settingsAskEverytime": {},
|
||||
"settingsModificationWarningDialogMessage": "其他设置将被修改。",
|
||||
"@settingsModificationWarningDialogMessage": {},
|
||||
|
|
|
@ -38,7 +38,7 @@ void mainCommon(AppFlavor flavor, {Map? debugIntentData}) {
|
|||
// cf https://docs.flutter.dev/testing/errors
|
||||
|
||||
LeakTracking.start();
|
||||
MemoryAllocations.instance.addListener(
|
||||
FlutterMemoryAllocations.instance.addListener(
|
||||
(event) => LeakTracking.dispatchObjectEvent(event.toMap()),
|
||||
);
|
||||
runApp(AvesApp(flavor: flavor, debugIntentData: debugIntentData));
|
||||
|
|
|
@ -77,6 +77,7 @@ class Contributors {
|
|||
Contributor('fuzfyy', 'egeozce35@gmail.com'),
|
||||
Contributor('minh', 'teaminh@skiff.com'),
|
||||
Contributor('luckris25', 'lk1thebestl@gmail.com'),
|
||||
Contributor('Marc Amorós', 'marquitus99@gmail.com'),
|
||||
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
||||
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
||||
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
|
||||
|
@ -84,6 +85,7 @@ class Contributors {
|
|||
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
|
||||
// Contributor('Idj', 'joneltmp+goahn@gmail.com'), // Hebrew
|
||||
// Contributor('Rohit Burman', 'rohitburman31p@rediffmail.com'), // Hindi
|
||||
// Contributor('AJ07', 'ajaykumarmeena676@gmail.com'), // Hindi
|
||||
// Contributor('Chethan', 'chethan@users.noreply.hosted.weblate.org'), // Kannada
|
||||
// Contributor('GoRaN', 'gorangharib.909@gmail.com'), // Kurdish (Central)
|
||||
// Contributor('Rasti K5', 'rasti.khdhr@gmail.com'), // Kurdish (Central)
|
||||
|
|
|
@ -72,7 +72,7 @@ class AvesEntry with AvesEntryBase {
|
|||
this.burstEntries,
|
||||
}) : id = id ?? 0 {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$AvesEntry',
|
||||
object: this,
|
||||
|
@ -188,7 +188,7 @@ class AvesEntry with AvesEntryBase {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
visualChangeNotifier.dispose();
|
||||
metadataChangeNotifier.dispose();
|
||||
|
|
|
@ -18,7 +18,7 @@ class MultiPageInfo {
|
|||
required List<SinglePageInfo> pages,
|
||||
}) : _pages = pages {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$MultiPageInfo',
|
||||
object: this,
|
||||
|
@ -44,7 +44,7 @@ class MultiPageInfo {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_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
|
||||
|
||||
Color? getWidgetOutline(int widgetId) {
|
||||
final value = getInt('${SettingKeys.widgetOutlinePrefixKey}$widgetId');
|
||||
return value != null ? Color(value) : null;
|
||||
}
|
||||
WidgetOutline getWidgetOutline(int widgetId) => getEnumOrDefault('${SettingKeys.widgetOutlinePrefixKey}$widgetId', WidgetOutline.none, WidgetOutline.values);
|
||||
|
||||
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);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class AnalysisController {
|
|||
this.progressOffset = 0,
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$AnalysisController',
|
||||
object: this,
|
||||
|
@ -25,7 +25,7 @@ class AnalysisController {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_stopSignal.dispose();
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ mixin SourceBase {
|
|||
|
||||
Map<int, AvesEntry> get entryById;
|
||||
|
||||
Set<AvesEntry> get allEntries;
|
||||
|
||||
Set<AvesEntry> get visibleEntries;
|
||||
|
||||
Set<AvesEntry> get trashedEntries;
|
||||
|
@ -62,7 +64,7 @@ mixin SourceBase {
|
|||
abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, PlaceMixin, StateMixin, LocationMixin, TagMixin, TrashMixin {
|
||||
CollectionSource() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$CollectionSource',
|
||||
object: this,
|
||||
|
@ -86,7 +88,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
@mustCallSuper
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_rawEntries.forEach((v) => v.dispose());
|
||||
}
|
||||
|
@ -103,6 +105,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
|
||||
final Set<AvesEntry> _rawEntries = {};
|
||||
|
||||
@override
|
||||
Set<AvesEntry> get allEntries => Set.unmodifiable(_rawEntries);
|
||||
|
||||
Set<AvesEntry>? _visibleEntries, _trashedEntries;
|
||||
|
@ -261,8 +264,13 @@ abstract class CollectionSource with SourceBase, AlbumMixin, CountryMixin, Place
|
|||
}
|
||||
});
|
||||
if (entry.trashed) {
|
||||
entry.contentId = null;
|
||||
entry.uri = 'file://${entry.trashDetails?.path}';
|
||||
final trashPath = entry.trashDetails?.path;
|
||||
if (trashPath != null) {
|
||||
entry.contentId = null;
|
||||
entry.uri = Uri.file(trashPath).toString();
|
||||
} else {
|
||||
debugPrint('failed to update uri from unknown trash path for uri=${entry.uri}');
|
||||
}
|
||||
}
|
||||
|
||||
if (persist) {
|
||||
|
|
|
@ -148,12 +148,21 @@ class MediaStoreSource extends CollectionSource {
|
|||
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
|
||||
debugPrint('$runtimeType refresh ${stopwatch.elapsed} fetch new entries');
|
||||
// refresh after the first 10 entries, then after 100 more, then every 1000 entries
|
||||
var refreshCount = 10;
|
||||
const refreshCountMax = 1000;
|
||||
final allNewEntries = <AvesEntry>{}, pendingNewEntries = <AvesEntry>{};
|
||||
final allNewEntries = <AvesEntry>{};
|
||||
void addPendingEntries() {
|
||||
allNewEntries.addAll(pendingNewEntries);
|
||||
addEntries(pendingNewEntries);
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/entry/entry.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/services/common/image_op_events.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 {
|
||||
static const Duration binKeepDuration = Duration(days: 30);
|
||||
|
@ -32,4 +37,50 @@ mixin TrashMixin on SourceBase {
|
|||
);
|
||||
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: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/services/common/services.dart';
|
||||
import 'package:aves_screen_state/aves_screen_state.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();
|
||||
|
||||
|
@ -14,6 +18,8 @@ class Vaults extends ChangeNotifier {
|
|||
Set<VaultDetails> _rows = {};
|
||||
final Set<String> _unlockedDirPaths = {};
|
||||
|
||||
static const _fileScheme = 'file';
|
||||
|
||||
Vaults._private();
|
||||
|
||||
Future<void> init() async {
|
||||
|
@ -118,7 +124,7 @@ class Vaults extends ChangeNotifier {
|
|||
|
||||
bool isVaultEntryUri(String 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');
|
||||
return vaultDirectories.any(path.startsWith);
|
||||
|
@ -132,13 +138,47 @@ class Vaults extends ChangeNotifier {
|
|||
_onLockStateChanged();
|
||||
}
|
||||
|
||||
void unlock(String dirPath) {
|
||||
Future<void> unlock(BuildContext context, String dirPath) async {
|
||||
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);
|
||||
_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 _onLockStateChanged() {
|
||||
|
|
|
@ -94,7 +94,7 @@ class Analyzer {
|
|||
Analyzer() {
|
||||
debugPrint('$runtimeType create');
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$Analyzer',
|
||||
object: this,
|
||||
|
@ -107,7 +107,7 @@ class Analyzer {
|
|||
void dispose() {
|
||||
debugPrint('$runtimeType dispose');
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_stopUpdateTimer();
|
||||
_controller?.dispose();
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/apps.dart';
|
||||
import 'package:aves/model/entry/entry.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:flutter/services.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
||||
abstract class AppService {
|
||||
Future<Set<Package>> getPackages();
|
||||
|
@ -15,7 +18,7 @@ abstract class AppService {
|
|||
|
||||
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});
|
||||
|
||||
|
@ -32,6 +35,7 @@ abstract class AppService {
|
|||
|
||||
class PlatformAppService implements AppService {
|
||||
static const _platform = MethodChannel('deckers.thibault/aves/app');
|
||||
static final _stream = StreamsChannel('deckers.thibault/aves/activity_result_stream');
|
||||
|
||||
static final _knownAppDirs = {
|
||||
'com.kakao.talk': {'KakaoTalkDownload'},
|
||||
|
@ -89,17 +93,29 @@ class PlatformAppService implements AppService {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<bool> edit(String uri, String mimeType) async {
|
||||
Future<Map<String, dynamic>> edit(String uri, String mimeType) async {
|
||||
try {
|
||||
final result = await _platform.invokeMethod('edit', <String, dynamic>{
|
||||
final completer = Completer<Map?>();
|
||||
_stream.receiveBroadcastStream(<String, dynamic>{
|
||||
'op': 'edit',
|
||||
'uri': uri,
|
||||
'mimeType': mimeType,
|
||||
});
|
||||
if (result != null) return result as bool;
|
||||
}).listen(
|
||||
(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) {
|
||||
await reportService.recordError(e, stack);
|
||||
return {'error': e.code};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -12,6 +12,10 @@ abstract class StorageService {
|
|||
|
||||
Future<Set<StorageVolume>> getStorageVolumes();
|
||||
|
||||
Future<Set<String>> getUntrackedTrashPaths(Iterable<String> knownPaths);
|
||||
|
||||
Future<Set<String>> getUntrackedVaultPaths(String vaultName, Iterable<String> knownPaths);
|
||||
|
||||
Future<String> getVaultRoot();
|
||||
|
||||
Future<int?> getFreeSpace(StorageVolume volume);
|
||||
|
@ -71,6 +75,33 @@ class PlatformStorageService implements StorageService {
|
|||
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
|
||||
Future<String> getVaultRoot() async {
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/widgets/aves_app.dart';
|
||||
import 'package:aves_utils/aves_utils.dart';
|
||||
|
@ -11,6 +10,8 @@ class Themes {
|
|||
fontFeatures: [FontFeature.enable('smcp')],
|
||||
);
|
||||
|
||||
static String asButtonLabel(String s) => s.toUpperCase();
|
||||
|
||||
static TextStyle searchFieldStyle(BuildContext context) => Theme.of(context).textTheme.bodyLarge!;
|
||||
|
||||
static Color overlayBackgroundColor({
|
||||
|
|
|
@ -20,7 +20,8 @@ class AndroidFileUtils {
|
|||
static const mediaStoreUriRoot = '$contentScheme://$mediaStoreAuthority/';
|
||||
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 dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath;
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:aves/app_flavor.dart';
|
||||
import 'package:aves/model/entry/entry.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/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/media_store_source.dart';
|
||||
|
@ -20,6 +21,7 @@ void widgetMainCommon(AppFlavor flavor) async {
|
|||
WidgetsFlutterBinding.ensureInitialized();
|
||||
initPlatformServices();
|
||||
await settings.init(monitorPlatformSettings: false);
|
||||
await reportService.init();
|
||||
|
||||
_widgetDrawChannel.setMethodCallHandler((call) async {
|
||||
// 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 drawEntryImage = args['drawEntryImage'] 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 painter = HomeWidgetPainter(
|
||||
|
@ -50,7 +56,7 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
|
|||
final bytes = await painter.drawWidget(
|
||||
widthPx: widthPx,
|
||||
heightPx: heightPx,
|
||||
outline: settings.getWidgetOutline(widgetId),
|
||||
outline: outline,
|
||||
shape: settings.getWidgetShape(widgetId),
|
||||
);
|
||||
return {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/device.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/media_edit_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/widgets/collection/collection_page.dart';
|
||||
import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
|
||||
|
@ -250,8 +251,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
if (toBin) {
|
||||
if (movedEntries.isNotEmpty) {
|
||||
action = SnackBarAction(
|
||||
// TODO TLAD [l10n] key for "RESTORE"
|
||||
label: l10n.entryActionRestore.toUpperCase(),
|
||||
label: Themes.asButtonLabel(l10n.entryActionRestore),
|
||||
onPressed: () {
|
||||
if (navigator != null) {
|
||||
doMove(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:aves/model/entry/entry.dart';
|
||||
import 'package:aves/model/entry/extensions/props.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/view/view.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
@ -61,7 +62,7 @@ mixin PermissionAwareMixin {
|
|||
TextButton(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(true),
|
||||
// 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';
|
||||
|
||||
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;
|
||||
|
||||
final details = vaults.detailsForPath(dirPath);
|
||||
|
@ -67,12 +67,12 @@ mixin VaultAwareMixin on FeedbackMixin {
|
|||
|
||||
if (confirmed == null || !confirmed) return false;
|
||||
|
||||
vaults.unlock(dirPath);
|
||||
await vaults.unlock(context, dirPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> unlockAlbum(BuildContext context, String dirPath) async {
|
||||
final success = await _tryUnlock(dirPath, context);
|
||||
final success = await _tryUnlock(context, dirPath);
|
||||
if (!success) {
|
||||
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/events.dart';
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
|
|||
|
||||
class ColorIndicator extends StatelessWidget {
|
||||
final Color? value;
|
||||
final Color? alternate;
|
||||
final Widget? child;
|
||||
|
||||
static const double radius = 16;
|
||||
|
@ -10,18 +11,33 @@ class ColorIndicator extends StatelessWidget {
|
|||
const ColorIndicator({
|
||||
super.key,
|
||||
required this.value,
|
||||
this.alternate,
|
||||
this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
height: dimension,
|
||||
width: dimension,
|
||||
decoration: BoxDecoration(
|
||||
color: value,
|
||||
color: _value,
|
||||
border: AvesBorder.border(context),
|
||||
gradient: gradient,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: child,
|
||||
|
|
|
@ -13,7 +13,7 @@ class DoubleBackPopHandler {
|
|||
|
||||
DoubleBackPopHandler() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$DoubleBackPopHandler',
|
||||
object: this,
|
||||
|
@ -23,7 +23,7 @@ class DoubleBackPopHandler {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_stopBackTimer();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/colors.dart';
|
||||
|
|
|
@ -20,7 +20,7 @@ abstract class AvesSearchDelegate extends SearchDelegate {
|
|||
required super.searchFieldStyle,
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$AvesSearchDelegate',
|
||||
object: this,
|
||||
|
@ -135,7 +135,7 @@ abstract class AvesSearchDelegate extends SearchDelegate {
|
|||
@mustCallSuper
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
currentBodyNotifier.dispose();
|
||||
queryTextController.dispose();
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/themes.dart';
|
||||
|
|
|
@ -28,7 +28,7 @@ class TileExtentController {
|
|||
required this.horizontalPadding,
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$TileExtentController',
|
||||
object: this,
|
||||
|
@ -42,7 +42,7 @@ class TileExtentController {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/themes.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -165,7 +166,7 @@ class CancelButton extends StatelessWidget {
|
|||
return TextButton(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(),
|
||||
// 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(
|
||||
onPressed: () => Navigator.maybeOf(context)?.pop(),
|
||||
// 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,
|
||||
firstDate: DateTime(0),
|
||||
lastDate: DateTime(2100),
|
||||
cancelText: MaterialLocalizations.of(context).cancelButtonLabel.toUpperCase(),
|
||||
cancelText: Themes.asButtonLabel(context.l10n.cancelTooltip),
|
||||
confirmText: context.l10n.nextButtonLabel,
|
||||
);
|
||||
if (_date == null) return;
|
||||
|
@ -309,7 +309,7 @@ class _EditEntryDateDialogState extends State<EditEntryDateDialog> {
|
|||
final _time = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(_customDateTime),
|
||||
cancelText: MaterialLocalizations.of(context).cancelButtonLabel.toUpperCase(),
|
||||
cancelText: Themes.asButtonLabel(context.l10n.cancelTooltip),
|
||||
);
|
||||
if (_time == null) return;
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ class _TagEditorPageState extends State<TagEditorPage> {
|
|||
const CancelButton(),
|
||||
TextButton(
|
||||
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) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$TransformController',
|
||||
object: this,
|
||||
|
@ -46,7 +46,7 @@ class TransformController {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
aspectRatioNotifier.dispose();
|
||||
}
|
||||
|
|
|
@ -14,9 +14,10 @@ class HomeWidgetPainter {
|
|||
final AvesEntry? entry;
|
||||
final double devicePixelRatio;
|
||||
|
||||
// do not use `AlignmentDirectional` as there is no `TextDirection` in context
|
||||
static const backgroundGradient = LinearGradient(
|
||||
begin: AlignmentDirectional.bottomStart,
|
||||
end: AlignmentDirectional.topEnd,
|
||||
begin: Alignment.bottomLeft,
|
||||
end: Alignment.topRight,
|
||||
colors: AColors.boraBoraGradient,
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/trash.dart';
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
|
|
|
@ -36,7 +36,7 @@ class AvailableActionPanel<T extends Object> extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DragTarget<T>(
|
||||
onWillAccept: (data) {
|
||||
onWillAcceptWithDetails: (details) {
|
||||
if (draggedQuickAction.value != null) {
|
||||
_setPanelHighlight(true);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class QuickActionButton<T extends Object> extends StatelessWidget {
|
|||
|
||||
DragTarget<T> _buildDragTarget(Widget? child) {
|
||||
return DragTarget<T>(
|
||||
onWillAccept: (data) {
|
||||
onWillAcceptWithDetails: (details) {
|
||||
if (draggedQuickAction.value != null) {
|
||||
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/settings/enums/widget_outline.dart';
|
||||
import 'package:aves/model/settings/enums/widget_shape.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/widget_service.dart';
|
||||
|
@ -33,10 +35,11 @@ class HomeWidgetSettingsPage extends StatefulWidget {
|
|||
|
||||
class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
||||
late WidgetShape _shape;
|
||||
late Color? _outline;
|
||||
late WidgetOutline _outline;
|
||||
late WidgetOpenPage _openPage;
|
||||
late WidgetDisplayedItem _displayedItem;
|
||||
late Set<CollectionFilter> _collectionFilters;
|
||||
Future<Map<Brightness, Map<WidgetOutline, Color?>>> _outlineColorsByBrightness = Future.value({});
|
||||
|
||||
int get widgetId => widget.widgetId;
|
||||
|
||||
|
@ -61,6 +64,24 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
_openPage = settings.getWidgetOpenPage(widgetId);
|
||||
_displayedItem = settings.getWidgetDisplayedItem(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
|
||||
|
@ -71,58 +92,70 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
title: Text(l10n.settingsWidgetPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildShapeSelector(),
|
||||
ListTile(
|
||||
title: Text(l10n.settingsWidgetShowOutline),
|
||||
trailing: HomeWidgetOutlineSelector(
|
||||
getter: () => _outline,
|
||||
setter: (v) => setState(() => _outline = v),
|
||||
),
|
||||
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: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
_buildShapeSelector(effectiveOutlineColors),
|
||||
ListTile(
|
||||
title: Text(l10n.settingsWidgetShowOutline),
|
||||
trailing: HomeWidgetOutlineSelector(
|
||||
getter: () => _outline,
|
||||
setter: (v) => setState(() => _outline = v),
|
||||
outlineColorsByBrightness: outlineColorsByBrightness,
|
||||
),
|
||||
),
|
||||
SettingsSelectionListTile<WidgetOpenPage>(
|
||||
values: WidgetOpenPage.values,
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => _openPage,
|
||||
onSelection: (v) => setState(() => _openPage = v),
|
||||
tileTitle: l10n.settingsWidgetOpenPage,
|
||||
),
|
||||
SettingsSelectionListTile<WidgetDisplayedItem>(
|
||||
values: WidgetDisplayedItem.values,
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => _displayedItem,
|
||||
onSelection: (v) => setState(() => _displayedItem = v),
|
||||
tileTitle: l10n.settingsWidgetDisplayedItem,
|
||||
),
|
||||
SettingsCollectionTile(
|
||||
filters: _collectionFilters,
|
||||
onSelection: (v) => setState(() => _collectionFilters = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSelectionListTile<WidgetOpenPage>(
|
||||
values: WidgetOpenPage.values,
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => _openPage,
|
||||
onSelection: (v) => setState(() => _openPage = v),
|
||||
tileTitle: l10n.settingsWidgetOpenPage,
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AvesOutlinedButton(
|
||||
label: l10n.saveTooltip,
|
||||
onPressed: () {
|
||||
_saveSettings();
|
||||
WidgetService.configure();
|
||||
},
|
||||
),
|
||||
SettingsSelectionListTile<WidgetDisplayedItem>(
|
||||
values: WidgetDisplayedItem.values,
|
||||
getName: (context, v) => v.getName(context),
|
||||
selector: (context, s) => _displayedItem,
|
||||
onSelection: (v) => setState(() => _displayedItem = v),
|
||||
tileTitle: l10n.settingsWidgetDisplayedItem,
|
||||
),
|
||||
SettingsCollectionTile(
|
||||
filters: _collectionFilters,
|
||||
onSelection: (v) => setState(() => _collectionFilters = v),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AvesOutlinedButton(
|
||||
label: l10n.saveTooltip,
|
||||
onPressed: () {
|
||||
_saveSettings();
|
||||
WidgetService.configure();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShapeSelector() {
|
||||
Widget _buildShapeSelector(Map<WidgetOutline, Color?> outlineColors) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
child: Wrap(
|
||||
|
@ -143,7 +176,7 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
height: 124,
|
||||
decoration: ShapeDecoration(
|
||||
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 {
|
||||
final Color? outline;
|
||||
final WidgetOutline outline;
|
||||
final WidgetShape shape;
|
||||
final Map<WidgetOutline, Color?> outlineColors;
|
||||
|
||||
static const _devicePixelRatio = 1.0;
|
||||
|
||||
const _WidgetShapeBorder(this.outline, this.shape);
|
||||
const _WidgetShapeBorder(this.outline, this.shape, this.outlineColors);
|
||||
|
||||
@override
|
||||
EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
|
||||
|
@ -191,10 +225,11 @@ class _WidgetShapeBorder extends ShapeBorder {
|
|||
|
||||
@override
|
||||
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);
|
||||
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 {
|
||||
final ValueGetter<Color?> getter;
|
||||
final ValueSetter<Color?> setter;
|
||||
final ValueGetter<WidgetOutline> getter;
|
||||
final ValueSetter<WidgetOutline> setter;
|
||||
final Map<Brightness, Map<WidgetOutline, Color?>> outlineColorsByBrightness;
|
||||
|
||||
const HomeWidgetOutlineSelector({
|
||||
super.key,
|
||||
required this.getter,
|
||||
required this.setter,
|
||||
required this.outlineColorsByBrightness,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -217,35 +254,40 @@ class HomeWidgetOutlineSelector extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> {
|
||||
static const List<Color?> options = [
|
||||
null,
|
||||
Colors.black,
|
||||
Colors.white,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton<Color?>(
|
||||
child: DropdownButton<WidgetOutline>(
|
||||
items: _buildItems(context),
|
||||
value: widget.getter(),
|
||||
onChanged: (selected) {
|
||||
widget.setter(selected);
|
||||
widget.setter(selected ?? WidgetOutline.none);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<Color?>> _buildItems(BuildContext context) {
|
||||
return options.map((selected) {
|
||||
return DropdownMenuItem<Color?>(
|
||||
List<DropdownMenuItem<WidgetOutline>> _buildItems(BuildContext context) {
|
||||
return supportedWidgetOutlines.map((selected) {
|
||||
final lightColors = widget.outlineColorsByBrightness[Brightness.light];
|
||||
final darkColors = widget.outlineColorsByBrightness[Brightness.dark];
|
||||
return DropdownMenuItem<WidgetOutline>(
|
||||
value: selected,
|
||||
child: ColorIndicator(
|
||||
value: selected,
|
||||
child: selected == null ? const Icon(AIcons.clear) : null,
|
||||
value: lightColors?[selected],
|
||||
alternate: darkColors?[selected],
|
||||
child: lightColors?[selected] == null ? const Icon(AIcons.clear) : null,
|
||||
),
|
||||
);
|
||||
}).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 = {
|
||||
'ar': 'العربية',
|
||||
'be': 'Беларуская мова',
|
||||
'ca': 'Català',
|
||||
'cs': 'Čeština',
|
||||
'de': 'Deutsch',
|
||||
'el': 'Ελληνικά',
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:aves/model/source/collection_source.dart';
|
|||
import 'package:aves/model/vaults/vaults.dart';
|
||||
import 'package:aves/services/common/services.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/feedback.dart';
|
||||
import 'package:aves/widgets/common/action_mixins/permission_aware.dart';
|
||||
|
@ -242,8 +243,15 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
).dispatch(context);
|
||||
}
|
||||
case EntryAction.edit:
|
||||
appService.edit(targetEntry.uri, targetEntry.mimeType).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
appService.edit(targetEntry.uri, targetEntry.mimeType).then((fields) async {
|
||||
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:
|
||||
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 {
|
||||
if (!await unlockAlbum(context, album)) return;
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
required this.collection,
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$VideoActionDelegate',
|
||||
object: this,
|
||||
|
@ -45,7 +45,7 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
stopOverlayHidingTimer();
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class ViewerController with CastMixin {
|
|||
this.autopilotAnimatedZoom = false,
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$ViewerController',
|
||||
object: this,
|
||||
|
@ -67,7 +67,7 @@ class ViewerController with CastMixin {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
entryNotifier.removeListener(_onEntryChanged);
|
||||
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/catalog.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/services/common/services.dart';
|
||||
import 'package:aves/widgets/viewer/info/common.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class DbTab extends StatefulWidget {
|
||||
final AvesEntry entry;
|
||||
|
@ -83,7 +85,14 @@ class _DbTabState extends State<DbTab> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
info: {
|
||||
'uri': data.uri,
|
||||
|
@ -101,6 +110,7 @@ class _DbTabState extends State<DbTab> {
|
|||
'trashed': '${data.trashed}',
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
@ -170,13 +180,22 @@ class _DbTabState extends State<DbTab> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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(
|
||||
info: {
|
||||
'dateMillis': '${data.dateMillis}',
|
||||
'path': data.path,
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@ class MultiPageConductor {
|
|||
|
||||
MultiPageConductor() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$MultiPageConductor',
|
||||
object: this,
|
||||
|
@ -22,7 +22,7 @@ class MultiPageConductor {
|
|||
|
||||
Future<void> dispose() async {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
await _disposeAll();
|
||||
_controllers.clear();
|
||||
|
|
|
@ -25,7 +25,7 @@ class MultiPageController {
|
|||
|
||||
MultiPageController(this.entry) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$MultiPageController',
|
||||
object: this,
|
||||
|
@ -48,7 +48,7 @@ class MultiPageController {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_info?.dispose();
|
||||
_disposed = true;
|
||||
|
|
|
@ -21,7 +21,7 @@ class VideoConductor {
|
|||
|
||||
VideoConductor({CollectionLens? collection}) : _collection = collection {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$VideoConductor',
|
||||
object: this,
|
||||
|
@ -31,7 +31,7 @@ class VideoConductor {
|
|||
|
||||
Future<void> dispose() async {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
|
|
|
@ -14,7 +14,7 @@ class ViewStateConductor {
|
|||
|
||||
ViewStateConductor() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$ViewStateConductor',
|
||||
object: this,
|
||||
|
@ -24,7 +24,7 @@ class ViewStateConductor {
|
|||
|
||||
Future<void> dispose() async {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_controllers.forEach((v) => v.dispose());
|
||||
_controllers.clear();
|
||||
|
|
|
@ -16,7 +16,7 @@ class ViewStateController with HistogramMixin {
|
|||
required this.viewStateNotifier,
|
||||
}) {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$ViewStateController',
|
||||
object: this,
|
||||
|
@ -26,7 +26,7 @@ class ViewStateController with HistogramMixin {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
viewStateNotifier.dispose();
|
||||
fullImageNotifier.dispose();
|
||||
|
|
|
@ -21,7 +21,7 @@ class AvesMagnifierController {
|
|||
MagnifierState? initialState,
|
||||
}) : super() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$AvesMagnifierController',
|
||||
object: this,
|
||||
|
@ -41,7 +41,7 @@ class AvesMagnifierController {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_disposed = true;
|
||||
_stateStreamController.close();
|
||||
|
|
|
@ -57,18 +57,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.11.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -98,14 +98,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
sdks:
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=1.16.0"
|
||||
|
|
|
@ -3,7 +3,7 @@ version: 0.0.1
|
|||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.0 <4.0.0'
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
|
|
@ -19,7 +19,7 @@ class AvesMapController {
|
|||
|
||||
AvesMapController() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectCreated(
|
||||
FlutterMemoryAllocations.instance.dispatchObjectCreated(
|
||||
library: 'aves',
|
||||
className: '$AvesMapController',
|
||||
object: this,
|
||||
|
@ -29,7 +29,7 @@ class AvesMapController {
|
|||
|
||||
void dispose() {
|
||||
if (kFlutterMemoryAllocationsEnabled) {
|
||||
MemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
FlutterMemoryAllocations.instance.dispatchObjectDisposed(object: this);
|
||||
}
|
||||
_streamController.close();
|
||||
}
|
||||
|
|
|
@ -89,10 +89,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
|
||||
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
version: "1.2.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -145,18 +145,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.11.0"
|
||||
mgrs_dart:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -262,10 +262,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
version: "0.5.0"
|
||||
wkt_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -275,5 +275,5 @@ packages:
|
|||
source: hosted
|
||||
version: "2.0.0"
|
||||
sdks:
|
||||
dart: ">=3.2.0 <4.0.0"
|
||||
dart: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
|
|
|
@ -3,7 +3,7 @@ version: 0.0.1
|
|||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.0 <4.0.0'
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
|
|
@ -48,4 +48,6 @@ enum WidgetDisplayedItem { random, mostRecent }
|
|||
|
||||
enum WidgetOpenPage { home, collection, viewer, updateWidget }
|
||||
|
||||
enum WidgetOutline { none, black, white, systemBlackAndWhite, systemDynamic }
|
||||
|
||||
enum WidgetShape { rrect, circle, heart }
|
||||
|
|
|
@ -50,18 +50,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: meta
|
||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.11.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -75,13 +75,5 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.0"
|
||||
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
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.0 <4.0.0'
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
|