Merge branch 'develop'

This commit is contained in:
Thibault Deckers 2025-06-02 23:52:45 +02:00
commit eca1d5dbfb
120 changed files with 947 additions and 664 deletions

@ -1 +1 @@
Subproject commit ea121f8859e4b13e47a8f845e4586164519588bc Subproject commit d8a9f9a52e5af486f80d932e838ee93861ffd863

View file

@ -28,6 +28,9 @@ jobs:
- name: Get Flutter packages - name: Get Flutter packages
run: ./flutterw pub get run: ./flutterw pub get
- name: Generate app localizations
run: ./flutterw gen-l10n
- name: Static analysis. - name: Static analysis.
run: ./flutterw analyze run: ./flutterw analyze
@ -69,7 +72,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -83,6 +86,6 @@ jobs:
./flutterw build apk --profile -t lib/main_play.dart --flavor play ./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View file

@ -36,6 +36,9 @@ jobs:
- name: Get Flutter packages - name: Get Flutter packages
run: ./flutterw pub get run: ./flutterw pub get
- name: Generate app localizations
run: ./flutterw gen-l10n
- name: Update Flutter version file - name: Update Flutter version file
run: scripts/update_flutter_version.sh run: scripts/update_flutter_version.sh

View file

@ -41,7 +41,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - 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: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
## <a id="v1.13.2"></a>[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
## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14 ## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14
### Fixed ### Fixed

View file

@ -9,6 +9,11 @@ analyzer:
# implicit-casts: false # implicit-casts: false
# implicit-dynamic: false # implicit-dynamic: false
# cf https://github.com/dart-lang/dart_style/wiki/Configuration
formatter:
page_width: 240
trailing_commas: preserve
linter: linter:
rules: rules:
# from 'flutter_lints', excluded # from 'flutter_lints', excluded

1
android/.gitignore vendored
View file

@ -7,6 +7,7 @@ gradle-wrapper.jar
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/ .cxx/
.kotlin/ .kotlin/
/build/
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore # See https://flutter.dev/to/reference-keystore

View file

