Compare commits
42 commits
Author | SHA1 | Date | |
---|---|---|---|
9037f8e610 | |||
05bb77a793 | |||
2f0f3da2fa | |||
31f85c3e01 | |||
84a822022a | |||
94c83914a4 | |||
a461e2c55f | |||
99c9f85eaf | |||
848ad5220e | |||
![]() |
7577466978 | ||
![]() |
dfcaf4d35a | ||
![]() |
171394056f | ||
![]() |
60211545e1 | ||
![]() |
edbf9744f5 | ||
![]() |
d272c82454 | ||
![]() |
20b4f10b62 | ||
![]() |
3db0478be2 | ||
![]() |
c9fd71056f | ||
![]() |
ca2d2c2026 | ||
![]() |
2e775b3906 | ||
![]() |
ea3cb3c063 | ||
![]() |
340ed6a6d9 | ||
![]() |
5e0f0b59d8 | ||
![]() |
a0163001bd | ||
![]() |
1222a711e0 | ||
![]() |
8c3d0f1b83 | ||
![]() |
43cb2cd101 | ||
![]() |
81a2b84c9f | ||
![]() |
bae6d2b7c4 | ||
![]() |
9a377ed7bc | ||
![]() |
1119fa1407 | ||
![]() |
7b0f72d6ee | ||
![]() |
6f9a581d99 | ||
![]() |
b6faf36671 | ||
![]() |
17f3ec437c | ||
![]() |
3ec5b96bc9 | ||
![]() |
540fbbc2b4 | ||
![]() |
f355efefc1 | ||
![]() |
3bcaab9a4b | ||
![]() |
ef091b9932 | ||
![]() |
33667e7e6e | ||
![]() |
2a3cce422b |
133 changed files with 1117 additions and 703 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
||||||
Subproject commit ea121f8859e4b13e47a8f845e4586164519588bc
|
Subproject commit d8a9f9a52e5af486f80d932e838ee93861ffd863
|
7
.github/workflows/quality-check.yml
vendored
7
.github/workflows/quality-check.yml
vendored
|
@ -28,6 +28,9 @@ jobs:
|
||||||
- name: Get Flutter packages
|
- name: Get Flutter packages
|
||||||
run: ./flutterw pub get
|
run: ./flutterw pub get
|
||||||
|
|
||||||
|
- name: Generate app localizations
|
||||||
|
run: ./flutterw gen-l10n
|
||||||
|
|
||||||
- name: Static analysis.
|
- name: Static analysis.
|
||||||
run: ./flutterw analyze
|
run: ./flutterw analyze
|
||||||
|
|
||||||
|
@ -69,7 +72,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
|
@ -83,6 +86,6 @@ jobs:
|
||||||
./flutterw build apk --profile -t lib/main_play.dart --flavor play
|
./flutterw build apk --profile -t lib/main_play.dart --flavor play
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
|
@ -36,6 +36,9 @@ jobs:
|
||||||
- name: Get Flutter packages
|
- name: Get Flutter packages
|
||||||
run: ./flutterw pub get
|
run: ./flutterw pub get
|
||||||
|
|
||||||
|
- name: Generate app localizations
|
||||||
|
run: ./flutterw gen-l10n
|
||||||
|
|
||||||
- name: Update Flutter version file
|
- name: Update Flutter version file
|
||||||
run: scripts/update_flutter_version.sh
|
run: scripts/update_flutter_version.sh
|
||||||
|
|
||||||
|
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
|
@ -41,7 +41,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: "Run analysis"
|
- name: "Run analysis"
|
||||||
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||||
with:
|
with:
|
||||||
results_file: results.sarif
|
results_file: results.sarif
|
||||||
results_format: sarif
|
results_format: sarif
|
||||||
|
@ -71,6 +71,6 @@ jobs:
|
||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -4,6 +4,29 @@ 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
|
## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
85
README.md
85
README.md
|
@ -111,17 +111,96 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,11 @@ analyzer:
|
||||||
# implicit-casts: false
|
# implicit-casts: false
|
||||||
# implicit-dynamic: false
|
# implicit-dynamic: false
|
||||||
|
|
||||||
|
# cf https://github.com/dart-lang/dart_style/wiki/Configuration
|
||||||
|
formatter:
|
||||||
|
page_width: 240
|
||||||
|
trailing_commas: preserve
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
# from 'flutter_lints', excluded
|
# from 'flutter_lints', excluded
|
||||||
|
|
1
android/.gitignore
vendored
1
android/.gitignore
vendored
|
@ -7,6 +7,7 @@ gradle-wrapper.jar
|
||||||
GeneratedPluginRegistrant.java
|
GeneratedPluginRegistrant.java
|
||||||
.cxx/
|
.cxx/
|
||||||
.kotlin/
|
.kotlin/
|
||||||
|
/build/
|
||||||
|
|
||||||
# Remember to never publicly share your keystore.
|
# Remember to never publicly share your keystore.
|
||||||
# See https://flutter.dev/to/reference-keystore
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
|
|
@ -33,13 +33,13 @@ kotlin {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'deckers.thibault.aves'
|
namespace = 'deckers.thibault.aves'
|
||||||
compileSdk 35
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId packageName
|
applicationId packageName
|
||||||
minSdk flutter.minSdkVersion
|
minSdk flutter.minSdkVersion
|
||||||
targetSdk 35
|
targetSdk 36
|
||||||
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.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
|
||||||
|
|
||||||
implementation "androidx.appcompat:appcompat:1.7.0"
|
implementation "androidx.appcompat:appcompat:1.7.1"
|
||||||
implementation 'androidx.core:core-ktx:1.15.0'
|
implementation 'androidx.core:core-ktx:1.16.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
|
implementation 'androidx.lifecycle:lifecycle-process:2.9.1'
|
||||||
implementation 'androidx.media:media:1.7.0'
|
implementation 'androidx.media:media:1.7.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
implementation 'androidx.security:security-crypto:1.1.0-beta01'
|
||||||
implementation 'androidx.work:work-runtime-ktx:2.10.0'
|
implementation 'androidx.work:work-runtime-ktx:2.10.1'
|
||||||
|
|
||||||
implementation 'com.commonsware.cwac:document:0.5.0'
|
implementation 'com.commonsware.cwac:document:0.5.0'
|
||||||
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
|
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
|
||||||
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.16'
|
implementation 'org.slf4j:slf4j-simple:2.0.17'
|
||||||
|
|
||||||
// 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:3ed067f021'
|
implementation 'com.github.deckerst:Android-TiffBitmapFactory:d6b2b0aa4f'
|
||||||
implementation 'com.github.deckerst:androidsvg:cc9d59a88f'
|
implementation 'com.github.deckerst:androidsvg:67db933051'
|
||||||
implementation 'com.github.deckerst.mp4parser:isoparser:d5caf7a3dd'
|
implementation 'com.github.deckerst.mp4parser:isoparser:c2898f1832'
|
||||||
implementation 'com.github.deckerst.mp4parser:muxer:d5caf7a3dd'
|
implementation 'com.github.deckerst.mp4parser:muxer:c2898f1832'
|
||||||
implementation 'com.github.deckerst:pixymeta-android:71eee77dc4'
|
implementation 'com.github.deckerst:pixymeta-android:cb1cdc932e'
|
||||||
implementation project(':exifinterface')
|
implementation project(':exifinterface')
|
||||||
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'
|
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.1'
|
||||||
|
|
||||||
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"
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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
|
||||||
|
@ -18,7 +19,6 @@ 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,9 +38,8 @@ 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)
|
||||||
with(preferences.edit()) {
|
preferences.edit {
|
||||||
putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
|
putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
|
||||||
apply()
|
|
||||||
}
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
@ -69,9 +68,8 @@ 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)
|
||||||
with(preferences.edit()) {
|
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(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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
|
||||||
|
@ -102,6 +103,7 @@ 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(",")))
|
||||||
}
|
}
|
||||||
|
|
|
@ -311,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
embeddedByteStream: InputStream,
|
embeddedByteStream: InputStream,
|
||||||
embeddedByteLength: Long,
|
embeddedByteLength: Long,
|
||||||
) {
|
) {
|
||||||
val extension = extensionFor(mimeType)
|
val extension = extensionFor(mimeType, defaultExtension = null)
|
||||||
val targetFile = StorageUtils.createTempFile(context, extension).apply {
|
val targetFile = StorageUtils.createTempFile(context, extension).apply {
|
||||||
transferFrom(embeddedByteStream, embeddedByteLength)
|
transferFrom(embeddedByteStream, embeddedByteLength)
|
||||||
}
|
}
|
||||||
|
@ -319,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
||||||
val authority = "${context.applicationContext.packageName}.file_provider"
|
val authority = "${context.applicationContext.packageName}.file_provider"
|
||||||
val uri = if (displayName != null) {
|
val uri = if (displayName != null) {
|
||||||
// add extension to ease type identification when sharing this content
|
// add extension to ease type identification when sharing this content
|
||||||
val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) {
|
val displayNameWithExtension = if (displayName.endsWith(extension, ignoreCase = true)) {
|
||||||
displayName
|
displayName
|
||||||
} else {
|
} else {
|
||||||
"$displayName$extension"
|
"$displayName$extension"
|
||||||
|
|
|
@ -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 localeString = call.argument<String>("locale")
|
val localeLanguageTag = call.argument<String>("localeLanguageTag")
|
||||||
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,11 +43,8 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
geocoder = geocoder ?: if (localeString != null) {
|
geocoder = geocoder ?: if (localeLanguageTag != null) {
|
||||||
val split = localeString.split("_")
|
Geocoder(context, Locale.forLanguageTag(localeLanguageTag))
|
||||||
val language = split[0]
|
|
||||||
val country = if (split.size > 1) split[1] else ""
|
|
||||||
Geocoder(context, Locale(language, country))
|
|
||||||
} else {
|
} else {
|
||||||
Geocoder(context)
|
Geocoder(context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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
|
||||||
|
@ -29,9 +30,8 @@ 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)
|
||||||
with(preferences.edit()) {
|
preferences.edit {
|
||||||
putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
|
putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||||
apply()
|
|
||||||
}
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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
|
||||||
|
@ -45,7 +46,7 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
val preferences = getStore()
|
val preferences = getStore()
|
||||||
with(preferences.edit()) {
|
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)
|
||||||
|
@ -58,7 +59,6 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
apply()
|
|
||||||
}
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,10 @@ 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
|
||||||
|
@ -17,6 +19,7 @@ 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
|
||||||
|
@ -25,6 +28,8 @@ 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,
|
||||||
|
@ -77,6 +82,29 @@ 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)
|
||||||
|
@ -144,4 +172,9 @@ 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
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -31,9 +31,15 @@ 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")
|
||||||
context.contentResolver.apply {
|
try {
|
||||||
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
context.contentResolver.apply {
|
||||||
registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
||||||
|
registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
// Trying to register an observer may yield a security exception with this message:
|
||||||
|
// "Failed to find provider media for user 0; expected to find a valid ContentProvider for this authority"
|
||||||
|
Log.w(LOG_TAG, "failed to register content observer", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,12 +81,12 @@ object PixyMetaHelper {
|
||||||
output: OutputStream,
|
output: OutputStream,
|
||||||
iptcDataList: List<FieldMap>?,
|
iptcDataList: List<FieldMap>?,
|
||||||
) {
|
) {
|
||||||
val iptc = iptcDataList?.flatMap {
|
val iptc: List<IPTCDataSet> = 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<IPTCDataSet>()
|
} ?: ArrayList()
|
||||||
Metadata.insertIPTC(input, output, iptc)
|
Metadata.insertIPTC(input, output, iptc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,16 +142,18 @@ abstract class ImageProvider {
|
||||||
|
|
||||||
val oldFile = File(sourcePath)
|
val oldFile = File(sourcePath)
|
||||||
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
|
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
|
||||||
|
val defaultExtension = oldFile.extension
|
||||||
oldFile.parent?.let { dir ->
|
oldFile.parent?.let { dir ->
|
||||||
val resolution = resolveTargetFileNameWithoutExtension(
|
val resolution = resolveTargetFileNameWithoutExtension(
|
||||||
contextWrapper = activity,
|
contextWrapper = activity,
|
||||||
dir = dir,
|
dir = dir,
|
||||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
|
defaultExtension = defaultExtension,
|
||||||
conflictStrategy = NameConflictStrategy.RENAME,
|
conflictStrategy = NameConflictStrategy.RENAME,
|
||||||
)
|
)
|
||||||
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
|
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
|
||||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}"
|
||||||
val newFile = File(dir, targetFileName)
|
val newFile = File(dir, targetFileName)
|
||||||
if (oldFile != newFile) {
|
if (oldFile != newFile) {
|
||||||
newFields = renameSingle(
|
newFields = renameSingle(
|
||||||
|
@ -277,11 +279,17 @@ abstract class ImageProvider {
|
||||||
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
|
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
|
||||||
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
|
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// there is no benefit providing input extension
|
||||||
|
// for known output MIME type
|
||||||
|
val defaultExtension = null
|
||||||
|
|
||||||
val resolution = resolveTargetFileNameWithoutExtension(
|
val resolution = resolveTargetFileNameWithoutExtension(
|
||||||
contextWrapper = activity,
|
contextWrapper = activity,
|
||||||
dir = targetDir,
|
dir = targetDir,
|
||||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||||
mimeType = exportMimeType,
|
mimeType = exportMimeType,
|
||||||
|
defaultExtension = defaultExtension,
|
||||||
conflictStrategy = nameConflictStrategy,
|
conflictStrategy = nameConflictStrategy,
|
||||||
)
|
)
|
||||||
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
||||||
|
@ -358,6 +366,7 @@ abstract class ImageProvider {
|
||||||
targetDir = targetDir,
|
targetDir = targetDir,
|
||||||
targetDirDocFile = targetDirDocFile,
|
targetDirDocFile = targetDirDocFile,
|
||||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||||
|
defaultExtension = defaultExtension,
|
||||||
write = write,
|
write = write,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -465,6 +474,7 @@ abstract class ImageProvider {
|
||||||
dir = targetDir,
|
dir = targetDir,
|
||||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||||
mimeType = captureMimeType,
|
mimeType = captureMimeType,
|
||||||
|
defaultExtension = null,
|
||||||
conflictStrategy = nameConflictStrategy,
|
conflictStrategy = nameConflictStrategy,
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -571,13 +581,14 @@ abstract class ImageProvider {
|
||||||
dir: String,
|
dir: String,
|
||||||
desiredNameWithoutExtension: String,
|
desiredNameWithoutExtension: String,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
|
defaultExtension: String?,
|
||||||
conflictStrategy: NameConflictStrategy,
|
conflictStrategy: NameConflictStrategy,
|
||||||
): NameConflictResolution {
|
): NameConflictResolution {
|
||||||
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
|
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
|
||||||
var resolvedName: String? = sanitizedNameWithoutExtension
|
var resolvedName: String? = sanitizedNameWithoutExtension
|
||||||
var replacementFile: File? = null
|
var replacementFile: File? = null
|
||||||
|
|
||||||
val extension = extensionFor(mimeType)
|
val extension = extensionFor(mimeType, defaultExtension)
|
||||||
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
|
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
|
||||||
when (conflictStrategy) {
|
when (conflictStrategy) {
|
||||||
NameConflictStrategy.RENAME -> {
|
NameConflictStrategy.RENAME -> {
|
||||||
|
|
|
@ -557,6 +557,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
toBin: Boolean,
|
toBin: Boolean,
|
||||||
): FieldMap {
|
): FieldMap {
|
||||||
val sourcePath = sourceFile?.path
|
val sourcePath = sourceFile?.path
|
||||||
|
val sourceExtension = sourceFile?.extension
|
||||||
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
|
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
|
||||||
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
|
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
|
||||||
// nothing to do unless it's a renamed copy
|
// nothing to do unless it's a renamed copy
|
||||||
|
@ -569,6 +570,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
dir = targetDir,
|
dir = targetDir,
|
||||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||||
mimeType = mimeType,
|
mimeType = mimeType,
|
||||||
|
defaultExtension = sourceExtension,
|
||||||
conflictStrategy = nameConflictStrategy,
|
conflictStrategy = nameConflictStrategy,
|
||||||
)
|
)
|
||||||
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
||||||
|
@ -580,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
targetDir = targetDir,
|
targetDir = targetDir,
|
||||||
targetDirDocFile = targetDirDocFile,
|
targetDirDocFile = targetDirDocFile,
|
||||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||||
|
defaultExtension = sourceExtension,
|
||||||
) { output: OutputStream ->
|
) { output: OutputStream ->
|
||||||
try {
|
try {
|
||||||
sourceDocFile.copyTo(output)
|
sourceDocFile.copyTo(output)
|
||||||
|
@ -615,12 +618,13 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
targetDir: String,
|
targetDir: String,
|
||||||
targetDirDocFile: DocumentFileCompat?,
|
targetDirDocFile: DocumentFileCompat?,
|
||||||
targetNameWithoutExtension: String,
|
targetNameWithoutExtension: String,
|
||||||
|
defaultExtension: String?,
|
||||||
write: (OutputStream) -> Unit,
|
write: (OutputStream) -> Unit,
|
||||||
): String {
|
): String {
|
||||||
if (StorageUtils.isInVault(activity, targetDir)) {
|
if (StorageUtils.isInVault(activity, targetDir)) {
|
||||||
return insertByFile(
|
return insertByFile(
|
||||||
targetDir = targetDir,
|
targetDir = targetDir,
|
||||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
|
||||||
write = write,
|
write = write,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -630,7 +634,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return insertByMediaStore(
|
return insertByMediaStore(
|
||||||
activity = activity,
|
activity = activity,
|
||||||
targetDir = targetDir,
|
targetDir = targetDir,
|
||||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
|
||||||
write = write,
|
write = write,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -642,6 +646,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
targetDir = targetDir,
|
targetDir = targetDir,
|
||||||
targetDirDocFile = targetDirDocFile,
|
targetDirDocFile = targetDirDocFile,
|
||||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||||
|
defaultExtension = defaultExtension,
|
||||||
write = write,
|
write = write,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -700,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
targetDir: String,
|
targetDir: String,
|
||||||
targetDirDocFile: DocumentFileCompat?,
|
targetDirDocFile: DocumentFileCompat?,
|
||||||
targetNameWithoutExtension: String,
|
targetNameWithoutExtension: String,
|
||||||
|
defaultExtension: String?,
|
||||||
write: (OutputStream) -> Unit,
|
write: (OutputStream) -> Unit,
|
||||||
): String {
|
): String {
|
||||||
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
|
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
|
||||||
|
@ -708,8 +714,22 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
|
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
|
||||||
// through a document URI, not a tree URI
|
// through a document URI, not a tree URI
|
||||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||||
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
|
var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
|
||||||
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||||
|
|
||||||
|
// providing a display name and a MIME type does not guarantee
|
||||||
|
// that the created document will be backed by a file with a valid media extension,
|
||||||
|
// but having an extension is essential for media detection by Android,
|
||||||
|
// so we retry with a display name that includes the extension
|
||||||
|
if ((targetDocFile.extension == null || targetDocFile.extension.isEmpty() || targetDocFile.extension == "bin") && defaultExtension != null) {
|
||||||
|
if (targetDocFile.exists()) {
|
||||||
|
targetDocFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
val extension = if (defaultExtension.startsWith(".")) defaultExtension else ".$defaultExtension"
|
||||||
|
targetTreeFile = targetDirDocFile.createFile(mimeType, "$targetNameWithoutExtension$extension")
|
||||||
|
targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
targetDocFile.openOutputStream().use(write)
|
targetDocFile.openOutputStream().use(write)
|
||||||
|
|
|
@ -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())
|
||||||
private fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
|
fun highestPowerOf2(x: Double): Int = if (x < 1) 0 else 2.toDouble().pow(log2(x).toInt()).toInt()
|
||||||
}
|
}
|
|
@ -163,12 +163,24 @@ object MimeTypes {
|
||||||
|
|
||||||
// among other refs:
|
// among other refs:
|
||||||
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
|
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
|
||||||
fun extensionFor(mimeType: String): String? = when (mimeType) {
|
fun extensionFor(mimeType: String, defaultExtension: String?): String = when (mimeType) {
|
||||||
AVI, AVI_VND -> ".avi"
|
AVI, AVI_VND -> ".avi"
|
||||||
|
DNG, DNG_ADOBE -> ".dng"
|
||||||
HEIC, HEIF -> ".heif"
|
HEIC, HEIF -> ".heif"
|
||||||
MP2T, MP2TS -> ".m2ts"
|
MP2T, MP2TS -> ".m2ts"
|
||||||
PSD_VND, PSD_X -> ".psd"
|
PSD_VND, PSD_X -> ".psd"
|
||||||
else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
|
else -> {
|
||||||
|
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: defaultExtension
|
||||||
|
if (ext != null) {
|
||||||
|
// fallback to provided extension when available,
|
||||||
|
// typically the original file extension when moving/renaming
|
||||||
|
if (ext.startsWith(".")) ext else ".$ext"
|
||||||
|
} else {
|
||||||
|
// fallback to generic extensions,
|
||||||
|
// as incorrect file extensions are better than none for media detection
|
||||||
|
if (isVideo(mimeType)) ".mp4" else ".jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
|
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
|
||||||
|
|
|
@ -8,4 +8,5 @@
|
||||||
<string name="videos_shortcut_short_label">ဗီဒီယိုများ</string>
|
<string name="videos_shortcut_short_label">ဗီဒီယိုများ</string>
|
||||||
<string name="analysis_notification_default_title">မီဒီယာ ကိုစကင်ဖတ်နေသည်</string>
|
<string name="analysis_notification_default_title">မီဒီယာ ကိုစကင်ဖတ်နေသည်</string>
|
||||||
<string name="analysis_notification_action_stop">ရပ်ရန်</string>
|
<string name="analysis_notification_action_stop">ရပ်ရန်</string>
|
||||||
|
<string name="map_shortcut_short_label">မြေပုံ</string>
|
||||||
</resources>
|
</resources>
|
|
@ -22,7 +22,6 @@ import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongA
|
||||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
|
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
|
||||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
|
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
|
||||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
|
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
|
||||||
|
|
||||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
import static java.nio.ByteOrder.BIG_ENDIAN;
|
import static java.nio.ByteOrder.BIG_ENDIAN;
|
||||||
import static java.nio.ByteOrder.LITTLE_ENDIAN;
|
import static java.nio.ByteOrder.LITTLE_ENDIAN;
|
||||||
|
@ -91,7 +90,7 @@ import java.util.regex.Pattern;
|
||||||
import java.util.zip.CRC32;
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Forked from 'androidx.exifinterface:exifinterface:1.4.0'
|
* Forked from 'androidx.exifinterface:exifinterface:1.4.1'
|
||||||
* Named differently to let ExifInterface be loaded as subdependency.
|
* Named differently to let ExifInterface be loaded as subdependency.
|
||||||
* cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface
|
* cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface
|
||||||
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
|
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
|
||||||
|
@ -139,6 +138,12 @@ public class ExifInterfaceFork {
|
||||||
// TLAD threshold for safer Exif attribute parsing
|
// TLAD threshold for safer Exif attribute parsing
|
||||||
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
|
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
|
||||||
|
|
||||||
|
// TLAD available heap size, to check allocations
|
||||||
|
private long getAvailableHeapSize() {
|
||||||
|
final Runtime runtime = Runtime.getRuntime();
|
||||||
|
return runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory());
|
||||||
|
}
|
||||||
|
|
||||||
private static final String TAG = "ExifInterface";
|
private static final String TAG = "ExifInterface";
|
||||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||||
|
|
||||||
|
@ -4553,7 +4558,7 @@ public class ExifInterfaceFork {
|
||||||
&& (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
|
&& (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
|
||||||
|| (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
|
|| (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
|
||||||
&& !containsTiff700Xmp)) {
|
&& !containsTiff700Xmp)) {
|
||||||
mXmpFromSeparateMarker = ExifAttribute.createByte(value);
|
mXmpFromSeparateMarker = value != null ? ExifAttribute.createByte(value) : null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6558,8 +6563,9 @@ public class ExifInterfaceFork {
|
||||||
// Exif data in WebP images (e.g.
|
// Exif data in WebP images (e.g.
|
||||||
// https://github.com/ImageMagick/ImageMagick/issues/3140)
|
// https://github.com/ImageMagick/ImageMagick/issues/3140)
|
||||||
if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
|
if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
|
||||||
payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length,
|
payload =
|
||||||
payload.length);
|
Arrays.copyOfRange(
|
||||||
|
payload, IDENTIFIER_EXIF_APP1.length, payload.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save offset to EXIF data for handling thumbnail and attribute offsets.
|
// Save offset to EXIF data for handling thumbnail and attribute offsets.
|
||||||
|
@ -6722,8 +6728,11 @@ public class ExifInterfaceFork {
|
||||||
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
|
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
|
||||||
|
|
||||||
boolean needToWriteExif = true;
|
boolean needToWriteExif = true;
|
||||||
boolean needToWriteXmp = mXmpFromSeparateMarker != null;
|
// Either there's some XMP data to write, or it has been cleared locally but was present in
|
||||||
while (needToWriteExif || needToWriteXmp) {
|
// the file when it was read (and so needs to be removed).
|
||||||
|
boolean needToHandleXmpChunk =
|
||||||
|
mXmpFromSeparateMarker != null || mFileOnDiskContainsSeparateXmpMarker;
|
||||||
|
while (needToWriteExif || needToHandleXmpChunk) {
|
||||||
int chunkLength = dataInputStream.readInt();
|
int chunkLength = dataInputStream.readInt();
|
||||||
int chunkType = dataInputStream.readInt();
|
int chunkType = dataInputStream.readInt();
|
||||||
if (chunkType == PNG_CHUNK_TYPE_IHDR) {
|
if (chunkType == PNG_CHUNK_TYPE_IHDR) {
|
||||||
|
@ -6738,7 +6747,7 @@ public class ExifInterfaceFork {
|
||||||
}
|
}
|
||||||
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
|
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
|
||||||
writePngXmpItxtChunk(dataOutputStream);
|
writePngXmpItxtChunk(dataOutputStream);
|
||||||
needToWriteXmp = false;
|
needToHandleXmpChunk = false;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
|
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
|
||||||
|
@ -6746,10 +6755,25 @@ public class ExifInterfaceFork {
|
||||||
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
|
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
|
||||||
needToWriteExif = false;
|
needToWriteExif = false;
|
||||||
continue;
|
continue;
|
||||||
} else if (chunkType == PNG_CHUNK_TYPE_ITXT && needToWriteXmp) {
|
} else if (chunkType == PNG_CHUNK_TYPE_ITXT
|
||||||
writePngXmpItxtChunk(dataOutputStream);
|
&& chunkLength >= PNG_ITXT_XMP_KEYWORD.length) {
|
||||||
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
|
// Read the 17 byte keyword and 5 expected null bytes.
|
||||||
needToWriteXmp = false;
|
byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length];
|
||||||
|
dataInputStream.readFully(keyword);
|
||||||
|
int remainingChunkBytes = chunkLength - keyword.length + PNG_CHUNK_CRC_BYTE_LENGTH;
|
||||||
|
if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) {
|
||||||
|
if (mXmpFromSeparateMarker != null) {
|
||||||
|
writePngXmpItxtChunk(dataOutputStream);
|
||||||
|
}
|
||||||
|
dataInputStream.skipFully(remainingChunkBytes);
|
||||||
|
needToHandleXmpChunk = false;
|
||||||
|
} else {
|
||||||
|
// This is a non-XMP iTXt chunk, so just copy it to the output and continue.
|
||||||
|
dataOutputStream.writeInt(chunkLength);
|
||||||
|
dataOutputStream.writeInt(chunkType);
|
||||||
|
dataOutputStream.write(keyword);
|
||||||
|
copy(dataInputStream, dataOutputStream, remainingChunkBytes);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
dataOutputStream.writeInt(chunkLength);
|
dataOutputStream.writeInt(chunkLength);
|
||||||
|
@ -7536,6 +7560,13 @@ public class ExifInterfaceFork {
|
||||||
Log.d(TAG, "Invalid strip offset value");
|
Log.d(TAG, "Invalid strip offset value");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TLAD start
|
||||||
|
if (bytesToSkip > getAvailableHeapSize()) {
|
||||||
|
throw new IOException("cannot allocate " + bytesToSkip + " bytes to skip to retrieve thumbnail");
|
||||||
|
}
|
||||||
|
// TLAD end
|
||||||
|
|
||||||
try {
|
try {
|
||||||
in.skipFully(bytesToSkip);
|
in.skipFully(bytesToSkip);
|
||||||
} catch (EOFException e) {
|
} catch (EOFException e) {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
|
* Forked from 'androidx.exifinterface:exifinterface:1.4.1'
|
||||||
* Named differently to let ExifInterface be loaded as subdependency.
|
* Named differently to let ExifInterface be loaded as subdependency.
|
||||||
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
|
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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.8.1" apply false
|
id("com.android.application") version "8.10.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
|
id("org.jetbrains.kotlin.android") version "2.1.21" apply false
|
||||||
id("com.google.devtools.ksp") version "2.1.10-1.0.29" apply false
|
id("com.google.devtools.ksp") version "2.1.21-2.0.1" apply false
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
4
fastlane/metadata/android/en-US/changelogs/153.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/153.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
In v1.13.2:
|
||||||
|
- group albums
|
||||||
|
- filter by day of the week
|
||||||
|
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/15301.txt
Normal file
4
fastlane/metadata/android/en-US/changelogs/15301.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
In v1.13.2:
|
||||||
|
- group albums
|
||||||
|
- filter by day of the week
|
||||||
|
Full changelog available on GitHub
|
|
@ -599,7 +599,7 @@
|
||||||
"@settingsLanguagePageTitle": {},
|
"@settingsLanguagePageTitle": {},
|
||||||
"rootDirectoryDescription": "دليل الجذر",
|
"rootDirectoryDescription": "دليل الجذر",
|
||||||
"@rootDirectoryDescription": {},
|
"@rootDirectoryDescription": {},
|
||||||
"viewDialogGroupSectionTitle": "مجموعة",
|
"viewDialogGroupSectionTitle": "الأقسام",
|
||||||
"@viewDialogGroupSectionTitle": {},
|
"@viewDialogGroupSectionTitle": {},
|
||||||
"maxBrightnessAlways": "دائماً",
|
"maxBrightnessAlways": "دائماً",
|
||||||
"@maxBrightnessAlways": {},
|
"@maxBrightnessAlways": {},
|
||||||
|
@ -1449,7 +1449,7 @@
|
||||||
"@binPageTitle": {},
|
"@binPageTitle": {},
|
||||||
"tagPlaceholderState": "الولاية",
|
"tagPlaceholderState": "الولاية",
|
||||||
"@tagPlaceholderState": {},
|
"@tagPlaceholderState": {},
|
||||||
"sortByAlbumFileName": "حسب الألبوم واسم الملف",
|
"sortByAlbumFileName": "حسب عنوان الألبوم والعنصر",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}",
|
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}",
|
||||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||||
|
@ -1599,8 +1599,28 @@
|
||||||
"@sortByPath": {},
|
"@sortByPath": {},
|
||||||
"searchFormatSectionTitle": "التنسيقات",
|
"searchFormatSectionTitle": "التنسيقات",
|
||||||
"@searchFormatSectionTitle": {},
|
"@searchFormatSectionTitle": {},
|
||||||
"chipActionGroup": "مجموعة",
|
"chipActionGroup": "تغيير التجميع",
|
||||||
"@chipActionGroup": {},
|
"@chipActionGroup": {},
|
||||||
"createButtonLabel": "خلق",
|
"createButtonLabel": "خلق",
|
||||||
"@createButtonLabel": {}
|
"@createButtonLabel": {},
|
||||||
|
"sectionNone": "لا يوجد أقسام",
|
||||||
|
"@sectionNone": {},
|
||||||
|
"chipActionCreateGroup": "إنشاء مجموعة",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"albumTierGroups": "المجموعات",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"newGroupDialogTitle": "مجموعة جديدة",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"newGroupDialogNameLabel": "اسم المجموعة",
|
||||||
|
"@newGroupDialogNameLabel": {},
|
||||||
|
"groupAlreadyExists": "المجموعة موجودة بالفعل",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"groupEmpty": "لا توجد مجموعات",
|
||||||
|
"@groupEmpty": {},
|
||||||
|
"ungrouped": "غير مجمعة",
|
||||||
|
"@ungrouped": {},
|
||||||
|
"groupPickerTitle": "اختر المجموعة",
|
||||||
|
"@groupPickerTitle": {},
|
||||||
|
"groupPickerUseThisGroupButton": "استخدم هذه المجموعة",
|
||||||
|
"@groupPickerUseThisGroupButton": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,5 +142,15 @@
|
||||||
"chipActionUnpin": "Sabitləməyin",
|
"chipActionUnpin": "Sabitləməyin",
|
||||||
"@chipActionUnpin": {},
|
"@chipActionUnpin": {},
|
||||||
"chipActionRename": "Bir də adlandır",
|
"chipActionRename": "Bir də adlandır",
|
||||||
"@chipActionRename": {}
|
"@chipActionRename": {},
|
||||||
|
"chipActionDecompose": "Böl",
|
||||||
|
"@chipActionDecompose": {},
|
||||||
|
"chipActionCreateAlbum": "Albom yarat",
|
||||||
|
"@chipActionCreateAlbum": {},
|
||||||
|
"createButtonLabel": "YARAT",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"chipActionGroup": "Qruplandırmanı dəyişdir",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"chipActionCreateGroup": "Qrup yarat",
|
||||||
|
"@chipActionCreateGroup": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1624,5 +1624,19 @@
|
||||||
"sortByPath": "Според пътя",
|
"sortByPath": "Според пътя",
|
||||||
"@sortByPath": {},
|
"@sortByPath": {},
|
||||||
"searchFormatSectionTitle": "Формати",
|
"searchFormatSectionTitle": "Формати",
|
||||||
"@searchFormatSectionTitle": {}
|
"@searchFormatSectionTitle": {},
|
||||||
|
"chipActionCreateGroup": "Създайте група",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"chipActionGroup": "Групиране",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"newGroupDialogTitle": "Нова Група",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"groupAlreadyExists": "Групата вече съществува",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"albumTierGroups": "Групи",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"groupPickerUseThisGroupButton": "Използвайте тази група",
|
||||||
|
"@groupPickerUseThisGroupButton": {},
|
||||||
|
"newGroupDialogNameLabel": "Име на групата",
|
||||||
|
"@newGroupDialogNameLabel": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -850,7 +850,7 @@
|
||||||
"@drawerCollectionRaws": {},
|
"@drawerCollectionRaws": {},
|
||||||
"sortByRating": "Efter bedømmelse",
|
"sortByRating": "Efter bedømmelse",
|
||||||
"@sortByRating": {},
|
"@sortByRating": {},
|
||||||
"sortByAlbumFileName": "Efter album og filnavn",
|
"sortByAlbumFileName": "Efter album og elementtitel",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"albumGroupVolume": "Efter lagervolume",
|
"albumGroupVolume": "Efter lagervolume",
|
||||||
"@albumGroupVolume": {},
|
"@albumGroupVolume": {},
|
||||||
|
@ -1627,7 +1627,7 @@
|
||||||
"@searchFormatSectionTitle": {},
|
"@searchFormatSectionTitle": {},
|
||||||
"createButtonLabel": "OPRET",
|
"createButtonLabel": "OPRET",
|
||||||
"@createButtonLabel": {},
|
"@createButtonLabel": {},
|
||||||
"chipActionGroup": "Gruppér",
|
"chipActionGroup": "Ændr gruppering",
|
||||||
"@chipActionGroup": {},
|
"@chipActionGroup": {},
|
||||||
"chipActionCreateGroup": "Opret gruppe",
|
"chipActionCreateGroup": "Opret gruppe",
|
||||||
"@chipActionCreateGroup": {},
|
"@chipActionCreateGroup": {},
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
"chipActionLock": "Lock",
|
"chipActionLock": "Lock",
|
||||||
"chipActionPin": "Pin to top",
|
"chipActionPin": "Pin to top",
|
||||||
"chipActionUnpin": "Unpin from top",
|
"chipActionUnpin": "Unpin from top",
|
||||||
"chipActionGroup": "Group",
|
"chipActionGroup": "Change grouping",
|
||||||
"chipActionRename": "Rename",
|
"chipActionRename": "Rename",
|
||||||
"chipActionSetCover": "Set cover",
|
"chipActionSetCover": "Set cover",
|
||||||
"chipActionShowCountryStates": "Show states",
|
"chipActionShowCountryStates": "Show states",
|
||||||
|
@ -767,7 +767,7 @@
|
||||||
"sortByName": "By name",
|
"sortByName": "By name",
|
||||||
"sortByItemCount": "By item count",
|
"sortByItemCount": "By item count",
|
||||||
"sortBySize": "By size",
|
"sortBySize": "By size",
|
||||||
"sortByAlbumFileName": "By album & file name",
|
"sortByAlbumFileName": "By album & item title",
|
||||||
"sortByRating": "By rating",
|
"sortByRating": "By rating",
|
||||||
"sortByDuration": "By duration",
|
"sortByDuration": "By duration",
|
||||||
"sortByPath": "By path",
|
"sortByPath": "By path",
|
||||||
|
|
|
@ -790,7 +790,7 @@
|
||||||
"@aboutLicensesDartPackagesSectionTitle": {},
|
"@aboutLicensesDartPackagesSectionTitle": {},
|
||||||
"aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse",
|
"aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse",
|
||||||
"@aboutLicensesShowAllButtonLabel": {},
|
"@aboutLicensesShowAllButtonLabel": {},
|
||||||
"policyPageTitle": "Privaatsuspoliitika",
|
"policyPageTitle": "Andmekaitsepõhimõtted",
|
||||||
"@policyPageTitle": {},
|
"@policyPageTitle": {},
|
||||||
"collectionPageTitle": "Meediakogu",
|
"collectionPageTitle": "Meediakogu",
|
||||||
"@collectionPageTitle": {},
|
"@collectionPageTitle": {},
|
||||||
|
@ -1036,7 +1036,7 @@
|
||||||
"@sortBySize": {},
|
"@sortBySize": {},
|
||||||
"sortByName": "Nime alusel",
|
"sortByName": "Nime alusel",
|
||||||
"@sortByName": {},
|
"@sortByName": {},
|
||||||
"sortByAlbumFileName": "Albumi ja failinime alusel",
|
"sortByAlbumFileName": "Albumi ja objekti nime alusel",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"sortByRating": "Hinnangu alusel",
|
"sortByRating": "Hinnangu alusel",
|
||||||
"@sortByRating": {},
|
"@sortByRating": {},
|
||||||
|
@ -1645,7 +1645,7 @@
|
||||||
"@groupPickerUseThisGroupButton": {},
|
"@groupPickerUseThisGroupButton": {},
|
||||||
"createButtonLabel": "LOO",
|
"createButtonLabel": "LOO",
|
||||||
"@createButtonLabel": {},
|
"@createButtonLabel": {},
|
||||||
"chipActionGroup": "Rühmita",
|
"chipActionGroup": "Muuda grupeerimist",
|
||||||
"@chipActionGroup": {},
|
"@chipActionGroup": {},
|
||||||
"sectionNone": "Rubriike pole",
|
"sectionNone": "Rubriike pole",
|
||||||
"@sectionNone": {}
|
"@sectionNone": {}
|
||||||
|
|
|
@ -637,7 +637,7 @@
|
||||||
"@sortByItemCount": {},
|
"@sortByItemCount": {},
|
||||||
"sortBySize": "par taille",
|
"sortBySize": "par taille",
|
||||||
"@sortBySize": {},
|
"@sortBySize": {},
|
||||||
"sortByAlbumFileName": "alphabétique",
|
"sortByAlbumFileName": "par titre d’album et élément",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"sortByRating": "par notation",
|
"sortByRating": "par notation",
|
||||||
"@sortByRating": {},
|
"@sortByRating": {},
|
||||||
|
@ -1407,7 +1407,7 @@
|
||||||
"@sortByPath": {},
|
"@sortByPath": {},
|
||||||
"searchFormatSectionTitle": "Formats",
|
"searchFormatSectionTitle": "Formats",
|
||||||
"@searchFormatSectionTitle": {},
|
"@searchFormatSectionTitle": {},
|
||||||
"chipActionGroup": "Grouper",
|
"chipActionGroup": "Modifier groupement",
|
||||||
"@chipActionGroup": {},
|
"@chipActionGroup": {},
|
||||||
"createButtonLabel": "CRÉER",
|
"createButtonLabel": "CRÉER",
|
||||||
"@createButtonLabel": {},
|
"@createButtonLabel": {},
|
||||||
|
|
|
@ -158,5 +158,49 @@
|
||||||
"chipActionCreateGroup": "צור קבוצה",
|
"chipActionCreateGroup": "צור קבוצה",
|
||||||
"@chipActionCreateGroup": {},
|
"@chipActionCreateGroup": {},
|
||||||
"chipActionCreateVault": "צור כספת",
|
"chipActionCreateVault": "צור כספת",
|
||||||
"@chipActionCreateVault": {}
|
"@chipActionCreateVault": {},
|
||||||
|
"newGroupDialogTitle": "קבוצה חדשה",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"groupAlreadyExists": "הקבוצה כבר קיימת",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"entryActionDelete": "מחיקה",
|
||||||
|
"@entryActionDelete": {},
|
||||||
|
"entryActionConvert": "המרה",
|
||||||
|
"@entryActionConvert": {},
|
||||||
|
"entryActionRotateCCW": "סובב נגד כיוון השעון",
|
||||||
|
"@entryActionRotateCCW": {},
|
||||||
|
"entryActionShare": "שיתוף",
|
||||||
|
"@entryActionShare": {},
|
||||||
|
"entryActionShareVideoOnly": "שיתוף וידיאו בלבד",
|
||||||
|
"@entryActionShareVideoOnly": {},
|
||||||
|
"videoActionSelectStreams": "בחר מסלולים",
|
||||||
|
"@videoActionSelectStreams": {},
|
||||||
|
"videoActionShowPreviousFrame": "הצג פריים קודם",
|
||||||
|
"@videoActionShowPreviousFrame": {},
|
||||||
|
"videoActionShowNextFrame": "הצג פריים הבא",
|
||||||
|
"@videoActionShowNextFrame": {},
|
||||||
|
"chipActionConfigureVault": "הגדרת כספת",
|
||||||
|
"@chipActionConfigureVault": {},
|
||||||
|
"entryActionCopyToClipboard": "הועתק ללוח",
|
||||||
|
"@entryActionCopyToClipboard": {},
|
||||||
|
"entryActionShareImageOnly": "שיתוף תמונה בלבד",
|
||||||
|
"@entryActionShareImageOnly": {},
|
||||||
|
"entryActionRotateCW": "סובב עם כיוון השעון",
|
||||||
|
"@entryActionRotateCW": {},
|
||||||
|
"entryActionFlip": "הפוך אופקית",
|
||||||
|
"@entryActionFlip": {},
|
||||||
|
"entryActionPrint": "הדפסה",
|
||||||
|
"@entryActionPrint": {},
|
||||||
|
"entryActionViewSource": "מקור וידאו",
|
||||||
|
"@entryActionViewSource": {},
|
||||||
|
"entryActionShowGeoTiffOnMap": "הצג כשכבת מפה",
|
||||||
|
"@entryActionShowGeoTiffOnMap": {},
|
||||||
|
"entryActionInfo": "מידע",
|
||||||
|
"@entryActionInfo": {},
|
||||||
|
"entryActionExport": "ייצוא",
|
||||||
|
"@entryActionExport": {},
|
||||||
|
"entryActionRename": "שינוי שם",
|
||||||
|
"@entryActionRename": {},
|
||||||
|
"entryActionRestore": "שחזור",
|
||||||
|
"@entryActionRestore": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,7 +291,7 @@
|
||||||
"@tileLayoutMosaic": {},
|
"@tileLayoutMosaic": {},
|
||||||
"tileLayoutGrid": "Rács",
|
"tileLayoutGrid": "Rács",
|
||||||
"@tileLayoutGrid": {},
|
"@tileLayoutGrid": {},
|
||||||
"viewDialogGroupSectionTitle": "Csoport",
|
"viewDialogGroupSectionTitle": "Szekciók",
|
||||||
"@viewDialogGroupSectionTitle": {},
|
"@viewDialogGroupSectionTitle": {},
|
||||||
"menuActionStats": "Statisztikák",
|
"menuActionStats": "Statisztikák",
|
||||||
"@menuActionStats": {},
|
"@menuActionStats": {},
|
||||||
|
@ -1594,5 +1594,33 @@
|
||||||
"editEntryLocationDialogTimeShift": "Időeltolódás",
|
"editEntryLocationDialogTimeShift": "Időeltolódás",
|
||||||
"@editEntryLocationDialogTimeShift": {},
|
"@editEntryLocationDialogTimeShift": {},
|
||||||
"removeEntryMetadataDialogAll": "Összes",
|
"removeEntryMetadataDialogAll": "Összes",
|
||||||
"@removeEntryMetadataDialogAll": {}
|
"@removeEntryMetadataDialogAll": {},
|
||||||
|
"sortByPath": "Útvonal szerint",
|
||||||
|
"@sortByPath": {},
|
||||||
|
"chipActionCreateGroup": "Csoport létrehozása",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"albumTierGroups": "Csoportok",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"chipActionGroup": "Csoportosítás",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"createButtonLabel": "LÉTREHOZÁS",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"newGroupDialogTitle": "Új csoport",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"newGroupDialogNameLabel": "Csoport neve",
|
||||||
|
"@newGroupDialogNameLabel": {},
|
||||||
|
"groupAlreadyExists": "Csoport már létezik",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"groupEmpty": "Nincsenek csoportok",
|
||||||
|
"@groupEmpty": {},
|
||||||
|
"ungrouped": "Csoportosítatlan",
|
||||||
|
"@ungrouped": {},
|
||||||
|
"groupPickerTitle": "Válassza ki a csoportot",
|
||||||
|
"@groupPickerTitle": {},
|
||||||
|
"groupPickerUseThisGroupButton": "Használja ezt a csoportot",
|
||||||
|
"@groupPickerUseThisGroupButton": {},
|
||||||
|
"searchFormatSectionTitle": "Formátumok",
|
||||||
|
"@searchFormatSectionTitle": {},
|
||||||
|
"sectionNone": "Semmi szerint",
|
||||||
|
"@sectionNone": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -453,7 +453,7 @@
|
||||||
"@menuActionStats": {},
|
"@menuActionStats": {},
|
||||||
"viewDialogSortSectionTitle": "Sortir",
|
"viewDialogSortSectionTitle": "Sortir",
|
||||||
"@viewDialogSortSectionTitle": {},
|
"@viewDialogSortSectionTitle": {},
|
||||||
"viewDialogGroupSectionTitle": "Grup",
|
"viewDialogGroupSectionTitle": "Bagian",
|
||||||
"@viewDialogGroupSectionTitle": {},
|
"@viewDialogGroupSectionTitle": {},
|
||||||
"viewDialogLayoutSectionTitle": "Tata letak",
|
"viewDialogLayoutSectionTitle": "Tata letak",
|
||||||
"@viewDialogLayoutSectionTitle": {},
|
"@viewDialogLayoutSectionTitle": {},
|
||||||
|
@ -1406,5 +1406,29 @@
|
||||||
"sortByPath": "Melalui lokasi",
|
"sortByPath": "Melalui lokasi",
|
||||||
"@sortByPath": {},
|
"@sortByPath": {},
|
||||||
"searchFormatSectionTitle": "Format",
|
"searchFormatSectionTitle": "Format",
|
||||||
"@searchFormatSectionTitle": {}
|
"@searchFormatSectionTitle": {},
|
||||||
|
"sectionNone": "Tidak ada bagian",
|
||||||
|
"@sectionNone": {},
|
||||||
|
"albumTierGroups": "Kelompok",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"createButtonLabel": "BUAT",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"chipActionGroup": "Kelompok",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"chipActionCreateGroup": "Buat kelompok",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"ungrouped": "Tidak dikelompokkan",
|
||||||
|
"@ungrouped": {},
|
||||||
|
"newGroupDialogTitle": "Kelompok Baru",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"newGroupDialogNameLabel": "Nama kelompok",
|
||||||
|
"@newGroupDialogNameLabel": {},
|
||||||
|
"groupAlreadyExists": "Kelompok sudah ada",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"groupEmpty": "Tidak ada kelompok",
|
||||||
|
"@groupEmpty": {},
|
||||||
|
"groupPickerTitle": "Pilih Kelompok",
|
||||||
|
"@groupPickerTitle": {},
|
||||||
|
"groupPickerUseThisGroupButton": "Gunakan kelompok ini",
|
||||||
|
"@groupPickerUseThisGroupButton": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1346,7 +1346,7 @@
|
||||||
"@binPageTitle": {},
|
"@binPageTitle": {},
|
||||||
"tagPlaceholderState": "Hérað",
|
"tagPlaceholderState": "Hérað",
|
||||||
"@tagPlaceholderState": {},
|
"@tagPlaceholderState": {},
|
||||||
"sortByAlbumFileName": "Eftir heiti albúma og skráa",
|
"sortByAlbumFileName": "Eftir heiti albúma og atriða",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Eyða þessum albúmum og atriðinu í þeim?} other{Eyða þessum albúmum og {count} atriðum í þeim??}}",
|
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Eyða þessum albúmum og atriðinu í þeim?} other{Eyða þessum albúmum og {count} atriðum í þeim??}}",
|
||||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||||
|
|
|
@ -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": {},
|
||||||
|
|
|
@ -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": {},
|
||||||
|
|
|
@ -1312,5 +1312,19 @@
|
||||||
"settingsVideoPlaybackTile": "ဖွင့်ကြည့်ခြင်း",
|
"settingsVideoPlaybackTile": "ဖွင့်ကြည့်ခြင်း",
|
||||||
"@settingsVideoPlaybackTile": {},
|
"@settingsVideoPlaybackTile": {},
|
||||||
"chipActionShowCollection": "စုစည်းမှုထဲမှာ ပြရန်",
|
"chipActionShowCollection": "စုစည်းမှုထဲမှာ ပြရန်",
|
||||||
"@chipActionShowCollection": {}
|
"@chipActionShowCollection": {},
|
||||||
|
"chipActionDecompose": "ဖြတ်ထုတ်ရန်",
|
||||||
|
"@chipActionDecompose": {},
|
||||||
|
"chipActionGroup": "အုပ်စုဖွဲ့မည်",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"stopTooltip": "ရပ်ရန်",
|
||||||
|
"@stopTooltip": {},
|
||||||
|
"createButtonLabel": "အသစ်ထည့်ရန်",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"chipActionRemove": "ဖယ်ရှားမည်",
|
||||||
|
"@chipActionRemove": {},
|
||||||
|
"chipActionGoToExplorerPage": "Explorer ထဲတွင်ပြမည်",
|
||||||
|
"@chipActionGoToExplorerPage": {},
|
||||||
|
"chipActionCreateGroup": "အုပ်စုအသစ်ပြုလုပ်မည်",
|
||||||
|
"@chipActionCreateGroup": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -627,7 +627,7 @@
|
||||||
"@sortByItemCount": {},
|
"@sortByItemCount": {},
|
||||||
"sortBySize": "Op grootte",
|
"sortBySize": "Op grootte",
|
||||||
"@sortBySize": {},
|
"@sortBySize": {},
|
||||||
"sortByAlbumFileName": "Op album- en bestandsnaam",
|
"sortByAlbumFileName": "Op album- en itemnaam",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"sortByRating": "Op waardering",
|
"sortByRating": "Op waardering",
|
||||||
"@sortByRating": {},
|
"@sortByRating": {},
|
||||||
|
@ -1416,7 +1416,7 @@
|
||||||
"@groupPickerUseThisGroupButton": {},
|
"@groupPickerUseThisGroupButton": {},
|
||||||
"newGroupDialogTitle": "Nieuwe groep",
|
"newGroupDialogTitle": "Nieuwe groep",
|
||||||
"@newGroupDialogTitle": {},
|
"@newGroupDialogTitle": {},
|
||||||
"chipActionGroup": "Groeperen",
|
"chipActionGroup": "Groepering wijzigen",
|
||||||
"@chipActionGroup": {},
|
"@chipActionGroup": {},
|
||||||
"chipActionCreateGroup": "Groep aanmaken",
|
"chipActionCreateGroup": "Groep aanmaken",
|
||||||
"@chipActionCreateGroup": {},
|
"@chipActionCreateGroup": {},
|
||||||
|
|
|
@ -769,7 +769,7 @@
|
||||||
"@drawerCollectionPanoramas": {},
|
"@drawerCollectionPanoramas": {},
|
||||||
"drawerCollectionRaws": "Nieprzetworzone zdjęcia",
|
"drawerCollectionRaws": "Nieprzetworzone zdjęcia",
|
||||||
"@drawerCollectionRaws": {},
|
"@drawerCollectionRaws": {},
|
||||||
"sortByAlbumFileName": "Według albumu i nazwy pliku",
|
"sortByAlbumFileName": "Według albumu i nazwy elementu",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"albumMimeTypeMixed": "Mieszane",
|
"albumMimeTypeMixed": "Mieszane",
|
||||||
"@albumMimeTypeMixed": {},
|
"@albumMimeTypeMixed": {},
|
||||||
|
@ -1603,7 +1603,7 @@
|
||||||
"@sectionNone": {},
|
"@sectionNone": {},
|
||||||
"createButtonLabel": "UTWÓRZ",
|
"createButtonLabel": "UTWÓRZ",
|
||||||
"@createButtonLabel": {},
|
"@createButtonLabel": {},
|
||||||
"chipActionGroup": "Grupuj",
|
"chipActionGroup": "Zmień grupowanie",
|
||||||
"@chipActionGroup": {},
|
"@chipActionGroup": {},
|
||||||
"chipActionCreateGroup": "Utwórz grupę",
|
"chipActionCreateGroup": "Utwórz grupę",
|
||||||
"@chipActionCreateGroup": {},
|
"@chipActionCreateGroup": {},
|
||||||
|
|
|
@ -463,7 +463,7 @@
|
||||||
"@menuActionStats": {},
|
"@menuActionStats": {},
|
||||||
"viewDialogSortSectionTitle": "Organizar",
|
"viewDialogSortSectionTitle": "Organizar",
|
||||||
"@viewDialogSortSectionTitle": {},
|
"@viewDialogSortSectionTitle": {},
|
||||||
"viewDialogGroupSectionTitle": "Grupo",
|
"viewDialogGroupSectionTitle": "Seções",
|
||||||
"@viewDialogGroupSectionTitle": {},
|
"@viewDialogGroupSectionTitle": {},
|
||||||
"viewDialogLayoutSectionTitle": "Layout",
|
"viewDialogLayoutSectionTitle": "Layout",
|
||||||
"@viewDialogLayoutSectionTitle": {},
|
"@viewDialogLayoutSectionTitle": {},
|
||||||
|
@ -633,7 +633,7 @@
|
||||||
"@sortByItemCount": {},
|
"@sortByItemCount": {},
|
||||||
"sortBySize": "Por tamanho",
|
"sortBySize": "Por tamanho",
|
||||||
"@sortBySize": {},
|
"@sortBySize": {},
|
||||||
"sortByAlbumFileName": "Por álbum e nome de arquivo",
|
"sortByAlbumFileName": "Por álbum e título do item",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"sortByRating": "Por classificação",
|
"sortByRating": "Por classificação",
|
||||||
"@sortByRating": {},
|
"@sortByRating": {},
|
||||||
|
@ -1406,5 +1406,29 @@
|
||||||
"sortByPath": "Pelo caminho",
|
"sortByPath": "Pelo caminho",
|
||||||
"@sortByPath": {},
|
"@sortByPath": {},
|
||||||
"searchFormatSectionTitle": "Formatos",
|
"searchFormatSectionTitle": "Formatos",
|
||||||
"@searchFormatSectionTitle": {}
|
"@searchFormatSectionTitle": {},
|
||||||
|
"createButtonLabel": "CRIAR",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"chipActionGroup": "Alterar agrupamento",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"chipActionCreateGroup": "Criar grupo",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"albumTierGroups": "Grupos",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"newGroupDialogTitle": "Novo Grupo",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"newGroupDialogNameLabel": "Nome do grupo",
|
||||||
|
"@newGroupDialogNameLabel": {},
|
||||||
|
"groupAlreadyExists": "O grupo já existe",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"groupEmpty": "Nenhum grupo",
|
||||||
|
"@groupEmpty": {},
|
||||||
|
"ungrouped": "Desagrupado",
|
||||||
|
"@ungrouped": {},
|
||||||
|
"groupPickerTitle": "Selecionar Grupo",
|
||||||
|
"@groupPickerTitle": {},
|
||||||
|
"groupPickerUseThisGroupButton": "Usar este grupo",
|
||||||
|
"@groupPickerUseThisGroupButton": {},
|
||||||
|
"sectionNone": "Nenhuma seção",
|
||||||
|
"@sectionNone": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -526,7 +526,7 @@
|
||||||
"@menuActionStats": {},
|
"@menuActionStats": {},
|
||||||
"viewDialogSortSectionTitle": "Sortează",
|
"viewDialogSortSectionTitle": "Sortează",
|
||||||
"@viewDialogSortSectionTitle": {},
|
"@viewDialogSortSectionTitle": {},
|
||||||
"viewDialogGroupSectionTitle": "Grup",
|
"viewDialogGroupSectionTitle": "Secțiuni",
|
||||||
"@viewDialogGroupSectionTitle": {},
|
"@viewDialogGroupSectionTitle": {},
|
||||||
"viewDialogLayoutSectionTitle": "Aspect",
|
"viewDialogLayoutSectionTitle": "Aspect",
|
||||||
"@viewDialogLayoutSectionTitle": {},
|
"@viewDialogLayoutSectionTitle": {},
|
||||||
|
@ -887,7 +887,7 @@
|
||||||
"@drawerCollectionSphericalVideos": {},
|
"@drawerCollectionSphericalVideos": {},
|
||||||
"drawerAlbumPage": "Albume",
|
"drawerAlbumPage": "Albume",
|
||||||
"@drawerAlbumPage": {},
|
"@drawerAlbumPage": {},
|
||||||
"sortByAlbumFileName": "După album și numele fișierului",
|
"sortByAlbumFileName": "După album și numele elementului",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"sortOrderZtoA": "De la Z la A",
|
"sortOrderZtoA": "De la Z la A",
|
||||||
"@sortOrderZtoA": {},
|
"@sortOrderZtoA": {},
|
||||||
|
@ -1598,5 +1598,29 @@
|
||||||
"sortByPath": "După cale",
|
"sortByPath": "După cale",
|
||||||
"@sortByPath": {},
|
"@sortByPath": {},
|
||||||
"searchFormatSectionTitle": "Formate",
|
"searchFormatSectionTitle": "Formate",
|
||||||
"@searchFormatSectionTitle": {}
|
"@searchFormatSectionTitle": {},
|
||||||
|
"createButtonLabel": "CREARE",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"chipActionCreateGroup": "Creați un grup",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"newGroupDialogTitle": "Grup nou",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"newGroupDialogNameLabel": "Nume grup",
|
||||||
|
"@newGroupDialogNameLabel": {},
|
||||||
|
"groupAlreadyExists": "Grupul deja există",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"chipActionGroup": "Grupe",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"albumTierGroups": "Grupe",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"groupPickerTitle": "Alege un grup",
|
||||||
|
"@groupPickerTitle": {},
|
||||||
|
"groupPickerUseThisGroupButton": "Folosește acest grup",
|
||||||
|
"@groupPickerUseThisGroupButton": {},
|
||||||
|
"sectionNone": "Nicio secțiune",
|
||||||
|
"@sectionNone": {},
|
||||||
|
"ungrouped": "Fără grup",
|
||||||
|
"@ungrouped": {},
|
||||||
|
"groupEmpty": "Niciun grup",
|
||||||
|
"@groupEmpty": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -463,7 +463,7 @@
|
||||||
"@menuActionStats": {},
|
"@menuActionStats": {},
|
||||||
"viewDialogSortSectionTitle": "Сортировка",
|
"viewDialogSortSectionTitle": "Сортировка",
|
||||||
"@viewDialogSortSectionTitle": {},
|
"@viewDialogSortSectionTitle": {},
|
||||||
"viewDialogGroupSectionTitle": "Группировка",
|
"viewDialogGroupSectionTitle": "Разделы",
|
||||||
"@viewDialogGroupSectionTitle": {},
|
"@viewDialogGroupSectionTitle": {},
|
||||||
"viewDialogLayoutSectionTitle": "Макет",
|
"viewDialogLayoutSectionTitle": "Макет",
|
||||||
"@viewDialogLayoutSectionTitle": {},
|
"@viewDialogLayoutSectionTitle": {},
|
||||||
|
@ -633,7 +633,7 @@
|
||||||
"@sortByItemCount": {},
|
"@sortByItemCount": {},
|
||||||
"sortBySize": "По размеру",
|
"sortBySize": "По размеру",
|
||||||
"@sortBySize": {},
|
"@sortBySize": {},
|
||||||
"sortByAlbumFileName": "По имени альбома и файла",
|
"sortByAlbumFileName": "По названию альбома и пункта",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"sortByRating": "По рейтингу",
|
"sortByRating": "По рейтингу",
|
||||||
"@sortByRating": {},
|
"@sortByRating": {},
|
||||||
|
@ -1406,5 +1406,29 @@
|
||||||
"searchFormatSectionTitle": "Форматы",
|
"searchFormatSectionTitle": "Форматы",
|
||||||
"@searchFormatSectionTitle": {},
|
"@searchFormatSectionTitle": {},
|
||||||
"sortByPath": "По пути",
|
"sortByPath": "По пути",
|
||||||
"@sortByPath": {}
|
"@sortByPath": {},
|
||||||
|
"chipActionGroup": "Изменить группировку",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"createButtonLabel": "СОЗДАТЬ",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"chipActionCreateGroup": "Создать группу",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"albumTierGroups": "Группы",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"newGroupDialogTitle": "Новая группа",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"newGroupDialogNameLabel": "Название группы",
|
||||||
|
"@newGroupDialogNameLabel": {},
|
||||||
|
"groupAlreadyExists": "Группа уже существует",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"groupEmpty": "Групп нету",
|
||||||
|
"@groupEmpty": {},
|
||||||
|
"ungrouped": "Без группировки",
|
||||||
|
"@ungrouped": {},
|
||||||
|
"groupPickerTitle": "Выбор группы",
|
||||||
|
"@groupPickerTitle": {},
|
||||||
|
"groupPickerUseThisGroupButton": "Использовать эту группу",
|
||||||
|
"@groupPickerUseThisGroupButton": {},
|
||||||
|
"sectionNone": "Без разделов",
|
||||||
|
"@sectionNone": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -417,7 +417,7 @@
|
||||||
"@menuActionStats": {},
|
"@menuActionStats": {},
|
||||||
"viewDialogSortSectionTitle": "Sırala",
|
"viewDialogSortSectionTitle": "Sırala",
|
||||||
"@viewDialogSortSectionTitle": {},
|
"@viewDialogSortSectionTitle": {},
|
||||||
"viewDialogGroupSectionTitle": "Grup",
|
"viewDialogGroupSectionTitle": "Bölümler",
|
||||||
"@viewDialogGroupSectionTitle": {},
|
"@viewDialogGroupSectionTitle": {},
|
||||||
"viewDialogLayoutSectionTitle": "Düzen",
|
"viewDialogLayoutSectionTitle": "Düzen",
|
||||||
"@viewDialogLayoutSectionTitle": {},
|
"@viewDialogLayoutSectionTitle": {},
|
||||||
|
@ -489,7 +489,7 @@
|
||||||
"@collectionActionHideTitleSearch": {},
|
"@collectionActionHideTitleSearch": {},
|
||||||
"collectionActionAddShortcut": "Kısayol ekle",
|
"collectionActionAddShortcut": "Kısayol ekle",
|
||||||
"@collectionActionAddShortcut": {},
|
"@collectionActionAddShortcut": {},
|
||||||
"collectionActionEmptyBin": "Boş çöp kutusu",
|
"collectionActionEmptyBin": "Çöp kutusu boş",
|
||||||
"@collectionActionEmptyBin": {},
|
"@collectionActionEmptyBin": {},
|
||||||
"collectionActionCopy": "Albüme kopyala",
|
"collectionActionCopy": "Albüme kopyala",
|
||||||
"@collectionActionCopy": {},
|
"@collectionActionCopy": {},
|
||||||
|
@ -583,7 +583,7 @@
|
||||||
"@sortByItemCount": {},
|
"@sortByItemCount": {},
|
||||||
"sortBySize": "Boyuta göre",
|
"sortBySize": "Boyuta göre",
|
||||||
"@sortBySize": {},
|
"@sortBySize": {},
|
||||||
"sortByAlbumFileName": "Albüm ve dosya adına göre",
|
"sortByAlbumFileName": "Albüm ve başlığı göre",
|
||||||
"@sortByAlbumFileName": {},
|
"@sortByAlbumFileName": {},
|
||||||
"sortByRating": "Derecelendirmeye göre",
|
"sortByRating": "Derecelendirmeye göre",
|
||||||
"@sortByRating": {},
|
"@sortByRating": {},
|
||||||
|
@ -1400,5 +1400,35 @@
|
||||||
"collectionActionAddDynamicAlbum": "Dinamik albüm ekle",
|
"collectionActionAddDynamicAlbum": "Dinamik albüm ekle",
|
||||||
"@collectionActionAddDynamicAlbum": {},
|
"@collectionActionAddDynamicAlbum": {},
|
||||||
"searchFormatSectionTitle": "Biçimler",
|
"searchFormatSectionTitle": "Biçimler",
|
||||||
"@searchFormatSectionTitle": {}
|
"@searchFormatSectionTitle": {},
|
||||||
|
"createButtonLabel": "YARAT",
|
||||||
|
"@createButtonLabel": {},
|
||||||
|
"chipActionGroup": "Gruplandırmayı değiştir",
|
||||||
|
"@chipActionGroup": {},
|
||||||
|
"chipActionCreateGroup": "Grup oluştur",
|
||||||
|
"@chipActionCreateGroup": {},
|
||||||
|
"albumTierGroups": "Gruplar",
|
||||||
|
"@albumTierGroups": {},
|
||||||
|
"coordinateFormatDdm": "DDS",
|
||||||
|
"@coordinateFormatDdm": {},
|
||||||
|
"newGroupDialogTitle": "Yeni grup",
|
||||||
|
"@newGroupDialogTitle": {},
|
||||||
|
"newGroupDialogNameLabel": "Grup adı",
|
||||||
|
"@newGroupDialogNameLabel": {},
|
||||||
|
"groupAlreadyExists": "Grup zaten var",
|
||||||
|
"@groupAlreadyExists": {},
|
||||||
|
"groupEmpty": "Grup yok",
|
||||||
|
"@groupEmpty": {},
|
||||||
|
"ungrouped": "Gruplandırılmamış",
|
||||||
|
"@ungrouped": {},
|
||||||
|
"groupPickerTitle": "Grubu seç",
|
||||||
|
"@groupPickerTitle": {},
|
||||||
|
"groupPickerUseThisGroupButton": "Bu grubu kullan",
|
||||||
|
"@groupPickerUseThisGroupButton": {},
|
||||||
|
"sectionNone": "Bölüm yok",
|
||||||
|
"@sectionNone": {},
|
||||||
|
"sortByPath": "Yolu",
|
||||||
|
"@sortByPath": {},
|
||||||
|
"editEntryLocationDialogTimeShift": "Zaman farkı",
|
||||||
|
"@editEntryLocationDialogTimeShift": {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": {},
|
||||||
|
|
|
@ -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": {},
|
||||||
|
|
|
@ -140,10 +140,14 @@ class Contributors {
|
||||||
Contributor('Miquel Martí', 'miquelmarti111@gmail.com'),
|
Contributor('Miquel Martí', 'miquelmarti111@gmail.com'),
|
||||||
Contributor('Yurt Page', 'yurtpage@gmail.com'),
|
Contributor('Yurt Page', 'yurtpage@gmail.com'),
|
||||||
Contributor('Murcielago', 'weblate.j9bmx@slmail.me'),
|
Contributor('Murcielago', 'weblate.j9bmx@slmail.me'),
|
||||||
|
Contributor('vm', 'varga.m007@gmail.com'),
|
||||||
|
Contributor('WMatheist', 'wmatheist@protonmail.com'),
|
||||||
// Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani
|
// Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani
|
||||||
|
// Contributor('Jamil Farajov', 'jamilfarajov@gmail.com'), // Azerbaijani
|
||||||
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
|
||||||
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
|
||||||
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
|
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
|
||||||
|
// Contributor('Thit Lwin', 'thitlwincoder@gmail.com'), // Burmese
|
||||||
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
|
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
|
||||||
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
|
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
|
||||||
// Contributor('Ricky Tigg', 'ricky.tigg@gmail.com'), // Finnish
|
// Contributor('Ricky Tigg', 'ricky.tigg@gmail.com'), // Finnish
|
||||||
|
|
|
@ -212,9 +212,9 @@ class Dependencies {
|
||||||
sourceUrl: 'https://github.com/fleaflet/flutter_map',
|
sourceUrl: 'https://github.com/fleaflet/flutter_map',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Flutter Markdown',
|
name: 'Flutter Markdown Plus',
|
||||||
license: bsd3,
|
license: bsd3,
|
||||||
sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/flutter_markdown',
|
sourceUrl: 'https://github.com/foresightmobile/flutter_markdown_plus',
|
||||||
),
|
),
|
||||||
Dependency(
|
Dependency(
|
||||||
name: 'Flutter Staggered Animations',
|
name: 'Flutter Staggered Animations',
|
||||||
|
|
|
@ -25,7 +25,7 @@ final Covers covers = Covers._private();
|
||||||
typedef CoverProps = (int? entryId, String? packageName, Color? color);
|
typedef CoverProps = (int? entryId, String? packageName, Color? color);
|
||||||
|
|
||||||
class Covers {
|
class Covers {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final _lock = Lock();
|
final _lock = Lock();
|
||||||
|
|
||||||
final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast();
|
final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast();
|
||||||
|
@ -40,6 +40,8 @@ class Covers {
|
||||||
|
|
||||||
Set<CoverRow> _rows = {};
|
Set<CoverRow> _rows = {};
|
||||||
|
|
||||||
|
// do not subscribe to events from other modules in constructor
|
||||||
|
// so that modules can subscribe to each other
|
||||||
Covers._private();
|
Covers._private();
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
|
|
|
@ -15,19 +15,21 @@ import 'package:synchronized/synchronized.dart';
|
||||||
final DynamicAlbums dynamicAlbums = DynamicAlbums._private();
|
final DynamicAlbums dynamicAlbums = DynamicAlbums._private();
|
||||||
|
|
||||||
class DynamicAlbums with ChangeNotifier {
|
class DynamicAlbums with ChangeNotifier {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final _lock = Lock();
|
final _lock = Lock();
|
||||||
Set<DynamicAlbumFilter> _rows = {};
|
Set<DynamicAlbumFilter> _rows = {};
|
||||||
|
|
||||||
final EventBus eventBus = EventBus();
|
final EventBus eventBus = EventBus();
|
||||||
|
|
||||||
|
// do not subscribe to events from other modules in constructor
|
||||||
|
// so that modules can subscribe to each other
|
||||||
DynamicAlbums._private() {
|
DynamicAlbums._private() {
|
||||||
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
|
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
|
||||||
_subscriptions.add(albumGrouping.eventBus.on<GroupUriChangedEvent>().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet();
|
_rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet();
|
||||||
|
_subscriptions.add(albumGrouping.eventBus.on<GroupUriChangedEvent>().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
|
||||||
}
|
}
|
||||||
|
|
||||||
int get count => _rows.length;
|
int get count => _rows.length;
|
||||||
|
@ -57,6 +59,7 @@ class DynamicAlbums with ChangeNotifier {
|
||||||
await _lock.synchronized(() async {
|
await _lock.synchronized(() async {
|
||||||
await _doRemove(filters.map((filter) => filter.name).toSet());
|
await _doRemove(filters.map((filter) => filter.name).toSet());
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
eventBus.fire(DynamicAlbumChangedEvent(Map.fromEntries(filters.map((v) => MapEntry(v, null)))));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,13 +84,7 @@ class DynamicAlbums with ChangeNotifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clear() async {
|
Future<void> clear() => remove(all);
|
||||||
await _lock.synchronized(() async {
|
|
||||||
await localMediaDb.clearDynamicAlbums();
|
|
||||||
_rows.clear();
|
|
||||||
notifyListeners();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name);
|
DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name);
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/keys.dart';
|
import 'package:aves/model/entry/extensions/keys.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/media/geotiff.dart';
|
import 'package:aves/model/media/geotiff.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
|
||||||
import 'package:aves/model/media/video/metadata.dart';
|
import 'package:aves/model/media/video/metadata.dart';
|
||||||
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/ref/mime_types.dart';
|
import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:aves/model/filters/container/container.dart';
|
import 'package:aves/model/filters/container/container.dart';
|
||||||
|
import 'package:aves/model/filters/covered/location.dart';
|
||||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/filters/covered/location.dart';
|
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
@ -38,8 +38,6 @@ class SetAndFilter extends CollectionFilter with ContainerFilter {
|
||||||
|
|
||||||
static SetAndFilter? fromMap(Map<String, dynamic> json) {
|
static SetAndFilter? fromMap(Map<String, dynamic> json) {
|
||||||
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
|
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
|
||||||
if (filters.isEmpty) return null;
|
|
||||||
|
|
||||||
return SetAndFilter(
|
return SetAndFilter(
|
||||||
filters,
|
filters,
|
||||||
reversed: json['reversed'] ?? false,
|
reversed: json['reversed'] ?? false,
|
||||||
|
|
|
@ -38,8 +38,6 @@ class SetOrFilter extends CollectionFilter with ContainerFilter {
|
||||||
|
|
||||||
static SetOrFilter? fromMap(Map<String, dynamic> json) {
|
static SetOrFilter? fromMap(Map<String, dynamic> json) {
|
||||||
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
|
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
|
||||||
if (filters.isEmpty) return null;
|
|
||||||
|
|
||||||
return SetOrFilter(
|
return SetOrFilter(
|
||||||
filters,
|
filters,
|
||||||
reversed: json['reversed'] ?? false,
|
reversed: json['reversed'] ?? false,
|
||||||
|
|
|
@ -14,4 +14,3 @@ mixin CoveredFilter on CollectionFilter {
|
||||||
return super.color(context);
|
return super.color(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,11 @@ import 'dart:convert';
|
||||||
|
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
import 'package:aves/model/filters/aspect_ratio.dart';
|
import 'package:aves/model/filters/aspect_ratio.dart';
|
||||||
import 'package:aves/model/filters/coordinate.dart';
|
|
||||||
import 'package:aves/model/filters/container/album_group.dart';
|
import 'package:aves/model/filters/container/album_group.dart';
|
||||||
import 'package:aves/model/filters/container/dynamic_album.dart';
|
import 'package:aves/model/filters/container/dynamic_album.dart';
|
||||||
|
import 'package:aves/model/filters/container/set_and.dart';
|
||||||
|
import 'package:aves/model/filters/container/set_or.dart';
|
||||||
|
import 'package:aves/model/filters/coordinate.dart';
|
||||||
import 'package:aves/model/filters/covered/location.dart';
|
import 'package:aves/model/filters/covered/location.dart';
|
||||||
import 'package:aves/model/filters/covered/stored_album.dart';
|
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||||
import 'package:aves/model/filters/covered/tag.dart';
|
import 'package:aves/model/filters/covered/tag.dart';
|
||||||
|
@ -17,8 +19,6 @@ import 'package:aves/model/filters/placeholder.dart';
|
||||||
import 'package:aves/model/filters/query.dart';
|
import 'package:aves/model/filters/query.dart';
|
||||||
import 'package:aves/model/filters/rating.dart';
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/filters/recent.dart';
|
import 'package:aves/model/filters/recent.dart';
|
||||||
import 'package:aves/model/filters/container/set_and.dart';
|
|
||||||
import 'package:aves/model/filters/container/set_or.dart';
|
|
||||||
import 'package:aves/model/filters/trash.dart';
|
import 'package:aves/model/filters/trash.dart';
|
||||||
import 'package:aves/model/filters/type.dart';
|
import 'package:aves/model/filters/type.dart';
|
||||||
import 'package:aves/model/filters/weekday.dart';
|
import 'package:aves/model/filters/weekday.dart';
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:aves/model/dynamic_albums.dart';
|
||||||
import 'package:aves/model/filters/container/album_group.dart';
|
import 'package:aves/model/filters/container/album_group.dart';
|
||||||
|
import 'package:aves/model/filters/container/dynamic_album.dart';
|
||||||
import 'package:aves/model/filters/container/group_base.dart';
|
import 'package:aves/model/filters/container/group_base.dart';
|
||||||
import 'package:aves/model/filters/container/set_or.dart';
|
import 'package:aves/model/filters/container/set_or.dart';
|
||||||
|
import 'package:aves/model/filters/covered/stored_album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/grouping/convert.dart';
|
import 'package:aves/model/grouping/convert.dart';
|
||||||
|
import 'package:aves/model/source/album.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/events.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/utils/collection_utils.dart';
|
import 'package:aves/utils/collection_utils.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
@ -28,18 +35,53 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
|
||||||
final String _host;
|
final String _host;
|
||||||
final T Function(Uri uri, SetOrFilter filter) _createGroupFilter;
|
final T Function(Uri uri, SetOrFilter filter) _createGroupFilter;
|
||||||
final Map<Uri, Set<Uri>> _groups = {};
|
final Map<Uri, Set<Uri>> _groups = {};
|
||||||
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
|
final Map<CollectionSource, Set<StreamSubscription>> _sourceSubscriptions = {};
|
||||||
|
CollectionSource? _source;
|
||||||
|
|
||||||
Map<Uri, Set<Uri>> get allGroups => Map.unmodifiable(_groups);
|
Map<Uri, Set<Uri>> get allGroups => Map.unmodifiable(_groups);
|
||||||
|
|
||||||
|
// do not subscribe to events from other modules in constructor
|
||||||
|
// so that modules can subscribe to each other
|
||||||
FilterGrouping._private(this._host, this._createGroupFilter) {
|
FilterGrouping._private(this._host, this._createGroupFilter) {
|
||||||
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
|
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void init(Map<Uri, Set<Uri>> groups) {
|
void init() {
|
||||||
|
_subscriptions.add(dynamicAlbums.eventBus.on<DynamicAlbumChangedEvent>().listen((e) => _clearObsoleteFilters()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setGroups(Map<Uri, Set<Uri>> groups) {
|
||||||
_groups.clear();
|
_groups.clear();
|
||||||
_groups.addAll(groups);
|
_groups.addAll(groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscriptions
|
||||||
|
..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
|
_sourceSubscriptions.keys.toSet().forEach(unregisterSource);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerSource(CollectionSource source) {
|
||||||
|
unregisterSource(_source);
|
||||||
|
final sourceEvents = source.eventBus;
|
||||||
|
_sourceSubscriptions[source] = {
|
||||||
|
sourceEvents.on<EntryMovedEvent>().listen((e) => _clearObsoleteFilters()),
|
||||||
|
sourceEvents.on<EntryRemovedEvent>().listen((e) => _clearObsoleteFilters()),
|
||||||
|
sourceEvents.on<AlbumsChangedEvent>().listen((e) => _clearObsoleteFilters()),
|
||||||
|
};
|
||||||
|
_source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregisterSource(CollectionSource? source) {
|
||||||
|
_sourceSubscriptions.remove(source)
|
||||||
|
?..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
|
}
|
||||||
|
|
||||||
void addToGroup(Set<Uri> childrenUris, Uri? destinationGroup) {
|
void addToGroup(Set<Uri> childrenUris, Uri? destinationGroup) {
|
||||||
_removeFromGroups(childrenUris);
|
_removeFromGroups(childrenUris);
|
||||||
if (destinationGroup != null) {
|
if (destinationGroup != null) {
|
||||||
|
@ -73,9 +115,9 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
|
||||||
int countLeaves(Uri? groupUri) {
|
int countLeaves(Uri? groupUri) {
|
||||||
int count = 0;
|
int count = 0;
|
||||||
if (groupUri != null) {
|
if (groupUri != null) {
|
||||||
final childrenUri = _groups[groupUri];
|
final childrenUris = _groups[groupUri];
|
||||||
if (childrenUri != null) {
|
if (childrenUris != null) {
|
||||||
childrenUri.map(uriToFilter).nonNulls.forEach((filter) {
|
childrenUris.map(uriToFilter).nonNulls.forEach((filter) {
|
||||||
if (filter is GroupBaseFilter) {
|
if (filter is GroupBaseFilter) {
|
||||||
count += countLeaves(filter.uri);
|
count += countLeaves(filter.uri);
|
||||||
} else {
|
} else {
|
||||||
|
@ -93,15 +135,15 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
|
||||||
if (currentGroupUri == null) {
|
if (currentGroupUri == null) {
|
||||||
return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) {
|
return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) {
|
||||||
final groupUri = kv.key;
|
final groupUri = kv.key;
|
||||||
final childrenUri = kv.value;
|
final childrenUris = kv.value;
|
||||||
final childrenFilters = childrenUri.map(uriToFilter).nonNulls.toSet();
|
final childrenFilters = childrenUris.map(uriToFilter).nonNulls.toSet();
|
||||||
return _createGroupFilter(groupUri, SetOrFilter(childrenFilters));
|
return _createGroupFilter(groupUri, SetOrFilter(childrenFilters));
|
||||||
}).toSet();
|
}).toSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
final childrenUri = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
|
final childrenUris = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
|
||||||
if (childrenUri != null) {
|
if (childrenUris != null) {
|
||||||
return childrenUri.map(uriToFilter).nonNulls.toSet();
|
return childrenUris.map(uriToFilter).nonNulls.toSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
@ -172,6 +214,46 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearObsoleteFilters() {
|
||||||
|
final source = _source;
|
||||||
|
if (source == null || source.targetScope != CollectionSource.fullScope || !source.isReady) return;
|
||||||
|
|
||||||
|
_groups.entries.forEach((kv) {
|
||||||
|
final groupUri = kv.key;
|
||||||
|
final childrenUris = kv.value;
|
||||||
|
|
||||||
|
final rawAlbums = source.rawAlbums;
|
||||||
|
final allEntries = source.allEntries;
|
||||||
|
|
||||||
|
childrenUris.toSet().forEach((childUri) {
|
||||||
|
final filter = uriToFilter(childUri);
|
||||||
|
var valid = false;
|
||||||
|
if (filter != null) {
|
||||||
|
switch (filter) {
|
||||||
|
case GroupBaseFilter _:
|
||||||
|
valid = true;
|
||||||
|
case StoredAlbumFilter _:
|
||||||
|
// check album itself
|
||||||
|
final isVisibleAlbum = rawAlbums.contains(filter.album);
|
||||||
|
if (isVisibleAlbum) {
|
||||||
|
valid = true;
|
||||||
|
} else {
|
||||||
|
// check non-visible content (hidden, trash, etc.)
|
||||||
|
valid = allEntries.any(filter.test);
|
||||||
|
}
|
||||||
|
case DynamicAlbumFilter _:
|
||||||
|
valid = dynamicAlbums.contains(filter.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!valid) {
|
||||||
|
childrenUris.remove(childUri);
|
||||||
|
debugPrint('Removed obsolete childUri=$childUri from group=$groupUri');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_cleanEmptyGroups();
|
||||||
|
}
|
||||||
|
|
||||||
// group uri / filter conversion
|
// group uri / filter conversion
|
||||||
|
|
||||||
static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey];
|
static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey];
|
||||||
|
|
|
@ -46,7 +46,7 @@ import 'package:latlong2/latlong.dart';
|
||||||
final Settings settings = Settings._private();
|
final Settings settings = Settings._private();
|
||||||
|
|
||||||
class Settings with ChangeNotifier, SettingsAccess, SearchSettings, AppSettings, CollectionSettings, DebugSettings, DisplaySettings, FilterGridsSettings, InfoSettings, NavigationSettings, PrivacySettings, ScreenSaverSettings, SlideshowSettings, SubtitlesSettings, VideoSettings, ViewerSettings, WidgetSettings {
|
class Settings with ChangeNotifier, SettingsAccess, SearchSettings, AppSettings, CollectionSettings, DebugSettings, DisplaySettings, FilterGridsSettings, InfoSettings, NavigationSettings, PrivacySettings, ScreenSaverSettings, SlideshowSettings, SubtitlesSettings, VideoSettings, ViewerSettings, WidgetSettings {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
|
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
|
||||||
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
|
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
|
||||||
final StreamController<SettingsChangedEvent> _updateTileExtentStreamController = StreamController.broadcast();
|
final StreamController<SettingsChangedEvent> _updateTileExtentStreamController = StreamController.broadcast();
|
||||||
|
|
|
@ -34,7 +34,7 @@ class CollectionLens with ChangeNotifier {
|
||||||
EntrySortFactor sortFactor;
|
EntrySortFactor sortFactor;
|
||||||
bool sortReverse;
|
bool sortReverse;
|
||||||
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
|
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
int? id;
|
int? id;
|
||||||
bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
|
bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
|
||||||
List<AvesEntry>? fixedSelection;
|
List<AvesEntry>? fixedSelection;
|
||||||
|
|
|
@ -60,7 +60,9 @@ class MediaStoreSource extends CollectionSource {
|
||||||
await localMediaDb.init();
|
await localMediaDb.init();
|
||||||
await vaults.init();
|
await vaults.init();
|
||||||
await favourites.init();
|
await favourites.init();
|
||||||
albumGrouping.init(settings.albumGroups);
|
albumGrouping.init();
|
||||||
|
albumGrouping.setGroups(settings.albumGroups);
|
||||||
|
albumGrouping.registerSource(this);
|
||||||
await covers.init();
|
await covers.init();
|
||||||
await dynamicAlbums.init();
|
await dynamicAlbums.init();
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import 'package:provider/provider.dart';
|
||||||
final Vaults vaults = Vaults._private();
|
final Vaults vaults = Vaults._private();
|
||||||
|
|
||||||
class Vaults extends ChangeNotifier {
|
class Vaults extends ChangeNotifier {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
Set<VaultDetails> _rows = {};
|
Set<VaultDetails> _rows = {};
|
||||||
final Set<String> _unlockedDirPaths = {};
|
final Set<String> _unlockedDirPaths = {};
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
'locale': locale.toString(),
|
'localeLanguageTag': locale.toLanguageTag(),
|
||||||
// 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,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:aves/model/app/support.dart';
|
import 'package:aves/model/app/support.dart';
|
||||||
import 'package:aves/model/entry/entry.dart';
|
import 'package:aves/model/entry/entry.dart';
|
||||||
|
@ -13,7 +14,6 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:streams_channel/streams_channel.dart';
|
import 'package:streams_channel/streams_channel.dart';
|
||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
abstract class MediaFetchService {
|
abstract class MediaFetchService {
|
||||||
Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false});
|
Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false});
|
||||||
|
@ -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)) {
|
if (_isUnknownVisual(mimeType) || e.code == 'getThumbnail-large') {
|
||||||
await reportService.recordError(e, stack);
|
await reportService.recordError(e, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ abstract class MediaSessionService {
|
||||||
class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
class PlatformMediaSessionService implements MediaSessionService, Disposable {
|
||||||
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
|
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
|
||||||
|
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command');
|
final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command');
|
||||||
final StreamController _streamController = StreamController.broadcast();
|
final StreamController _streamController = StreamController.broadcast();
|
||||||
|
|
||||||
|
|
|
@ -74,20 +74,24 @@ class PlatformWindowService implements WindowService {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cf https://developer.android.com/guide/topics/manifest/activity-element#screen
|
||||||
|
// cf Android `ActivityInfo.ScreenOrientation`
|
||||||
|
static const screenOrientationUnspecified = -1; // SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
// use the `USER` variants rather than the `SENSOR` ones,
|
||||||
|
// so that it does not flip even if it is reversed by sensor
|
||||||
|
static const screenOrientationUserLandscape = 11; // SCREEN_ORIENTATION_USER_LANDSCAPE
|
||||||
|
static const screenOrientationUserPortrait = 12; // SCREEN_ORIENTATION_USER_PORTRAIT
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> requestOrientation([Orientation? orientation]) async {
|
Future<void> requestOrientation([Orientation? orientation]) async {
|
||||||
// cf Android `ActivityInfo.ScreenOrientation`
|
|
||||||
late final int orientationCode;
|
late final int orientationCode;
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case Orientation.landscape:
|
case Orientation.landscape:
|
||||||
// SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
orientationCode = screenOrientationUserLandscape;
|
||||||
orientationCode = 6;
|
|
||||||
case Orientation.portrait:
|
case Orientation.portrait:
|
||||||
// SCREEN_ORIENTATION_SENSOR_PORTRAIT
|
orientationCode = screenOrientationUserPortrait;
|
||||||
orientationCode = 7;
|
|
||||||
default:
|
default:
|
||||||
// SCREEN_ORIENTATION_UNSPECIFIED
|
orientationCode = screenOrientationUnspecified;
|
||||||
orientationCode = -1;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await _platform.invokeMethod('requestOrientation', <String, dynamic>{
|
await _platform.invokeMethod('requestOrientation', <String, dynamic>{
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
|
||||||
class AStyles {
|
class AStyles {
|
||||||
|
|
|
@ -160,7 +160,7 @@ class AvesApp extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
late final Future<void> _appSetup;
|
late final Future<void> _appSetup;
|
||||||
late final Future<bool> _shouldUseBoldFontLoader;
|
late final Future<bool> _shouldUseBoldFontLoader;
|
||||||
final TvRailController _tvRailController = TvRailController();
|
final TvRailController _tvRailController = TvRailController();
|
||||||
|
|
|
@ -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,6 +18,7 @@ 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';
|
||||||
|
@ -56,8 +57,8 @@ class CollectionAppBar extends StatefulWidget {
|
||||||
State<CollectionAppBar> createState() => _CollectionAppBarState();
|
State<CollectionAppBar> createState() => _CollectionAppBarState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
class _CollectionAppBarState extends State<CollectionAppBar> with RouteAware, SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
final EntrySetActionDelegate _actionDelegate = EntrySetActionDelegate();
|
||||||
late AnimationController _browseToSelectAnimation;
|
late AnimationController _browseToSelectAnimation;
|
||||||
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
||||||
|
@ -122,6 +123,15 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
@ -140,6 +150,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
..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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +162,13 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
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
|
||||||
|
|
|
@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CollectionPageState extends State<CollectionPage> {
|
class _CollectionPageState extends State<CollectionPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
late CollectionLens _collection;
|
late CollectionLens _collection;
|
||||||
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,8 @@ import 'package:aves/model/entry/extensions/multipage.dart';
|
||||||
import 'package:aves/model/entry/extensions/props.dart';
|
import 'package:aves/model/entry/extensions/props.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
import 'package:aves/model/filters/container/dynamic_album.dart';
|
import 'package:aves/model/filters/container/dynamic_album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
|
||||||
import 'package:aves/model/filters/container/set_and.dart';
|
import 'package:aves/model/filters/container/set_and.dart';
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/grouping/common.dart';
|
import 'package:aves/model/grouping/common.dart';
|
||||||
import 'package:aves/model/highlight.dart';
|
import 'package:aves/model/highlight.dart';
|
||||||
import 'package:aves/model/metadata/date_modifier.dart';
|
import 'package:aves/model/metadata/date_modifier.dart';
|
||||||
|
|
|
@ -9,8 +9,8 @@ import 'package:provider/provider.dart';
|
||||||
class FilterBar extends StatefulWidget {
|
class FilterBar extends StatefulWidget {
|
||||||
static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4);
|
static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4);
|
||||||
static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4);
|
static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4);
|
||||||
static const double verticalPadding = 16;
|
static const EdgeInsets padding = EdgeInsets.only(top: 4, bottom: 8);
|
||||||
static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding;
|
static final double preferredHeight = AvesFilterChip.minChipHeight + padding.vertical;
|
||||||
|
|
||||||
final List<CollectionFilter> filters;
|
final List<CollectionFilter> filters;
|
||||||
final bool interactive;
|
final bool interactive;
|
||||||
|
@ -84,6 +84,7 @@ class _FilterBarState extends State<FilterBar> {
|
||||||
return Container(
|
return Container(
|
||||||
// specify transparent as a workaround to prevent
|
// specify transparent as a workaround to prevent
|
||||||
// chip border clipping when the floating app bar is fading
|
// chip border clipping when the floating app bar is fading
|
||||||
|
padding: FilterBar.padding,
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
height: FilterBar.preferredHeight,
|
height: FilterBar.preferredHeight,
|
||||||
child: AnimatedList(
|
child: AnimatedList(
|
||||||
|
|
|
@ -45,7 +45,7 @@ class MenuQuickChooser<T> extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
|
class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
|
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
int _scrollDirection = 0;
|
int _scrollDirection = 0;
|
||||||
|
|
|
@ -23,7 +23,7 @@ class RateQuickChooser extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RateQuickChooserState extends State<RateQuickChooser> {
|
class _RateQuickChooserState extends State<RateQuickChooser> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
|
|
||||||
ValueNotifier<int?> get valueNotifier => widget.valueNotifier;
|
ValueNotifier<int?> get valueNotifier => widget.valueNotifier;
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ class PlayToggler extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStateMixin {
|
class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStateMixin {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
late AnimationController _playPauseAnimation;
|
late AnimationController _playPauseAnimation;
|
||||||
|
|
||||||
AvesVideoController? get controller => widget.controller;
|
AvesVideoController? get controller => widget.controller;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/events.dart';
|
import 'package:aves/model/source/events.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
|
|
|
@ -8,6 +8,8 @@ class CrumbLine<T> extends StatefulWidget {
|
||||||
final T Function(BuildContext context, int index) combine;
|
final T Function(BuildContext context, int index) combine;
|
||||||
final void Function(T combined) onTap;
|
final void Function(T combined) onTap;
|
||||||
|
|
||||||
|
static const EdgeInsets padding = EdgeInsets.only(top: 6, bottom: 20);
|
||||||
|
|
||||||
const CrumbLine({
|
const CrumbLine({
|
||||||
super.key,
|
super.key,
|
||||||
required this.split,
|
required this.split,
|
||||||
|
@ -18,7 +20,7 @@ class CrumbLine<T> extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
State<CrumbLine<T>> createState() => _CrumbLineState<T>();
|
State<CrumbLine<T>> createState() => _CrumbLineState<T>();
|
||||||
|
|
||||||
static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(kToolbarHeight);
|
static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(22) + padding.vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CrumbLineState<T> extends State<CrumbLine<T>> {
|
class _CrumbLineState<T> extends State<CrumbLine<T>> {
|
||||||
|
|
|
@ -522,7 +522,7 @@ class _InkResponseStateWidget extends StatefulWidget {
|
||||||
if (onSecondaryTap != null) 'secondary tap',
|
if (onSecondaryTap != null) 'secondary tap',
|
||||||
if (onSecondaryTapUp != null) 'secondary tap up',
|
if (onSecondaryTapUp != null) 'secondary tap up',
|
||||||
if (onSecondaryTapDown != null) 'secondary tap down',
|
if (onSecondaryTapDown != null) 'secondary tap down',
|
||||||
if (onSecondaryTapCancel != null) 'secondary tap cancel'
|
if (onSecondaryTapCancel != null) 'secondary tap cancel',
|
||||||
];
|
];
|
||||||
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
|
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
|
||||||
properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
|
properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
|
||||||
|
@ -544,10 +544,7 @@ enum _HighlightType {
|
||||||
focus,
|
focus,
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InkResponseState extends State<_InkResponseStateWidget>
|
class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKeepAliveClientMixin<_InkResponseStateWidget> implements _ParentInkResponseState {
|
||||||
with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
|
|
||||||
implements _ParentInkResponseState
|
|
||||||
{
|
|
||||||
Set<InteractiveInkFeature>? _splashes;
|
Set<InteractiveInkFeature>? _splashes;
|
||||||
InteractiveInkFeature? _currentSplash;
|
InteractiveInkFeature? _currentSplash;
|
||||||
bool _hovering = false;
|
bool _hovering = false;
|
||||||
|
@ -578,6 +575,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed);
|
widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
|
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
|
||||||
|
|
||||||
void activateOnIntent(Intent? intent) {
|
void activateOnIntent(Intent? intent) {
|
||||||
|
@ -611,7 +609,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
|
|
||||||
void handleStatesControllerChange() {
|
void handleStatesControllerChange() {
|
||||||
// Force a rebuild to resolve widget.overlayColor, widget.mouseCursor
|
// Force a rebuild to resolve widget.overlayColor, widget.mouseCursor
|
||||||
setState(() { });
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetStatesController get statesController => widget.statesController ?? internalStatesController!;
|
WidgetStatesController get statesController => widget.statesController ?? internalStatesController!;
|
||||||
|
@ -642,9 +640,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
}
|
}
|
||||||
initStatesController();
|
initStatesController();
|
||||||
}
|
}
|
||||||
if (widget.radius != oldWidget.radius ||
|
if (widget.radius != oldWidget.radius || widget.highlightShape != oldWidget.highlightShape || widget.borderRadius != oldWidget.borderRadius) {
|
||||||
widget.highlightShape != oldWidget.highlightShape ||
|
|
||||||
widget.borderRadius != oldWidget.borderRadius) {
|
|
||||||
final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
|
final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
|
||||||
if (hoverHighlight != null) {
|
if (hoverHighlight != null) {
|
||||||
hoverHighlight.dispose();
|
hoverHighlight.dispose();
|
||||||
|
@ -701,7 +697,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateHighlight(_HighlightType type, { required bool value, bool callOnHover = true }) {
|
void updateHighlight(_HighlightType type, {required bool value, bool callOnHover = true}) {
|
||||||
final InkHighlight? highlight = _highlights[type];
|
final InkHighlight? highlight = _highlights[type];
|
||||||
void handleInkRemoval() {
|
void handleInkRemoval() {
|
||||||
assert(_highlights[type] != null);
|
assert(_highlights[type] != null);
|
||||||
|
@ -717,7 +713,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
statesController.update(WidgetState.hovered, value);
|
statesController.update(WidgetState.hovered, value);
|
||||||
}
|
}
|
||||||
case _HighlightType.focus:
|
case _HighlightType.focus:
|
||||||
// see handleFocusUpdate()
|
// see handleFocusUpdate()
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -730,9 +726,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
if (highlight == null) {
|
if (highlight == null) {
|
||||||
final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value)
|
final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value) ??
|
||||||
?? switch (type) {
|
switch (type) {
|
||||||
// Use the backwards compatible defaults
|
// Use the backwards compatible defaults
|
||||||
_HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor,
|
_HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor,
|
||||||
_HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor,
|
_HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor,
|
||||||
_HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor,
|
_HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor,
|
||||||
|
@ -789,7 +785,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
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;
|
||||||
|
@ -846,6 +842,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasFocus = false;
|
bool _hasFocus = false;
|
||||||
|
|
||||||
void handleFocusUpdate(bool hasFocus) {
|
void handleFocusUpdate(bool hasFocus) {
|
||||||
_hasFocus = hasFocus;
|
_hasFocus = hasFocus;
|
||||||
// Set here rather than updateHighlight because this widget's
|
// Set here rather than updateHighlight because this widget's
|
||||||
|
@ -978,21 +975,17 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
|
bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
|
||||||
return widget.onTap != null
|
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || widget.onTapUp != null || widget.onTapDown != null;
|
||||||
|| widget.onDoubleTap != null
|
|
||||||
|| widget.onLongPress != null
|
|
||||||
|| widget.onTapUp != null
|
|
||||||
|| widget.onTapDown != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _secondaryButtonEnabled(_InkResponseStateWidget widget) {
|
bool _secondaryButtonEnabled(_InkResponseStateWidget widget) {
|
||||||
return widget.onSecondaryTap != null
|
return widget.onSecondaryTap != null || widget.onSecondaryTapUp != null || widget.onSecondaryTapDown != null;
|
||||||
|| widget.onSecondaryTapUp != null
|
|
||||||
|| widget.onSecondaryTapDown != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get enabled => isWidgetEnabled(widget);
|
bool get enabled => isWidgetEnabled(widget);
|
||||||
|
|
||||||
bool get _primaryEnabled => _primaryButtonEnabled(widget);
|
bool get _primaryEnabled => _primaryButtonEnabled(widget);
|
||||||
|
|
||||||
bool get _secondaryEnabled => _secondaryButtonEnabled(widget);
|
bool get _secondaryEnabled => _secondaryButtonEnabled(widget);
|
||||||
|
|
||||||
void handleMouseEnter(PointerEnterEvent event) {
|
void handleMouseEnter(PointerEnterEvent event) {
|
||||||
|
@ -1032,14 +1025,15 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
@ -1077,7 +1071,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
|
||||||
onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null,
|
onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null,
|
||||||
onLongPress: widget.onLongPress != null ? handleLongPress : null,
|
onLongPress: widget.onLongPress != null ? handleLongPress : null,
|
||||||
onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null,
|
onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null,
|
||||||
onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp: null,
|
onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp : null,
|
||||||
onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null,
|
onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null,
|
||||||
onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null,
|
onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null,
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/widgets/aves_app.dart';
|
import 'package:aves/widgets/aves_app.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
|
||||||
class MarkdownContainer extends StatelessWidget {
|
class MarkdownContainer extends StatelessWidget {
|
||||||
final String data;
|
final String data;
|
||||||
|
|
|
@ -20,7 +20,8 @@ class BlurredRect extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRect(
|
return ClipRect(
|
||||||
child: BackdropFilter.grouped(
|
// TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
|
||||||
|
child: BackdropFilter(
|
||||||
// do not modify tree when disabling filter
|
// do not modify tree when disabling filter
|
||||||
filter: enabled ? _filter : _identity,
|
filter: enabled ? _filter : _identity,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -59,7 +60,8 @@ class BlurredRRect extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipRRect(
|
return ClipRRect(
|
||||||
borderRadius: borderRadius ?? BorderRadius.zero,
|
borderRadius: borderRadius ?? BorderRadius.zero,
|
||||||
child: BackdropFilter.grouped(
|
// TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
|
||||||
|
child: BackdropFilter(
|
||||||
// do not modify tree when disabling filter
|
// do not modify tree when disabling filter
|
||||||
filter: enabled ? _filter : _identity,
|
filter: enabled ? _filter : _identity,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -81,7 +83,8 @@ class BlurredOval extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ClipOval(
|
return ClipOval(
|
||||||
child: BackdropFilter.grouped(
|
// TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
|
||||||
|
child: BackdropFilter(
|
||||||
// do not modify tree when disabling filter
|
// do not modify tree when disabling filter
|
||||||
filter: enabled ? _filter : _identity,
|
filter: enabled ? _filter : _identity,
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
@ -44,7 +44,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
|
||||||
return (scrollableContext.findRenderObject() as RenderBox).size;
|
return (scrollableContext.findRenderObject() as RenderBox).size;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
|
|
||||||
// grid section metrics before the app is laid out with the new orientation
|
// grid section metrics before the app is laid out with the new orientation
|
||||||
late SectionedListLayout<T> _lastSectionedListLayout;
|
late SectionedListLayout<T> _lastSectionedListLayout;
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@ class AvesFilterChip extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AvesFilterChipState extends State<AvesFilterChip> {
|
class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
late Future<Color> _colorFuture;
|
late Future<Color> _colorFuture;
|
||||||
late Color _outlineColor;
|
late Color _outlineColor;
|
||||||
late bool _tapped;
|
late bool _tapped;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves_map/aves_map.dart';
|
import 'package:aves_map/aves_map.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class Attribution extends StatelessWidget {
|
class Attribution extends StatelessWidget {
|
||||||
|
|
|
@ -83,7 +83,7 @@ class GeoMap extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GeoMapState extends State<GeoMap> {
|
class _GeoMapState extends State<GeoMap> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
|
|
||||||
// as of google_maps_flutter v2.0.6, Google map initialization is blocking
|
// as of google_maps_flutter v2.0.6, Google map initialization is blocking
|
||||||
// cf https://github.com/flutter/flutter/issues/28493
|
// cf https://github.com/flutter/flutter/issues/28493
|
||||||
|
@ -249,14 +249,13 @@ class _GeoMapState extends State<GeoMap> {
|
||||||
child = Column(
|
child = Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
BackdropGroup(
|
// TODO TLAD [flutter vNext] wrap into `BackdropGroup`
|
||||||
child: mapHeight != null
|
mapHeight != null
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
height: mapHeight,
|
height: mapHeight,
|
||||||
child: child,
|
child: child,
|
||||||
)
|
)
|
||||||
: Expanded(child: child),
|
: Expanded(child: child),
|
||||||
),
|
|
||||||
SafeArea(
|
SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
bottom: false,
|
bottom: false,
|
||||||
|
|
|
@ -66,7 +66,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
|
||||||
|
|
||||||
class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin {
|
class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin {
|
||||||
final MapController _leafletMapController = MapController();
|
final MapController _leafletMapController = MapController();
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
|
Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
|
||||||
final Debouncer _debouncer = Debouncer(delay: ADurations.mapIdleDebounceDelay);
|
final Debouncer _debouncer = Debouncer(delay: ADurations.mapIdleDebounceDelay);
|
||||||
|
|
||||||
|
|
|
@ -43,10 +43,12 @@ abstract class AvesSearchDelegate extends SearchDelegate {
|
||||||
final animate = context.read<Settings>().animate;
|
final animate = context.read<Settings>().animate;
|
||||||
return canPop
|
return canPop
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: animate ? AnimatedIcon(
|
icon: animate
|
||||||
icon: AnimatedIcons.menu_arrow,
|
? AnimatedIcon(
|
||||||
progress: transitionAnimation,
|
icon: AnimatedIcons.menu_arrow,
|
||||||
): const Icon(Icons.arrow_back),
|
progress: transitionAnimation,
|
||||||
|
)
|
||||||
|
: const Icon(Icons.arrow_back),
|
||||||
onPressed: () => goBack(context),
|
onPressed: () => goBack(context),
|
||||||
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,7 +15,7 @@ class TileExtentController {
|
||||||
|
|
||||||
late double userPreferredExtent;
|
late double userPreferredExtent;
|
||||||
Size _viewportSize = Size.zero;
|
Size _viewportSize = Size.zero;
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
|
|
||||||
Size get viewportSize => _viewportSize;
|
Size get viewportSize => _viewportSize;
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ class EditEntryLocationDialog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> with FeedbackMixin {
|
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> with FeedbackMixin {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
LocationEditAction _action = LocationEditAction.chooseOnMap;
|
LocationEditAction _action = LocationEditAction.chooseOnMap;
|
||||||
LatLng? _mapCoordinates;
|
LatLng? _mapCoordinates;
|
||||||
late final AvesEntry mainEntry;
|
late final AvesEntry mainEntry;
|
||||||
|
|
|
@ -63,7 +63,7 @@ class _Content extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContentState extends State<_Content> with SingleTickerProviderStateMixin {
|
class _ContentState extends State<_Content> with SingleTickerProviderStateMixin {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final AvesMapController _mapController = AvesMapController();
|
final AvesMapController _mapController = AvesMapController();
|
||||||
late final ValueNotifier<bool> _isPageAnimatingNotifier;
|
late final ValueNotifier<bool> _isPageAnimatingNotifier;
|
||||||
final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null), _infoLocationNotifier = ValueNotifier(null);
|
final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null), _infoLocationNotifier = ValueNotifier(null);
|
||||||
|
|
|
@ -28,7 +28,7 @@ class ImageEditorPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImageEditorPageState extends State<ImageEditorPage> {
|
class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null);
|
final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null);
|
||||||
final ValueNotifier<EdgeInsets> _marginNotifier = ValueNotifier(EdgeInsets.zero);
|
final ValueNotifier<EdgeInsets> _marginNotifier = ValueNotifier(EdgeInsets.zero);
|
||||||
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
|
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
|
||||||
|
@ -118,7 +118,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onActionChanged() {
|
void _onActionChanged() {
|
||||||
switch(_actionNotifier.value) {
|
switch (_actionNotifier.value) {
|
||||||
case EditorAction.transform:
|
case EditorAction.transform:
|
||||||
_transformController.reset();
|
_transformController.reset();
|
||||||
_marginNotifier.value = Cropper.imageMargin;
|
_marginNotifier.value = Cropper.imageMargin;
|
||||||
|
|
|
@ -36,7 +36,7 @@ class EditorImage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EditorImageState extends State<EditorImage> {
|
class _EditorImageState extends State<EditorImage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final ValueNotifier<double> _scrimOpacityNotifier = ValueNotifier(0);
|
final ValueNotifier<double> _scrimOpacityNotifier = ValueNotifier(0);
|
||||||
|
|
||||||
AvesEntry get entry => widget.entry;
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
|
@ -38,7 +38,7 @@ class Cropper extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
|
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
|
||||||
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
|
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
|
||||||
late AnimationController _gridAnimationController;
|
late AnimationController _gridAnimationController;
|
||||||
|
|
|
@ -65,7 +65,8 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
|
||||||
actions: _buildActions,
|
actions: _buildActions,
|
||||||
bottom: LayoutBuilder(
|
bottom: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return SizedBox(
|
return Container(
|
||||||
|
padding: CrumbLine.padding,
|
||||||
width: constraints.maxWidth,
|
width: constraints.maxWidth,
|
||||||
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
|
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
|
||||||
child: ValueListenableBuilder<VolumeRelativeDirectory?>(
|
child: ValueListenableBuilder<VolumeRelativeDirectory?>(
|
||||||
|
|
|
@ -40,7 +40,7 @@ class ExplorerPage extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ExplorerPageState extends State<ExplorerPage> {
|
class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
final ValueNotifier<VolumeRelativeDirectory?> _directory = ValueNotifier(null);
|
final ValueNotifier<VolumeRelativeDirectory?> _directory = ValueNotifier(null);
|
||||||
final ValueNotifier<VolumeRelativeDirectory?> _contentsDirectory = ValueNotifier(null);
|
final ValueNotifier<VolumeRelativeDirectory?> _contentsDirectory = ValueNotifier(null);
|
||||||
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);
|
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);
|
||||||
|
|
|
@ -44,12 +44,12 @@ class AlbumListPage extends StatelessWidget {
|
||||||
child: Builder(
|
child: Builder(
|
||||||
// to access filter group provider from subtree context
|
// to access filter group provider from subtree context
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return Selector<Settings, (AlbumChipSectionFactor, ChipSortFactor, bool, Set<CollectionFilter>)>(
|
return Selector<Settings, (AlbumChipSectionFactor, ChipSortFactor, bool, Set<CollectionFilter>, Set<CollectionFilter>)>(
|
||||||
selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.pinnedFilters),
|
selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.hiddenFilters, s.pinnedFilters),
|
||||||
shouldRebuild: (t1, t2) {
|
shouldRebuild: (t1, t2) {
|
||||||
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within records
|
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within records
|
||||||
const eq = DeepCollectionEquality();
|
const eq = DeepCollectionEquality();
|
||||||
return !(eq.equals(t1.$1, t2.$1) && eq.equals(t1.$2, t2.$2) && eq.equals(t1.$3, t2.$3) && eq.equals(t1.$4, t2.$4));
|
return !(eq.equals(t1.$1, t2.$1) && eq.equals(t1.$2, t2.$2) && eq.equals(t1.$3, t2.$3) && eq.equals(t1.$4, t2.$4) && eq.equals(t1.$5, t2.$5));
|
||||||
},
|
},
|
||||||
builder: (context, s, child) {
|
builder: (context, s, child) {
|
||||||
return ValueListenableBuilder<bool>(
|
return ValueListenableBuilder<bool>(
|
||||||
|
@ -123,7 +123,7 @@ class AlbumListPage extends StatelessWidget {
|
||||||
|
|
||||||
final listedDynamicAlbums = <DynamicAlbumFilter>{};
|
final listedDynamicAlbums = <DynamicAlbumFilter>{};
|
||||||
if (albumChipTypes.contains(AlbumChipType.dynamic)) {
|
if (albumChipTypes.contains(AlbumChipType.dynamic)) {
|
||||||
final allDynamicAlbums = dynamicAlbums.all;
|
final allDynamicAlbums = dynamicAlbums.all.whereNot(settings.hiddenFilters.contains).toSet();
|
||||||
if (groupUri == null) {
|
if (groupUri == null) {
|
||||||
final withinGroups = whereTypeRecursively<DynamicAlbumFilter>(groupContent).toSet();
|
final withinGroups = whereTypeRecursively<DynamicAlbumFilter>(groupContent).toSet();
|
||||||
listedDynamicAlbums.addAll(allDynamicAlbums.whereNot(withinGroups.contains));
|
listedDynamicAlbums.addAll(allDynamicAlbums.whereNot(withinGroups.contains));
|
||||||
|
@ -134,7 +134,7 @@ class AlbumListPage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// always show groups, which are needed to navigate to other types
|
// always show groups, which are needed to navigate to other types
|
||||||
final albumGroupFilters = groupContent.whereType<AlbumGroupFilter>().toSet();
|
final albumGroupFilters = groupContent.whereType<AlbumGroupFilter>().whereNot(settings.hiddenFilters.contains).toSet();
|
||||||
|
|
||||||
final filters = <AlbumBaseFilter>{
|
final filters = <AlbumBaseFilter>{
|
||||||
...albumGroupFilters,
|
...albumGroupFilters,
|
||||||
|
|
|
@ -11,6 +11,7 @@ import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.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/common/action_controls/togglers/title_search.dart';
|
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
|
||||||
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
|
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
|
||||||
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
|
||||||
|
@ -78,8 +79,8 @@ class FilterGridAppBar<T extends CollectionFilter, CSAD extends ChipSetActionDel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with RouteAware, SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||||
final List<StreamSubscription> _subscriptions = [];
|
final Set<StreamSubscription> _subscriptions = {};
|
||||||
late AnimationController _browseToSelectAnimation;
|
late AnimationController _browseToSelectAnimation;
|
||||||
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
|
||||||
final FocusNode _queryBarFocusNode = FocusNode();
|
final FocusNode _queryBarFocusNode = FocusNode();
|
||||||
|
@ -112,6 +113,15 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateAppBarHeight());
|
WidgetsBinding.instance.addPostFrameCallback((_) => _updateAppBarHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final route = ModalRoute.of(context);
|
||||||
|
if (route is PageRoute) {
|
||||||
|
AvesApp.pageRouteObserver.subscribe(this, route);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_queryBarFocusNode.dispose();
|
_queryBarFocusNode.dispose();
|
||||||
|
@ -122,9 +132,17 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
|
||||||
..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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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 text scale factor changes
|
// when text scale factor changes
|
||||||
|
@ -170,7 +188,8 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
|
||||||
if (_showGroupCrumbLine(context))
|
if (_showGroupCrumbLine(context))
|
||||||
LayoutBuilder(
|
LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return SizedBox(
|
return Container(
|
||||||
|
padding: CrumbLine.padding,
|
||||||
width: constraints.maxWidth,
|
width: constraints.maxWidth,
|
||||||
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
|
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
|
||||||
child: Selector<FilterGroupNotifier, Uri?>(
|
child: Selector<FilterGroupNotifier, Uri?>(
|
||||||
|
|
|
@ -61,9 +61,13 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||||
|
|
||||||
static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4));
|
static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4));
|
||||||
|
|
||||||
static double detailIconSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 8);
|
static double detailIconSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 7);
|
||||||
|
|
||||||
static double detailFontSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 6);
|
static double detailFontSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 7);
|
||||||
|
|
||||||
|
static double detailIconPadding(double extent) => min<double>(8.0, extent / 16);
|
||||||
|
|
||||||
|
static double detailIconTextPadding(double extent) => detailIconPadding(extent) / 2;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -201,30 +205,33 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
|
||||||
if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked),
|
if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked),
|
||||||
if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum),
|
if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum),
|
||||||
if (filter is AlbumGroupFilter) ...[
|
if (filter is AlbumGroupFilter) ...[
|
||||||
_buildDetailIcon(context, AIcons.album),
|
_buildDetailIcon(context, AIcons.album, padding: detailIconTextPadding(extent)),
|
||||||
Text(
|
Text(
|
||||||
'${NumberFormat.decimalPattern(context.locale).format(albumGrouping.countLeaves(filter.uri))}${AText.separator}',
|
'${NumberFormat.decimalPattern(context.locale).format(albumGrouping.countLeaves(filter.uri))}${AText.separator}',
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Text(
|
Flexible(
|
||||||
locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)),
|
child: Text(
|
||||||
style: textStyle,
|
locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)),
|
||||||
|
style: textStyle,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetailIcon(BuildContext context, IconData icon) {
|
Widget _buildDetailIcon(BuildContext context, IconData icon, {double? padding}) {
|
||||||
final padding = min<double>(8.0, extent / 16);
|
|
||||||
final iconSize = detailIconSize(extent);
|
|
||||||
return AnimatedPadding(
|
return AnimatedPadding(
|
||||||
padding: EdgeInsetsDirectional.only(end: padding),
|
padding: EdgeInsetsDirectional.only(end: padding ?? detailIconPadding(extent)),
|
||||||
duration: ADurations.chipDecorationAnimation,
|
duration: ADurations.chipDecorationAnimation,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
icon,
|
icon,
|
||||||
color: _detailColor(context),
|
color: _detailColor(context),
|
||||||
size: iconSize,
|
size: detailIconSize(extent),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue