Compare commits

..

60 commits

Author SHA1 Message Date
Thibault Deckers
fafebe799e Merge branch 'develop' 2025-05-12 23:09:53 +02:00
Thibault Deckers
b9a66a1949 Merge branch 'develop' 2025-04-16 23:37:28 +02:00
Thibault Deckers
4dd77483cd Merge branch 'develop' 2025-04-06 23:13:41 +02:00
Thibault Deckers
30f96334eb Merge branch 'develop' 2025-03-25 23:38:23 +01:00
Thibault Deckers
160544926c Merge branch 'develop' 2025-03-16 19:44:30 +01:00
Thibault Deckers
12a89782cd Merge branch 'develop' 2025-03-11 00:33:19 +01:00
Thibault Deckers
ea496e5f52 Merge branch 'develop' 2025-03-07 20:11:42 +01:00
Thibault Deckers
d4d6b40be5 Merge branch 'develop' 2025-03-05 23:30:01 +01:00
Thibault Deckers
7bf59106f9 Merge branch 'develop' 2025-02-06 17:19:03 +01:00
Thibault Deckers
94ad4c01f4 Merge branch 'develop' 2025-01-13 10:45:04 +01:00
Thibault Deckers
6803dae438 Merge branch 'develop' 2025-01-05 16:35:42 +01:00
Thibault Deckers
3ebb6e8cbf Merge branch 'develop' 2024-12-19 21:56:08 +01:00
Thibault Deckers
c5f06baef1 Merge branch 'develop' 2024-12-11 21:51:37 +01:00
Thibault Deckers
1d7deac84d Merge branch 'develop' 2024-11-24 23:16:41 +01:00
Thibault Deckers
c62e68399c Merge branch 'develop' 2024-11-18 23:46:35 +01:00
Thibault Deckers
530bd1a8b0 Merge branch 'develop' 2024-10-30 23:14:23 +01:00
Thibault Deckers
1c1be8ba1c Merge branch 'develop' 2024-10-10 19:10:20 +02:00
Thibault Deckers
e6afda2bc9 Merge branch 'develop' 2024-10-09 01:04:19 +02:00
Thibault Deckers
35aeefe34b Merge branch 'develop' 2024-10-09 00:10:04 +02:00
Thibault Deckers
53b8e5b37b Merge branch 'develop' 2024-09-17 20:22:40 +02:00
Thibault Deckers
4688ba01f2 Merge branch 'develop' 2024-09-16 20:02:52 +02:00
Thibault Deckers
50416ad59a Merge branch 'develop' 2024-09-16 00:17:24 +02:00
Thibault Deckers
ce3afd87a0 Merge branch 'develop' 2024-09-16 00:14:08 +02:00
Thibault Deckers
5565ac08f7 Merge branch 'develop' 2024-09-01 01:32:32 +02:00
Thibault Deckers
00681cbaee Merge branch 'develop' 2024-08-07 22:01:05 +02:00
Thibault Deckers
9ea43d951c Merge branch 'develop' 2024-07-19 08:31:52 +02:00
Thibault Deckers
86b0d16ad1 Merge branch 'develop' 2024-07-18 23:59:02 +02:00
Thibault Deckers
d813a61b9b Merge branch 'develop' 2024-07-17 22:39:17 +02:00
Thibault Deckers
6d4c765613 Merge branch 'develop' 2024-07-11 19:03:40 +02:00
Thibault Deckers
a396635639 Merge branch 'develop' 2024-07-09 00:18:12 +02:00
Thibault Deckers
90fa60df69 Merge branch 'develop' 2024-06-17 21:40:55 +02:00
Thibault Deckers
ce11587482 Merge branch 'develop' 2024-06-11 23:11:21 +02:00
Thibault Deckers
07ac7b3fda Merge branch 'develop' 2024-05-03 00:43:58 +02:00
Thibault Deckers
645c199b33 Merge branch 'develop' 2024-05-01 18:03:08 +02:00
Thibault Deckers
ed75dba228 Merge branch 'develop' 2024-04-14 23:25:04 +02:00
Thibault Deckers
2619613ee5 Merge branch 'develop' 2024-04-02 00:01:32 +02:00
Thibault Deckers
82f070f8a1 Merge branch 'develop' 2024-04-01 23:10:42 +02:00
Thibault Deckers
42a869908c Merge branch 'develop' 2024-03-12 19:54:44 +01:00
Thibault Deckers
a02131c7c8 Merge branch 'develop' 2024-03-11 23:33:40 +01:00
Thibault Deckers
579e7a2db0 Merge branch 'develop' 2024-02-22 20:24:59 +01:00
Thibault Deckers
283a3eba60 Merge branch 'develop' 2024-02-07 22:30:36 +01:00
Thibault Deckers
6f7f70babe Merge branch 'develop' 2024-01-29 23:49:00 +01:00
Thibault Deckers
7cd170baf9 Merge branch 'develop' 2023-12-24 16:51:18 +01:00
Thibault Deckers
32fff626d2 Merge branch 'develop' 2023-12-21 18:57:11 +01:00
Thibault Deckers
5b6d7af0ac Merge branch 'develop' 2023-12-02 17:33:59 +01:00
Thibault Deckers
aa2b4c14e0 Merge branch 'develop' 2023-10-17 00:21:24 +02:00
Thibault Deckers
bf322c0aa9 Merge branch 'develop' 2023-09-25 00:38:11 +02:00
Thibault Deckers
1b87fb896d Merge branch 'develop' 2023-09-17 22:55:45 +02:00
Thibault Deckers
20f6d3f2a7 Merge branch 'develop' 2023-09-13 23:07:00 +02:00
Thibault Deckers
ffc6201e28 Merge branch 'develop' 2023-08-28 23:05:34 +02:00
Thibault Deckers
c4e06113b4 Merge branch 'develop' 2023-08-24 19:59:21 +02:00
Thibault Deckers
271809e189 Merge branch 'develop' 2023-08-22 23:43:28 +02:00
Thibault Deckers
fb8a97c5c6 Merge branch 'develop' 2023-08-21 00:20:29 +02:00
Thibault Deckers
cf64527c4b Merge branch 'develop' 2023-06-04 23:04:22 +02:00
Thibault Deckers
055d341a84 Merge branch 'develop' 2023-05-28 13:39:12 +02:00
Thibault Deckers
dc34fc6bc2 Merge branch 'develop' 2023-05-28 12:51:03 +02:00
Thibault Deckers
fa16430063 Merge branch 'develop' 2023-05-27 00:45:35 +02:00
Thibault Deckers
58e7adecde Merge branch 'develop' 2023-05-26 23:45:09 +02:00
Thibault Deckers
b8e8e3bfba Merge branch 'develop' 2023-05-26 23:16:42 +02:00
Thibault Deckers
79c5f5777b Merge branch 'develop' 2023-05-26 23:08:12 +02:00
143 changed files with 727 additions and 1228 deletions

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