@ -134,14 +134,14 @@ flutter {
repositories { repositories {
maven { maven {
url 'https://jitpack.io' url = 'https://jitpack.io'
content { content {
includeGroup "com.github.deckerst" includeGroup "com.github.deckerst"
includeGroup "com.github.deckerst.mp4parser" includeGroup "com.github.deckerst.mp4parser"
} }
} }
maven { maven {
url 'https://s3.amazonaws.com/repo.commonsware.com' url = 'https://s3.amazonaws.com/repo.commonsware.com'
content { content {
excludeGroupByRegex "com\\.github\\.deckerst.*" excludeGroupByRegex "com\\.github\\.deckerst.*"
} }
@ -152,12 +152,12 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1'
implementation "androidx.appcompat:appcompat:1.7.0" implementation "androidx.appcompat:appcompat:1.7.0"
implementation 'androidx.core:core-ktx:1.15.0' implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.lifecycle:lifecycle-process:2.8.7' implementation 'androidx.lifecycle:lifecycle-process:2.9.0'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'androidx.security:security-crypto:1.1.0-alpha07'
implementation 'androidx.work:work-runtime-ktx:2.10.0' implementation 'androidx.work:work-runtime-ktx:2.10.1'
implementation 'com.commonsware.cwac:document:0.5.0' implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.19.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/androidsvg
// - https://jitpack.io/p/deckerst/mp4parser // - https://jitpack.io/p/deckerst/mp4parser
// - https://jitpack.io/p/deckerst/pixymeta-android // - https://jitpack.io/p/deckerst/pixymeta-android
implementation 'com.github.deckerst:Android-TiffBitmapFactory:3ed067f021' implementation 'com.github.deckerst:Android-TiffBitmapFactory:d6b2b0aa4f'
implementation 'com.github.deckerst:androidsvg:cc9d59a88f' implementation 'com.github.deckerst:androidsvg:67db933051'
implementation 'com.github.deckerst.mp4parser:isoparser:d5caf7a3dd' implementation 'com.github.deckerst.mp4parser:isoparser:c2898f1832'
implementation 'com.github.deckerst.mp4parser:muxer:d5caf7a3dd' implementation 'com.github.deckerst.mp4parser:muxer:c2898f1832'
implementation 'com.github.deckerst:pixymeta-android:71eee77dc4' implementation 'com.github.deckerst:pixymeta-android:cb1cdc932e'
implementation project(':exifinterface') implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'

View file

@ -311,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
embeddedByteStream: InputStream, embeddedByteStream: InputStream,
embeddedByteLength: Long, embeddedByteLength: Long,
) { ) {
val extension = extensionFor(mimeType) val extension = extensionFor(mimeType, defaultExtension = null)
val targetFile = StorageUtils.createTempFile(context, extension).apply { val targetFile = StorageUtils.createTempFile(context, extension).apply {
transferFrom(embeddedByteStream, embeddedByteLength) transferFrom(embeddedByteStream, embeddedByteLength)
} }
@ -319,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val authority = "${context.applicationContext.packageName}.file_provider" val authority = "${context.applicationContext.packageName}.file_provider"
val uri = if (displayName != null) { val uri = if (displayName != null) {
// add extension to ease type identification when sharing this content // 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 displayName
} else { } else {
"$displayName$extension" "$displayName$extension"

View file

@ -31,10 +31,16 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
init { init {
Log.i(LOG_TAG, "start listening to Media Store") Log.i(LOG_TAG, "start listening to Media Store")
try {
context.contentResolver.apply { context.contentResolver.apply {
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver) registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
registerContentObserver(MediaStore.Video.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)
}
} }
fun dispose() { fun dispose() {

View file

@ -142,16 +142,18 @@ abstract class ImageProvider {
val oldFile = File(sourcePath) val oldFile = File(sourcePath)
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) { if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
val defaultExtension = oldFile.extension
oldFile.parent?.let { dir -> oldFile.parent?.let { dir ->
val resolution = resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = dir, dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
defaultExtension = defaultExtension,
conflictStrategy = NameConflictStrategy.RENAME, conflictStrategy = NameConflictStrategy.RENAME,
) )
resolution.nameWithoutExtension?.let { targetNameWithoutExtension -> resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}"
val newFile = File(dir, targetFileName) val newFile = File(dir, targetFileName)
if (oldFile != newFile) { if (oldFile != newFile) {
newFields = renameSingle( newFields = renameSingle(
@ -277,11 +279,17 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
} }
// there is no benefit providing input extension
// for known output MIME type
val defaultExtension = null
val resolution = resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType, mimeType = exportMimeType,
defaultExtension = defaultExtension,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@ -358,6 +366,7 @@ abstract class ImageProvider {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
@ -465,6 +474,7 @@ abstract class ImageProvider {
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType, mimeType = captureMimeType,
defaultExtension = null,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -571,13 +581,14 @@ abstract class ImageProvider {
dir: String, dir: String,
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
mimeType: String, mimeType: String,
defaultExtension: String?,
conflictStrategy: NameConflictStrategy, conflictStrategy: NameConflictStrategy,
): NameConflictResolution { ): NameConflictResolution {
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension) val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
var resolvedName: String? = sanitizedNameWithoutExtension var resolvedName: String? = sanitizedNameWithoutExtension
var replacementFile: File? = null var replacementFile: File? = null
val extension = extensionFor(mimeType) val extension = extensionFor(mimeType, defaultExtension)
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension") val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) { when (conflictStrategy) {
NameConflictStrategy.RENAME -> { NameConflictStrategy.RENAME -> {

View file

@ -557,6 +557,7 @@ class MediaStoreImageProvider : ImageProvider() {
toBin: Boolean, toBin: Boolean,
): FieldMap { ): FieldMap {
val sourcePath = sourceFile?.path val sourcePath = sourceFile?.path
val sourceExtension = sourceFile?.extension
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) } val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) { if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy // nothing to do unless it's a renamed copy
@ -569,6 +570,7 @@ class MediaStoreImageProvider : ImageProvider() {
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
defaultExtension = sourceExtension,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@ -580,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = sourceExtension,
) { output: OutputStream -> ) { output: OutputStream ->
try { try {
sourceDocFile.copyTo(output) sourceDocFile.copyTo(output)
@ -615,12 +618,13 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String, targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit, write: (OutputStream) -> Unit,
): String { ): String {
if (StorageUtils.isInVault(activity, targetDir)) { if (StorageUtils.isInVault(activity, targetDir)) {
return insertByFile( return insertByFile(
targetDir = targetDir, targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write, write = write,
) )
} }
@ -630,7 +634,7 @@ class MediaStoreImageProvider : ImageProvider() {
return insertByMediaStore( return insertByMediaStore(
activity = activity, activity = activity,
targetDir = targetDir, targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write, write = write,
) )
} }
@ -642,6 +646,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
} }
@ -700,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String, targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit, write: (OutputStream) -> Unit,
): String { ): String {
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") 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` // but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI // through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) 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 { try {
targetDocFile.openOutputStream().use(write) targetDocFile.openOutputStream().use(write)

View file

@ -163,12 +163,24 @@ object MimeTypes {
// among other refs: // among other refs:
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types // - 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" AVI, AVI_VND -> ".avi"
DNG, DNG_ADOBE -> ".dng"
HEIC, HEIF -> ".heif" HEIC, HEIF -> ".heif"
MP2T, MP2TS -> ".m2ts" MP2T, MP2TS -> ".m2ts"
PSD_VND, PSD_X -> ".psd" 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) val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)

View file

@ -8,4 +8,5 @@
<string name="videos_shortcut_short_label">ဗီဒီယိုများ</string> <string name="videos_shortcut_short_label">ဗီဒီယိုများ</string>
<string name="analysis_notification_default_title">မီဒီယာ ကိုစကင်ဖတ်နေသည်</string> <string name="analysis_notification_default_title">မီဒီယာ ကိုစကင်ဖတ်နေသည်</string>
<string name="analysis_notification_action_stop">ရပ်ရန်</string> <string name="analysis_notification_action_stop">ရပ်ရန်</string>
<string name="map_shortcut_short_label">မြေပုံ</string>
</resources> </resources>

View file

@ -22,7 +22,6 @@ import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongA
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN;
@ -91,7 +90,7 @@ import java.util.regex.Pattern;
import java.util.zip.CRC32; 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. * Named differently to let ExifInterface be loaded as subdependency.
* cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface * 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 * 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 // TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB 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 String TAG = "ExifInterface";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@ -4553,7 +4558,7 @@ public class ExifInterfaceFork {
&& (mXmpFromSeparateMarker != null || !containsTiff700Xmp)) && (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
|| (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT || (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
&& !containsTiff700Xmp)) { && !containsTiff700Xmp)) {
mXmpFromSeparateMarker = ExifAttribute.createByte(value); mXmpFromSeparateMarker = value != null ? ExifAttribute.createByte(value) : null;
return; return;
} }
} }
@ -6558,8 +6563,9 @@ public class ExifInterfaceFork {
// Exif data in WebP images (e.g. // Exif data in WebP images (e.g.
// https://github.com/ImageMagick/ImageMagick/issues/3140) // https://github.com/ImageMagick/ImageMagick/issues/3140)
if (startsWith(payload, IDENTIFIER_EXIF_APP1)) { if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length, payload =
payload.length); Arrays.copyOfRange(
payload, IDENTIFIER_EXIF_APP1.length, payload.length);
} }
// Save offset to EXIF data for handling thumbnail and attribute offsets. // Save offset to EXIF data for handling thumbnail and attribute offsets.
@ -6722,8 +6728,11 @@ public class ExifInterfaceFork {
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length); copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
boolean needToWriteExif = true; boolean needToWriteExif = true;
boolean needToWriteXmp = mXmpFromSeparateMarker != null; // Either there's some XMP data to write, or it has been cleared locally but was present in
while (needToWriteExif || needToWriteXmp) { // 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 chunkLength = dataInputStream.readInt();
int chunkType = dataInputStream.readInt(); int chunkType = dataInputStream.readInt();
if (chunkType == PNG_CHUNK_TYPE_IHDR) { if (chunkType == PNG_CHUNK_TYPE_IHDR) {
@ -6738,7 +6747,7 @@ public class ExifInterfaceFork {
} }
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) { if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
writePngXmpItxtChunk(dataOutputStream); writePngXmpItxtChunk(dataOutputStream);
needToWriteXmp = false; needToHandleXmpChunk = false;
} }
continue; continue;
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) { } else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
@ -6746,10 +6755,25 @@ public class ExifInterfaceFork {
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
needToWriteExif = false; needToWriteExif = false;
continue; continue;
} else if (chunkType == PNG_CHUNK_TYPE_ITXT && needToWriteXmp) { } 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); writePngXmpItxtChunk(dataOutputStream);
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); }
needToWriteXmp = false; 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; continue;
} }
dataOutputStream.writeInt(chunkLength); dataOutputStream.writeInt(chunkLength);
@ -7536,6 +7560,13 @@ public class ExifInterfaceFork {
Log.d(TAG, "Invalid strip offset value"); Log.d(TAG, "Invalid strip offset value");
return; return;
} }
// TLAD start
if (bytesToSkip > getAvailableHeapSize()) {
throw new IOException("cannot allocate " + bytesToSkip + " bytes to skip to retrieve thumbnail");
}
// TLAD end
try { try {
in.skipFully(bytesToSkip); in.skipFully(bytesToSkip);
} catch (EOFException e) { } catch (EOFException e) {

View file

@ -31,7 +31,7 @@ import java.io.InputStream;
import java.io.OutputStream; 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. * 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 * cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
*/ */

View file

@ -0,0 +1,4 @@
In v1.13.2:
- group albums
- filter by day of the week
Full changelog available on GitHub

View file

@ -0,0 +1,4 @@
In v1.13.2:
- group albums
- filter by day of the week
Full changelog available on GitHub

View file

@ -599,7 +599,7 @@
"@settingsLanguagePageTitle": {}, "@settingsLanguagePageTitle": {},
"rootDirectoryDescription": "دليل الجذر", "rootDirectoryDescription": "دليل الجذر",
"@rootDirectoryDescription": {}, "@rootDirectoryDescription": {},
"viewDialogGroupSectionTitle": "مجموعة", "viewDialogGroupSectionTitle": "الأقسام",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"maxBrightnessAlways": "دائماً", "maxBrightnessAlways": "دائماً",
"@maxBrightnessAlways": {}, "@maxBrightnessAlways": {},
@ -1449,7 +1449,7 @@
"@binPageTitle": {}, "@binPageTitle": {},
"tagPlaceholderState": "الولاية", "tagPlaceholderState": "الولاية",
"@tagPlaceholderState": {}, "@tagPlaceholderState": {},
"sortByAlbumFileName": "حسب الألبوم واسم الملف", "sortByAlbumFileName": "حسب عنوان الألبوم والعنصر",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}",
"@deleteMultiAlbumConfirmationDialogMessage": { "@deleteMultiAlbumConfirmationDialogMessage": {
@ -1599,8 +1599,28 @@
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "التنسيقات", "searchFormatSectionTitle": "التنسيقات",
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"chipActionGroup": "مجموعة", "chipActionGroup": "تغيير التجميع",
"@chipActionGroup": {}, "@chipActionGroup": {},
"createButtonLabel": "خلق", "createButtonLabel": "خلق",
"@createButtonLabel": {} "@createButtonLabel": {},
"sectionNone": "لا يوجد أقسام",
"@sectionNone": {},
"chipActionCreateGroup": "إنشاء مجموعة",
"@chipActionCreateGroup": {},
"albumTierGroups": "المجموعات",
"@albumTierGroups": {},
"newGroupDialogTitle": "مجموعة جديدة",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "اسم المجموعة",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "المجموعة موجودة بالفعل",
"@groupAlreadyExists": {},
"groupEmpty": "لا توجد مجموعات",
"@groupEmpty": {},
"ungrouped": "غير مجمعة",
"@ungrouped": {},
"groupPickerTitle": "اختر المجموعة",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "استخدم هذه المجموعة",
"@groupPickerUseThisGroupButton": {}
} }

View file

@ -142,5 +142,15 @@
"chipActionUnpin": "Sabitləməyin", "chipActionUnpin": "Sabitləməyin",
"@chipActionUnpin": {}, "@chipActionUnpin": {},
"chipActionRename": "Bir də adlandır", "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": {}
} }

View file

@ -1624,5 +1624,19 @@
"sortByPath": "Според пътя", "sortByPath": "Според пътя",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Формати", "searchFormatSectionTitle": "Формати",
"@searchFormatSectionTitle": {} "@searchFormatSectionTitle": {},
"chipActionCreateGroup": "Създайте група",
"@chipActionCreateGroup": {},
"chipActionGroup": "Групиране",
"@chipActionGroup": {},
"newGroupDialogTitle": "Нова Група",
"@newGroupDialogTitle": {},
"groupAlreadyExists": "Групата вече съществува",
"@groupAlreadyExists": {},
"albumTierGroups": "Групи",
"@albumTierGroups": {},
"groupPickerUseThisGroupButton": "Използвайте тази група",
"@groupPickerUseThisGroupButton": {},
"newGroupDialogNameLabel": "Име на групата",
"@newGroupDialogNameLabel": {}
} }

View file

@ -850,7 +850,7 @@
"@drawerCollectionRaws": {}, "@drawerCollectionRaws": {},
"sortByRating": "Efter bedømmelse", "sortByRating": "Efter bedømmelse",
"@sortByRating": {}, "@sortByRating": {},
"sortByAlbumFileName": "Efter album og filnavn", "sortByAlbumFileName": "Efter album og elementtitel",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"albumGroupVolume": "Efter lagervolume", "albumGroupVolume": "Efter lagervolume",
"@albumGroupVolume": {}, "@albumGroupVolume": {},
@ -1627,7 +1627,7 @@
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"createButtonLabel": "OPRET", "createButtonLabel": "OPRET",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Gruppér", "chipActionGroup": "Ændr gruppering",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Opret gruppe", "chipActionCreateGroup": "Opret gruppe",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -105,7 +105,7 @@
"chipActionLock": "Lock", "chipActionLock": "Lock",
"chipActionPin": "Pin to top", "chipActionPin": "Pin to top",
"chipActionUnpin": "Unpin from top", "chipActionUnpin": "Unpin from top",
"chipActionGroup": "Group", "chipActionGroup": "Change grouping",
"chipActionRename": "Rename", "chipActionRename": "Rename",
"chipActionSetCover": "Set cover", "chipActionSetCover": "Set cover",
"chipActionShowCountryStates": "Show states", "chipActionShowCountryStates": "Show states",
@ -767,7 +767,7 @@
"sortByName": "By name", "sortByName": "By name",
"sortByItemCount": "By item count", "sortByItemCount": "By item count",
"sortBySize": "By size", "sortBySize": "By size",
"sortByAlbumFileName": "By album & file name", "sortByAlbumFileName": "By album & item title",
"sortByRating": "By rating", "sortByRating": "By rating",
"sortByDuration": "By duration", "sortByDuration": "By duration",
"sortByPath": "By path", "sortByPath": "By path",

