diff --git a/.flutter b/.flutter
index ea121f885..d8a9f9a52 160000
--- a/.flutter
+++ b/.flutter
@@ -1 +1 @@
-Subproject commit ea121f8859e4b13e47a8f845e4586164519588bc
+Subproject commit d8a9f9a52e5af486f80d932e838ee93861ffd863
diff --git a/.github/workflows/quality-check.yml b/.github/workflows/quality-check.yml
index 0f696248d..a738c7a58 100644
--- a/.github/workflows/quality-check.yml
+++ b/.github/workflows/quality-check.yml
@@ -28,6 +28,9 @@ jobs:
- name: Get Flutter packages
run: ./flutterw pub get
+ - name: Generate app localizations
+ run: ./flutterw gen-l10n
+
- name: Static analysis.
run: ./flutterw analyze
@@ -69,7 +72,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
+ uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -83,6 +86,6 @@ jobs:
./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
+ uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 25d2aa731..de3ee347d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -36,6 +36,9 @@ jobs:
- name: Get Flutter packages
run: ./flutterw pub get
+ - name: Generate app localizations
+ run: ./flutterw gen-l10n
+
- name: Update Flutter version file
run: scripts/update_flutter_version.sh
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index c7170c96d..8f9422486 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -41,7 +41,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
+ uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
results_file: results.sarif
results_format: sarif
@@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
+ uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with:
sarif_file: results.sarif
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98fb110e9..a46ac3322 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.13.2] - 2025-06-02
+
+### Changed
+
+- downgraded Flutter to stable v3.27.4
+- prevent display orientation flip when device rotation is locked
+
+### Fixed
+
+- moved file losing its extension and no longer being detected as media in some cases
+- opening home when launching app as media picker
+- removing groups with obsolete albums
+- loading group custom covers
+- crash when parsing some large media with trailing thumbnail
+
## [v1.13.1] - 2025-05-14
### Fixed
diff --git a/analysis_options.yaml b/analysis_options.yaml
index f9925ff6f..a6531e827 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -9,6 +9,11 @@ analyzer:
# implicit-casts: false
# implicit-dynamic: false
+# cf https://github.com/dart-lang/dart_style/wiki/Configuration
+formatter:
+ page_width: 240
+ trailing_commas: preserve
+
linter:
rules:
# from 'flutter_lints', excluded
diff --git a/android/.gitignore b/android/.gitignore
index 82677b89f..5343532a4 100644
--- a/android/.gitignore
+++ b/android/.gitignore
@@ -7,6 +7,7 @@ gradle-wrapper.jar
GeneratedPluginRegistrant.java
.cxx/
.kotlin/
+/build/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 42711a403..bb6a533fb 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -134,14 +134,14 @@ flutter {
repositories {
maven {
- url 'https://jitpack.io'
+ url = 'https://jitpack.io'
content {
includeGroup "com.github.deckerst"
includeGroup "com.github.deckerst.mp4parser"
}
}
maven {
- url 'https://s3.amazonaws.com/repo.commonsware.com'
+ url = 'https://s3.amazonaws.com/repo.commonsware.com'
content {
excludeGroupByRegex "com\\.github\\.deckerst.*"
}
@@ -152,12 +152,12 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1'
implementation "androidx.appcompat:appcompat:1.7.0"
- implementation 'androidx.core:core-ktx:1.15.0'
- implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
+ implementation 'androidx.core:core-ktx:1.16.0'
+ implementation 'androidx.lifecycle:lifecycle-process:2.9.0'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
- implementation 'androidx.security:security-crypto:1.1.0-alpha06'
- implementation 'androidx.work:work-runtime-ktx:2.10.0'
+ implementation 'androidx.security:security-crypto:1.1.0-alpha07'
+ implementation 'androidx.work:work-runtime-ktx:2.10.1'
implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
@@ -171,11 +171,11 @@ dependencies {
// - https://jitpack.io/p/deckerst/androidsvg
// - https://jitpack.io/p/deckerst/mp4parser
// - https://jitpack.io/p/deckerst/pixymeta-android
- implementation 'com.github.deckerst:Android-TiffBitmapFactory:3ed067f021'
- implementation 'com.github.deckerst:androidsvg:cc9d59a88f'
- implementation 'com.github.deckerst.mp4parser:isoparser:d5caf7a3dd'
- implementation 'com.github.deckerst.mp4parser:muxer:d5caf7a3dd'
- implementation 'com.github.deckerst:pixymeta-android:71eee77dc4'
+ implementation 'com.github.deckerst:Android-TiffBitmapFactory:d6b2b0aa4f'
+ implementation 'com.github.deckerst:androidsvg:67db933051'
+ implementation 'com.github.deckerst.mp4parser:isoparser:c2898f1832'
+ implementation 'com.github.deckerst.mp4parser:muxer:c2898f1832'
+ implementation 'com.github.deckerst:pixymeta-android:cb1cdc932e'
implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
index 58c07fbf4..9b23fbc9d 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
@@ -311,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
embeddedByteStream: InputStream,
embeddedByteLength: Long,
) {
- val extension = extensionFor(mimeType)
+ val extension = extensionFor(mimeType, defaultExtension = null)
val targetFile = StorageUtils.createTempFile(context, extension).apply {
transferFrom(embeddedByteStream, embeddedByteLength)
}
@@ -319,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val authority = "${context.applicationContext.packageName}.file_provider"
val uri = if (displayName != null) {
// add extension to ease type identification when sharing this content
- val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) {
+ val displayNameWithExtension = if (displayName.endsWith(extension, ignoreCase = true)) {
displayName
} else {
"$displayName$extension"
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt
index e926b5b6a..0f16007e5 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreChangeStreamHandler.kt
@@ -31,9 +31,15 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
init {
Log.i(LOG_TAG, "start listening to Media Store")
- context.contentResolver.apply {
- registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
- registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
+ try {
+ context.contentResolver.apply {
+ registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
+ registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
+ }
+ } catch (e: SecurityException) {
+ // Trying to register an observer may yield a security exception with this message:
+ // "Failed to find provider media for user 0; expected to find a valid ContentProvider for this authority"
+ Log.w(LOG_TAG, "failed to register content observer", e)
}
}
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
index 9d36f1812..2804bda5f 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt
@@ -142,16 +142,18 @@ abstract class ImageProvider {
val oldFile = File(sourcePath)
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
+ val defaultExtension = oldFile.extension
oldFile.parent?.let { dir ->
val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity,
dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
+ defaultExtension = defaultExtension,
conflictStrategy = NameConflictStrategy.RENAME,
)
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
- val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
+ val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}"
val newFile = File(dir, targetFileName)
if (oldFile != newFile) {
newFields = renameSingle(
@@ -277,11 +279,17 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
}
+
+ // there is no benefit providing input extension
+ // for known output MIME type
+ val defaultExtension = null
+
val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity,
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType,
+ defaultExtension = defaultExtension,
conflictStrategy = nameConflictStrategy,
)
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@@ -358,6 +366,7 @@ abstract class ImageProvider {
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
+ defaultExtension = defaultExtension,
write = write,
)
@@ -465,6 +474,7 @@ abstract class ImageProvider {
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType,
+ defaultExtension = null,
conflictStrategy = nameConflictStrategy,
)
} catch (e: Exception) {
@@ -571,13 +581,14 @@ abstract class ImageProvider {
dir: String,
desiredNameWithoutExtension: String,
mimeType: String,
+ defaultExtension: String?,
conflictStrategy: NameConflictStrategy,
): NameConflictResolution {
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
var resolvedName: String? = sanitizedNameWithoutExtension
var replacementFile: File? = null
- val extension = extensionFor(mimeType)
+ val extension = extensionFor(mimeType, defaultExtension)
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) {
NameConflictStrategy.RENAME -> {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
index 7b1b7b190..7aca99e80 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
@@ -557,6 +557,7 @@ class MediaStoreImageProvider : ImageProvider() {
toBin: Boolean,
): FieldMap {
val sourcePath = sourceFile?.path
+ val sourceExtension = sourceFile?.extension
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy
@@ -569,6 +570,7 @@ class MediaStoreImageProvider : ImageProvider() {
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
+ defaultExtension = sourceExtension,
conflictStrategy = nameConflictStrategy,
)
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@@ -580,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
+ defaultExtension = sourceExtension,
) { output: OutputStream ->
try {
sourceDocFile.copyTo(output)
@@ -615,12 +618,13 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String,
+ defaultExtension: String?,
write: (OutputStream) -> Unit,
): String {
if (StorageUtils.isInVault(activity, targetDir)) {
return insertByFile(
targetDir = targetDir,
- targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
+ targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write,
)
}
@@ -630,7 +634,7 @@ class MediaStoreImageProvider : ImageProvider() {
return insertByMediaStore(
activity = activity,
targetDir = targetDir,
- targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
+ targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write,
)
}
@@ -642,6 +646,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
+ defaultExtension = defaultExtension,
write = write,
)
}
@@ -700,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String,
+ defaultExtension: String?,
write: (OutputStream) -> Unit,
): String {
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
@@ -708,8 +714,22 @@ class MediaStoreImageProvider : ImageProvider() {
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
- val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
- val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
+ var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
+ var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
+
+ // providing a display name and a MIME type does not guarantee
+ // that the created document will be backed by a file with a valid media extension,
+ // but having an extension is essential for media detection by Android,
+ // so we retry with a display name that includes the extension
+ if ((targetDocFile.extension == null || targetDocFile.extension.isEmpty() || targetDocFile.extension == "bin") && defaultExtension != null) {
+ if (targetDocFile.exists()) {
+ targetDocFile.delete()
+ }
+
+ val extension = if (defaultExtension.startsWith(".")) defaultExtension else ".$defaultExtension"
+ targetTreeFile = targetDirDocFile.createFile(mimeType, "$targetNameWithoutExtension$extension")
+ targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
+ }
try {
targetDocFile.openOutputStream().use(write)
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
index c0bef41c9..1e6ae58cd 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt
@@ -163,12 +163,24 @@ object MimeTypes {
// among other refs:
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
- fun extensionFor(mimeType: String): String? = when (mimeType) {
+ fun extensionFor(mimeType: String, defaultExtension: String?): String = when (mimeType) {
AVI, AVI_VND -> ".avi"
+ DNG, DNG_ADOBE -> ".dng"
HEIC, HEIF -> ".heif"
MP2T, MP2TS -> ".m2ts"
PSD_VND, PSD_X -> ".psd"
- else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
+ else -> {
+ val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: defaultExtension
+ if (ext != null) {
+ // fallback to provided extension when available,
+ // typically the original file extension when moving/renaming
+ if (ext.startsWith(".")) ext else ".$ext"
+ } else {
+ // fallback to generic extensions,
+ // as incorrect file extensions are better than none for media detection
+ if (isVideo(mimeType)) ".mp4" else ".jpg"
+ }
+ }
}
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
diff --git a/android/app/src/main/res/values-my/strings.xml b/android/app/src/main/res/values-my/strings.xml
index dfacebf6d..ae4237c81 100644
--- a/android/app/src/main/res/values-my/strings.xml
+++ b/android/app/src/main/res/values-my/strings.xml
@@ -8,4 +8,5 @@
ဗီဒီယိုများ
မီဒီယာ ကိုစကင်ဖတ်နေသည်
ရပ်ရန်
-
\ No newline at end of file
+ မြေပုံ
+
diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
index 7643c4193..19b6a15ae 100644
--- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
+++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceFork.java
@@ -22,7 +22,6 @@ import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongA
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
-
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
@@ -91,7 +90,7 @@ import java.util.regex.Pattern;
import java.util.zip.CRC32;
/*
- * Forked from 'androidx.exifinterface:exifinterface:1.4.0'
+ * Forked from 'androidx.exifinterface:exifinterface:1.4.1'
* Named differently to let ExifInterface be loaded as subdependency.
* cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
@@ -139,6 +138,12 @@ public class ExifInterfaceFork {
// TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
+ // TLAD available heap size, to check allocations
+ private long getAvailableHeapSize() {
+ final Runtime runtime = Runtime.getRuntime();
+ return runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory());
+ }
+
private static final String TAG = "ExifInterface";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@@ -4553,7 +4558,7 @@ public class ExifInterfaceFork {
&& (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
|| (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
&& !containsTiff700Xmp)) {
- mXmpFromSeparateMarker = ExifAttribute.createByte(value);
+ mXmpFromSeparateMarker = value != null ? ExifAttribute.createByte(value) : null;
return;
}
}
@@ -6558,8 +6563,9 @@ public class ExifInterfaceFork {
// Exif data in WebP images (e.g.
// https://github.com/ImageMagick/ImageMagick/issues/3140)
if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
- payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length,
- payload.length);
+ payload =
+ Arrays.copyOfRange(
+ payload, IDENTIFIER_EXIF_APP1.length, payload.length);
}
// Save offset to EXIF data for handling thumbnail and attribute offsets.
@@ -6722,8 +6728,11 @@ public class ExifInterfaceFork {
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
boolean needToWriteExif = true;
- boolean needToWriteXmp = mXmpFromSeparateMarker != null;
- while (needToWriteExif || needToWriteXmp) {
+ // Either there's some XMP data to write, or it has been cleared locally but was present in
+ // the file when it was read (and so needs to be removed).
+ boolean needToHandleXmpChunk =
+ mXmpFromSeparateMarker != null || mFileOnDiskContainsSeparateXmpMarker;
+ while (needToWriteExif || needToHandleXmpChunk) {
int chunkLength = dataInputStream.readInt();
int chunkType = dataInputStream.readInt();
if (chunkType == PNG_CHUNK_TYPE_IHDR) {
@@ -6738,7 +6747,7 @@ public class ExifInterfaceFork {
}
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
writePngXmpItxtChunk(dataOutputStream);
- needToWriteXmp = false;
+ needToHandleXmpChunk = false;
}
continue;
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
@@ -6746,10 +6755,25 @@ public class ExifInterfaceFork {
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
needToWriteExif = false;
continue;
- } else if (chunkType == PNG_CHUNK_TYPE_ITXT && needToWriteXmp) {
- writePngXmpItxtChunk(dataOutputStream);
- dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
- needToWriteXmp = false;
+ } else if (chunkType == PNG_CHUNK_TYPE_ITXT
+ && chunkLength >= PNG_ITXT_XMP_KEYWORD.length) {
+ // Read the 17 byte keyword and 5 expected null bytes.
+ byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length];
+ dataInputStream.readFully(keyword);
+ int remainingChunkBytes = chunkLength - keyword.length + PNG_CHUNK_CRC_BYTE_LENGTH;
+ if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) {
+ if (mXmpFromSeparateMarker != null) {
+ writePngXmpItxtChunk(dataOutputStream);
+ }
+ dataInputStream.skipFully(remainingChunkBytes);
+ needToHandleXmpChunk = false;
+ } else {
+ // This is a non-XMP iTXt chunk, so just copy it to the output and continue.
+ dataOutputStream.writeInt(chunkLength);
+ dataOutputStream.writeInt(chunkType);
+ dataOutputStream.write(keyword);
+ copy(dataInputStream, dataOutputStream, remainingChunkBytes);
+ }
continue;
}
dataOutputStream.writeInt(chunkLength);
@@ -7536,6 +7560,13 @@ public class ExifInterfaceFork {
Log.d(TAG, "Invalid strip offset value");
return;
}
+
+ // TLAD start
+ if (bytesToSkip > getAvailableHeapSize()) {
+ throw new IOException("cannot allocate " + bytesToSkip + " bytes to skip to retrieve thumbnail");
+ }
+ // TLAD end
+
try {
in.skipFully(bytesToSkip);
} catch (EOFException e) {
diff --git a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
index 301e306fa..266959a5c 100644
--- a/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
+++ b/android/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtilsFork.java
@@ -31,7 +31,7 @@ import java.io.InputStream;
import java.io.OutputStream;
/*
- * Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
+ * Forked from 'androidx.exifinterface:exifinterface:1.4.1'
* Named differently to let ExifInterface be loaded as subdependency.
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
*/
diff --git a/fastlane/metadata/android/en-US/changelogs/153.txt b/fastlane/metadata/android/en-US/changelogs/153.txt
new file mode 100644
index 000000000..34f08c4ad
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/153.txt
@@ -0,0 +1,4 @@
+In v1.13.2:
+- group albums
+- filter by day of the week
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/15301.txt b/fastlane/metadata/android/en-US/changelogs/15301.txt
new file mode 100644
index 000000000..34f08c4ad
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/15301.txt
@@ -0,0 +1,4 @@
+In v1.13.2:
+- group albums
+- filter by day of the week
+Full changelog available on GitHub
\ No newline at end of file
diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb
index 07e322eb4..02fd24fc6 100644
--- a/lib/l10n/app_ar.arb
+++ b/lib/l10n/app_ar.arb
@@ -599,7 +599,7 @@
"@settingsLanguagePageTitle": {},
"rootDirectoryDescription": "دليل الجذر",
"@rootDirectoryDescription": {},
- "viewDialogGroupSectionTitle": "مجموعة",
+ "viewDialogGroupSectionTitle": "الأقسام",
"@viewDialogGroupSectionTitle": {},
"maxBrightnessAlways": "دائماً",
"@maxBrightnessAlways": {},
@@ -1449,7 +1449,7 @@
"@binPageTitle": {},
"tagPlaceholderState": "الولاية",
"@tagPlaceholderState": {},
- "sortByAlbumFileName": "حسب الألبوم واسم الملف",
+ "sortByAlbumFileName": "حسب عنوان الألبوم والعنصر",
"@sortByAlbumFileName": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
@@ -1599,8 +1599,28 @@
"@sortByPath": {},
"searchFormatSectionTitle": "التنسيقات",
"@searchFormatSectionTitle": {},
- "chipActionGroup": "مجموعة",
+ "chipActionGroup": "تغيير التجميع",
"@chipActionGroup": {},
"createButtonLabel": "خلق",
- "@createButtonLabel": {}
+ "@createButtonLabel": {},
+ "sectionNone": "لا يوجد أقسام",
+ "@sectionNone": {},
+ "chipActionCreateGroup": "إنشاء مجموعة",
+ "@chipActionCreateGroup": {},
+ "albumTierGroups": "المجموعات",
+ "@albumTierGroups": {},
+ "newGroupDialogTitle": "مجموعة جديدة",
+ "@newGroupDialogTitle": {},
+ "newGroupDialogNameLabel": "اسم المجموعة",
+ "@newGroupDialogNameLabel": {},
+ "groupAlreadyExists": "المجموعة موجودة بالفعل",
+ "@groupAlreadyExists": {},
+ "groupEmpty": "لا توجد مجموعات",
+ "@groupEmpty": {},
+ "ungrouped": "غير مجمعة",
+ "@ungrouped": {},
+ "groupPickerTitle": "اختر المجموعة",
+ "@groupPickerTitle": {},
+ "groupPickerUseThisGroupButton": "استخدم هذه المجموعة",
+ "@groupPickerUseThisGroupButton": {}
}
diff --git a/lib/l10n/app_az.arb b/lib/l10n/app_az.arb
index 90ebd78a5..920c5e318 100644
--- a/lib/l10n/app_az.arb
+++ b/lib/l10n/app_az.arb
@@ -142,5 +142,15 @@
"chipActionUnpin": "Sabitləməyin",
"@chipActionUnpin": {},
"chipActionRename": "Bir də adlandır",
- "@chipActionRename": {}
+ "@chipActionRename": {},
+ "chipActionDecompose": "Böl",
+ "@chipActionDecompose": {},
+ "chipActionCreateAlbum": "Albom yarat",
+ "@chipActionCreateAlbum": {},
+ "createButtonLabel": "YARAT",
+ "@createButtonLabel": {},
+ "chipActionGroup": "Qruplandırmanı dəyişdir",
+ "@chipActionGroup": {},
+ "chipActionCreateGroup": "Qrup yarat",
+ "@chipActionCreateGroup": {}
}
diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb
index a31471659..e02243331 100644
--- a/lib/l10n/app_bg.arb
+++ b/lib/l10n/app_bg.arb
@@ -1624,5 +1624,19 @@
"sortByPath": "Според пътя",
"@sortByPath": {},
"searchFormatSectionTitle": "Формати",
- "@searchFormatSectionTitle": {}
+ "@searchFormatSectionTitle": {},
+ "chipActionCreateGroup": "Създайте група",
+ "@chipActionCreateGroup": {},
+ "chipActionGroup": "Групиране",
+ "@chipActionGroup": {},
+ "newGroupDialogTitle": "Нова Група",
+ "@newGroupDialogTitle": {},
+ "groupAlreadyExists": "Групата вече съществува",
+ "@groupAlreadyExists": {},
+ "albumTierGroups": "Групи",
+ "@albumTierGroups": {},
+ "groupPickerUseThisGroupButton": "Използвайте тази група",
+ "@groupPickerUseThisGroupButton": {},
+ "newGroupDialogNameLabel": "Име на групата",
+ "@newGroupDialogNameLabel": {}
}
diff --git a/lib/l10n/app_da.arb b/lib/l10n/app_da.arb
index 46cd5bbbb..bc845fd97 100644
--- a/lib/l10n/app_da.arb
+++ b/lib/l10n/app_da.arb
@@ -850,7 +850,7 @@
"@drawerCollectionRaws": {},
"sortByRating": "Efter bedømmelse",
"@sortByRating": {},
- "sortByAlbumFileName": "Efter album og filnavn",
+ "sortByAlbumFileName": "Efter album og elementtitel",
"@sortByAlbumFileName": {},
"albumGroupVolume": "Efter lagervolume",
"@albumGroupVolume": {},
@@ -1627,7 +1627,7 @@
"@searchFormatSectionTitle": {},
"createButtonLabel": "OPRET",
"@createButtonLabel": {},
- "chipActionGroup": "Gruppér",
+ "chipActionGroup": "Ændr gruppering",
"@chipActionGroup": {},
"chipActionCreateGroup": "Opret gruppe",
"@chipActionCreateGroup": {},
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 50dafb3b1..258a0e464 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -105,7 +105,7 @@
"chipActionLock": "Lock",
"chipActionPin": "Pin to top",
"chipActionUnpin": "Unpin from top",
- "chipActionGroup": "Group",
+ "chipActionGroup": "Change grouping",
"chipActionRename": "Rename",
"chipActionSetCover": "Set cover",
"chipActionShowCountryStates": "Show states",
@@ -767,7 +767,7 @@
"sortByName": "By name",
"sortByItemCount": "By item count",
"sortBySize": "By size",
- "sortByAlbumFileName": "By album & file name",
+ "sortByAlbumFileName": "By album & item title",
"sortByRating": "By rating",
"sortByDuration": "By duration",
"sortByPath": "By path",
diff --git a/lib/l10n/app_et.arb b/lib/l10n/app_et.arb
index fe0e6b1a6..ce5eba554 100644
--- a/lib/l10n/app_et.arb
+++ b/lib/l10n/app_et.arb
@@ -790,7 +790,7 @@
"@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse",
"@aboutLicensesShowAllButtonLabel": {},
- "policyPageTitle": "Privaatsuspoliitika",
+ "policyPageTitle": "Andmekaitsepõhimõtted",
"@policyPageTitle": {},
"collectionPageTitle": "Meediakogu",
"@collectionPageTitle": {},
@@ -1036,7 +1036,7 @@
"@sortBySize": {},
"sortByName": "Nime alusel",
"@sortByName": {},
- "sortByAlbumFileName": "Albumi ja failinime alusel",
+ "sortByAlbumFileName": "Albumi ja objekti nime alusel",
"@sortByAlbumFileName": {},
"sortByRating": "Hinnangu alusel",
"@sortByRating": {},
@@ -1645,7 +1645,7 @@
"@groupPickerUseThisGroupButton": {},
"createButtonLabel": "LOO",
"@createButtonLabel": {},
- "chipActionGroup": "Rühmita",
+ "chipActionGroup": "Muuda grupeerimist",
"@chipActionGroup": {},
"sectionNone": "Rubriike pole",
"@sectionNone": {}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index bd5eedfc9..560c9aad3 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -637,7 +637,7 @@
"@sortByItemCount": {},
"sortBySize": "par taille",
"@sortBySize": {},
- "sortByAlbumFileName": "alphabétique",
+ "sortByAlbumFileName": "par titre d’album et élément",
"@sortByAlbumFileName": {},
"sortByRating": "par notation",
"@sortByRating": {},
@@ -1407,7 +1407,7 @@
"@sortByPath": {},
"searchFormatSectionTitle": "Formats",
"@searchFormatSectionTitle": {},
- "chipActionGroup": "Grouper",
+ "chipActionGroup": "Modifier groupement",
"@chipActionGroup": {},
"createButtonLabel": "CRÉER",
"@createButtonLabel": {},
diff --git a/lib/l10n/app_he.arb b/lib/l10n/app_he.arb
index 0cdef30e9..1b4456987 100644
--- a/lib/l10n/app_he.arb
+++ b/lib/l10n/app_he.arb
@@ -158,5 +158,49 @@
"chipActionCreateGroup": "צור קבוצה",
"@chipActionCreateGroup": {},
"chipActionCreateVault": "צור כספת",
- "@chipActionCreateVault": {}
+ "@chipActionCreateVault": {},
+ "newGroupDialogTitle": "קבוצה חדשה",
+ "@newGroupDialogTitle": {},
+ "groupAlreadyExists": "הקבוצה כבר קיימת",
+ "@groupAlreadyExists": {},
+ "entryActionDelete": "מחיקה",
+ "@entryActionDelete": {},
+ "entryActionConvert": "המרה",
+ "@entryActionConvert": {},
+ "entryActionRotateCCW": "סובב נגד כיוון השעון",
+ "@entryActionRotateCCW": {},
+ "entryActionShare": "שיתוף",
+ "@entryActionShare": {},
+ "entryActionShareVideoOnly": "שיתוף וידיאו בלבד",
+ "@entryActionShareVideoOnly": {},
+ "videoActionSelectStreams": "בחר מסלולים",
+ "@videoActionSelectStreams": {},
+ "videoActionShowPreviousFrame": "הצג פריים קודם",
+ "@videoActionShowPreviousFrame": {},
+ "videoActionShowNextFrame": "הצג פריים הבא",
+ "@videoActionShowNextFrame": {},
+ "chipActionConfigureVault": "הגדרת כספת",
+ "@chipActionConfigureVault": {},
+ "entryActionCopyToClipboard": "הועתק ללוח",
+ "@entryActionCopyToClipboard": {},
+ "entryActionShareImageOnly": "שיתוף תמונה בלבד",
+ "@entryActionShareImageOnly": {},
+ "entryActionRotateCW": "סובב עם כיוון השעון",
+ "@entryActionRotateCW": {},
+ "entryActionFlip": "הפוך אופקית",
+ "@entryActionFlip": {},
+ "entryActionPrint": "הדפסה",
+ "@entryActionPrint": {},
+ "entryActionViewSource": "מקור וידאו",
+ "@entryActionViewSource": {},
+ "entryActionShowGeoTiffOnMap": "הצג כשכבת מפה",
+ "@entryActionShowGeoTiffOnMap": {},
+ "entryActionInfo": "מידע",
+ "@entryActionInfo": {},
+ "entryActionExport": "ייצוא",
+ "@entryActionExport": {},
+ "entryActionRename": "שינוי שם",
+ "@entryActionRename": {},
+ "entryActionRestore": "שחזור",
+ "@entryActionRestore": {}
}
diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb
index 28afd1bd1..41c85250c 100644
--- a/lib/l10n/app_hu.arb
+++ b/lib/l10n/app_hu.arb
@@ -291,7 +291,7 @@
"@tileLayoutMosaic": {},
"tileLayoutGrid": "Rács",
"@tileLayoutGrid": {},
- "viewDialogGroupSectionTitle": "Csoport",
+ "viewDialogGroupSectionTitle": "Szekciók",
"@viewDialogGroupSectionTitle": {},
"menuActionStats": "Statisztikák",
"@menuActionStats": {},
@@ -1594,5 +1594,33 @@
"editEntryLocationDialogTimeShift": "Időeltolódás",
"@editEntryLocationDialogTimeShift": {},
"removeEntryMetadataDialogAll": "Összes",
- "@removeEntryMetadataDialogAll": {}
+ "@removeEntryMetadataDialogAll": {},
+ "sortByPath": "Útvonal szerint",
+ "@sortByPath": {},
+ "chipActionCreateGroup": "Csoport létrehozása",
+ "@chipActionCreateGroup": {},
+ "albumTierGroups": "Csoportok",
+ "@albumTierGroups": {},
+ "chipActionGroup": "Csoportosítás",
+ "@chipActionGroup": {},
+ "createButtonLabel": "LÉTREHOZÁS",
+ "@createButtonLabel": {},
+ "newGroupDialogTitle": "Új csoport",
+ "@newGroupDialogTitle": {},
+ "newGroupDialogNameLabel": "Csoport neve",
+ "@newGroupDialogNameLabel": {},
+ "groupAlreadyExists": "Csoport már létezik",
+ "@groupAlreadyExists": {},
+ "groupEmpty": "Nincsenek csoportok",
+ "@groupEmpty": {},
+ "ungrouped": "Csoportosítatlan",
+ "@ungrouped": {},
+ "groupPickerTitle": "Válassza ki a csoportot",
+ "@groupPickerTitle": {},
+ "groupPickerUseThisGroupButton": "Használja ezt a csoportot",
+ "@groupPickerUseThisGroupButton": {},
+ "searchFormatSectionTitle": "Formátumok",
+ "@searchFormatSectionTitle": {},
+ "sectionNone": "Semmi szerint",
+ "@sectionNone": {}
}
diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb
index 31920cddc..78272d192 100644
--- a/lib/l10n/app_id.arb
+++ b/lib/l10n/app_id.arb
@@ -453,7 +453,7 @@
"@menuActionStats": {},
"viewDialogSortSectionTitle": "Sortir",
"@viewDialogSortSectionTitle": {},
- "viewDialogGroupSectionTitle": "Grup",
+ "viewDialogGroupSectionTitle": "Bagian",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Tata letak",
"@viewDialogLayoutSectionTitle": {},
@@ -1406,5 +1406,29 @@
"sortByPath": "Melalui lokasi",
"@sortByPath": {},
"searchFormatSectionTitle": "Format",
- "@searchFormatSectionTitle": {}
+ "@searchFormatSectionTitle": {},
+ "sectionNone": "Tidak ada bagian",
+ "@sectionNone": {},
+ "albumTierGroups": "Kelompok",
+ "@albumTierGroups": {},
+ "createButtonLabel": "BUAT",
+ "@createButtonLabel": {},
+ "chipActionGroup": "Kelompok",
+ "@chipActionGroup": {},
+ "chipActionCreateGroup": "Buat kelompok",
+ "@chipActionCreateGroup": {},
+ "ungrouped": "Tidak dikelompokkan",
+ "@ungrouped": {},
+ "newGroupDialogTitle": "Kelompok Baru",
+ "@newGroupDialogTitle": {},
+ "newGroupDialogNameLabel": "Nama kelompok",
+ "@newGroupDialogNameLabel": {},
+ "groupAlreadyExists": "Kelompok sudah ada",
+ "@groupAlreadyExists": {},
+ "groupEmpty": "Tidak ada kelompok",
+ "@groupEmpty": {},
+ "groupPickerTitle": "Pilih Kelompok",
+ "@groupPickerTitle": {},
+ "groupPickerUseThisGroupButton": "Gunakan kelompok ini",
+ "@groupPickerUseThisGroupButton": {}
}
diff --git a/lib/l10n/app_is.arb b/lib/l10n/app_is.arb
index 8cc3550cf..7f02e481a 100644
--- a/lib/l10n/app_is.arb
+++ b/lib/l10n/app_is.arb
@@ -1346,7 +1346,7 @@
"@binPageTitle": {},
"tagPlaceholderState": "Hérað",
"@tagPlaceholderState": {},
- "sortByAlbumFileName": "Eftir heiti albúma og skráa",
+ "sortByAlbumFileName": "Eftir heiti albúma og atriða",
"@sortByAlbumFileName": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Eyða þessum albúmum og atriðinu í þeim?} other{Eyða þessum albúmum og {count} atriðum í þeim??}}",
"@deleteMultiAlbumConfirmationDialogMessage": {
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
index d4c4dc39a..3dda76d12 100644
--- a/lib/l10n/app_ja.arb
+++ b/lib/l10n/app_ja.arb
@@ -317,7 +317,7 @@
"@binEntriesConfirmationDialogMessage": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}",
"@deleteEntriesConfirmationDialogMessage": {},
- "moveUndatedConfirmationDialogMessage": "いくつかのアイテムはメタデータ上に日付がありません。メタデータ上の日付が設定されない場合、この操作によりこれらの現在の日付はリセットされます",
+ "moveUndatedConfirmationDialogMessage": "続行する前にアイテムの日付を保存しますか?",
"@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "日付を設定",
"@moveUndatedConfirmationDialogSetDate": {},
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
index 1b98169ea..ba7163367 100644
--- a/lib/l10n/app_ko.arb
+++ b/lib/l10n/app_ko.arb
@@ -637,7 +637,7 @@
"@sortByItemCount": {},
"sortBySize": "크기",
"@sortBySize": {},
- "sortByAlbumFileName": "이름",
+ "sortByAlbumFileName": "앨범 및 항목 제목",
"@sortByAlbumFileName": {},
"sortByRating": "별점",
"@sortByRating": {},
@@ -1409,7 +1409,7 @@
"@searchFormatSectionTitle": {},
"chipActionCreateGroup": "그룹 만들기",
"@chipActionCreateGroup": {},
- "chipActionGroup": "그룹으로 이동",
+ "chipActionGroup": "그룹 변경",
"@chipActionGroup": {},
"albumTierGroups": "그룹",
"@albumTierGroups": {},
diff --git a/lib/l10n/app_my.arb b/lib/l10n/app_my.arb
index eea760908..fdb5da7fb 100644
--- a/lib/l10n/app_my.arb
+++ b/lib/l10n/app_my.arb
@@ -1312,5 +1312,19 @@
"settingsVideoPlaybackTile": "ဖွင့်ကြည့်ခြင်း",
"@settingsVideoPlaybackTile": {},
"chipActionShowCollection": "စုစည်းမှုထဲမှာ ပြရန်",
- "@chipActionShowCollection": {}
+ "@chipActionShowCollection": {},
+ "chipActionDecompose": "ဖြတ်ထုတ်ရန်",
+ "@chipActionDecompose": {},
+ "chipActionGroup": "အုပ်စုဖွဲ့မည်",
+ "@chipActionGroup": {},
+ "stopTooltip": "ရပ်ရန်",
+ "@stopTooltip": {},
+ "createButtonLabel": "အသစ်ထည့်ရန်",
+ "@createButtonLabel": {},
+ "chipActionRemove": "ဖယ်ရှားမည်",
+ "@chipActionRemove": {},
+ "chipActionGoToExplorerPage": "Explorer ထဲတွင်ပြမည်",
+ "@chipActionGoToExplorerPage": {},
+ "chipActionCreateGroup": "အုပ်စုအသစ်ပြုလုပ်မည်",
+ "@chipActionCreateGroup": {}
}
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index 8c5a1ef28..dd6de2ab7 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -627,7 +627,7 @@
"@sortByItemCount": {},
"sortBySize": "Op grootte",
"@sortBySize": {},
- "sortByAlbumFileName": "Op album- en bestandsnaam",
+ "sortByAlbumFileName": "Op album- en itemnaam",
"@sortByAlbumFileName": {},
"sortByRating": "Op waardering",
"@sortByRating": {},
@@ -1416,7 +1416,7 @@
"@groupPickerUseThisGroupButton": {},
"newGroupDialogTitle": "Nieuwe groep",
"@newGroupDialogTitle": {},
- "chipActionGroup": "Groeperen",
+ "chipActionGroup": "Groepering wijzigen",
"@chipActionGroup": {},
"chipActionCreateGroup": "Groep aanmaken",
"@chipActionCreateGroup": {},
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index cb2d24739..083d7d734 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -769,7 +769,7 @@
"@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "Nieprzetworzone zdjęcia",
"@drawerCollectionRaws": {},
- "sortByAlbumFileName": "Według albumu i nazwy pliku",
+ "sortByAlbumFileName": "Według albumu i nazwy elementu",
"@sortByAlbumFileName": {},
"albumMimeTypeMixed": "Mieszane",
"@albumMimeTypeMixed": {},
@@ -1603,7 +1603,7 @@
"@sectionNone": {},
"createButtonLabel": "UTWÓRZ",
"@createButtonLabel": {},
- "chipActionGroup": "Grupuj",
+ "chipActionGroup": "Zmień grupowanie",
"@chipActionGroup": {},
"chipActionCreateGroup": "Utwórz grupę",
"@chipActionCreateGroup": {},
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index e99663ee3..7cb1973b1 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -463,7 +463,7 @@
"@menuActionStats": {},
"viewDialogSortSectionTitle": "Organizar",
"@viewDialogSortSectionTitle": {},
- "viewDialogGroupSectionTitle": "Grupo",
+ "viewDialogGroupSectionTitle": "Seções",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Layout",
"@viewDialogLayoutSectionTitle": {},
@@ -633,7 +633,7 @@
"@sortByItemCount": {},
"sortBySize": "Por tamanho",
"@sortBySize": {},
- "sortByAlbumFileName": "Por álbum e nome de arquivo",
+ "sortByAlbumFileName": "Por álbum e título do item",
"@sortByAlbumFileName": {},
"sortByRating": "Por classificação",
"@sortByRating": {},
@@ -1406,5 +1406,29 @@
"sortByPath": "Pelo caminho",
"@sortByPath": {},
"searchFormatSectionTitle": "Formatos",
- "@searchFormatSectionTitle": {}
+ "@searchFormatSectionTitle": {},
+ "createButtonLabel": "CRIAR",
+ "@createButtonLabel": {},
+ "chipActionGroup": "Alterar agrupamento",
+ "@chipActionGroup": {},
+ "chipActionCreateGroup": "Criar grupo",
+ "@chipActionCreateGroup": {},
+ "albumTierGroups": "Grupos",
+ "@albumTierGroups": {},
+ "newGroupDialogTitle": "Novo Grupo",
+ "@newGroupDialogTitle": {},
+ "newGroupDialogNameLabel": "Nome do grupo",
+ "@newGroupDialogNameLabel": {},
+ "groupAlreadyExists": "O grupo já existe",
+ "@groupAlreadyExists": {},
+ "groupEmpty": "Nenhum grupo",
+ "@groupEmpty": {},
+ "ungrouped": "Desagrupado",
+ "@ungrouped": {},
+ "groupPickerTitle": "Selecionar Grupo",
+ "@groupPickerTitle": {},
+ "groupPickerUseThisGroupButton": "Usar este grupo",
+ "@groupPickerUseThisGroupButton": {},
+ "sectionNone": "Nenhuma seção",
+ "@sectionNone": {}
}
diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb
index 0fec3d949..0ec2a544c 100644
--- a/lib/l10n/app_ro.arb
+++ b/lib/l10n/app_ro.arb
@@ -526,7 +526,7 @@
"@menuActionStats": {},
"viewDialogSortSectionTitle": "Sortează",
"@viewDialogSortSectionTitle": {},
- "viewDialogGroupSectionTitle": "Grup",
+ "viewDialogGroupSectionTitle": "Secțiuni",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Aspect",
"@viewDialogLayoutSectionTitle": {},
@@ -887,7 +887,7 @@
"@drawerCollectionSphericalVideos": {},
"drawerAlbumPage": "Albume",
"@drawerAlbumPage": {},
- "sortByAlbumFileName": "După album și numele fișierului",
+ "sortByAlbumFileName": "După album și numele elementului",
"@sortByAlbumFileName": {},
"sortOrderZtoA": "De la Z la A",
"@sortOrderZtoA": {},
@@ -1598,5 +1598,29 @@
"sortByPath": "După cale",
"@sortByPath": {},
"searchFormatSectionTitle": "Formate",
- "@searchFormatSectionTitle": {}
+ "@searchFormatSectionTitle": {},
+ "createButtonLabel": "CREARE",
+ "@createButtonLabel": {},
+ "chipActionCreateGroup": "Creați un grup",
+ "@chipActionCreateGroup": {},
+ "newGroupDialogTitle": "Grup nou",
+ "@newGroupDialogTitle": {},
+ "newGroupDialogNameLabel": "Nume grup",
+ "@newGroupDialogNameLabel": {},
+ "groupAlreadyExists": "Grupul deja există",
+ "@groupAlreadyExists": {},
+ "chipActionGroup": "Grupe",
+ "@chipActionGroup": {},
+ "albumTierGroups": "Grupe",
+ "@albumTierGroups": {},
+ "groupPickerTitle": "Alege un grup",
+ "@groupPickerTitle": {},
+ "groupPickerUseThisGroupButton": "Folosește acest grup",
+ "@groupPickerUseThisGroupButton": {},
+ "sectionNone": "Nicio secțiune",
+ "@sectionNone": {},
+ "ungrouped": "Fără grup",
+ "@ungrouped": {},
+ "groupEmpty": "Niciun grup",
+ "@groupEmpty": {}
}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 4313c7ee2..b39efc64f 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -463,7 +463,7 @@
"@menuActionStats": {},
"viewDialogSortSectionTitle": "Сортировка",
"@viewDialogSortSectionTitle": {},
- "viewDialogGroupSectionTitle": "Группировка",
+ "viewDialogGroupSectionTitle": "Разделы",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Макет",
"@viewDialogLayoutSectionTitle": {},
@@ -633,7 +633,7 @@
"@sortByItemCount": {},
"sortBySize": "По размеру",
"@sortBySize": {},
- "sortByAlbumFileName": "По имени альбома и файла",
+ "sortByAlbumFileName": "По названию альбома и пункта",
"@sortByAlbumFileName": {},
"sortByRating": "По рейтингу",
"@sortByRating": {},
@@ -1406,5 +1406,29 @@
"searchFormatSectionTitle": "Форматы",
"@searchFormatSectionTitle": {},
"sortByPath": "По пути",
- "@sortByPath": {}
+ "@sortByPath": {},
+ "chipActionGroup": "Изменить группировку",
+ "@chipActionGroup": {},
+ "createButtonLabel": "СОЗДАТЬ",
+ "@createButtonLabel": {},
+ "chipActionCreateGroup": "Создать группу",
+ "@chipActionCreateGroup": {},
+ "albumTierGroups": "Группы",
+ "@albumTierGroups": {},
+ "newGroupDialogTitle": "Новая группа",
+ "@newGroupDialogTitle": {},
+ "newGroupDialogNameLabel": "Название группы",
+ "@newGroupDialogNameLabel": {},
+ "groupAlreadyExists": "Группа уже существует",
+ "@groupAlreadyExists": {},
+ "groupEmpty": "Групп нету",
+ "@groupEmpty": {},
+ "ungrouped": "Без группировки",
+ "@ungrouped": {},
+ "groupPickerTitle": "Выбор группы",
+ "@groupPickerTitle": {},
+ "groupPickerUseThisGroupButton": "Использовать эту группу",
+ "@groupPickerUseThisGroupButton": {},
+ "sectionNone": "Без разделов",
+ "@sectionNone": {}
}
diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb
index dbbfd44fb..25677d9f2 100644
--- a/lib/l10n/app_tr.arb
+++ b/lib/l10n/app_tr.arb
@@ -417,7 +417,7 @@
"@menuActionStats": {},
"viewDialogSortSectionTitle": "Sırala",
"@viewDialogSortSectionTitle": {},
- "viewDialogGroupSectionTitle": "Grup",
+ "viewDialogGroupSectionTitle": "Bölümler",
"@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Düzen",
"@viewDialogLayoutSectionTitle": {},
@@ -489,7 +489,7 @@
"@collectionActionHideTitleSearch": {},
"collectionActionAddShortcut": "Kısayol ekle",
"@collectionActionAddShortcut": {},
- "collectionActionEmptyBin": "Boş çöp kutusu",
+ "collectionActionEmptyBin": "Çöp kutusu boş",
"@collectionActionEmptyBin": {},
"collectionActionCopy": "Albüme kopyala",
"@collectionActionCopy": {},
@@ -583,7 +583,7 @@
"@sortByItemCount": {},
"sortBySize": "Boyuta göre",
"@sortBySize": {},
- "sortByAlbumFileName": "Albüm ve dosya adına göre",
+ "sortByAlbumFileName": "Albüm ve başlığı göre",
"@sortByAlbumFileName": {},
"sortByRating": "Derecelendirmeye göre",
"@sortByRating": {},
@@ -1400,5 +1400,35 @@
"collectionActionAddDynamicAlbum": "Dinamik albüm ekle",
"@collectionActionAddDynamicAlbum": {},
"searchFormatSectionTitle": "Biçimler",
- "@searchFormatSectionTitle": {}
+ "@searchFormatSectionTitle": {},
+ "createButtonLabel": "YARAT",
+ "@createButtonLabel": {},
+ "chipActionGroup": "Gruplandırmayı değiştir",
+ "@chipActionGroup": {},
+ "chipActionCreateGroup": "Grup oluştur",
+ "@chipActionCreateGroup": {},
+ "albumTierGroups": "Gruplar",
+ "@albumTierGroups": {},
+ "coordinateFormatDdm": "DDS",
+ "@coordinateFormatDdm": {},
+ "newGroupDialogTitle": "Yeni grup",
+ "@newGroupDialogTitle": {},
+ "newGroupDialogNameLabel": "Grup adı",
+ "@newGroupDialogNameLabel": {},
+ "groupAlreadyExists": "Grup zaten var",
+ "@groupAlreadyExists": {},
+ "groupEmpty": "Grup yok",
+ "@groupEmpty": {},
+ "ungrouped": "Gruplandırılmamış",
+ "@ungrouped": {},
+ "groupPickerTitle": "Grubu seç",
+ "@groupPickerTitle": {},
+ "groupPickerUseThisGroupButton": "Bu grubu kullan",
+ "@groupPickerUseThisGroupButton": {},
+ "sectionNone": "Bölüm yok",
+ "@sectionNone": {},
+ "sortByPath": "Yolu",
+ "@sortByPath": {},
+ "editEntryLocationDialogTimeShift": "Zaman farkı",
+ "@editEntryLocationDialogTimeShift": {}
}
diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb
index fbb700f5c..7d123bc3d 100644
--- a/lib/l10n/app_uk.arb
+++ b/lib/l10n/app_uk.arb
@@ -607,7 +607,7 @@
"@drawerCountryPage": {},
"sortByName": "За назвою",
"@sortByName": {},
- "sortByAlbumFileName": "За назвою альбому та файлу",
+ "sortByAlbumFileName": "За назвою альбому та елемента",
"@sortByAlbumFileName": {},
"sortByItemCount": "За кількістю елементів",
"@sortByItemCount": {},
@@ -1601,7 +1601,7 @@
"@sortByPath": {},
"createButtonLabel": "СТВОРИТИ",
"@createButtonLabel": {},
- "chipActionGroup": "Згрупувати",
+ "chipActionGroup": "Змінити групування",
"@chipActionGroup": {},
"chipActionCreateGroup": "Створити групу",
"@chipActionCreateGroup": {},
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 2ae75db41..a82594120 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -625,7 +625,7 @@
"@sortByItemCount": {},
"sortBySize": "按大小",
"@sortBySize": {},
- "sortByAlbumFileName": "按相册和文件名",
+ "sortByAlbumFileName": "按相册和项目标题",
"@sortByAlbumFileName": {},
"sortByRating": "按评分",
"@sortByRating": {},
@@ -1419,7 +1419,7 @@
"@newGroupDialogTitle": {},
"createButtonLabel": "创建",
"@createButtonLabel": {},
- "chipActionGroup": "分组",
+ "chipActionGroup": "更改分组",
"@chipActionGroup": {},
"groupAlreadyExists": "组已存在",
"@groupAlreadyExists": {},
diff --git a/lib/model/app/contributors.dart b/lib/model/app/contributors.dart
index f2c810b43..7b511b648 100644
--- a/lib/model/app/contributors.dart
+++ b/lib/model/app/contributors.dart
@@ -140,10 +140,14 @@ class Contributors {
Contributor('Miquel Martí', 'miquelmarti111@gmail.com'),
Contributor('Yurt Page', 'yurtpage@gmail.com'),
Contributor('Murcielago', 'weblate.j9bmx@slmail.me'),
+ Contributor('vm', 'varga.m007@gmail.com'),
+ Contributor('WMatheist', 'wmatheist@protonmail.com'),
// Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani
+ // Contributor('Jamil Farajov', 'jamilfarajov@gmail.com'), // Azerbaijani
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
+ // Contributor('Thit Lwin', 'thitlwincoder@gmail.com'), // Burmese
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
// Contributor('Ricky Tigg', 'ricky.tigg@gmail.com'), // Finnish
diff --git a/lib/model/app/dependencies.dart b/lib/model/app/dependencies.dart
index cf408038b..c660a2a87 100644
--- a/lib/model/app/dependencies.dart
+++ b/lib/model/app/dependencies.dart
@@ -212,9 +212,9 @@ class Dependencies {
sourceUrl: 'https://github.com/fleaflet/flutter_map',
),
Dependency(
- name: 'Flutter Markdown',
+ name: 'Flutter Markdown Plus',
license: bsd3,
- sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/flutter_markdown',
+ sourceUrl: 'https://github.com/foresightmobile/flutter_markdown_plus',
),
Dependency(
name: 'Flutter Staggered Animations',
diff --git a/lib/model/covers.dart b/lib/model/covers.dart
index a99308f35..0dcb9e023 100644
--- a/lib/model/covers.dart
+++ b/lib/model/covers.dart
@@ -25,7 +25,7 @@ final Covers covers = Covers._private();
typedef CoverProps = (int? entryId, String? packageName, Color? color);
class Covers {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final _lock = Lock();
final StreamController?> _entryChangeStreamController = StreamController.broadcast();
@@ -40,6 +40,8 @@ class Covers {
Set _rows = {};
+ // do not subscribe to events from other modules in constructor
+ // so that modules can subscribe to each other
Covers._private();
Future init() async {
diff --git a/lib/model/dynamic_albums.dart b/lib/model/dynamic_albums.dart
index 2313f0773..f943803bb 100644
--- a/lib/model/dynamic_albums.dart
+++ b/lib/model/dynamic_albums.dart
@@ -15,19 +15,21 @@ import 'package:synchronized/synchronized.dart';
final DynamicAlbums dynamicAlbums = DynamicAlbums._private();
class DynamicAlbums with ChangeNotifier {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final _lock = Lock();
Set _rows = {};
final EventBus eventBus = EventBus();
+ // do not subscribe to events from other modules in constructor
+ // so that modules can subscribe to each other
DynamicAlbums._private() {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
- _subscriptions.add(albumGrouping.eventBus.on().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
}
Future init() async {
_rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet();
+ _subscriptions.add(albumGrouping.eventBus.on().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
}
int get count => _rows.length;
@@ -57,6 +59,7 @@ class DynamicAlbums with ChangeNotifier {
await _lock.synchronized(() async {
await _doRemove(filters.map((filter) => filter.name).toSet());
notifyListeners();
+ eventBus.fire(DynamicAlbumChangedEvent(Map.fromEntries(filters.map((v) => MapEntry(v, null)))));
});
}
@@ -81,13 +84,7 @@ class DynamicAlbums with ChangeNotifier {
});
}
- Future clear() async {
- await _lock.synchronized(() async {
- await localMediaDb.clearDynamicAlbums();
- _rows.clear();
- notifyListeners();
- });
- }
+ Future clear() => remove(all);
DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name);
diff --git a/lib/model/entry/extensions/catalog.dart b/lib/model/entry/extensions/catalog.dart
index 5312f8655..900cfe8e5 100644
--- a/lib/model/entry/extensions/catalog.dart
+++ b/lib/model/entry/extensions/catalog.dart
@@ -4,8 +4,8 @@ import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/keys.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/media/geotiff.dart';
-import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/media/video/metadata.dart';
+import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart';
diff --git a/lib/model/filters/container/set_and.dart b/lib/model/filters/container/set_and.dart
index 396f5d2ea..82b09fe90 100644
--- a/lib/model/filters/container/set_and.dart
+++ b/lib/model/filters/container/set_and.dart
@@ -1,7 +1,7 @@
import 'package:aves/model/filters/container/container.dart';
+import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart';
-import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/theme/icons.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
@@ -38,8 +38,6 @@ class SetAndFilter extends CollectionFilter with ContainerFilter {
static SetAndFilter? fromMap(Map json) {
final filters = (json['filters'] as List).cast().map(CollectionFilter.fromJson).nonNulls.toSet();
- if (filters.isEmpty) return null;
-
return SetAndFilter(
filters,
reversed: json['reversed'] ?? false,
diff --git a/lib/model/filters/container/set_or.dart b/lib/model/filters/container/set_or.dart
index 10d1f74de..27a271bbf 100644
--- a/lib/model/filters/container/set_or.dart
+++ b/lib/model/filters/container/set_or.dart
@@ -38,8 +38,6 @@ class SetOrFilter extends CollectionFilter with ContainerFilter {
static SetOrFilter? fromMap(Map json) {
final filters = (json['filters'] as List).cast().map(CollectionFilter.fromJson).nonNulls.toSet();
- if (filters.isEmpty) return null;
-
return SetOrFilter(
filters,
reversed: json['reversed'] ?? false,
diff --git a/lib/model/filters/covered/covered.dart b/lib/model/filters/covered/covered.dart
index bea2e8506..6e4b2c6f4 100644
--- a/lib/model/filters/covered/covered.dart
+++ b/lib/model/filters/covered/covered.dart
@@ -14,4 +14,3 @@ mixin CoveredFilter on CollectionFilter {
return super.color(context);
}
}
-
diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart
index 5aa653568..cfc8c608f 100644
--- a/lib/model/filters/filters.dart
+++ b/lib/model/filters/filters.dart
@@ -2,9 +2,11 @@ import 'dart:convert';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/aspect_ratio.dart';
-import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/container/album_group.dart';
import 'package:aves/model/filters/container/dynamic_album.dart';
+import 'package:aves/model/filters/container/set_and.dart';
+import 'package:aves/model/filters/container/set_or.dart';
+import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/covered/tag.dart';
@@ -17,8 +19,6 @@ import 'package:aves/model/filters/placeholder.dart';
import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/recent.dart';
-import 'package:aves/model/filters/container/set_and.dart';
-import 'package:aves/model/filters/container/set_or.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/filters/type.dart';
import 'package:aves/model/filters/weekday.dart';
diff --git a/lib/model/grouping/common.dart b/lib/model/grouping/common.dart
index 423d86fbd..e91a1c13a 100644
--- a/lib/model/grouping/common.dart
+++ b/lib/model/grouping/common.dart
@@ -1,10 +1,17 @@
+import 'dart:async';
import 'dart:convert';
+import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/filters/container/album_group.dart';
+import 'package:aves/model/filters/container/dynamic_album.dart';
import 'package:aves/model/filters/container/group_base.dart';
import 'package:aves/model/filters/container/set_or.dart';
+import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/grouping/convert.dart';
+import 'package:aves/model/source/album.dart';
+import 'package:aves/model/source/collection_source.dart';
+import 'package:aves/model/source/events.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart';
@@ -28,18 +35,53 @@ class FilterGrouping with ChangeNotifier {
final String _host;
final T Function(Uri uri, SetOrFilter filter) _createGroupFilter;
final Map> _groups = {};
+ final Set _subscriptions = {};
+ final Map> _sourceSubscriptions = {};
+ CollectionSource? _source;
Map> get allGroups => Map.unmodifiable(_groups);
+ // do not subscribe to events from other modules in constructor
+ // so that modules can subscribe to each other
FilterGrouping._private(this._host, this._createGroupFilter) {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
}
- void init(Map> groups) {
+ void init() {
+ _subscriptions.add(dynamicAlbums.eventBus.on().listen((e) => _clearObsoleteFilters()));
+ }
+
+ void setGroups(Map> groups) {
_groups.clear();
_groups.addAll(groups);
}
+ @override
+ void dispose() {
+ _subscriptions
+ ..forEach((sub) => sub.cancel())
+ ..clear();
+ _sourceSubscriptions.keys.toSet().forEach(unregisterSource);
+ super.dispose();
+ }
+
+ void registerSource(CollectionSource source) {
+ unregisterSource(_source);
+ final sourceEvents = source.eventBus;
+ _sourceSubscriptions[source] = {
+ sourceEvents.on().listen((e) => _clearObsoleteFilters()),
+ sourceEvents.on().listen((e) => _clearObsoleteFilters()),
+ sourceEvents.on().listen((e) => _clearObsoleteFilters()),
+ };
+ _source = source;
+ }
+
+ void unregisterSource(CollectionSource? source) {
+ _sourceSubscriptions.remove(source)
+ ?..forEach((sub) => sub.cancel())
+ ..clear();
+ }
+
void addToGroup(Set childrenUris, Uri? destinationGroup) {
_removeFromGroups(childrenUris);
if (destinationGroup != null) {
@@ -73,9 +115,9 @@ class FilterGrouping with ChangeNotifier {
int countLeaves(Uri? groupUri) {
int count = 0;
if (groupUri != null) {
- final childrenUri = _groups[groupUri];
- if (childrenUri != null) {
- childrenUri.map(uriToFilter).nonNulls.forEach((filter) {
+ final childrenUris = _groups[groupUri];
+ if (childrenUris != null) {
+ childrenUris.map(uriToFilter).nonNulls.forEach((filter) {
if (filter is GroupBaseFilter) {
count += countLeaves(filter.uri);
} else {
@@ -93,15 +135,15 @@ class FilterGrouping with ChangeNotifier {
if (currentGroupUri == null) {
return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) {
final groupUri = kv.key;
- final childrenUri = kv.value;
- final childrenFilters = childrenUri.map(uriToFilter).nonNulls.toSet();
+ final childrenUris = kv.value;
+ final childrenFilters = childrenUris.map(uriToFilter).nonNulls.toSet();
return _createGroupFilter(groupUri, SetOrFilter(childrenFilters));
}).toSet();
}
- final childrenUri = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
- if (childrenUri != null) {
- return childrenUri.map(uriToFilter).nonNulls.toSet();
+ final childrenUris = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
+ if (childrenUris != null) {
+ return childrenUris.map(uriToFilter).nonNulls.toSet();
}
return {};
@@ -172,6 +214,46 @@ class FilterGrouping with ChangeNotifier {
}
}
+ void _clearObsoleteFilters() {
+ final source = _source;
+ if (source == null || source.targetScope != CollectionSource.fullScope || !source.isReady) return;
+
+ _groups.entries.forEach((kv) {
+ final groupUri = kv.key;
+ final childrenUris = kv.value;
+
+ final rawAlbums = source.rawAlbums;
+ final allEntries = source.allEntries;
+
+ childrenUris.toSet().forEach((childUri) {
+ final filter = uriToFilter(childUri);
+ var valid = false;
+ if (filter != null) {
+ switch (filter) {
+ case GroupBaseFilter _:
+ valid = true;
+ case StoredAlbumFilter _:
+ // check album itself
+ final isVisibleAlbum = rawAlbums.contains(filter.album);
+ if (isVisibleAlbum) {
+ valid = true;
+ } else {
+ // check non-visible content (hidden, trash, etc.)
+ valid = allEntries.any(filter.test);
+ }
+ case DynamicAlbumFilter _:
+ valid = dynamicAlbums.contains(filter.name);
+ }
+ }
+ if (!valid) {
+ childrenUris.remove(childUri);
+ debugPrint('Removed obsolete childUri=$childUri from group=$groupUri');
+ }
+ });
+ });
+ _cleanEmptyGroups();
+ }
+
// group uri / filter conversion
static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey];
diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart
index 873900386..592e1768b 100644
--- a/lib/model/settings/settings.dart
+++ b/lib/model/settings/settings.dart
@@ -46,7 +46,7 @@ import 'package:latlong2/latlong.dart';
final Settings settings = Settings._private();
class Settings with ChangeNotifier, SettingsAccess, SearchSettings, AppSettings, CollectionSettings, DebugSettings, DisplaySettings, FilterGridsSettings, InfoSettings, NavigationSettings, PrivacySettings, ScreenSaverSettings, SlideshowSettings, SubtitlesSettings, VideoSettings, ViewerSettings, WidgetSettings {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
final StreamController _updateStreamController = StreamController.broadcast();
final StreamController _updateTileExtentStreamController = StreamController.broadcast();
diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart
index 97b29fc9d..f8be3a91d 100644
--- a/lib/model/source/collection_lens.dart
+++ b/lib/model/source/collection_lens.dart
@@ -34,7 +34,7 @@ class CollectionLens with ChangeNotifier {
EntrySortFactor sortFactor;
bool sortReverse;
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
- final List _subscriptions = [];
+ final Set _subscriptions = {};
int? id;
bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
List? fixedSelection;
diff --git a/lib/model/source/media_store_source.dart b/lib/model/source/media_store_source.dart
index eb53c729e..7d1f1f131 100644
--- a/lib/model/source/media_store_source.dart
+++ b/lib/model/source/media_store_source.dart
@@ -60,7 +60,9 @@ class MediaStoreSource extends CollectionSource {
await localMediaDb.init();
await vaults.init();
await favourites.init();
- albumGrouping.init(settings.albumGroups);
+ albumGrouping.init();
+ albumGrouping.setGroups(settings.albumGroups);
+ albumGrouping.registerSource(this);
await covers.init();
await dynamicAlbums.init();
diff --git a/lib/model/vaults/vaults.dart b/lib/model/vaults/vaults.dart
index 7f80c563a..c2bfd653a 100644
--- a/lib/model/vaults/vaults.dart
+++ b/lib/model/vaults/vaults.dart
@@ -15,7 +15,7 @@ import 'package:provider/provider.dart';
final Vaults vaults = Vaults._private();
class Vaults extends ChangeNotifier {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
Set _rows = {};
final Set _unlockedDirPaths = {};
diff --git a/lib/services/media/media_fetch_service.dart b/lib/services/media/media_fetch_service.dart
index 4d1bb10b0..057f544aa 100644
--- a/lib/services/media/media_fetch_service.dart
+++ b/lib/services/media/media_fetch_service.dart
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:math';
+import 'dart:ui' as ui;
import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/entry.dart';
@@ -13,7 +14,6 @@ import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart';
-import 'dart:ui' as ui;
abstract class MediaFetchService {
Future getEntry(String uri, String? mimeType, {bool allowUnsized = false});
diff --git a/lib/services/media/media_session_service.dart b/lib/services/media/media_session_service.dart
index 8b970a7a6..33db933ac 100644
--- a/lib/services/media/media_session_service.dart
+++ b/lib/services/media/media_session_service.dart
@@ -25,7 +25,7 @@ abstract class MediaSessionService {
class PlatformMediaSessionService implements MediaSessionService, Disposable {
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command');
final StreamController _streamController = StreamController.broadcast();
diff --git a/lib/services/window_service.dart b/lib/services/window_service.dart
index f0a9b8087..341f5ad07 100644
--- a/lib/services/window_service.dart
+++ b/lib/services/window_service.dart
@@ -74,20 +74,24 @@ class PlatformWindowService implements WindowService {
return false;
}
+ // cf https://developer.android.com/guide/topics/manifest/activity-element#screen
+ // cf Android `ActivityInfo.ScreenOrientation`
+ static const screenOrientationUnspecified = -1; // SCREEN_ORIENTATION_UNSPECIFIED
+ // use the `USER` variants rather than the `SENSOR` ones,
+ // so that it does not flip even if it is reversed by sensor
+ static const screenOrientationUserLandscape = 11; // SCREEN_ORIENTATION_USER_LANDSCAPE
+ static const screenOrientationUserPortrait = 12; // SCREEN_ORIENTATION_USER_PORTRAIT
+
@override
Future requestOrientation([Orientation? orientation]) async {
- // cf Android `ActivityInfo.ScreenOrientation`
late final int orientationCode;
switch (orientation) {
case Orientation.landscape:
- // SCREEN_ORIENTATION_SENSOR_LANDSCAPE
- orientationCode = 6;
+ orientationCode = screenOrientationUserLandscape;
case Orientation.portrait:
- // SCREEN_ORIENTATION_SENSOR_PORTRAIT
- orientationCode = 7;
+ orientationCode = screenOrientationUserPortrait;
default:
- // SCREEN_ORIENTATION_UNSPECIFIED
- orientationCode = -1;
+ orientationCode = screenOrientationUnspecified;
}
try {
await _platform.invokeMethod('requestOrientation', {
diff --git a/lib/theme/styles.dart b/lib/theme/styles.dart
index bc679416d..94b0a21ac 100644
--- a/lib/theme/styles.dart
+++ b/lib/theme/styles.dart
@@ -1,4 +1,3 @@
-
import 'package:flutter/painting.dart';
class AStyles {
diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart
index 0c4b507d0..45e76d824 100644
--- a/lib/widgets/aves_app.dart
+++ b/lib/widgets/aves_app.dart
@@ -160,7 +160,7 @@ class AvesApp extends StatefulWidget {
}
class _AvesAppState extends State with WidgetsBindingObserver {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
late final Future _appSetup;
late final Future _shouldUseBoldFontLoader;
final TvRailController _tvRailController = TvRailController();
diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart
index f85689966..7cfa92611 100644
--- a/lib/widgets/collection/app_bar.dart
+++ b/lib/widgets/collection/app_bar.dart
@@ -4,9 +4,9 @@ import 'dart:math';
import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/container/dynamic_album.dart';
+import 'package:aves/model/filters/container/set_and.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart';
-import 'package:aves/model/filters/container/set_and.dart';
import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart';
@@ -57,7 +57,7 @@ class CollectionAppBar extends StatefulWidget {
}
class _CollectionAppBarState extends State with SingleTickerProviderStateMixin, WidgetsBindingObserver {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
late AnimationController _browseToSelectAnimation;
final ValueNotifier _isSelectingNotifier = ValueNotifier(false);
diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart
index e2631b6bd..3d4d958f6 100644
--- a/lib/widgets/collection/collection_page.dart
+++ b/lib/widgets/collection/collection_page.dart
@@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
}
class _CollectionPageState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
late CollectionLens _collection;
final StreamController _draggableScrollBarEventStreamController = StreamController.broadcast();
diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart
index 407f61315..53edf37fe 100644
--- a/lib/widgets/collection/entry_set_action_delegate.dart
+++ b/lib/widgets/collection/entry_set_action_delegate.dart
@@ -10,8 +10,8 @@ import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/container/dynamic_album.dart';
-import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/container/set_and.dart';
+import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/grouping/common.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/metadata/date_modifier.dart';
diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart
index a88ff1be9..b565d7bc6 100644
--- a/lib/widgets/collection/filter_bar.dart
+++ b/lib/widgets/collection/filter_bar.dart
@@ -9,8 +9,8 @@ import 'package:provider/provider.dart';
class FilterBar extends StatefulWidget {
static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4);
static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4);
- static const double verticalPadding = 16;
- static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding;
+ static const EdgeInsets padding = EdgeInsets.only(top: 4, bottom: 8);
+ static final double preferredHeight = AvesFilterChip.minChipHeight + padding.vertical;
final List filters;
final bool interactive;
@@ -84,6 +84,7 @@ class _FilterBarState extends State {
return Container(
// specify transparent as a workaround to prevent
// chip border clipping when the floating app bar is fading
+ padding: FilterBar.padding,
color: Colors.transparent,
height: FilterBar.preferredHeight,
child: AnimatedList(
diff --git a/lib/widgets/common/action_controls/quick_choosers/common/menu.dart b/lib/widgets/common/action_controls/quick_choosers/common/menu.dart
index a70fbee60..68660854f 100644
--- a/lib/widgets/common/action_controls/quick_choosers/common/menu.dart
+++ b/lib/widgets/common/action_controls/quick_choosers/common/menu.dart
@@ -45,7 +45,7 @@ class MenuQuickChooser extends StatefulWidget {
}
class _MenuQuickChooserState extends State> {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final ValueNotifier _selectedRowRect = ValueNotifier(Rect.zero);
final ScrollController _scrollController = ScrollController();
int _scrollDirection = 0;
diff --git a/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart b/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart
index cdb857522..687d4ea65 100644
--- a/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart
+++ b/lib/widgets/common/action_controls/quick_choosers/rate_chooser.dart
@@ -23,7 +23,7 @@ class RateQuickChooser extends StatefulWidget {
}
class _RateQuickChooserState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
ValueNotifier get valueNotifier => widget.valueNotifier;
diff --git a/lib/widgets/common/action_controls/togglers/play.dart b/lib/widgets/common/action_controls/togglers/play.dart
index 9e76d2d13..67c5a5bf6 100644
--- a/lib/widgets/common/action_controls/togglers/play.dart
+++ b/lib/widgets/common/action_controls/togglers/play.dart
@@ -28,7 +28,7 @@ class PlayToggler extends StatefulWidget {
}
class _PlayTogglerState extends State with SingleTickerProviderStateMixin {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
late AnimationController _playPauseAnimation;
AvesVideoController? get controller => widget.controller;
diff --git a/lib/widgets/common/app_bar/app_bar_subtitle.dart b/lib/widgets/common/app_bar/app_bar_subtitle.dart
index 5a30727e9..4eb44ee5b 100644
--- a/lib/widgets/common/app_bar/app_bar_subtitle.dart
+++ b/lib/widgets/common/app_bar/app_bar_subtitle.dart
@@ -1,4 +1,3 @@
-
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/events.dart';
import 'package:aves/theme/durations.dart';
diff --git a/lib/widgets/common/app_bar/crumb_line.dart b/lib/widgets/common/app_bar/crumb_line.dart
index 4ad8e3414..580f1ecd0 100644
--- a/lib/widgets/common/app_bar/crumb_line.dart
+++ b/lib/widgets/common/app_bar/crumb_line.dart
@@ -8,6 +8,8 @@ class CrumbLine extends StatefulWidget {
final T Function(BuildContext context, int index) combine;
final void Function(T combined) onTap;
+ static const EdgeInsets padding = EdgeInsets.only(top: 6, bottom: 20);
+
const CrumbLine({
super.key,
required this.split,
@@ -18,7 +20,7 @@ class CrumbLine extends StatefulWidget {
@override
State> createState() => _CrumbLineState();
- static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(kToolbarHeight);
+ static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(22) + padding.vertical;
}
class _CrumbLineState extends State> {
diff --git a/lib/widgets/common/basic/gestures/ink_well.dart b/lib/widgets/common/basic/gestures/ink_well.dart
index ffc21b4cc..1c3c8546e 100644
--- a/lib/widgets/common/basic/gestures/ink_well.dart
+++ b/lib/widgets/common/basic/gestures/ink_well.dart
@@ -522,7 +522,7 @@ class _InkResponseStateWidget extends StatefulWidget {
if (onSecondaryTap != null) 'secondary tap',
if (onSecondaryTapUp != null) 'secondary tap up',
if (onSecondaryTapDown != null) 'secondary tap down',
- if (onSecondaryTapCancel != null) 'secondary tap cancel'
+ if (onSecondaryTapCancel != null) 'secondary tap cancel',
];
properties.add(IterableProperty('gestures', gestures, ifEmpty: ''));
properties.add(DiagnosticsProperty('mouseCursor', mouseCursor));
@@ -544,10 +544,7 @@ enum _HighlightType {
focus,
}
-class _InkResponseState extends State<_InkResponseStateWidget>
- with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
- implements _ParentInkResponseState
-{
+class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKeepAliveClientMixin<_InkResponseStateWidget> implements _ParentInkResponseState {
Set? _splashes;
InteractiveInkFeature? _currentSplash;
bool _hovering = false;
@@ -578,6 +575,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed);
}
}
+
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
void activateOnIntent(Intent? intent) {
@@ -611,7 +609,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
void handleStatesControllerChange() {
// Force a rebuild to resolve widget.overlayColor, widget.mouseCursor
- setState(() { });
+ setState(() {});
}
WidgetStatesController get statesController => widget.statesController ?? internalStatesController!;
@@ -642,9 +640,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}
initStatesController();
}
- if (widget.radius != oldWidget.radius ||
- widget.highlightShape != oldWidget.highlightShape ||
- widget.borderRadius != oldWidget.borderRadius) {
+ if (widget.radius != oldWidget.radius || widget.highlightShape != oldWidget.highlightShape || widget.borderRadius != oldWidget.borderRadius) {
final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
if (hoverHighlight != null) {
hoverHighlight.dispose();
@@ -701,7 +697,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}
}
- void updateHighlight(_HighlightType type, { required bool value, bool callOnHover = true }) {
+ void updateHighlight(_HighlightType type, {required bool value, bool callOnHover = true}) {
final InkHighlight? highlight = _highlights[type];
void handleInkRemoval() {
assert(_highlights[type] != null);
@@ -717,7 +713,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
statesController.update(WidgetState.hovered, value);
}
case _HighlightType.focus:
- // see handleFocusUpdate()
+ // see handleFocusUpdate()
break;
}
@@ -730,9 +726,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
if (value) {
if (highlight == null) {
- final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value)
- ?? switch (type) {
- // Use the backwards compatible defaults
+ final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value) ??
+ switch (type) {
+ // Use the backwards compatible defaults
_HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor,
_HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor,
_HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor,
@@ -789,7 +785,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
final MaterialInkController inkController = Material.of(context);
final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
final Offset position = referenceBox.globalToLocal(globalPosition);
- final Color color = widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor;
+ final Color color = widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor;
final RectCallback? rectCallback = widget.containedInkWell ? widget.getRectCallback!(referenceBox) : null;
final BorderRadius? borderRadius = widget.borderRadius;
final ShapeBorder? customBorder = widget.customBorder;
@@ -846,6 +842,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}
bool _hasFocus = false;
+
void handleFocusUpdate(bool hasFocus) {
_hasFocus = hasFocus;
// Set here rather than updateHighlight because this widget's
@@ -978,21 +975,17 @@ class _InkResponseState extends State<_InkResponseStateWidget>
}
bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
- return widget.onTap != null
- || widget.onDoubleTap != null
- || widget.onLongPress != null
- || widget.onTapUp != null
- || widget.onTapDown != null;
+ return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || widget.onTapUp != null || widget.onTapDown != null;
}
bool _secondaryButtonEnabled(_InkResponseStateWidget widget) {
- return widget.onSecondaryTap != null
- || widget.onSecondaryTapUp != null
- || widget.onSecondaryTapDown != null;
+ return widget.onSecondaryTap != null || widget.onSecondaryTapUp != null || widget.onSecondaryTapDown != null;
}
bool get enabled => isWidgetEnabled(widget);
+
bool get _primaryEnabled => _primaryButtonEnabled(widget);
+
bool get _secondaryEnabled => _secondaryButtonEnabled(widget);
void handleMouseEnter(PointerEnterEvent event) {
@@ -1032,14 +1025,15 @@ class _InkResponseState extends State<_InkResponseStateWidget>
final ThemeData theme = Theme.of(context);
return switch (type) {
- // The pressed state triggers a ripple (ink splash), per the current
- // Material Design spec. A separate highlight is no longer used.
- // See https://material.io/design/interaction/states.html#pressed
+ // The pressed state triggers a ripple (ink splash), per the current
+ // Material Design spec. A separate highlight is no longer used.
+ // See https://material.io/design/interaction/states.html#pressed
_HighlightType.pressed => widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor,
- _HighlightType.focus => widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor,
- _HighlightType.hover => widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor,
+ _HighlightType.focus => widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor,
+ _HighlightType.hover => widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor,
};
}
+
for (final _HighlightType type in _highlights.keys) {
_highlights[type]?.color = getHighlightColorForType(type);
}
@@ -1077,7 +1071,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? handleLongPress : null,
onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null,
- onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp: null,
+ onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp : null,
onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null,
onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null,
behavior: HitTestBehavior.opaque,
diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart
index de44dcd4f..391b34a95 100644
--- a/lib/widgets/common/basic/markdown_container.dart
+++ b/lib/widgets/common/basic/markdown_container.dart
@@ -3,7 +3,7 @@ import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
class MarkdownContainer extends StatelessWidget {
final String data;
diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart
index 796188fa7..f1e41390e 100644
--- a/lib/widgets/common/fx/blurred.dart
+++ b/lib/widgets/common/fx/blurred.dart
@@ -20,7 +20,8 @@ class BlurredRect extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClipRect(
- child: BackdropFilter.grouped(
+ // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
+ child: BackdropFilter(
// do not modify tree when disabling filter
filter: enabled ? _filter : _identity,
child: child,
@@ -59,7 +60,8 @@ class BlurredRRect extends StatelessWidget {
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: borderRadius ?? BorderRadius.zero,
- child: BackdropFilter.grouped(
+ // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
+ child: BackdropFilter(
// do not modify tree when disabling filter
filter: enabled ? _filter : _identity,
child: child,
@@ -81,7 +83,8 @@ class BlurredOval extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ClipOval(
- child: BackdropFilter.grouped(
+ // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
+ child: BackdropFilter(
// do not modify tree when disabling filter
filter: enabled ? _filter : _identity,
child: child,
diff --git a/lib/widgets/common/grid/item_tracker.dart b/lib/widgets/common/grid/item_tracker.dart
index 52048a563..ea8aadf68 100644
--- a/lib/widgets/common/grid/item_tracker.dart
+++ b/lib/widgets/common/grid/item_tracker.dart
@@ -44,7 +44,7 @@ class _GridItemTrackerState extends State> with WidgetsBin
return (scrollableContext.findRenderObject() as RenderBox).size;
}
- final List _subscriptions = [];
+ final Set _subscriptions = {};
// grid section metrics before the app is laid out with the new orientation
late SectionedListLayout _lastSectionedListLayout;
diff --git a/lib/widgets/common/grid/sliver.dart b/lib/widgets/common/grid/sliver.dart
index 299e70134..3d7ed7502 100644
--- a/lib/widgets/common/grid/sliver.dart
+++ b/lib/widgets/common/grid/sliver.dart
@@ -71,7 +71,7 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
_RenderSliverKnownExtentBoxAdaptor({
required super.childManager,
required List sectionLayouts,
- }) : _sectionLayouts = sectionLayouts;
+ }) : _sectionLayouts = sectionLayouts;
SectionLayout? sectionAtIndex(int index) => sectionLayouts.firstWhereOrNull((section) => section.hasChild(index));
diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart
index 163017c57..1bbe9f22a 100644
--- a/lib/widgets/common/identity/aves_filter_chip.dart
+++ b/lib/widgets/common/identity/aves_filter_chip.dart
@@ -150,7 +150,7 @@ class AvesFilterChip extends StatefulWidget {
}
class _AvesFilterChipState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
late Future _colorFuture;
late Color _outlineColor;
late bool _tapped;
diff --git a/lib/widgets/common/map/attribution.dart b/lib/widgets/common/map/attribution.dart
index bfe8c7aee..f1cd7b023 100644
--- a/lib/widgets/common/map/attribution.dart
+++ b/lib/widgets/common/map/attribution.dart
@@ -5,7 +5,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:provider/provider.dart';
class Attribution extends StatelessWidget {
diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart
index b8a43c7aa..c19aab548 100644
--- a/lib/widgets/common/map/geo_map.dart
+++ b/lib/widgets/common/map/geo_map.dart
@@ -83,7 +83,7 @@ class GeoMap extends StatefulWidget {
}
class _GeoMapState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
// as of google_maps_flutter v2.0.6, Google map initialization is blocking
// cf https://github.com/flutter/flutter/issues/28493
@@ -249,14 +249,13 @@ class _GeoMapState extends State {
child = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- BackdropGroup(
- child: mapHeight != null
- ? SizedBox(
- height: mapHeight,
- child: child,
- )
- : Expanded(child: child),
- ),
+ // TODO TLAD [flutter vNext] wrap into `BackdropGroup`
+ mapHeight != null
+ ? SizedBox(
+ height: mapHeight,
+ child: child,
+ )
+ : Expanded(child: child),
SafeArea(
top: false,
bottom: false,
diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart
index 39ee02248..829476be7 100644
--- a/lib/widgets/common/map/leaflet/map.dart
+++ b/lib/widgets/common/map/leaflet/map.dart
@@ -66,7 +66,7 @@ class EntryLeafletMap extends StatefulWidget {
class _EntryLeafletMapState extends State> with TickerProviderStateMixin {
final MapController _leafletMapController = MapController();
- final List _subscriptions = [];
+ final Set _subscriptions = {};
Map, GeoEntry> _geoEntryByMarkerKey = {};
final Debouncer _debouncer = Debouncer(delay: ADurations.mapIdleDebounceDelay);
diff --git a/lib/widgets/common/search/delegate.dart b/lib/widgets/common/search/delegate.dart
index da4176455..e48a21f5b 100644
--- a/lib/widgets/common/search/delegate.dart
+++ b/lib/widgets/common/search/delegate.dart
@@ -43,10 +43,12 @@ abstract class AvesSearchDelegate extends SearchDelegate {
final animate = context.read().animate;
return canPop
? IconButton(
- icon: animate ? AnimatedIcon(
- icon: AnimatedIcons.menu_arrow,
- progress: transitionAnimation,
- ): const Icon(Icons.arrow_back),
+ icon: animate
+ ? AnimatedIcon(
+ icon: AnimatedIcons.menu_arrow,
+ progress: transitionAnimation,
+ )
+ : const Icon(Icons.arrow_back),
onPressed: () => goBack(context),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
)
diff --git a/lib/widgets/common/tile_extent_controller.dart b/lib/widgets/common/tile_extent_controller.dart
index f328f12a4..bf4567d02 100644
--- a/lib/widgets/common/tile_extent_controller.dart
+++ b/lib/widgets/common/tile_extent_controller.dart
@@ -15,7 +15,7 @@ class TileExtentController {
late double userPreferredExtent;
Size _viewportSize = Size.zero;
- final List _subscriptions = [];
+ final Set _subscriptions = {};
Size get viewportSize => _viewportSize;
diff --git a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart
index fbdbef1d1..a133210ae 100644
--- a/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart
+++ b/lib/widgets/dialogs/entry_editors/edit_location_dialog.dart
@@ -56,7 +56,7 @@ class EditEntryLocationDialog extends StatefulWidget {
}
class _EditEntryLocationDialogState extends State with FeedbackMixin {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
LocationEditAction _action = LocationEditAction.chooseOnMap;
LatLng? _mapCoordinates;
late final AvesEntry mainEntry;
diff --git a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart
index 217e2b282..ca2fa32b2 100644
--- a/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart
+++ b/lib/widgets/dialogs/pick_dialogs/location_pick_page.dart
@@ -63,7 +63,7 @@ class _Content extends StatefulWidget {
}
class _ContentState extends State<_Content> with SingleTickerProviderStateMixin {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final AvesMapController _mapController = AvesMapController();
late final ValueNotifier _isPageAnimatingNotifier;
final ValueNotifier _dotLocationNotifier = ValueNotifier(null), _infoLocationNotifier = ValueNotifier(null);
diff --git a/lib/widgets/editor/entry_editor_page.dart b/lib/widgets/editor/entry_editor_page.dart
index c8cba2b81..87eca7848 100644
--- a/lib/widgets/editor/entry_editor_page.dart
+++ b/lib/widgets/editor/entry_editor_page.dart
@@ -28,7 +28,7 @@ class ImageEditorPage extends StatefulWidget {
}
class _ImageEditorPageState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final ValueNotifier _actionNotifier = ValueNotifier(null);
final ValueNotifier _marginNotifier = ValueNotifier(EdgeInsets.zero);
final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero);
@@ -118,7 +118,7 @@ class _ImageEditorPageState extends State {
}
void _onActionChanged() {
- switch(_actionNotifier.value) {
+ switch (_actionNotifier.value) {
case EditorAction.transform:
_transformController.reset();
_marginNotifier.value = Cropper.imageMargin;
diff --git a/lib/widgets/editor/image.dart b/lib/widgets/editor/image.dart
index ce5836c84..df6213591 100644
--- a/lib/widgets/editor/image.dart
+++ b/lib/widgets/editor/image.dart
@@ -36,7 +36,7 @@ class EditorImage extends StatefulWidget {
}
class _EditorImageState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final ValueNotifier _scrimOpacityNotifier = ValueNotifier(0);
AvesEntry get entry => widget.entry;
diff --git a/lib/widgets/editor/transform/cropper.dart b/lib/widgets/editor/transform/cropper.dart
index a160ef408..1c37f85bc 100644
--- a/lib/widgets/editor/transform/cropper.dart
+++ b/lib/widgets/editor/transform/cropper.dart
@@ -38,7 +38,7 @@ class Cropper extends StatefulWidget {
}
class _CropperState extends State with SingleTickerProviderStateMixin {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final ValueNotifier _outlineNotifier = ValueNotifier(Rect.zero);
final ValueNotifier _gridDivisionNotifier = ValueNotifier(0);
late AnimationController _gridAnimationController;
diff --git a/lib/widgets/explorer/app_bar.dart b/lib/widgets/explorer/app_bar.dart
index 7ee7e14a5..dc43f661f 100644
--- a/lib/widgets/explorer/app_bar.dart
+++ b/lib/widgets/explorer/app_bar.dart
@@ -65,7 +65,8 @@ class _ExplorerAppBarState extends State with WidgetsBindingObse
actions: _buildActions,
bottom: LayoutBuilder(
builder: (context, constraints) {
- return SizedBox(
+ return Container(
+ padding: CrumbLine.padding,
width: constraints.maxWidth,
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
child: ValueListenableBuilder(
diff --git a/lib/widgets/explorer/explorer_page.dart b/lib/widgets/explorer/explorer_page.dart
index cadf2e5f5..749f1a0e8 100644
--- a/lib/widgets/explorer/explorer_page.dart
+++ b/lib/widgets/explorer/explorer_page.dart
@@ -40,7 +40,7 @@ class ExplorerPage extends StatefulWidget {
}
class _ExplorerPageState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final ValueNotifier _directory = ValueNotifier(null);
final ValueNotifier _contentsDirectory = ValueNotifier(null);
final ValueNotifier> _contents = ValueNotifier([]);
diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart
index 01ceee68c..b7d1f96a9 100644
--- a/lib/widgets/filter_grids/albums_page.dart
+++ b/lib/widgets/filter_grids/albums_page.dart
@@ -44,12 +44,12 @@ class AlbumListPage extends StatelessWidget {
child: Builder(
// to access filter group provider from subtree context
builder: (context) {
- return Selector)>(
- selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.pinnedFilters),
+ return Selector, Set)>(
+ selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.hiddenFilters, s.pinnedFilters),
shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within records
const eq = DeepCollectionEquality();
- return !(eq.equals(t1.$1, t2.$1) && eq.equals(t1.$2, t2.$2) && eq.equals(t1.$3, t2.$3) && eq.equals(t1.$4, t2.$4));
+ return !(eq.equals(t1.$1, t2.$1) && eq.equals(t1.$2, t2.$2) && eq.equals(t1.$3, t2.$3) && eq.equals(t1.$4, t2.$4) && eq.equals(t1.$5, t2.$5));
},
builder: (context, s, child) {
return ValueListenableBuilder(
@@ -123,7 +123,7 @@ class AlbumListPage extends StatelessWidget {
final listedDynamicAlbums = {};
if (albumChipTypes.contains(AlbumChipType.dynamic)) {
- final allDynamicAlbums = dynamicAlbums.all;
+ final allDynamicAlbums = dynamicAlbums.all.whereNot(settings.hiddenFilters.contains).toSet();
if (groupUri == null) {
final withinGroups = whereTypeRecursively(groupContent).toSet();
listedDynamicAlbums.addAll(allDynamicAlbums.whereNot(withinGroups.contains));
@@ -134,7 +134,7 @@ class AlbumListPage extends StatelessWidget {
}
// always show groups, which are needed to navigate to other types
- final albumGroupFilters = groupContent.whereType().toSet();
+ final albumGroupFilters = groupContent.whereType().whereNot(settings.hiddenFilters.contains).toSet();
final filters = {
...albumGroupFilters,
diff --git a/lib/widgets/filter_grids/common/app_bar.dart b/lib/widgets/filter_grids/common/app_bar.dart
index 322e8fe1d..680ab82bc 100644
--- a/lib/widgets/filter_grids/common/app_bar.dart
+++ b/lib/widgets/filter_grids/common/app_bar.dart
@@ -79,7 +79,7 @@ class FilterGridAppBar> extends State> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
late AnimationController _browseToSelectAnimation;
final ValueNotifier _isSelectingNotifier = ValueNotifier(false);
final FocusNode _queryBarFocusNode = FocusNode();
@@ -170,7 +170,8 @@ class _FilterGridAppBarState(
diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart
index 83058c16d..13c74802e 100644
--- a/lib/widgets/filter_grids/common/covered_filter_chip.dart
+++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart
@@ -61,9 +61,13 @@ class CoveredFilterChip extends StatelessWidget {
static Radius radius(double extent) => Radius.circular(min(AvesFilterChip.defaultRadius, extent / 4));
- static double detailIconSize(double extent) => min(AvesFilterChip.fontSize, extent / 8);
+ static double detailIconSize(double extent) => min(AvesFilterChip.fontSize, extent / 7);
- static double detailFontSize(double extent) => min(AvesFilterChip.fontSize, extent / 6);
+ static double detailFontSize(double extent) => min(AvesFilterChip.fontSize, extent / 7);
+
+ static double detailIconPadding(double extent) => min(8.0, extent / 16);
+
+ static double detailIconTextPadding(double extent) => detailIconPadding(extent) / 2;
@override
Widget build(BuildContext context) {
@@ -201,30 +205,33 @@ class CoveredFilterChip extends StatelessWidget {
if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked),
if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum),
if (filter is AlbumGroupFilter) ...[
- _buildDetailIcon(context, AIcons.album),
+ _buildDetailIcon(context, AIcons.album, padding: detailIconTextPadding(extent)),
Text(
'${NumberFormat.decimalPattern(context.locale).format(albumGrouping.countLeaves(filter.uri))}${AText.separator}',
style: textStyle,
),
],
- Text(
- locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)),
- style: textStyle,
+ Flexible(
+ child: Text(
+ locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)),
+ style: textStyle,
+ softWrap: false,
+ overflow: TextOverflow.fade,
+ maxLines: 1,
+ ),
),
],
);
}
- Widget _buildDetailIcon(BuildContext context, IconData icon) {
- final padding = min(8.0, extent / 16);
- final iconSize = detailIconSize(extent);
+ Widget _buildDetailIcon(BuildContext context, IconData icon, {double? padding}) {
return AnimatedPadding(
- padding: EdgeInsetsDirectional.only(end: padding),
+ padding: EdgeInsetsDirectional.only(end: padding ?? detailIconPadding(extent)),
duration: ADurations.chipDecorationAnimation,
child: Icon(
icon,
color: _detailColor(context),
- size: iconSize,
+ size: detailIconSize(extent),
),
);
}
diff --git a/lib/widgets/home/home_page.dart b/lib/widgets/home/home_page.dart
index 98c353fa2..12ee7e758 100644
--- a/lib/widgets/home/home_page.dart
+++ b/lib/widgets/home/home_page.dart
@@ -304,9 +304,6 @@ class _HomePageState extends State {
String routeName;
Set? filters;
switch (appMode) {
- case AppMode.pickSingleMediaExternal:
- case AppMode.pickMultipleMediaExternal:
- routeName = CollectionPage.routeName;
case AppMode.setWallpaper:
return DirectMaterialPageRoute(
settings: const RouteSettings(name: WallpaperPage.routeName),
@@ -374,7 +371,17 @@ class _HomePageState extends State {
);
},
);
- default:
+ case AppMode.initialization:
+ case AppMode.main:
+ case AppMode.pickCollectionFiltersExternal:
+ case AppMode.pickSingleMediaExternal:
+ case AppMode.pickMultipleMediaExternal:
+ case AppMode.pickFilteredMediaInternal:
+ case AppMode.pickUnfilteredMediaInternal:
+ case AppMode.pickFilterInternal:
+ case AppMode.previewMap:
+ case AppMode.screenSaver:
+ case AppMode.slideshow:
routeName = _initialRouteName ?? settings.homePage.routeName;
filters = _initialFilters ?? (settings.homePage == HomePageSetting.collection ? settings.homeCustomCollection : {});
}
diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart
index 0131ea2b4..83bca4a6b 100644
--- a/lib/widgets/map/map_page.dart
+++ b/lib/widgets/map/map_page.dart
@@ -115,7 +115,7 @@ class _Content extends StatefulWidget {
}
class _ContentState extends State<_Content> with SingleTickerProviderStateMixin {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final AvesMapController _mapController = AvesMapController();
final ValueNotifier _isPageAnimatingNotifier = ValueNotifier(false);
final ValueNotifier _selectedIndexNotifier = ValueNotifier(0);
diff --git a/lib/widgets/navigation/nav_bar/floating.dart b/lib/widgets/navigation/nav_bar/floating.dart
index 63ffb53a4..da0e7d51b 100644
--- a/lib/widgets/navigation/nav_bar/floating.dart
+++ b/lib/widgets/navigation/nav_bar/floating.dart
@@ -23,7 +23,7 @@ class FloatingNavBar extends StatefulWidget {
}
class _FloatingNavBarState extends State with SingleTickerProviderStateMixin {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
late AnimationController _controller;
late CurvedAnimation _animation;
late Animation _offset;
diff --git a/lib/widgets/settings/app_export/items.dart b/lib/widgets/settings/app_export/items.dart
index 2a69b82c9..a77f690c9 100644
--- a/lib/widgets/settings/app_export/items.dart
+++ b/lib/widgets/settings/app_export/items.dart
@@ -39,7 +39,7 @@ extension ExtraAppExportItem on AppExportItem {
favourites.import(jsonMap, source);
case AppExportItem.settings:
await settings.import(jsonMap);
- albumGrouping.init(settings.albumGroups);
+ albumGrouping.setGroups(settings.albumGroups);
}
}
}
diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart
index b0e6730b1..3bba84db9 100644
--- a/lib/widgets/settings/language/language.dart
+++ b/lib/widgets/settings/language/language.dart
@@ -82,8 +82,8 @@ class SettingsTileLanguageNumerals extends SettingsTile {
@override
Widget build(BuildContext context) => SettingsSwitchListTile(
- selector: (context, s) => s.forceWesternArabicNumerals,
- onChanged: (v) => settings.forceWesternArabicNumerals = v,
- title: title(context),
- );
+ selector: (context, s) => s.forceWesternArabicNumerals,
+ onChanged: (v) => settings.forceWesternArabicNumerals = v,
+ title: title(context),
+ );
}
diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart
index 8f32956cb..61ec662c2 100644
--- a/lib/widgets/viewer/entry_vertical_pager.dart
+++ b/lib/widgets/viewer/entry_vertical_pager.dart
@@ -64,7 +64,7 @@ class ViewerVerticalPageView extends StatefulWidget {
}
class _ViewerVerticalPageViewState extends State {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
final ValueNotifier _backgroundOpacityNotifier = ValueNotifier(1);
final ValueNotifier _isVerticallyScrollingNotifier = ValueNotifier(false);
final ValueNotifier _isImageFocusedNotifier = ValueNotifier(true);
diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart
index b5fe46efd..b9e17d973 100644
--- a/lib/widgets/viewer/entry_viewer_stack.dart
+++ b/lib/widgets/viewer/entry_viewer_stack.dart
@@ -261,29 +261,35 @@ class _EntryViewerStackState extends State with EntryViewContr
return ValueListenableBuilder(
valueListenable: _viewLocked,
builder: (context, locked, child) {
- return BackdropGroup(
- child: Stack(
- children: [
- child!,
- if (!pipEnabled) ...[
- if (locked) ...[
- const Positioned.fill(
- child: AbsorbPointer(),
- ),
- Positioned.fill(
- child: GestureDetector(
- onTap: () => _overlayVisible.value = !_overlayVisible.value,
- ),
- ),
- _buildViewerLockedBottomOverlay(),
- ] else
- ..._buildOverlays(availableSize).map(_decorateOverlay),
- const TopGestureAreaProtector(),
- const SideGestureAreaProtector(),
- const BottomGestureAreaProtector(),
- ],
- ],
- ),
+ final children = [child!];
+ if (!pipEnabled) {
+ if (locked) {
+ children.addAll([
+ const Positioned.fill(
+ child: AbsorbPointer(),
+ ),
+ Positioned.fill(
+ child: GestureDetector(
+ onTap: () => _overlayVisible.value = !_overlayVisible.value,
+ ),
+ ),
+ _buildViewerLockedBottomOverlay(),
+ ]);
+ } else {
+ // regular overlay
+ children.addAll(_buildOverlays(availableSize).map(_decorateOverlay));
+ }
+
+ children.addAll([
+ const TopGestureAreaProtector(),
+ const SideGestureAreaProtector(),
+ const BottomGestureAreaProtector(),
+ ]);
+ }
+
+ // TODO TLAD [flutter vNext] wrap into `BackdropGroup`
+ return Stack(
+ children: children,
);
},
child: viewer,
diff --git a/lib/widgets/viewer/info/info_page.dart b/lib/widgets/viewer/info/info_page.dart
index 2b88828f2..0326d636d 100644
--- a/lib/widgets/viewer/info/info_page.dart
+++ b/lib/widgets/viewer/info/info_page.dart
@@ -150,7 +150,7 @@ class _InfoPageContent extends StatefulWidget {
}
class _InfoPageContentState extends State<_InfoPageContent> {
- final List _subscriptions = [];
+ final Set _subscriptions = {};
late EntryInfoActionDelegate _actionDelegate;
final ValueNotifier