Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2024-02-22 20:24:59 +01:00
commit 579e7a2db0
137 changed files with 11181 additions and 605 deletions

@ -1 +1 @@
Subproject commit 41456452f29d64e8deb623a3c927524bcf9f111b
Subproject commit abb292a07e20d696c4568099f918f6c5f330e6b0

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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"

View file

@ -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)

View file

@ -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

View file

@ -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
}
}

View file

@ -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))
}

View file

@ -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")
}

View file

@ -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 {

View file

@ -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)
}
}

View 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>

View file

@ -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
View file

@ -0,0 +1 @@
/build

View 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

View 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'
}

View file

View 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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View file

@ -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);
}
}
}

View file

@ -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")

View 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>.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

View file

@ -0,0 +1 @@
Gallery and metadata explorer

View file

@ -0,0 +1,3 @@
In v1.10.5:
- enjoy the app in Catalan
Full changelog available on GitHub

View file

@ -0,0 +1,3 @@
In v1.10.5:
- enjoy the app in Catalan
Full changelog available on GitHub

View file

@ -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": {},

View file

@ -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

File diff suppressed because it is too large Load diff

View 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": {}
}

View file

@ -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": {}
}

View file

@ -71,7 +71,7 @@
"@chipActionGoToCountryPage": {},
"chipActionGoToTagPage": "在标签中显示",
"@chipActionGoToTagPage": {},
"chipActionFilterOut": "除",
"chipActionFilterOut": "除",
"@chipActionFilterOut": {},
"chipActionFilterIn": "筛选",
"@chipActionFilterIn": {},
@ -1243,7 +1243,7 @@
"@exportEntryDialogQuality": {},
"placeEmpty": "没有地点",
"@placeEmpty": {},
"settingsAskEverytime": "每次询问",
"settingsAskEverytime": "每次询问",
"@settingsAskEverytime": {},
"settingsModificationWarningDialogMessage": "其他设置将被修改。",
"@settingsModificationWarningDialogMessage": {},

View file

@ -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));

View file

@ -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)

View file

@ -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();

View file

@ -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());
}

View 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);
}
}
}

View file

@ -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);

View file

@ -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();
}

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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() {

View file

@ -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();

View file

@ -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

View file

@ -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 {

View file

@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:flutter/painting.dart';

View file

@ -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({

View file

@ -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;

View file

@ -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 {

View file

@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:aves/model/device.dart';
import 'package:aves/theme/icons.dart';

View file

@ -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(

View file

@ -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)),
),
],
);

View file

@ -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);
}

View file

@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/events.dart';

View file

@ -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,

View file

@ -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();
}

View file

@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/colors.dart';

View file

@ -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();

View file

@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart';

View file

@ -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())

View file

@ -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)),
);
}
}

View file

@ -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;

View file

@ -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)),
),
],
),

View file

@ -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();
}

View file

@ -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,
);

View file

@ -1,4 +1,3 @@
import 'dart:ui';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/trash.dart';

View file

@ -1,5 +1,4 @@
import 'dart:math';
import 'dart:ui';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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,
];
}

View file

@ -5,6 +5,7 @@ class SupportedLocales {
static const languagesByLanguageCode = {
'ar': 'العربية',
'be': 'Беларуская мова',
'ca': 'Català',
'cs': 'Čeština',
'de': 'Deutsch',
'el': 'Ελληνικά',

View file

@ -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;

View file

@ -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();
}

View file

@ -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);

View file

@ -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,
},
),
],
],
);
},

View file

@ -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();

View file

@ -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;

View file

@ -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())

View file

@ -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();

View file

@ -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();

View file

@ -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();

View file

@ -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"

View file

@ -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:

View file

@ -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();
}

View file

@ -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"

View file

@ -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:

View file

@ -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 }

View file

@ -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"

View file

@ -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:

Some files were not shown because too many files have changed in this diff Show more