View file

@ -790,7 +790,7 @@
"@aboutLicensesDartPackagesSectionTitle": {}, "@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse", "aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse",
"@aboutLicensesShowAllButtonLabel": {}, "@aboutLicensesShowAllButtonLabel": {},
"policyPageTitle": "Privaatsuspoliitika", "policyPageTitle": "Andmekaitsepõhimõtted",
"@policyPageTitle": {}, "@policyPageTitle": {},
"collectionPageTitle": "Meediakogu", "collectionPageTitle": "Meediakogu",
"@collectionPageTitle": {}, "@collectionPageTitle": {},
@ -1036,7 +1036,7 @@
"@sortBySize": {}, "@sortBySize": {},
"sortByName": "Nime alusel", "sortByName": "Nime alusel",
"@sortByName": {}, "@sortByName": {},
"sortByAlbumFileName": "Albumi ja failinime alusel", "sortByAlbumFileName": "Albumi ja objekti nime alusel",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Hinnangu alusel", "sortByRating": "Hinnangu alusel",
"@sortByRating": {}, "@sortByRating": {},
@ -1645,7 +1645,7 @@
"@groupPickerUseThisGroupButton": {}, "@groupPickerUseThisGroupButton": {},
"createButtonLabel": "LOO", "createButtonLabel": "LOO",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Rühmita", "chipActionGroup": "Muuda grupeerimist",
"@chipActionGroup": {}, "@chipActionGroup": {},
"sectionNone": "Rubriike pole", "sectionNone": "Rubriike pole",
"@sectionNone": {} "@sectionNone": {}

View file

@ -637,7 +637,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "par taille", "sortBySize": "par taille",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "alphabétique", "sortByAlbumFileName": "par titre dalbum et élément",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "par notation", "sortByRating": "par notation",
"@sortByRating": {}, "@sortByRating": {},
@ -1407,7 +1407,7 @@
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Formats", "searchFormatSectionTitle": "Formats",
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"chipActionGroup": "Grouper", "chipActionGroup": "Modifier groupement",
"@chipActionGroup": {}, "@chipActionGroup": {},
"createButtonLabel": "CRÉER", "createButtonLabel": "CRÉER",
"@createButtonLabel": {}, "@createButtonLabel": {},

View file

@ -158,5 +158,49 @@
"chipActionCreateGroup": "צור קבוצה", "chipActionCreateGroup": "צור קבוצה",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},
"chipActionCreateVault": "צור כספת", "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": {}
} }

View file

@ -291,7 +291,7 @@
"@tileLayoutMosaic": {}, "@tileLayoutMosaic": {},
"tileLayoutGrid": "Rács", "tileLayoutGrid": "Rács",
"@tileLayoutGrid": {}, "@tileLayoutGrid": {},
"viewDialogGroupSectionTitle": "Csoport", "viewDialogGroupSectionTitle": "Szekciók",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"menuActionStats": "Statisztikák", "menuActionStats": "Statisztikák",
"@menuActionStats": {}, "@menuActionStats": {},
@ -1594,5 +1594,33 @@
"editEntryLocationDialogTimeShift": "Időeltolódás", "editEntryLocationDialogTimeShift": "Időeltolódás",
"@editEntryLocationDialogTimeShift": {}, "@editEntryLocationDialogTimeShift": {},
"removeEntryMetadataDialogAll": "Összes", "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": {}
} }

View file

@ -453,7 +453,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Sortir", "viewDialogSortSectionTitle": "Sortir",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grup", "viewDialogGroupSectionTitle": "Bagian",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Tata letak", "viewDialogLayoutSectionTitle": "Tata letak",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -1406,5 +1406,29 @@
"sortByPath": "Melalui lokasi", "sortByPath": "Melalui lokasi",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Format", "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": {}
} }

View file

@ -1346,7 +1346,7 @@
"@binPageTitle": {}, "@binPageTitle": {},
"tagPlaceholderState": "Hérað", "tagPlaceholderState": "Hérað",
"@tagPlaceholderState": {}, "@tagPlaceholderState": {},
"sortByAlbumFileName": "Eftir heiti albúma og skráa", "sortByAlbumFileName": "Eftir heiti albúma og atriða",
"@sortByAlbumFileName": {}, "@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": "{count, plural, =1{Eyða þessum albúmum og atriðinu í þeim?} other{Eyða þessum albúmum og {count} atriðum í þeim??}}",
"@deleteMultiAlbumConfirmationDialogMessage": { "@deleteMultiAlbumConfirmationDialogMessage": {

View file

@ -317,7 +317,7 @@
"@binEntriesConfirmationDialogMessage": {}, "@binEntriesConfirmationDialogMessage": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}",
"@deleteEntriesConfirmationDialogMessage": {}, "@deleteEntriesConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogMessage": "いくつかのアイテムはメタデータ上に日付がありません。メタデータ上の日付が設定されない場合、この操作によりこれらの現在の日付はリセットされます", "moveUndatedConfirmationDialogMessage": "続行する前にアイテムの日付を保存しますか?",
"@moveUndatedConfirmationDialogMessage": {}, "@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "日付を設定", "moveUndatedConfirmationDialogSetDate": "日付を設定",
"@moveUndatedConfirmationDialogSetDate": {}, "@moveUndatedConfirmationDialogSetDate": {},

View file

@ -637,7 +637,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "크기", "sortBySize": "크기",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "이름", "sortByAlbumFileName": "앨범 및 항목 제목",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "별점", "sortByRating": "별점",
"@sortByRating": {}, "@sortByRating": {},
@ -1409,7 +1409,7 @@
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"chipActionCreateGroup": "그룹 만들기", "chipActionCreateGroup": "그룹 만들기",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},
"chipActionGroup": "그룹으로 이동", "chipActionGroup": "그룹 변경",
"@chipActionGroup": {}, "@chipActionGroup": {},
"albumTierGroups": "그룹", "albumTierGroups": "그룹",
"@albumTierGroups": {}, "@albumTierGroups": {},

View file

@ -1312,5 +1312,19 @@
"settingsVideoPlaybackTile": "ဖွင့်ကြည့်ခြင်း", "settingsVideoPlaybackTile": "ဖွင့်ကြည့်ခြင်း",
"@settingsVideoPlaybackTile": {}, "@settingsVideoPlaybackTile": {},
"chipActionShowCollection": "စုစည်းမှုထဲမှာ ပြရန်", "chipActionShowCollection": "စုစည်းမှုထဲမှာ ပြရန်",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionDecompose": "ဖြတ်ထုတ်ရန်",
"@chipActionDecompose": {},
"chipActionGroup": "အုပ်စုဖွဲ့မည်",
"@chipActionGroup": {},
"stopTooltip": "ရပ်ရန်",
"@stopTooltip": {},
"createButtonLabel": "အသစ်ထည့်ရန်",
"@createButtonLabel": {},
"chipActionRemove": "ဖယ်ရှားမည်",
"@chipActionRemove": {},
"chipActionGoToExplorerPage": "Explorer ထဲတွင်ပြမည်",
"@chipActionGoToExplorerPage": {},
"chipActionCreateGroup": "အုပ်စုအသစ်ပြုလုပ်မည်",
"@chipActionCreateGroup": {}
} }

View file

@ -627,7 +627,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "Op grootte", "sortBySize": "Op grootte",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "Op album- en bestandsnaam", "sortByAlbumFileName": "Op album- en itemnaam",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Op waardering", "sortByRating": "Op waardering",
"@sortByRating": {}, "@sortByRating": {},
@ -1416,7 +1416,7 @@
"@groupPickerUseThisGroupButton": {}, "@groupPickerUseThisGroupButton": {},
"newGroupDialogTitle": "Nieuwe groep", "newGroupDialogTitle": "Nieuwe groep",
"@newGroupDialogTitle": {}, "@newGroupDialogTitle": {},
"chipActionGroup": "Groeperen", "chipActionGroup": "Groepering wijzigen",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Groep aanmaken", "chipActionCreateGroup": "Groep aanmaken",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -769,7 +769,7 @@
"@drawerCollectionPanoramas": {}, "@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "Nieprzetworzone zdjęcia", "drawerCollectionRaws": "Nieprzetworzone zdjęcia",
"@drawerCollectionRaws": {}, "@drawerCollectionRaws": {},
"sortByAlbumFileName": "Według albumu i nazwy pliku", "sortByAlbumFileName": "Według albumu i nazwy elementu",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"albumMimeTypeMixed": "Mieszane", "albumMimeTypeMixed": "Mieszane",
"@albumMimeTypeMixed": {}, "@albumMimeTypeMixed": {},
@ -1603,7 +1603,7 @@
"@sectionNone": {}, "@sectionNone": {},
"createButtonLabel": "UTWÓRZ", "createButtonLabel": "UTWÓRZ",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Grupuj", "chipActionGroup": "Zmień grupowanie",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Utwórz grupę", "chipActionCreateGroup": "Utwórz grupę",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -463,7 +463,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Organizar", "viewDialogSortSectionTitle": "Organizar",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grupo", "viewDialogGroupSectionTitle": "Seções",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Layout", "viewDialogLayoutSectionTitle": "Layout",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -633,7 +633,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "Por tamanho", "sortBySize": "Por tamanho",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "Por álbum e nome de arquivo", "sortByAlbumFileName": "Por álbum e título do item",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Por classificação", "sortByRating": "Por classificação",
"@sortByRating": {}, "@sortByRating": {},
@ -1406,5 +1406,29 @@
"sortByPath": "Pelo caminho", "sortByPath": "Pelo caminho",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Formatos", "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": {}
} }