View file

@ -24,4 +24,4 @@ jobs:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 uses: actions/dependency-review-action@38ecb5b593bf0eb19e335c03f97670f792489a8b # v4.7.0

View file

@ -28,9 +28,6 @@ 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
@ -72,7 +69,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@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -86,6 +83,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@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View file

@ -36,9 +36,6 @@ 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@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
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@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -4,36 +4,6 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
### Added
- Info: show matching dynamic albums
### Fixed
- crash when decoding some large thumbnails
## <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
### Fixed
- albums: show groups to move/copy/export items
- albums: hide grouped albums containing hidden items only
## <a id="v1.13.0"></a>[v1.13.0] - 2025-05-12 ## <a id="v1.13.0"></a>[v1.13.0] - 2025-05-12
### Added ### Added

View file

@ -111,96 +111,17 @@ Some users have expressed the wish to financially support the project. Thanks!
## Project Setup ## Project Setup
### Install dependencies
Before running or building the app, update the dependencies for the desired flavor: Before running or building the app, update the dependencies for the desired flavor:
``` ```
scripts/apply_flavor_play.sh # scripts/apply_flavor_play.sh
``` ```
To build the project, create a file named `<app dir>/android/key.properties`. It should contain a reference to a keystore for app signing, and other necessary credentials. See [key_template.properties](https://github.com/deckerst/aves/blob/develop/android/key_template.properties) for the expected keys. To build the project, create a file named `<app dir>/android/key.properties`. It should contain a reference to a keystore for app signing, and other necessary credentials. See [key_template.properties](https://github.com/deckerst/aves/blob/develop/android/key_template.properties) for the expected keys.
### To run the app: To run the app:
``` ```
./flutterw run -t lib/main_play.dart --flavor play # ./flutterw run -t lib/main_play.dart --flavor play
```
### To build the app:
creare file con le tue credenziali file.keystore
dove YOUR_ALIAS_NAME è il tuo unico alias name
e YOUR_ALIAS_PWD è la password del tuo alias
```sh
keytool -genkey -v -keystore file.keystore -alias YOUR_ALIAS_NAME -storepass YOUR_ALIAS_PWD -keypass YOUR_ALIAS_PWD -keyalg RSA -validity 36500
```
in questo caso ho inserito
```sh
cd android
keytool -genkey -v -keystore file.keystore -alias FabioMich66 -storepass Master66 -keypass Master66 -keyalg RSA -validity 36500
```
se non puoi eseguire keytool perchè non è nel path di sistema cercalo usando
```sh
cd /
sudo find -name keytool
```
compilare il file `<app dir>/android/key.properties`
```
nano android/key.properties
```
questi i miei dati utilizzando il format key_template.properties
```
storeFile=/Users/fabio/flutter_apps/aves/android/file.keystore
storePassword=Master66
keyAlias=FabioMich66
keyPassword=Master66
googleApiKey=<GOOGLE_API_KEY>
```
infine compilare l'apk
```
./flutterw build apk -t lib/main_play.dart --flavor play
``` ```
[Version badge]: https://img.shields.io/github/v/release/deckerst/aves?include_prereleases&sort=semver [Version badge]: https://img.shields.io/github/v/release/deckerst/aves?include_prereleases&sort=semver
[Build badge]: https://img.shields.io/github/actions/workflow/status/deckerst/aves/quality-check.yml?branch=develop [Build badge]: https://img.shields.io/github/actions/workflow/status/deckerst/aves/quality-check.yml?branch=develop
## Android studio
caricare il file da github selezionando le mnù a tendina File-New-project from Version Control
selezionare version control tipo: git
inserire URL di aves
https://github.com/deckerst/aves
flaggare shallow clone with history troncated 1 commits
aprire la console sulla dir aves appena creata e caricare le dipendenze
```
scripts/apply_flavor_izzy.sh
```
in settings - Languages and Framework - Dart inserire il path
```
/home/fabio/flutter/bin/cache/
```
e spuntare project aves
Edit configurations e aggiungere shell script con un nome x es izzi
poi flaggare script text e inserire
./flutterw run -t lib/main_izzy.dart --flavor izzy
la working directory sarà una cosa così
/home/fabio/StudioProjects/aves

View file

@ -9,11 +9,6 @@ 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,7 +7,6 @@ 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

@ -33,13 +33,13 @@ kotlin {
} }
android { android {
namespace = 'deckers.thibault.aves' namespace 'deckers.thibault.aves'
compileSdk = 36 compileSdk 35
defaultConfig { defaultConfig {
applicationId packageName applicationId packageName
minSdk flutter.minSdkVersion minSdk flutter.minSdkVersion
targetSdk 36 targetSdk 35
versionCode flutter.versionCode versionCode flutter.versionCode
versionName flutter.versionName versionName flutter.versionName
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"] manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
@ -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.*"
} }
@ -149,36 +149,36 @@ repositories {
} }
dependencies { dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.1'
implementation "androidx.appcompat:appcompat:1.7.1" implementation "androidx.appcompat:appcompat:1.7.0"
implementation 'androidx.core:core-ktx:1.16.0' implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.lifecycle:lifecycle-process:2.9.1' implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
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-beta01' implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.work:work-runtime-ktx:2.10.1' implementation 'androidx.work:work-runtime-ktx:2.10.0'
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'
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
// SLF4J implementation for `mp4parser` // SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.17' implementation 'org.slf4j:slf4j-simple:2.0.16'
// forked, built by JitPack: // forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory // - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
// - 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:d6b2b0aa4f' implementation 'com.github.deckerst:Android-TiffBitmapFactory:3ed067f021'
implementation 'com.github.deckerst:androidsvg:67db933051' implementation 'com.github.deckerst:androidsvg:cc9d59a88f'
implementation 'com.github.deckerst.mp4parser:isoparser:c2898f1832' implementation 'com.github.deckerst.mp4parser:isoparser:d5caf7a3dd'
implementation 'com.github.deckerst.mp4parser:muxer:c2898f1832' implementation 'com.github.deckerst.mp4parser:muxer:d5caf7a3dd'
implementation 'com.github.deckerst:pixymeta-android:cb1cdc932e' implementation 'com.github.deckerst:pixymeta-android:71eee77dc4'
implementation project(':exifinterface') implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.1' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'
kapt 'androidx.annotation:annotation:1.9.1' kapt 'androidx.annotation:annotation:1.9.1'
ksp "com.github.bumptech.glide:ksp:$glide_version" ksp "com.github.bumptech.glide:ksp:$glide_version"

View file

@ -2,7 +2,6 @@ package deckers.thibault.aves.channel.calls
import android.app.ActivityManager import android.app.ActivityManager
import android.content.Context import android.content.Context
import androidx.core.content.edit
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
@ -19,6 +18,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class AnalysisHandler(private val activity: FlutterFragmentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler { class AnalysisHandler(private val activity: FlutterFragmentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -38,8 +38,9 @@ class AnalysisHandler(private val activity: FlutterFragmentActivity, private val
} }
val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
preferences.edit { with(preferences.edit()) {
putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle) putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
apply()
} }
result.success(true) result.success(true)
} }
@ -68,8 +69,9 @@ class AnalysisHandler(private val activity: FlutterFragmentActivity, private val
// work `Data` cannot occupy more than 10240 bytes when serialized // work `Data` cannot occupy more than 10240 bytes when serialized
// so we save the possibly long list of entry IDs to shared preferences // so we save the possibly long list of entry IDs to shared preferences
val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
preferences.edit { with(preferences.edit()) {
putStringSet(AnalysisWorker.PREF_ENTRY_IDS_KEY, allEntryIds?.map { it.toString() }?.toSet()) putStringSet(AnalysisWorker.PREF_ENTRY_IDS_KEY, allEntryIds?.map { it.toString() }?.toSet())
apply()
} }
val workData = workDataOf( val workData = workDataOf(

View file

@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint
import android.app.LocaleConfig import android.app.LocaleConfig
import android.app.LocaleManager import android.app.LocaleManager
import android.content.Context import android.content.Context
@ -103,7 +102,6 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@SuppressLint("WrongConstant")
val lm = context.getSystemService(Context.LOCALE_SERVICE) as? LocaleManager val lm = context.getSystemService(Context.LOCALE_SERVICE) as? LocaleManager
lm?.overrideLocaleConfig = LocaleConfig(LocaleList.forLanguageTags(locales.joinToString(","))) lm?.overrideLocaleConfig = LocaleConfig(LocaleList.forLanguageTags(locales.joinToString(",")))
} }

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, defaultExtension = null) val extension = extensionFor(mimeType)
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 (displayName.endsWith(extension, ignoreCase = true)) { val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) {
displayName displayName
} else { } else {
"$displayName$extension" "$displayName$extension"

View file

@ -31,7 +31,7 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
private fun getAddress(call: MethodCall, result: MethodChannel.Result) { private fun getAddress(call: MethodCall, result: MethodChannel.Result) {
val latitude = call.argument<Number>("latitude")?.toDouble() val latitude = call.argument<Number>("latitude")?.toDouble()
val longitude = call.argument<Number>("longitude")?.toDouble() val longitude = call.argument<Number>("longitude")?.toDouble()
val localeLanguageTag = call.argument<String>("localeLanguageTag") val localeString = call.argument<String>("locale")
val maxResults = call.argument<Int>("maxResults") ?: 1 val maxResults = call.argument<Int>("maxResults") ?: 1
if (latitude == null || longitude == null) { if (latitude == null || longitude == null) {
result.error("getAddress-args", "missing arguments", null) result.error("getAddress-args", "missing arguments", null)
@ -43,8 +43,11 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
return return
} }
geocoder = geocoder ?: if (localeLanguageTag != null) { geocoder = geocoder ?: if (localeString != null) {
Geocoder(context, Locale.forLanguageTag(localeLanguageTag)) val split = localeString.split("_")
val language = split[0]
val country = if (split.size > 1) split[1] else ""
Geocoder(context, Locale(language, country))
} else { } else {
Geocoder(context) Geocoder(context)
} }

View file

@ -1,7 +1,6 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import androidx.core.content.edit
import deckers.thibault.aves.SearchSuggestionsProvider import deckers.thibault.aves.SearchSuggestionsProvider
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -30,8 +29,9 @@ class GlobalSearchHandler(private val context: Context) : MethodCallHandler {
} }
val preferences = context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) val preferences = context.getSharedPreferences(SearchSuggestionsProvider.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
preferences.edit { with(preferences.edit()) {
putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle) putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
apply()
} }
result.success(true) result.success(true)
} }