View file

@ -526,7 +526,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Sortează", "viewDialogSortSectionTitle": "Sortează",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grup", "viewDialogGroupSectionTitle": "Secțiuni",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Aspect", "viewDialogLayoutSectionTitle": "Aspect",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -887,7 +887,7 @@
"@drawerCollectionSphericalVideos": {}, "@drawerCollectionSphericalVideos": {},
"drawerAlbumPage": "Albume", "drawerAlbumPage": "Albume",
"@drawerAlbumPage": {}, "@drawerAlbumPage": {},
"sortByAlbumFileName": "După album și numele fișierului", "sortByAlbumFileName": "După album și numele elementului",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortOrderZtoA": "De la Z la A", "sortOrderZtoA": "De la Z la A",
"@sortOrderZtoA": {}, "@sortOrderZtoA": {},
@ -1598,5 +1598,29 @@
"sortByPath": "După cale", "sortByPath": "După cale",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Formate", "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": {}
} }

View file

@ -463,7 +463,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Сортировка", "viewDialogSortSectionTitle": "Сортировка",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Группировка", "viewDialogGroupSectionTitle": "Разделы",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Макет", "viewDialogLayoutSectionTitle": "Макет",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -633,7 +633,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "По размеру", "sortBySize": "По размеру",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "По имени альбома и файла", "sortByAlbumFileName": "По названию альбома и пункта",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "По рейтингу", "sortByRating": "По рейтингу",
"@sortByRating": {}, "@sortByRating": {},
@ -1406,5 +1406,29 @@
"searchFormatSectionTitle": "Форматы", "searchFormatSectionTitle": "Форматы",
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"sortByPath": "По пути", "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": {}
} }

View file

@ -417,7 +417,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Sırala", "viewDialogSortSectionTitle": "Sırala",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grup", "viewDialogGroupSectionTitle": "Bölümler",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Düzen", "viewDialogLayoutSectionTitle": "Düzen",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -489,7 +489,7 @@
"@collectionActionHideTitleSearch": {}, "@collectionActionHideTitleSearch": {},
"collectionActionAddShortcut": "Kısayol ekle", "collectionActionAddShortcut": "Kısayol ekle",
"@collectionActionAddShortcut": {}, "@collectionActionAddShortcut": {},
"collectionActionEmptyBin": "Boş çöp kutusu", "collectionActionEmptyBin": "Çöp kutusu boş",
"@collectionActionEmptyBin": {}, "@collectionActionEmptyBin": {},
"collectionActionCopy": "Albüme kopyala", "collectionActionCopy": "Albüme kopyala",
"@collectionActionCopy": {}, "@collectionActionCopy": {},
@ -583,7 +583,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "Boyuta göre", "sortBySize": "Boyuta göre",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "Albüm ve dosya adına göre", "sortByAlbumFileName": "Albüm ve başlığı göre",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Derecelendirmeye göre", "sortByRating": "Derecelendirmeye göre",
"@sortByRating": {}, "@sortByRating": {},
@ -1400,5 +1400,35 @@
"collectionActionAddDynamicAlbum": "Dinamik albüm ekle", "collectionActionAddDynamicAlbum": "Dinamik albüm ekle",
"@collectionActionAddDynamicAlbum": {}, "@collectionActionAddDynamicAlbum": {},
"searchFormatSectionTitle": "Biçimler", "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": {}
} }

View file

@ -607,7 +607,7 @@
"@drawerCountryPage": {}, "@drawerCountryPage": {},
"sortByName": "За назвою", "sortByName": "За назвою",
"@sortByName": {}, "@sortByName": {},
"sortByAlbumFileName": "За назвою альбому та файлу", "sortByAlbumFileName": "За назвою альбому та елемента",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByItemCount": "За кількістю елементів", "sortByItemCount": "За кількістю елементів",
"@sortByItemCount": {}, "@sortByItemCount": {},
@ -1601,7 +1601,7 @@
"@sortByPath": {}, "@sortByPath": {},
"createButtonLabel": "СТВОРИТИ", "createButtonLabel": "СТВОРИТИ",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Згрупувати", "chipActionGroup": "Змінити групування",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Створити групу", "chipActionCreateGroup": "Створити групу",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -625,7 +625,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "按大小", "sortBySize": "按大小",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "按相册和文件名", "sortByAlbumFileName": "按相册和项目标题",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "按评分", "sortByRating": "按评分",
"@sortByRating": {}, "@sortByRating": {},
@ -1419,7 +1419,7 @@
"@newGroupDialogTitle": {}, "@newGroupDialogTitle": {},
"createButtonLabel": "创建", "createButtonLabel": "创建",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "分组", "chipActionGroup": "更改分组",
"@chipActionGroup": {}, "@chipActionGroup": {},
"groupAlreadyExists": "组已存在", "groupAlreadyExists": "组已存在",
"@groupAlreadyExists": {}, "@groupAlreadyExists": {},

View file

@ -140,10 +140,14 @@ class Contributors {
Contributor('Miquel Martí', 'miquelmarti111@gmail.com'), Contributor('Miquel Martí', 'miquelmarti111@gmail.com'),
Contributor('Yurt Page', 'yurtpage@gmail.com'), Contributor('Yurt Page', 'yurtpage@gmail.com'),
Contributor('Murcielago', 'weblate.j9bmx@slmail.me'), Contributor('Murcielago', 'weblate.j9bmx@slmail.me'),
Contributor('vm', 'varga.m007@gmail.com'),
Contributor('WMatheist', 'wmatheist@protonmail.com'),
// Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani // Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani
// Contributor('Jamil Farajov', 'jamilfarajov@gmail.com'), // Azerbaijani
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali // Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese // Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese // Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
// Contributor('Thit Lwin', 'thitlwincoder@gmail.com'), // Burmese
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish // Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish // Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
// Contributor('Ricky Tigg', 'ricky.tigg@gmail.com'), // Finnish // Contributor('Ricky Tigg', 'ricky.tigg@gmail.com'), // Finnish

View file

@ -212,9 +212,9 @@ class Dependencies {
sourceUrl: 'https://github.com/fleaflet/flutter_map', sourceUrl: 'https://github.com/fleaflet/flutter_map',
), ),
Dependency( Dependency(
name: 'Flutter Markdown', name: 'Flutter Markdown Plus',
license: bsd3, license: bsd3,
sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/flutter_markdown', sourceUrl: 'https://github.com/foresightmobile/flutter_markdown_plus',
), ),
Dependency( Dependency(
name: 'Flutter Staggered Animations', name: 'Flutter Staggered Animations',

View file

@ -25,7 +25,7 @@ final Covers covers = Covers._private();
typedef CoverProps = (int? entryId, String? packageName, Color? color); typedef CoverProps = (int? entryId, String? packageName, Color? color);
class Covers { class Covers {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final _lock = Lock(); final _lock = Lock();
final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast(); final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast();
@ -40,6 +40,8 @@ class Covers {
Set<CoverRow> _rows = {}; Set<CoverRow> _rows = {};
// do not subscribe to events from other modules in constructor
// so that modules can subscribe to each other
Covers._private(); Covers._private();
Future<void> init() async { Future<void> init() async {

View file

@ -15,19 +15,21 @@ import 'package:synchronized/synchronized.dart';
final DynamicAlbums dynamicAlbums = DynamicAlbums._private(); final DynamicAlbums dynamicAlbums = DynamicAlbums._private();
class DynamicAlbums with ChangeNotifier { class DynamicAlbums with ChangeNotifier {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final _lock = Lock(); final _lock = Lock();
Set<DynamicAlbumFilter> _rows = {}; Set<DynamicAlbumFilter> _rows = {};
final EventBus eventBus = EventBus(); final EventBus eventBus = EventBus();
// do not subscribe to events from other modules in constructor
// so that modules can subscribe to each other
DynamicAlbums._private() { DynamicAlbums._private() {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this); if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
_subscriptions.add(albumGrouping.eventBus.on<GroupUriChangedEvent>().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
} }
Future<void> init() async { Future<void> init() async {
_rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet(); _rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet();
_subscriptions.add(albumGrouping.eventBus.on<GroupUriChangedEvent>().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
} }
int get count => _rows.length; int get count => _rows.length;
@ -57,6 +59,7 @@ class DynamicAlbums with ChangeNotifier {
await _lock.synchronized(() async { await _lock.synchronized(() async {
await _doRemove(filters.map((filter) => filter.name).toSet()); await _doRemove(filters.map((filter) => filter.name).toSet());
notifyListeners(); notifyListeners();
eventBus.fire(DynamicAlbumChangedEvent(Map.fromEntries(filters.map((v) => MapEntry(v, null)))));
}); });
} }
@ -81,13 +84,7 @@ class DynamicAlbums with ChangeNotifier {
}); });
} }
Future<void> clear() async { Future<void> clear() => remove(all);
await _lock.synchronized(() async {
await localMediaDb.clearDynamicAlbums();
_rows.clear();
notifyListeners();
});
}
DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name); DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name);

View file

@ -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/keys.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/media/geotiff.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/media/video/metadata.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart';

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/container/container.dart'; 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/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -38,8 +38,6 @@ class SetAndFilter extends CollectionFilter with ContainerFilter {
static SetAndFilter? fromMap(Map<String, dynamic> json) { static SetAndFilter? fromMap(Map<String, dynamic> json) {
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet(); final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
if (filters.isEmpty) return null;
return SetAndFilter( return SetAndFilter(
filters, filters,
reversed: json['reversed'] ?? false, reversed: json['reversed'] ?? false,

View file

@ -38,8 +38,6 @@ class SetOrFilter extends CollectionFilter with ContainerFilter {
static SetOrFilter? fromMap(Map<String, dynamic> json) { static SetOrFilter? fromMap(Map<String, dynamic> json) {
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet(); final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
if (filters.isEmpty) return null;
return SetOrFilter( return SetOrFilter(
filters, filters,
reversed: json['reversed'] ?? false, reversed: json['reversed'] ?? false,

View file

@ -14,4 +14,3 @@ mixin CoveredFilter on CollectionFilter {
return super.color(context); return super.color(context);
} }
} }

View file

@ -2,9 +2,11 @@ import 'dart:convert';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/aspect_ratio.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/album_group.dart';
import 'package:aves/model/filters/container/dynamic_album.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/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart'; import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/covered/tag.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/query.dart';
import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/recent.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/trash.dart';
import 'package:aves/model/filters/type.dart'; import 'package:aves/model/filters/type.dart';
import 'package:aves/model/filters/weekday.dart'; import 'package:aves/model/filters/weekday.dart';

View file

@ -1,10 +1,17 @@
import 'dart:async';
import 'dart:convert'; 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/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/group_base.dart';
import 'package:aves/model/filters/container/set_or.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/filters/filters.dart';
import 'package:aves/model/grouping/convert.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/services/common/services.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -28,18 +35,53 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
final String _host; final String _host;
final T Function(Uri uri, SetOrFilter filter) _createGroupFilter; final T Function(Uri uri, SetOrFilter filter) _createGroupFilter;
final Map<Uri, Set<Uri>> _groups = {}; final Map<Uri, Set<Uri>> _groups = {};
final Set<StreamSubscription> _subscriptions = {};
final Map<CollectionSource, Set<StreamSubscription>> _sourceSubscriptions = {};
CollectionSource? _source;
Map<Uri, Set<Uri>> get allGroups => Map.unmodifiable(_groups); Map<Uri, Set<Uri>> 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) { FilterGrouping._private(this._host, this._createGroupFilter) {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this); if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
} }
void init(Map<Uri, Set<Uri>> groups) { void init() {
_subscriptions.add(dynamicAlbums.eventBus.on<DynamicAlbumChangedEvent>().listen((e) => _clearObsoleteFilters()));
}
void setGroups(Map<Uri, Set<Uri>> groups) {
_groups.clear(); _groups.clear();
_groups.addAll(groups); _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<EntryMovedEvent>().listen((e) => _clearObsoleteFilters()),
sourceEvents.on<EntryRemovedEvent>().listen((e) => _clearObsoleteFilters()),
sourceEvents.on<AlbumsChangedEvent>().listen((e) => _clearObsoleteFilters()),
};
_source = source;
}
void unregisterSource(CollectionSource? source) {
_sourceSubscriptions.remove(source)
?..forEach((sub) => sub.cancel())
..clear();
}
void addToGroup(Set<Uri> childrenUris, Uri? destinationGroup) { void addToGroup(Set<Uri> childrenUris, Uri? destinationGroup) {
_removeFromGroups(childrenUris); _removeFromGroups(childrenUris);
if (destinationGroup != null) { if (destinationGroup != null) {
@ -73,9 +115,9 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
int countLeaves(Uri? groupUri) { int countLeaves(Uri? groupUri) {
int count = 0; int count = 0;
if (groupUri != null) { if (groupUri != null) {
final childrenUri = _groups[groupUri]; final childrenUris = _groups[groupUri];
if (childrenUri != null) { if (childrenUris != null) {
childrenUri.map(uriToFilter).nonNulls.forEach((filter) { childrenUris.map(uriToFilter).nonNulls.forEach((filter) {
if (filter is GroupBaseFilter) { if (filter is GroupBaseFilter) {
count += countLeaves(filter.uri); count += countLeaves(filter.uri);
} else { } else {
@ -93,15 +135,15 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
if (currentGroupUri == null) { if (currentGroupUri == null) {
return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) { return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) {
final groupUri = kv.key; final groupUri = kv.key;
final childrenUri = kv.value; final childrenUris = kv.value;
final childrenFilters = childrenUri.map(uriToFilter).nonNulls.toSet(); final childrenFilters = childrenUris.map(uriToFilter).nonNulls.toSet();
return _createGroupFilter(groupUri, SetOrFilter(childrenFilters)); return _createGroupFilter(groupUri, SetOrFilter(childrenFilters));
}).toSet(); }).toSet();
} }
final childrenUri = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value; final childrenUris = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
if (childrenUri != null) { if (childrenUris != null) {
return childrenUri.map(uriToFilter).nonNulls.toSet(); return childrenUris.map(uriToFilter).nonNulls.toSet();
} }
return {}; return {};
@ -172,6 +214,46 @@ class FilterGrouping<T extends GroupBaseFilter> 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 // group uri / filter conversion
static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey]; static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey];

View file

@ -46,7 +46,7 @@ import 'package:latlong2/latlong.dart';
final Settings settings = Settings._private(); 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 { class Settings with ChangeNotifier, SettingsAccess, SearchSettings, AppSettings, CollectionSettings, DebugSettings, DisplaySettings, FilterGridsSettings, InfoSettings, NavigationSettings, PrivacySettings, ScreenSaverSettings, SlideshowSettings, SubtitlesSettings, VideoSettings, ViewerSettings, WidgetSettings {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change'); final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast(); final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
final StreamController<SettingsChangedEvent> _updateTileExtentStreamController = StreamController.broadcast(); final StreamController<SettingsChangedEvent> _updateTileExtentStreamController = StreamController.broadcast();

View file

@ -34,7 +34,7 @@ class CollectionLens with ChangeNotifier {
EntrySortFactor sortFactor; EntrySortFactor sortFactor;
bool sortReverse; bool sortReverse;
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
int? id; int? id;
bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort; bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
List<AvesEntry>? fixedSelection; List<AvesEntry>? fixedSelection;

View file

@ -60,7 +60,9 @@ class MediaStoreSource extends CollectionSource {
await localMediaDb.init(); await localMediaDb.init();
await vaults.init(); await vaults.init();
await favourites.init(); await favourites.init();
albumGrouping.init(settings.albumGroups); albumGrouping.init();
albumGrouping.setGroups(settings.albumGroups);
albumGrouping.registerSource(this);
await covers.init(); await covers.init();
await dynamicAlbums.init(); await dynamicAlbums.init();

View file

@ -15,7 +15,7 @@ import 'package:provider/provider.dart';
final Vaults vaults = Vaults._private(); final Vaults vaults = Vaults._private();
class Vaults extends ChangeNotifier { class Vaults extends ChangeNotifier {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
Set<VaultDetails> _rows = {}; Set<VaultDetails> _rows = {};
final Set<String> _unlockedDirPaths = {}; final Set<String> _unlockedDirPaths = {};

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui;
import 'package:aves/model/app/support.dart'; import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/entry.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/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';
import 'dart:ui' as ui;
abstract class MediaFetchService { abstract class MediaFetchService {
Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false}); Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false});

View file

@ -25,7 +25,7 @@ abstract class MediaSessionService {
class PlatformMediaSessionService implements MediaSessionService, Disposable { class PlatformMediaSessionService implements MediaSessionService, Disposable {
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session'); static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command'); final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command');
final StreamController _streamController = StreamController.broadcast(); final StreamController _streamController = StreamController.broadcast();

View file

@ -74,20 +74,24 @@ class PlatformWindowService implements WindowService {
return false; 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 @override
Future<void> requestOrientation([Orientation? orientation]) async { Future<void> requestOrientation([Orientation? orientation]) async {
// cf Android `ActivityInfo.ScreenOrientation`
late final int orientationCode; late final int orientationCode;
switch (orientation) { switch (orientation) {
case Orientation.landscape: case Orientation.landscape:
// SCREEN_ORIENTATION_SENSOR_LANDSCAPE orientationCode = screenOrientationUserLandscape;
orientationCode = 6;
case Orientation.portrait: case Orientation.portrait:
// SCREEN_ORIENTATION_SENSOR_PORTRAIT orientationCode = screenOrientationUserPortrait;
orientationCode = 7;
default: default:
// SCREEN_ORIENTATION_UNSPECIFIED orientationCode = screenOrientationUnspecified;
orientationCode = -1;
} }
try { try {
await _platform.invokeMethod('requestOrientation', <String, dynamic>{ await _platform.invokeMethod('requestOrientation', <String, dynamic>{

View file

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

View file

@ -160,7 +160,7 @@ class AvesApp extends StatefulWidget {
} }
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late final Future<void> _appSetup; late final Future<void> _appSetup;
late final Future<bool> _shouldUseBoldFontLoader; late final Future<bool> _shouldUseBoldFontLoader;
final TvRailController _tvRailController = TvRailController(); final TvRailController _tvRailController = TvRailController();

View file

@ -4,9 +4,9 @@ import 'dart:math';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/container/dynamic_album.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/filters.dart';
import 'package:aves/model/filters/query.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/filters/trash.dart';
import 'package:aves/model/query.dart'; import 'package:aves/model/query.dart';
import 'package:aves/model/selection.dart'; import 'package:aves/model/selection.dart';
@ -57,7 +57,7 @@ class CollectionAppBar extends StatefulWidget {
} }
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin, WidgetsBindingObserver { class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate(); final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
late AnimationController _browseToSelectAnimation; late AnimationController _browseToSelectAnimation;
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);

View file

@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
} }
class _CollectionPageState extends State<CollectionPage> { class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late CollectionLens _collection; late CollectionLens _collection;
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast(); final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();

View file

@ -10,8 +10,8 @@ import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/container/dynamic_album.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/container/set_and.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/grouping/common.dart'; import 'package:aves/model/grouping/common.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/date_modifier.dart';

View file

@ -9,8 +9,8 @@ import 'package:provider/provider.dart';
class FilterBar extends StatefulWidget { class FilterBar extends StatefulWidget {
static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4); static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4);
static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4); static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4);
static const double verticalPadding = 16; static const EdgeInsets padding = EdgeInsets.only(top: 4, bottom: 8);
static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding; static final double preferredHeight = AvesFilterChip.minChipHeight + padding.vertical;
final List<CollectionFilter> filters; final List<CollectionFilter> filters;
final bool interactive; final bool interactive;
@ -84,6 +84,7 @@ class _FilterBarState extends State<FilterBar> {
return Container( return Container(
// specify transparent as a workaround to prevent // specify transparent as a workaround to prevent
// chip border clipping when the floating app bar is fading // chip border clipping when the floating app bar is fading
padding: FilterBar.padding,
color: Colors.transparent, color: Colors.transparent,
height: FilterBar.preferredHeight, height: FilterBar.preferredHeight,
child: AnimatedList( child: AnimatedList(

View file

@ -45,7 +45,7 @@ class MenuQuickChooser<T> extends StatefulWidget {
} }
class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> { class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero); final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
int _scrollDirection = 0; int _scrollDirection = 0;

View file

@ -23,7 +23,7 @@ class RateQuickChooser extends StatefulWidget {
} }
class _RateQuickChooserState extends State<RateQuickChooser> { class _RateQuickChooserState extends State<RateQuickChooser> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
ValueNotifier<int?> get valueNotifier => widget.valueNotifier; ValueNotifier<int?> get valueNotifier => widget.valueNotifier;

View file

@ -28,7 +28,7 @@ class PlayToggler extends StatefulWidget {
} }
class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStateMixin { class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late AnimationController _playPauseAnimation; late AnimationController _playPauseAnimation;
AvesVideoController? get controller => widget.controller; AvesVideoController? get controller => widget.controller;

View file

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

View file

@ -8,6 +8,8 @@ class CrumbLine<T> extends StatefulWidget {
final T Function(BuildContext context, int index) combine; final T Function(BuildContext context, int index) combine;
final void Function(T combined) onTap; final void Function(T combined) onTap;
static const EdgeInsets padding = EdgeInsets.only(top: 6, bottom: 20);
const CrumbLine({ const CrumbLine({
super.key, super.key,
required this.split, required this.split,
@ -18,7 +20,7 @@ class CrumbLine<T> extends StatefulWidget {
@override @override
State<CrumbLine<T>> createState() => _CrumbLineState<T>(); State<CrumbLine<T>> createState() => _CrumbLineState<T>();
static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(kToolbarHeight); static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(22) + padding.vertical;
} }
class _CrumbLineState<T> extends State<CrumbLine<T>> { class _CrumbLineState<T> extends State<CrumbLine<T>> {

View file

@ -522,7 +522,7 @@ class _InkResponseStateWidget extends StatefulWidget {
if (onSecondaryTap != null) 'secondary tap', if (onSecondaryTap != null) 'secondary tap',
if (onSecondaryTapUp != null) 'secondary tap up', if (onSecondaryTapUp != null) 'secondary tap up',
if (onSecondaryTapDown != null) 'secondary tap down', if (onSecondaryTapDown != null) 'secondary tap down',
if (onSecondaryTapCancel != null) 'secondary tap cancel' if (onSecondaryTapCancel != null) 'secondary tap cancel',
]; ];
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>')); properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor)); properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
@ -544,10 +544,7 @@ enum _HighlightType {
focus, focus,
} }
class _InkResponseState extends State<_InkResponseStateWidget> class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKeepAliveClientMixin<_InkResponseStateWidget> implements _ParentInkResponseState {
with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
implements _ParentInkResponseState
{
Set<InteractiveInkFeature>? _splashes; Set<InteractiveInkFeature>? _splashes;
InteractiveInkFeature? _currentSplash; InteractiveInkFeature? _currentSplash;
bool _hovering = false; bool _hovering = false;
@ -578,6 +575,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed); widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed);
} }
} }
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty; bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
void activateOnIntent(Intent? intent) { void activateOnIntent(Intent? intent) {
@ -611,7 +609,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
void handleStatesControllerChange() { void handleStatesControllerChange() {
// Force a rebuild to resolve widget.overlayColor, widget.mouseCursor // Force a rebuild to resolve widget.overlayColor, widget.mouseCursor
setState(() { }); setState(() {});
} }
WidgetStatesController get statesController => widget.statesController ?? internalStatesController!; WidgetStatesController get statesController => widget.statesController ?? internalStatesController!;
@ -642,9 +640,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
initStatesController(); initStatesController();
} }
if (widget.radius != oldWidget.radius || if (widget.radius != oldWidget.radius || widget.highlightShape != oldWidget.highlightShape || widget.borderRadius != oldWidget.borderRadius) {
widget.highlightShape != oldWidget.highlightShape ||
widget.borderRadius != oldWidget.borderRadius) {
final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover]; final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
if (hoverHighlight != null) { if (hoverHighlight != null) {
hoverHighlight.dispose(); 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]; final InkHighlight? highlight = _highlights[type];
void handleInkRemoval() { void handleInkRemoval() {
assert(_highlights[type] != null); assert(_highlights[type] != null);
@ -730,8 +726,8 @@ class _InkResponseState extends State<_InkResponseStateWidget>
if (value) { if (value) {
if (highlight == null) { if (highlight == null) {
final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value) final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value) ??
?? switch (type) { switch (type) {
// Use the backwards compatible defaults // Use the backwards compatible defaults
_HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor, _HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor,
_HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor, _HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor,
@ -846,6 +842,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
bool _hasFocus = false; bool _hasFocus = false;
void handleFocusUpdate(bool hasFocus) { void handleFocusUpdate(bool hasFocus) {
_hasFocus = hasFocus; _hasFocus = hasFocus;
// Set here rather than updateHighlight because this widget's // Set here rather than updateHighlight because this widget's
@ -978,21 +975,17 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
bool _primaryButtonEnabled(_InkResponseStateWidget widget) { bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
return widget.onTap != null return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || widget.onTapUp != null || widget.onTapDown != null;
|| widget.onDoubleTap != null
|| widget.onLongPress != null
|| widget.onTapUp != null
|| widget.onTapDown != null;
} }
bool _secondaryButtonEnabled(_InkResponseStateWidget widget) { bool _secondaryButtonEnabled(_InkResponseStateWidget widget) {
return widget.onSecondaryTap != null return widget.onSecondaryTap != null || widget.onSecondaryTapUp != null || widget.onSecondaryTapDown != null;
|| widget.onSecondaryTapUp != null
|| widget.onSecondaryTapDown != null;
} }
bool get enabled => isWidgetEnabled(widget); bool get enabled => isWidgetEnabled(widget);
bool get _primaryEnabled => _primaryButtonEnabled(widget); bool get _primaryEnabled => _primaryButtonEnabled(widget);
bool get _secondaryEnabled => _secondaryButtonEnabled(widget); bool get _secondaryEnabled => _secondaryButtonEnabled(widget);
void handleMouseEnter(PointerEnterEvent event) { void handleMouseEnter(PointerEnterEvent event) {
@ -1040,6 +1033,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
_HighlightType.hover => widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor, _HighlightType.hover => widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor,
}; };
} }
for (final _HighlightType type in _highlights.keys) { for (final _HighlightType type in _highlights.keys) {
_highlights[type]?.color = getHighlightColorForType(type); _highlights[type]?.color = getHighlightColorForType(type);
} }
@ -1077,7 +1071,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null, onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? handleLongPress : null, onLongPress: widget.onLongPress != null ? handleLongPress : null,
onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null, onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null,
onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp: null, onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp : null,
onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null, onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null,
onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null, onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,

View file

@ -3,7 +3,7 @@ import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:flutter/material.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 { class MarkdownContainer extends StatelessWidget {
final String data; final String data;

View file

@ -20,7 +20,8 @@ class BlurredRect extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRect( return ClipRect(
child: BackdropFilter.grouped( // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
child: BackdropFilter(
// do not modify tree when disabling filter // do not modify tree when disabling filter
filter: enabled ? _filter : _identity, filter: enabled ? _filter : _identity,
child: child, child: child,
@ -59,7 +60,8 @@ class BlurredRRect extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: borderRadius ?? BorderRadius.zero, borderRadius: borderRadius ?? BorderRadius.zero,
child: BackdropFilter.grouped( // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
child: BackdropFilter(
// do not modify tree when disabling filter // do not modify tree when disabling filter
filter: enabled ? _filter : _identity, filter: enabled ? _filter : _identity,
child: child, child: child,
@ -81,7 +83,8 @@ class BlurredOval extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipOval( return ClipOval(
child: BackdropFilter.grouped( // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
child: BackdropFilter(
// do not modify tree when disabling filter // do not modify tree when disabling filter
filter: enabled ? _filter : _identity, filter: enabled ? _filter : _identity,
child: child, child: child,

View file

@ -44,7 +44,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
return (scrollableContext.findRenderObject() as RenderBox).size; return (scrollableContext.findRenderObject() as RenderBox).size;
} }
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
// grid section metrics before the app is laid out with the new orientation // grid section metrics before the app is laid out with the new orientation
late SectionedListLayout<T> _lastSectionedListLayout; late SectionedListLayout<T> _lastSectionedListLayout;

View file

@ -150,7 +150,7 @@ class AvesFilterChip extends StatefulWidget {
} }
class _AvesFilterChipState extends State<AvesFilterChip> { class _AvesFilterChipState extends State<AvesFilterChip> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late Future<Color> _colorFuture; late Future<Color> _colorFuture;
late Color _outlineColor; late Color _outlineColor;
late bool _tapped; late bool _tapped;

View file

@ -5,7 +5,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.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'; import 'package:provider/provider.dart';
class Attribution extends StatelessWidget { class Attribution extends StatelessWidget {

View file

@ -83,7 +83,7 @@ class GeoMap extends StatefulWidget {
} }
class _GeoMapState extends State<GeoMap> { class _GeoMapState extends State<GeoMap> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
// as of google_maps_flutter v2.0.6, Google map initialization is blocking // as of google_maps_flutter v2.0.6, Google map initialization is blocking
// cf https://github.com/flutter/flutter/issues/28493 // cf https://github.com/flutter/flutter/issues/28493
@ -249,14 +249,13 @@ class _GeoMapState extends State<GeoMap> {
child = Column( child = Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
BackdropGroup( // TODO TLAD [flutter vNext] wrap into `BackdropGroup`
child: mapHeight != null mapHeight != null
? SizedBox( ? SizedBox(
height: mapHeight, height: mapHeight,
child: child, child: child,
) )
: Expanded(child: child), : Expanded(child: child),
),
SafeArea( SafeArea(
top: false, top: false,
bottom: false, bottom: false,

View file

@ -66,7 +66,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin { class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin {
final MapController _leafletMapController = MapController(); final MapController _leafletMapController = MapController();
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {}; Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
final Debouncer _debouncer = Debouncer(delay: ADurations.mapIdleDebounceDelay); final Debouncer _debouncer = Debouncer(delay: ADurations.mapIdleDebounceDelay);

View file

@ -43,10 +43,12 @@ abstract class AvesSearchDelegate extends SearchDelegate {
final animate = context.read<Settings>().animate; final animate = context.read<Settings>().animate;
return canPop return canPop
? IconButton( ? IconButton(
icon: animate ? AnimatedIcon( icon: animate
? AnimatedIcon(
icon: AnimatedIcons.menu_arrow, icon: AnimatedIcons.menu_arrow,
progress: transitionAnimation, progress: transitionAnimation,
): const Icon(Icons.arrow_back), )
: const Icon(Icons.arrow_back),
onPressed: () => goBack(context), onPressed: () => goBack(context),
tooltip: MaterialLocalizations.of(context).backButtonTooltip, tooltip: MaterialLocalizations.of(context).backButtonTooltip,
) )

View file

@ -15,7 +15,7 @@ class TileExtentController {
late double userPreferredExtent; late double userPreferredExtent;
Size _viewportSize = Size.zero; Size _viewportSize = Size.zero;
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
Size get viewportSize => _viewportSize; Size get viewportSize => _viewportSize;

View file

@ -56,7 +56,7 @@ class EditEntryLocationDialog extends StatefulWidget {
} }
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> with FeedbackMixin { class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> with FeedbackMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
LocationEditAction _action = LocationEditAction.chooseOnMap; LocationEditAction _action = LocationEditAction.chooseOnMap;
LatLng? _mapCoordinates; LatLng? _mapCoordinates;
late final AvesEntry mainEntry; late final AvesEntry mainEntry;

View file

@ -63,7 +63,7 @@ class _Content extends StatefulWidget {
} }
class _ContentState extends State<_Content> with SingleTickerProviderStateMixin { class _ContentState extends State<_Content> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final AvesMapController _mapController = AvesMapController(); final AvesMapController _mapController = AvesMapController();
late final ValueNotifier<bool> _isPageAnimatingNotifier; late final ValueNotifier<bool> _isPageAnimatingNotifier;
final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null), _infoLocationNotifier = ValueNotifier(null); final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null), _infoLocationNotifier = ValueNotifier(null);

View file

@ -28,7 +28,7 @@ class ImageEditorPage extends StatefulWidget {
} }
class _ImageEditorPageState extends State<ImageEditorPage> { class _ImageEditorPageState extends State<ImageEditorPage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null); final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null);
final ValueNotifier<EdgeInsets> _marginNotifier = ValueNotifier(EdgeInsets.zero); final ValueNotifier<EdgeInsets> _marginNotifier = ValueNotifier(EdgeInsets.zero);
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero); final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
@ -118,7 +118,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
} }
void _onActionChanged() { void _onActionChanged() {
switch(_actionNotifier.value) { switch (_actionNotifier.value) {
case EditorAction.transform: case EditorAction.transform:
_transformController.reset(); _transformController.reset();
_marginNotifier.value = Cropper.imageMargin; _marginNotifier.value = Cropper.imageMargin;

View file

@ -36,7 +36,7 @@ class EditorImage extends StatefulWidget {
} }
class _EditorImageState extends State<EditorImage> { class _EditorImageState extends State<EditorImage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<double> _scrimOpacityNotifier = ValueNotifier(0); final ValueNotifier<double> _scrimOpacityNotifier = ValueNotifier(0);
AvesEntry get entry => widget.entry; AvesEntry get entry => widget.entry;

View file

@ -38,7 +38,7 @@ class Cropper extends StatefulWidget {
} }
class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin { class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero); final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0); final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
late AnimationController _gridAnimationController; late AnimationController _gridAnimationController;

View file

@ -65,7 +65,8 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
actions: _buildActions, actions: _buildActions,
bottom: LayoutBuilder( bottom: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SizedBox( return Container(
padding: CrumbLine.padding,
width: constraints.maxWidth, width: constraints.maxWidth,
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)), height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
child: ValueListenableBuilder<VolumeRelativeDirectory?>( child: ValueListenableBuilder<VolumeRelativeDirectory?>(

View file

@ -40,7 +40,7 @@ class ExplorerPage extends StatefulWidget {
} }
class _ExplorerPageState extends State<ExplorerPage> { class _ExplorerPageState extends State<ExplorerPage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<VolumeRelativeDirectory?> _directory = ValueNotifier(null); final ValueNotifier<VolumeRelativeDirectory?> _directory = ValueNotifier(null);
final ValueNotifier<VolumeRelativeDirectory?> _contentsDirectory = ValueNotifier(null); final ValueNotifier<VolumeRelativeDirectory?> _contentsDirectory = ValueNotifier(null);
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]); final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);

View file

@ -44,12 +44,12 @@ class AlbumListPage extends StatelessWidget {
child: Builder( child: Builder(
// to access filter group provider from subtree context // to access filter group provider from subtree context
builder: (context) { builder: (context) {
return Selector<Settings, (AlbumChipSectionFactor, ChipSortFactor, bool, Set<CollectionFilter>)>( return Selector<Settings, (AlbumChipSectionFactor, ChipSortFactor, bool, Set<CollectionFilter>, Set<CollectionFilter>)>(
selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.pinnedFilters), selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.hiddenFilters, s.pinnedFilters),
shouldRebuild: (t1, t2) { shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within records // `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within records
const eq = DeepCollectionEquality(); 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) { builder: (context, s, child) {
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
@ -123,7 +123,7 @@ class AlbumListPage extends StatelessWidget {
final listedDynamicAlbums = <DynamicAlbumFilter>{}; final listedDynamicAlbums = <DynamicAlbumFilter>{};
if (albumChipTypes.contains(AlbumChipType.dynamic)) { if (albumChipTypes.contains(AlbumChipType.dynamic)) {
final allDynamicAlbums = dynamicAlbums.all; final allDynamicAlbums = dynamicAlbums.all.whereNot(settings.hiddenFilters.contains).toSet();
if (groupUri == null) { if (groupUri == null) {
final withinGroups = whereTypeRecursively<DynamicAlbumFilter>(groupContent).toSet(); final withinGroups = whereTypeRecursively<DynamicAlbumFilter>(groupContent).toSet();
listedDynamicAlbums.addAll(allDynamicAlbums.whereNot(withinGroups.contains)); 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 // always show groups, which are needed to navigate to other types
final albumGroupFilters = groupContent.whereType<AlbumGroupFilter>().toSet(); final albumGroupFilters = groupContent.whereType<AlbumGroupFilter>().whereNot(settings.hiddenFilters.contains).toSet();
final filters = <AlbumBaseFilter>{ final filters = <AlbumBaseFilter>{
...albumGroupFilters, ...albumGroupFilters,

View file

@ -79,7 +79,7 @@ class FilterGridAppBar<T extends CollectionFilter, CSAD extends ChipSetActionDel
} }
class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with SingleTickerProviderStateMixin, WidgetsBindingObserver { class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late AnimationController _browseToSelectAnimation; late AnimationController _browseToSelectAnimation;
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
final FocusNode _queryBarFocusNode = FocusNode(); final FocusNode _queryBarFocusNode = FocusNode();
@ -170,7 +170,8 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
if (_showGroupCrumbLine(context)) if (_showGroupCrumbLine(context))
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SizedBox( return Container(
padding: CrumbLine.padding,
width: constraints.maxWidth, width: constraints.maxWidth,
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)), height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
child: Selector<FilterGroupNotifier, Uri?>( child: Selector<FilterGroupNotifier, Uri?>(

View file

@ -61,9 +61,13 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4)); static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4));
static double detailIconSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 8); static double detailIconSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 7);
static double detailFontSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 6); static double detailFontSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 7);
static double detailIconPadding(double extent) => min<double>(8.0, extent / 16);
static double detailIconTextPadding(double extent) => detailIconPadding(extent) / 2;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -201,30 +205,33 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked), if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked),
if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum), if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum),
if (filter is AlbumGroupFilter) ...[ if (filter is AlbumGroupFilter) ...[
_buildDetailIcon(context, AIcons.album), _buildDetailIcon(context, AIcons.album, padding: detailIconTextPadding(extent)),
Text( Text(
'${NumberFormat.decimalPattern(context.locale).format(albumGrouping.countLeaves(filter.uri))}${AText.separator}', '${NumberFormat.decimalPattern(context.locale).format(albumGrouping.countLeaves(filter.uri))}${AText.separator}',
style: textStyle, style: textStyle,
), ),
], ],
Text( Flexible(
child: Text(
locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)), locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)),
style: textStyle, style: textStyle,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
), ),
], ],
); );
} }
Widget _buildDetailIcon(BuildContext context, IconData icon) { Widget _buildDetailIcon(BuildContext context, IconData icon, {double? padding}) {
final padding = min<double>(8.0, extent / 16);
final iconSize = detailIconSize(extent);
return AnimatedPadding( return AnimatedPadding(
padding: EdgeInsetsDirectional.only(end: padding), padding: EdgeInsetsDirectional.only(end: padding ?? detailIconPadding(extent)),
duration: ADurations.chipDecorationAnimation, duration: ADurations.chipDecorationAnimation,
child: Icon( child: Icon(
icon, icon,
color: _detailColor(context), color: _detailColor(context),
size: iconSize, size: detailIconSize(extent),
), ),
); );
} }

View file

@ -304,9 +304,6 @@ class _HomePageState extends State<HomePage> {
String routeName; String routeName;
Set<CollectionFilter?>? filters; Set<CollectionFilter?>? filters;
switch (appMode) { switch (appMode) {
case AppMode.pickSingleMediaExternal:
case AppMode.pickMultipleMediaExternal:
routeName = CollectionPage.routeName;
case AppMode.setWallpaper: case AppMode.setWallpaper:
return DirectMaterialPageRoute( return DirectMaterialPageRoute(
settings: const RouteSettings(name: WallpaperPage.routeName), settings: const RouteSettings(name: WallpaperPage.routeName),
@ -374,7 +371,17 @@ class _HomePageState extends State<HomePage> {
); );
}, },
); );
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; routeName = _initialRouteName ?? settings.homePage.routeName;
filters = _initialFilters ?? (settings.homePage == HomePageSetting.collection ? settings.homeCustomCollection : {}); filters = _initialFilters ?? (settings.homePage == HomePageSetting.collection ? settings.homeCustomCollection : {});
} }

View file

@ -115,7 +115,7 @@ class _Content extends StatefulWidget {
} }
class _ContentState extends State<_Content> with SingleTickerProviderStateMixin { class _ContentState extends State<_Content> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final AvesMapController _mapController = AvesMapController(); final AvesMapController _mapController = AvesMapController();
final ValueNotifier<bool> _isPageAnimatingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isPageAnimatingNotifier = ValueNotifier(false);
final ValueNotifier<int?> _selectedIndexNotifier = ValueNotifier(0); final ValueNotifier<int?> _selectedIndexNotifier = ValueNotifier(0);

View file

@ -23,7 +23,7 @@ class FloatingNavBar extends StatefulWidget {
} }
class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProviderStateMixin { class _FloatingNavBarState extends State<FloatingNavBar> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late AnimationController _controller; late AnimationController _controller;
late CurvedAnimation _animation; late CurvedAnimation _animation;
late Animation<Offset> _offset; late Animation<Offset> _offset;

View file

@ -39,7 +39,7 @@ extension ExtraAppExportItem on AppExportItem {
favourites.import(jsonMap, source); favourites.import(jsonMap, source);
case AppExportItem.settings: case AppExportItem.settings:
await settings.import(jsonMap); await settings.import(jsonMap);
albumGrouping.init(settings.albumGroups); albumGrouping.setGroups(settings.albumGroups);
} }
} }
} }

View file

@ -64,7 +64,7 @@ class ViewerVerticalPageView extends StatefulWidget {
} }
class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> { class _ViewerVerticalPageViewState extends State<ViewerVerticalPageView> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<double> _backgroundOpacityNotifier = ValueNotifier(1); final ValueNotifier<double> _backgroundOpacityNotifier = ValueNotifier(1);
final ValueNotifier<bool> _isVerticallyScrollingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isVerticallyScrollingNotifier = ValueNotifier(false);
final ValueNotifier<bool> _isImageFocusedNotifier = ValueNotifier(true); final ValueNotifier<bool> _isImageFocusedNotifier = ValueNotifier(true);

View file

@ -261,12 +261,10 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
valueListenable: _viewLocked, valueListenable: _viewLocked,
builder: (context, locked, child) { builder: (context, locked, child) {
return BackdropGroup( final children = [child!];
child: Stack( if (!pipEnabled) {
children: [ if (locked) {
child!, children.addAll([
if (!pipEnabled) ...[
if (locked) ...[
const Positioned.fill( const Positioned.fill(
child: AbsorbPointer(), child: AbsorbPointer(),
), ),
@ -276,14 +274,22 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
), ),
), ),
_buildViewerLockedBottomOverlay(), _buildViewerLockedBottomOverlay(),
] else ]);
..._buildOverlays(availableSize).map(_decorateOverlay), } else {
// regular overlay
children.addAll(_buildOverlays(availableSize).map(_decorateOverlay));
}
children.addAll([
const TopGestureAreaProtector(), const TopGestureAreaProtector(),
const SideGestureAreaProtector(), const SideGestureAreaProtector(),
const BottomGestureAreaProtector(), const BottomGestureAreaProtector(),
], ]);
], }
),
// TODO TLAD [flutter vNext] wrap into `BackdropGroup`
return Stack(
children: children,
); );
}, },
child: viewer, child: viewer,

View file

@ -150,7 +150,7 @@ class _InfoPageContent extends StatefulWidget {
} }
class _InfoPageContentState extends State<_InfoPageContent> { class _InfoPageContentState extends State<_InfoPageContent> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late EntryInfoActionDelegate _actionDelegate; late EntryInfoActionDelegate _actionDelegate;
final ValueNotifier<Map<String, MetadataDirectory>> _metadataNotifier = ValueNotifier({}); final ValueNotifier<Map<String, MetadataDirectory>> _metadataNotifier = ValueNotifier({});
final ValueNotifier<EntryAction?> _isEditingMetadataNotifier = ValueNotifier(null); final ValueNotifier<EntryAction?> _isEditingMetadataNotifier = ValueNotifier(null);

View file

@ -54,7 +54,7 @@ class EntryPageView extends StatefulWidget {
class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateMixin { class _EntryPageViewState extends State<EntryPageView> with TickerProviderStateMixin {
late ValueNotifier<ViewState> _viewStateNotifier; late ValueNotifier<ViewState> _viewStateNotifier;
late AvesMagnifierController _magnifierController; late AvesMagnifierController _magnifierController;
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null); final ValueNotifier<Widget?> _actionFeedbackChildNotifier = ValueNotifier(null);
OverlayEntry? _actionFeedbackOverlayEntry; OverlayEntry? _actionFeedbackOverlayEntry;

View file

@ -1,45 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View file

@ -1,30 +0,0 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
version:
revision: 85684f9300908116a78138ea4c6036c35c9a1236
channel: stable
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
- platform: android
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

View file

@ -1 +0,0 @@
include: ../../analysis_options.yaml

View file

@ -1,15 +0,0 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
.kotlin/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

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