View file

@ -2,7 +2,6 @@ package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
@ -46,7 +45,7 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
} }
val preferences = getStore() val preferences = getStore()
preferences.edit { with(preferences.edit()) {
when (value) { when (value) {
is Boolean -> putBoolean(key, value) is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value) is Float -> putFloat(key, value)
@ -59,6 +58,7 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
return return
} }
} }
apply()
} }
result.success(true) result.success(true)
} }

View file

@ -5,10 +5,8 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log
import android.util.Size import android.util.Size
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.graphics.scale
import androidx.core.net.toUri import androidx.core.net.toUri
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
@ -19,7 +17,6 @@ import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.MultiPageImage import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.SVG import deckers.thibault.aves.utils.MimeTypes.SVG
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
@ -28,8 +25,6 @@ import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlin.math.min
import kotlin.math.roundToInt
class ThumbnailFetcher internal constructor( class ThumbnailFetcher internal constructor(
private val context: Context, private val context: Context,
@ -82,29 +77,6 @@ class ThumbnailFetcher internal constructor(
} }
} }
if (bitmap != null) {
if (bitmap.width > width && bitmap.height > height) {
val scalingFactor: Double = min(bitmap.width.toDouble() / width, bitmap.height.toDouble() / height)
val dstWidth = (bitmap.width / scalingFactor).roundToInt()
val dstHeight = (bitmap.height / scalingFactor).roundToInt()
Log.d(
LOG_TAG, "rescale thumbnail for mimeType=$mimeType uri=$uri width=$width height=$height" +
", with bitmap byteCount=${bitmap.byteCount} size=${bitmap.width}x${bitmap.height}" +
", to target=${dstWidth}x${dstHeight}"
)
bitmap = bitmap.scale(dstWidth, dstHeight)
}
if (bitmap.byteCount > BITMAP_SIZE_DANGER_THRESHOLD) {
result.error(
"getThumbnail-large", "thumbnail bitmap dangerously large" +
" for mimeType=$mimeType uri=$uri pageId=$pageId width=$width height=$height" +
", with bitmap byteCount=${bitmap.byteCount} size=${bitmap.width}x${bitmap.height} config=${bitmap.config?.name}", null
)
return
}
}
// do not recycle bitmaps fetched from `ContentResolver` or Glide as their lifecycle is unknown // do not recycle bitmaps fetched from `ContentResolver` or Glide as their lifecycle is unknown
val recycle = false val recycle = false
val bytes = BitmapUtils.getRawBytes(bitmap, recycle = recycle) val bytes = BitmapUtils.getRawBytes(bitmap, recycle = recycle)
@ -172,9 +144,4 @@ class ThumbnailFetcher internal constructor(
Glide.with(context).clear(target) Glide.with(context).clear(target)
} }
} }
companion object {
private val LOG_TAG = LogUtils.createTag<ThumbnailFetcher>()
private const val BITMAP_SIZE_DANGER_THRESHOLD = 20 * (1 shl 20) // MB
}
} }

View file

@ -31,15 +31,9 @@ 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)
} }
} }

View file

@ -81,12 +81,12 @@ object PixyMetaHelper {
output: OutputStream, output: OutputStream,
iptcDataList: List<FieldMap>?, iptcDataList: List<FieldMap>?,
) { ) {
val iptc: List<IPTCDataSet> = iptcDataList?.flatMap { val iptc = iptcDataList?.flatMap {
val record = it["record"] as Int val record = it["record"] as Int
val tag = it["tag"] as Int val tag = it["tag"] as Int
val values = it["values"] as List<*> val values = it["values"] as List<*>
values.map { data -> IPTCDataSet(IPTCRecord.fromRecordNumber(record), tag, data as ByteArray) } values.map { data -> IPTCDataSet(IPTCRecord.fromRecordNumber(record), tag, data as ByteArray) }
} ?: ArrayList() } ?: ArrayList<IPTCDataSet>()
Metadata.insertIPTC(input, output, iptc) Metadata.insertIPTC(input, output, iptc)
} }

View file

@ -142,18 +142,16 @@ 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, defaultExtension)}" val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
val newFile = File(dir, targetFileName) val newFile = File(dir, targetFileName)
if (oldFile != newFile) { if (oldFile != newFile) {
newFields = renameSingle( newFields = renameSingle(
@ -279,17 +277,11 @@ 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
@ -366,7 +358,6 @@ abstract class ImageProvider {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
@ -474,7 +465,6 @@ 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) {
@ -581,14 +571,13 @@ 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, defaultExtension) val extension = extensionFor(mimeType)
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension") val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) { when (conflictStrategy) {
NameConflictStrategy.RENAME -> { NameConflictStrategy.RENAME -> {

View file

@ -557,7 +557,6 @@ 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
@ -570,7 +569,6 @@ 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
@ -582,7 +580,6 @@ 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)
@ -618,13 +615,12 @@ 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, defaultExtension)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
write = write, write = write,
) )
} }
@ -634,7 +630,7 @@ class MediaStoreImageProvider : ImageProvider() {
return insertByMediaStore( return insertByMediaStore(
activity = activity, activity = activity,
targetDir = targetDir, targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
write = write, write = write,
) )
} }
@ -646,7 +642,6 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
} }
@ -705,7 +700,6 @@ 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")
@ -714,22 +708,8 @@ 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
var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) val 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

@ -5,5 +5,5 @@ import kotlin.math.pow
object MathUtils { object MathUtils {
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble()) fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt() private fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
} }

View file

@ -163,24 +163,12 @@ 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, defaultExtension: String?): String = when (mimeType) { fun extensionFor(mimeType: 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 -> { else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
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,5 +8,4 @@
<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,6 +22,7 @@ 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;
@ -90,7 +91,7 @@ import java.util.regex.Pattern;
import java.util.zip.CRC32; import java.util.zip.CRC32;
/* /*
* Forked from 'androidx.exifinterface:exifinterface:1.4.1' * Forked from 'androidx.exifinterface:exifinterface:1.4.0'
* 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
@ -138,12 +139,6 @@ 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);
@ -4558,7 +4553,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 = value != null ? ExifAttribute.createByte(value) : null; mXmpFromSeparateMarker = ExifAttribute.createByte(value);
return; return;
} }
} }
@ -6563,9 +6558,8 @@ 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 = payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length,
Arrays.copyOfRange( payload.length);
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.
@ -6728,11 +6722,8 @@ public class ExifInterfaceFork {
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length); copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
boolean needToWriteExif = true; boolean needToWriteExif = true;
// Either there's some XMP data to write, or it has been cleared locally but was present in boolean needToWriteXmp = mXmpFromSeparateMarker != null;
// the file when it was read (and so needs to be removed). while (needToWriteExif || needToWriteXmp) {
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) {
@ -6747,7 +6738,7 @@ public class ExifInterfaceFork {
} }
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) { if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
writePngXmpItxtChunk(dataOutputStream); writePngXmpItxtChunk(dataOutputStream);
needToHandleXmpChunk = false; needToWriteXmp = false;
} }
continue; continue;
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) { } else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
@ -6755,25 +6746,10 @@ 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 } else if (chunkType == PNG_CHUNK_TYPE_ITXT && needToWriteXmp) {
&& chunkLength >= PNG_ITXT_XMP_KEYWORD.length) { writePngXmpItxtChunk(dataOutputStream);
// Read the 17 byte keyword and 5 expected null bytes. dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length]; needToWriteXmp = false;
dataInputStream.readFully(keyword);
int remainingChunkBytes = chunkLength - keyword.length + PNG_CHUNK_CRC_BYTE_LENGTH;
if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) {
if (mXmpFromSeparateMarker != null) {
writePngXmpItxtChunk(dataOutputStream);
}
dataInputStream.skipFully(remainingChunkBytes);
needToHandleXmpChunk = false;
} else {
// This is a non-XMP iTXt chunk, so just copy it to the output and continue.
dataOutputStream.writeInt(chunkLength);
dataOutputStream.writeInt(chunkType);
dataOutputStream.write(keyword);
copy(dataInputStream, dataOutputStream, remainingChunkBytes);
}
continue; continue;
} }
dataOutputStream.writeInt(chunkLength); dataOutputStream.writeInt(chunkLength);
@ -7560,13 +7536,6 @@ 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.1' * Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
* 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

@ -18,10 +18,10 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.10.1" apply false id("com.android.application") version "8.8.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.21" apply false id("org.jetbrains.kotlin.android") version "2.1.10" apply false
id("com.google.devtools.ksp") version "2.1.21-2.0.1" apply false id("com.google.devtools.ksp") version "2.1.10-1.0.29" apply false
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
} }
include(":app") include(":app")

View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
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,28 +1599,8 @@
"@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,15 +142,5 @@
"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,19 +1624,5 @@
"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 elementtitel", "sortByAlbumFileName": "Efter album og filnavn",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"albumGroupVolume": "Efter lagervolume", "albumGroupVolume": "Efter lagervolume",
"@albumGroupVolume": {}, "@albumGroupVolume": {},
@ -1627,7 +1627,7 @@
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"createButtonLabel": "OPRET", "createButtonLabel": "OPRET",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Ændr gruppering", "chipActionGroup": "Gruppér",
"@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": "Change grouping", "chipActionGroup": "Group",
"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 & item title", "sortByAlbumFileName": "By album & file name",
"sortByRating": "By rating", "sortByRating": "By rating",
"sortByDuration": "By duration", "sortByDuration": "By duration",
"sortByPath": "By path", "sortByPath": "By path",

View file

@ -445,7 +445,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Ordenar", "viewDialogSortSectionTitle": "Ordenar",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Secciones", "viewDialogGroupSectionTitle": "Grupo",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Disposición", "viewDialogLayoutSectionTitle": "Disposición",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -1402,33 +1402,5 @@
"editEntryLocationDialogTimeShift": "Desplazamiento de tiempo", "editEntryLocationDialogTimeShift": "Desplazamiento de tiempo",
"@editEntryLocationDialogTimeShift": {}, "@editEntryLocationDialogTimeShift": {},
"coordinateFormatDdm": "DDM", "coordinateFormatDdm": "DDM",
"@coordinateFormatDdm": {}, "@coordinateFormatDdm": {}
"sortByPath": "Por ruta",
"@sortByPath": {},
"searchFormatSectionTitle": "Formatos",
"@searchFormatSectionTitle": {},
"newGroupDialogTitle": "Nuevo grupo",
"@newGroupDialogTitle": {},
"ungrouped": "No agrupado",
"@ungrouped": {},
"albumTierGroups": "Grupos",
"@albumTierGroups": {},
"groupEmpty": "Sin grupos",
"@groupEmpty": {},
"newGroupDialogNameLabel": "Nombre del grupo",
"@newGroupDialogNameLabel": {},
"createButtonLabel": "CREAR",
"@createButtonLabel": {},
"chipActionGroup": "Agrupar",
"@chipActionGroup": {},
"chipActionCreateGroup": "Crear grupo",
"@chipActionCreateGroup": {},
"groupAlreadyExists": "Ya existe el grupo",
"@groupAlreadyExists": {},
"groupPickerTitle": "Seleccionar grupo",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Usar este grupo",
"@groupPickerUseThisGroupButton": {},
"sectionNone": "Sin secciones",
"@sectionNone": {}
} }

View file

@ -790,7 +790,7 @@
"@aboutLicensesDartPackagesSectionTitle": {}, "@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse", "aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse",
"@aboutLicensesShowAllButtonLabel": {}, "@aboutLicensesShowAllButtonLabel": {},
"policyPageTitle": "Andmekaitsepõhimõtted", "policyPageTitle": "Privaatsuspoliitika",
"@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 objekti nime alusel", "sortByAlbumFileName": "Albumi ja failinime alusel",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Hinnangu alusel", "sortByRating": "Hinnangu alusel",
"@sortByRating": {}, "@sortByRating": {},
@ -1645,7 +1645,7 @@
"@groupPickerUseThisGroupButton": {}, "@groupPickerUseThisGroupButton": {},
"createButtonLabel": "LOO", "createButtonLabel": "LOO",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Muuda grupeerimist", "chipActionGroup": "Rühmita",
"@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": "par titre dalbum et élément", "sortByAlbumFileName": "alphabétique",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "par notation", "sortByRating": "par notation",
"@sortByRating": {}, "@sortByRating": {},
@ -1407,7 +1407,7 @@
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Formats", "searchFormatSectionTitle": "Formats",
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"chipActionGroup": "Modifier groupement", "chipActionGroup": "Grouper",
"@chipActionGroup": {}, "@chipActionGroup": {},
"createButtonLabel": "CRÉER", "createButtonLabel": "CRÉER",
"@createButtonLabel": {}, "@createButtonLabel": {},

View file

@ -823,7 +823,7 @@
"@menuActionSlideshow": {}, "@menuActionSlideshow": {},
"menuActionStats": "Estatísticas", "menuActionStats": "Estatísticas",
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogGroupSectionTitle": "Seccións", "viewDialogGroupSectionTitle": "Agrupar",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"castDialogTitle": "Dispositivos de emisión", "castDialogTitle": "Dispositivos de emisión",
"@castDialogTitle": {}, "@castDialogTitle": {},
@ -1610,33 +1610,5 @@
"panoramaDisableSensorControl": "Desactivar control do sensor", "panoramaDisableSensorControl": "Desactivar control do sensor",
"@panoramaDisableSensorControl": {}, "@panoramaDisableSensorControl": {},
"settingsHiddenFiltersBanner": "As fotos e vídeos que cadren cos filtros ocultos non se amosarán na súa colección.", "settingsHiddenFiltersBanner": "As fotos e vídeos que cadren cos filtros ocultos non se amosarán na súa colección.",
"@settingsHiddenFiltersBanner": {}, "@settingsHiddenFiltersBanner": {}
"createButtonLabel": "CREAR",
"@createButtonLabel": {},
"chipActionGroup": "Agrupar",
"@chipActionGroup": {},
"chipActionCreateGroup": "Crear grupo",
"@chipActionCreateGroup": {},
"albumTierGroups": "Grupos",
"@albumTierGroups": {},
"newGroupDialogTitle": "Novo grupo",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "Nome do grupo",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "Xa existe o grupo",
"@groupAlreadyExists": {},
"groupEmpty": "Sen grupos",
"@groupEmpty": {},
"ungrouped": "Non agrupado",
"@ungrouped": {},
"groupPickerTitle": "Escolmar grupo",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Usar este grupo",
"@groupPickerUseThisGroupButton": {},
"sectionNone": "Sen seccións",
"@sectionNone": {},
"sortByPath": "Por ruta",
"@sortByPath": {},
"searchFormatSectionTitle": "Formatos",
"@searchFormatSectionTitle": {}
} }

View file

@ -144,63 +144,5 @@
"stopTooltip": "עצור", "stopTooltip": "עצור",
"@stopTooltip": {}, "@stopTooltip": {},
"chipActionGoToExplorerPage": "הצג בסייר", "chipActionGoToExplorerPage": "הצג בסייר",
"@chipActionGoToExplorerPage": {}, "@chipActionGoToExplorerPage": {}
"chipActionSetCover": "הגדר עטיפה",
"@chipActionSetCover": {},
"chipActionCreateAlbum": "צור אלבום",
"@chipActionCreateAlbum": {},
"chipActionShowCountryStates": "הצג סטטיסטיקות",
"@chipActionShowCountryStates": {},
"createButtonLabel": "צור",
"@createButtonLabel": {},
"chipActionGroup": "קבוצה",
"@chipActionGroup": {},
"chipActionCreateGroup": "צור קבוצה",
"@chipActionCreateGroup": {},
"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": "Szekciók", "viewDialogGroupSectionTitle": "Csoport",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"menuActionStats": "Statisztikák", "menuActionStats": "Statisztikák",
"@menuActionStats": {}, "@menuActionStats": {},
@ -1594,33 +1594,5 @@
"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": "Bagian", "viewDialogGroupSectionTitle": "Grup",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Tata letak", "viewDialogLayoutSectionTitle": "Tata letak",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -1406,29 +1406,5 @@
"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 atriða", "sortByAlbumFileName": "Eftir heiti albúma og skrá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,19 +1312,5 @@
"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 itemnaam", "sortByAlbumFileName": "Op album- en bestandsnaam",
"@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": "Groepering wijzigen", "chipActionGroup": "Groeperen",
"@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 elementu", "sortByAlbumFileName": "Według albumu i nazwy pliku",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"albumMimeTypeMixed": "Mieszane", "albumMimeTypeMixed": "Mieszane",
"@albumMimeTypeMixed": {}, "@albumMimeTypeMixed": {},
@ -1603,7 +1603,7 @@
"@sectionNone": {}, "@sectionNone": {},
"createButtonLabel": "UTWÓRZ", "createButtonLabel": "UTWÓRZ",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Zmień grupowanie", "chipActionGroup": "Grupuj",
"@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": "Seções", "viewDialogGroupSectionTitle": "Grupo",
"@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 título do item", "sortByAlbumFileName": "Por álbum e nome de arquivo",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Por classificação", "sortByRating": "Por classificação",
"@sortByRating": {}, "@sortByRating": {},
@ -1406,29 +1406,5 @@
"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": "Secțiuni", "viewDialogGroupSectionTitle": "Grup",
"@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 elementului", "sortByAlbumFileName": "După album și numele fișierului",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortOrderZtoA": "De la Z la A", "sortOrderZtoA": "De la Z la A",
"@sortOrderZtoA": {}, "@sortOrderZtoA": {},
@ -1598,29 +1598,5 @@
"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,29 +1406,5 @@
"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": "Bölümler", "viewDialogGroupSectionTitle": "Grup",
"@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": "Çöp kutusu boş", "collectionActionEmptyBin": "Boş çöp kutusu",
"@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 başlığı göre", "sortByAlbumFileName": "Albüm ve dosya adına göre",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Derecelendirmeye göre", "sortByRating": "Derecelendirmeye göre",
"@sortByRating": {}, "@sortByRating": {},
@ -1400,35 +1400,5 @@
"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,14 +140,10 @@ 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 Plus', name: 'Flutter Markdown',
license: bsd3, license: bsd3,
sourceUrl: 'https://github.com/foresightmobile/flutter_markdown_plus', sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/flutter_markdown',
), ),
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 Set<StreamSubscription> _subscriptions = {}; final List<StreamSubscription> _subscriptions = [];
final _lock = Lock(); final _lock = Lock();
final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast(); final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast();
@ -40,8 +40,6 @@ 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,21 +15,19 @@ import 'package:synchronized/synchronized.dart';
final DynamicAlbums dynamicAlbums = DynamicAlbums._private(); final DynamicAlbums dynamicAlbums = DynamicAlbums._private();
class DynamicAlbums with ChangeNotifier { class DynamicAlbums with ChangeNotifier {
final Set<StreamSubscription> _subscriptions = {}; final List<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;
@ -59,7 +57,6 @@ 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)))));
}); });
} }
@ -84,7 +81,13 @@ class DynamicAlbums with ChangeNotifier {
}); });
} }
Future<void> clear() => remove(all); Future<void> clear() async {
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/media/video/metadata.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/media/video/metadata.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,6 +38,8 @@ 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,6 +38,8 @@ 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,3 +14,4 @@ mixin CoveredFilter on CollectionFilter {
return super.color(context); return super.color(context);
} }
} }

View file

@ -2,11 +2,9 @@ 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';
@ -19,6 +17,8 @@ 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,17 +1,10 @@
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';
@ -35,53 +28,18 @@ 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() { void init(Map<Uri, Set<Uri>> groups) {
_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) {
@ -115,9 +73,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 childrenUris = _groups[groupUri]; final childrenUri = _groups[groupUri];
if (childrenUris != null) { if (childrenUri != null) {
childrenUris.map(uriToFilter).nonNulls.forEach((filter) { childrenUri.map(uriToFilter).nonNulls.forEach((filter) {
if (filter is GroupBaseFilter) { if (filter is GroupBaseFilter) {
count += countLeaves(filter.uri); count += countLeaves(filter.uri);
} else { } else {
@ -135,15 +93,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 childrenUris = kv.value; final childrenUri = kv.value;
final childrenFilters = childrenUris.map(uriToFilter).nonNulls.toSet(); final childrenFilters = childrenUri.map(uriToFilter).nonNulls.toSet();
return _createGroupFilter(groupUri, SetOrFilter(childrenFilters)); return _createGroupFilter(groupUri, SetOrFilter(childrenFilters));
}).toSet(); }).toSet();
} }
final childrenUris = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value; final childrenUri = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
if (childrenUris != null) { if (childrenUri != null) {
return childrenUris.map(uriToFilter).nonNulls.toSet(); return childrenUri.map(uriToFilter).nonNulls.toSet();
} }
return {}; return {};
@ -214,46 +172,6 @@ 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 Set<StreamSubscription> _subscriptions = {}; final List<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 Set<StreamSubscription> _subscriptions = {}; final List<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,9 +60,7 @@ class MediaStoreSource extends CollectionSource {
await localMediaDb.init(); await localMediaDb.init();
await vaults.init(); await vaults.init();
await favourites.init(); await favourites.init();
albumGrouping.init(); albumGrouping.init(settings.albumGroups);
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 Set<StreamSubscription> _subscriptions = {}; final List<StreamSubscription> _subscriptions = [];
Set<VaultDetails> _rows = {}; Set<VaultDetails> _rows = {};
final Set<String> _unlockedDirPaths = {}; final Set<String> _unlockedDirPaths = {};

View file

@ -15,7 +15,7 @@ class GeocodingService {
final result = await _platform.invokeMethod('getAddress', <String, dynamic>{ final result = await _platform.invokeMethod('getAddress', <String, dynamic>{
'latitude': coordinates.latitude, 'latitude': coordinates.latitude,
'longitude': coordinates.longitude, 'longitude': coordinates.longitude,
'localeLanguageTag': locale.toLanguageTag(), 'locale': locale.toString(),
// we only really need one address, but sometimes the native geocoder // we only really need one address, but sometimes the native geocoder
// returns nothing with `maxResults` of 1, but succeeds with `maxResults` of 2+ // returns nothing with `maxResults` of 1, but succeeds with `maxResults` of 2+
'maxResults': 2, 'maxResults': 2,

View file

@ -1,6 +1,5 @@
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';
@ -14,6 +13,7 @@ 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});
@ -247,7 +247,7 @@ class PlatformMediaFetchService implements MediaFetchService {
return InteropDecoding.bytesToCodec(bytes); return InteropDecoding.bytesToCodec(bytes);
} }
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (_isUnknownVisual(mimeType) || e.code == 'getThumbnail-large') { if (_isUnknownVisual(mimeType)) {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
} }

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 Set<StreamSubscription> _subscriptions = {}; final List<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,24 +74,20 @@ 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:
orientationCode = screenOrientationUserLandscape; // SCREEN_ORIENTATION_SENSOR_LANDSCAPE
orientationCode = 6;
case Orientation.portrait: case Orientation.portrait:
orientationCode = screenOrientationUserPortrait; // SCREEN_ORIENTATION_SENSOR_PORTRAIT
orientationCode = 7;
default: default:
orientationCode = screenOrientationUnspecified; // SCREEN_ORIENTATION_UNSPECIFIED
orientationCode = -1;
} }
try { try {
await _platform.invokeMethod('requestOrientation', <String, dynamic>{ await _platform.invokeMethod('requestOrientation', <String, dynamic>{

View file

@ -1,3 +1,4 @@
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 Set<StreamSubscription> _subscriptions = {}; final List<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';
@ -18,7 +18,6 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/collection/filter_bar.dart';
@ -57,8 +56,8 @@ class CollectionAppBar extends StatefulWidget {
State<CollectionAppBar> createState() => _CollectionAppBarState(); State<CollectionAppBar> createState() => _CollectionAppBarState();
} }
class _CollectionAppBarState extends State<CollectionAppBar> with RouteAware, SingleTickerProviderStateMixin, WidgetsBindingObserver { class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
final Set<StreamSubscription> _subscriptions = {}; final List<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);
@ -123,15 +122,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with RouteAware, Si
}); });
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute) {
AvesApp.pageRouteObserver.subscribe(this, route);
}
}
@override @override
void didUpdateWidget(covariant CollectionAppBar oldWidget) { void didUpdateWidget(covariant CollectionAppBar oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@ -150,7 +140,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with RouteAware, Si
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
..clear(); ..clear();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
AvesApp.pageRouteObserver.unsubscribe(this);
super.dispose(); super.dispose();
} }
@ -162,13 +151,6 @@ class _CollectionAppBarState extends State<CollectionAppBar> with RouteAware, Si
widget.collection.filterChangeNotifier.removeListener(_onFilterChanged); widget.collection.filterChangeNotifier.removeListener(_onFilterChanged);
} }
@override
void didPushNext() {
// unfocus when navigating away, so that when navigating back,
// the query bar does not get back focus and bring the keyboard
_queryBarFocusNode.unfocus();
}
@override @override
void didChangeMetrics() { void didChangeMetrics() {
// when top padding or text scale factor change // when top padding or text scale factor change

View file

@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
} }
class _CollectionPageState extends State<CollectionPage> { class _CollectionPageState extends State<CollectionPage> {
final Set<StreamSubscription> _subscriptions = {}; final List<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/container/set_and.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/container/set_and.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 EdgeInsets padding = EdgeInsets.only(top: 4, bottom: 8); static const double verticalPadding = 16;
static final double preferredHeight = AvesFilterChip.minChipHeight + padding.vertical; static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding;
final List<CollectionFilter> filters; final List<CollectionFilter> filters;
final bool interactive; final bool interactive;
@ -84,7 +84,6 @@ 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 Set<StreamSubscription> _subscriptions = {}; final List<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 Set<StreamSubscription> _subscriptions = {}; final List<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 Set<StreamSubscription> _subscriptions = {}; final List<StreamSubscription> _subscriptions = [];
late AnimationController _playPauseAnimation; late AnimationController _playPauseAnimation;
AvesVideoController? get controller => widget.controller; AvesVideoController? get controller => widget.controller;

View file

@ -46,7 +46,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final destinationAlbumFilter = await pickAlbum( final destinationAlbumFilter = await pickAlbum(
context: context, context: context,
moveType: MoveType.export, moveType: MoveType.export,
albumChipTypes: {AlbumChipType.stored}, albumTypes: {AlbumChipType.stored},
initialGroup: null, initialGroup: null,
); );
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false; if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false;
@ -379,7 +379,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
final destinationAlbumFilter = await pickAlbum( final destinationAlbumFilter = await pickAlbum(
context: context, context: context,
moveType: moveType, moveType: moveType,
albumChipTypes: {AlbumChipType.stored}, albumTypes: {AlbumChipType.stored},
initialGroup: null, initialGroup: null,
); );
if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false; if (destinationAlbumFilter == null || destinationAlbumFilter is! StoredAlbumFilter) return false;

View file

@ -1,3 +1,4 @@
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,8 +8,6 @@ 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,
@ -20,7 +18,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(22) + padding.vertical; static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(kToolbarHeight);
} }
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,7 +544,10 @@ enum _HighlightType {
focus, focus,
} }
class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKeepAliveClientMixin<_InkResponseStateWidget> implements _ParentInkResponseState { class _InkResponseState extends State<_InkResponseStateWidget>
with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
implements _ParentInkResponseState
{
Set<InteractiveInkFeature>? _splashes; Set<InteractiveInkFeature>? _splashes;
InteractiveInkFeature? _currentSplash; InteractiveInkFeature? _currentSplash;
bool _hovering = false; bool _hovering = false;
@ -575,7 +578,6 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
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) {
@ -609,7 +611,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
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!;
@ -640,7 +642,9 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
} }
initStatesController(); initStatesController();
} }
if (widget.radius != oldWidget.radius || widget.highlightShape != oldWidget.highlightShape || widget.borderRadius != oldWidget.borderRadius) { if (widget.radius != oldWidget.radius ||
widget.highlightShape != oldWidget.highlightShape ||
widget.borderRadius != oldWidget.borderRadius) {
final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover]; final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
if (hoverHighlight != null) { if (hoverHighlight != null) {
hoverHighlight.dispose(); hoverHighlight.dispose();
@ -697,7 +701,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
} }
} }
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);
@ -713,7 +717,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
statesController.update(WidgetState.hovered, value); statesController.update(WidgetState.hovered, value);
} }
case _HighlightType.focus: case _HighlightType.focus:
// see handleFocusUpdate() // see handleFocusUpdate()
break; break;
} }
@ -726,9 +730,9 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
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,
_HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor, _HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor,
@ -785,7 +789,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
final MaterialInkController inkController = Material.of(context); final MaterialInkController inkController = Material.of(context);
final RenderBox referenceBox = context.findRenderObject()! as RenderBox; final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
final Offset position = referenceBox.globalToLocal(globalPosition); final Offset position = referenceBox.globalToLocal(globalPosition);
final Color color = widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor; final Color color = widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor;
final RectCallback? rectCallback = widget.containedInkWell ? widget.getRectCallback!(referenceBox) : null; final RectCallback? rectCallback = widget.containedInkWell ? widget.getRectCallback!(referenceBox) : null;
final BorderRadius? borderRadius = widget.borderRadius; final BorderRadius? borderRadius = widget.borderRadius;
final ShapeBorder? customBorder = widget.customBorder; final ShapeBorder? customBorder = widget.customBorder;
@ -842,7 +846,6 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
} }
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
@ -975,17 +978,21 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
} }
bool _primaryButtonEnabled(_InkResponseStateWidget widget) { bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || widget.onTapUp != null || widget.onTapDown != null; return widget.onTap != null
|| widget.onDoubleTap != null
|| widget.onLongPress != null
|| widget.onTapUp != null
|| widget.onTapDown != null;
} }
bool _secondaryButtonEnabled(_InkResponseStateWidget widget) { bool _secondaryButtonEnabled(_InkResponseStateWidget widget) {
return widget.onSecondaryTap != null || widget.onSecondaryTapUp != null || widget.onSecondaryTapDown != null; return widget.onSecondaryTap != null
|| widget.onSecondaryTapUp != null
|| widget.onSecondaryTapDown != null;
} }
bool get enabled => isWidgetEnabled(widget); bool get 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) {
@ -1025,15 +1032,14 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
return switch (type) { return switch (type) {
// The pressed state triggers a ripple (ink splash), per the current // The pressed state triggers a ripple (ink splash), per the current
// Material Design spec. A separate highlight is no longer used. // Material Design spec. A separate highlight is no longer used.
// See https://material.io/design/interaction/states.html#pressed // See https://material.io/design/interaction/states.html#pressed
_HighlightType.pressed => widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor, _HighlightType.pressed => widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor,
_HighlightType.focus => widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor, _HighlightType.focus => widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor,
_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);
} }
@ -1071,7 +1077,7 @@ class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKee
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_plus/flutter_markdown_plus.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
class MarkdownContainer extends StatelessWidget { class MarkdownContainer extends StatelessWidget {
final String data; final String data;

View file

@ -20,8 +20,7 @@ class BlurredRect extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRect( return ClipRect(
// TODO TLAD [flutter vNext] use `BackdropFilter.grouped` child: 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,
@ -60,8 +59,7 @@ class BlurredRRect extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: borderRadius ?? BorderRadius.zero, borderRadius: borderRadius ?? BorderRadius.zero,
// TODO TLAD [flutter vNext] use `BackdropFilter.grouped` child: 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,
@ -83,8 +81,7 @@ class BlurredOval extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipOval( return ClipOval(
// TODO TLAD [flutter vNext] use `BackdropFilter.grouped` child: 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 Set<StreamSubscription> _subscriptions = {}; final List<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

@ -71,7 +71,7 @@ class _RenderSliverKnownExtentBoxAdaptor extends RenderSliverMultiBoxAdaptor {
_RenderSliverKnownExtentBoxAdaptor({ _RenderSliverKnownExtentBoxAdaptor({
required super.childManager, required super.childManager,
required List<SectionLayout> sectionLayouts, required List<SectionLayout> sectionLayouts,
}) : _sectionLayouts = sectionLayouts; }) : _sectionLayouts = sectionLayouts;
SectionLayout? sectionAtIndex(int index) => sectionLayouts.firstWhereOrNull((section) => section.hasChild(index)); SectionLayout? sectionAtIndex(int index) => sectionLayouts.firstWhereOrNull((section) => section.hasChild(index));

View file

@ -150,7 +150,7 @@ class AvesFilterChip extends StatefulWidget {
} }
class _AvesFilterChipState extends State<AvesFilterChip> { class _AvesFilterChipState extends State<AvesFilterChip> {
final Set<StreamSubscription> _subscriptions = {}; final List<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_plus/flutter_markdown_plus.dart'; import 'package:flutter_markdown/flutter_markdown.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 Set<StreamSubscription> _subscriptions = {}; final List<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,13 +249,14 @@ class _GeoMapState extends State<GeoMap> {
child = Column( child = Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// TODO TLAD [flutter vNext] wrap into `BackdropGroup` BackdropGroup(
mapHeight != null child: 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 Set<StreamSubscription> _subscriptions = {}; final List<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,12 +43,10 @@ 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 icon: animate ? AnimatedIcon(
? 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 Set<StreamSubscription> _subscriptions = {}; final List<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 Set<StreamSubscription> _subscriptions = {}; final List<StreamSubscription> _subscriptions = [];
LocationEditAction _action = LocationEditAction.chooseOnMap; LocationEditAction _action = LocationEditAction.chooseOnMap;
LatLng? _mapCoordinates; LatLng? _mapCoordinates;
late final AvesEntry mainEntry; late final AvesEntry mainEntry;

View file

@ -40,7 +40,7 @@ import 'package:provider/provider.dart';
Future<AlbumBaseFilter?> pickAlbum({ Future<AlbumBaseFilter?> pickAlbum({
required BuildContext context, required BuildContext context,
required MoveType? moveType, required MoveType? moveType,
required Iterable<AlbumChipType> albumChipTypes, required Iterable<AlbumChipType> albumTypes,
required Uri? initialGroup, required Uri? initialGroup,
}) async { }) async {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
@ -56,7 +56,7 @@ Future<AlbumBaseFilter?> pickAlbum({
builder: (context) => _AlbumPickPage( builder: (context) => _AlbumPickPage(
source: source, source: source,
moveType: moveType, moveType: moveType,
albumChipTypes: albumChipTypes, albumChipTypes: albumTypes,
initialGroup: initialGroup, initialGroup: initialGroup,
), ),
), ),

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 Set<StreamSubscription> _subscriptions = {}; final List<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 Set<StreamSubscription> _subscriptions = {}; final List<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;

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