Compare commits
141 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 | ||
![]() |
e0af21f098 | ||
![]() |
8636e4b73e | ||
![]() |
0ad4b2f16f | ||
![]() |
df63f06897 | ||
![]() |
09df269ee0 | ||
![]() |
39d7587ac9 | ||
![]() |
0f6a8230d8 | ||
![]() |
a6c1fd52a6 | ||
![]() |
eaa4fe3317 | ||
![]() |
5358a38bd8 | ||
![]() |
244c1a293d | ||
![]() |
2ef03f1592 | ||
![]() |
f2ef5c6f32 | ||
![]() |
b582dbb3b2 | ||
![]() |
b864a4dae3 | ||
![]() |
2b01fb41e2 | ||
![]() |
244217417b | ||
![]() |
651b5926dc | ||
![]() |
27879a900d | ||
![]() |
4b87717cd2 | ||
![]() |
91cfe01af3 | ||
![]() |
cb6ccab6ca | ||
![]() |
f04c55e901 | ||
![]() |
8e0c69cd66 | ||
![]() |
c31b64535d | ||
![]() |
89dee8d508 | ||
![]() |
93c2c7d34d | ||
![]() |
573b7c4593 | ||
![]() |
f9f21fbe76 | ||
![]() |
9cfb4436fa | ||
![]() |
925998b709 | ||
![]() |
a8bb2eb69f | ||
![]() |
bb6c2c341b | ||
![]() |
cbaaf2fd87 | ||
![]() |
e1fad28411 | ||
![]() |
bb26b18017 | ||
![]() |
3a6ad33ea1 | ||
![]() |
75421faf46 | ||
![]() |
4df4738dd3 | ||
![]() |
e8eae7e9db | ||
![]() |
f47cd57692 | ||
![]() |
bb332d5adf | ||
![]() |
7f54befb72 | ||
![]() |
af4ca96da8 | ||
![]() |
90d0256bf7 | ||
![]() |
353cde0ee8 | ||
![]() |
63130de577 | ||
![]() |
ad5a9c848d | ||
![]() |
be75d5a284 | ||
![]() |
0142ed7f4f | ||
![]() |
6c2db18af2 | ||
![]() |
93e2b1d310 | ||
![]() |
9baa4a0441 | ||
![]() |
f123faeee8 | ||
![]() |
e3ece7425f | ||
![]() |
ff4c49718e | ||
![]() |
5598b0a69d | ||
![]() |
dac91a2d1d | ||
![]() |
759f666085 | ||
![]() |
c89441bb78 | ||
![]() |
969187444b | ||
![]() |
1fd3d77bf9 | ||
![]() |
32202cc603 | ||
![]() |
bce7009ab0 | ||
![]() |
b00b17a473 | ||
![]() |
8be5b8d9c5 | ||
![]() |
503757da0e | ||
![]() |
85a1b33e83 | ||
![]() |
022ad0334e | ||
![]() |
e09b3e4440 | ||
![]() |
409d80df4e | ||
![]() |
a6ae2fd4cb | ||
![]() |
59aa75e46c | ||
![]() |
1df2154f85 | ||
![]() |
326e74c04c | ||
![]() |
e1aee40f0c | ||
![]() |
8193c48234 | ||
![]() |
9c641e0f49 | ||
![]() |
988d9b2c8d | ||
![]() |
305dcb4528 | ||
![]() |
a13da39af2 | ||
![]() |
9d7e6bb9ff | ||
![]() |
509c402227 | ||
![]() |
77443202aa | ||
![]() |
508e5ae739 | ||
![]() |
f38178ab7e | ||
![]() |
a670b683e5 | ||
![]() |
9a4c567c2b | ||
![]() |
672b6fd2dc | ||
![]() |
b8e9786f4d | ||
![]() |
cb067aa1ac | ||
![]() |
cf74e75d58 | ||
![]() |
9e33db5b4d | ||
![]() |
8e6a995c4e | ||
![]() |
62d666fb34 | ||
![]() |
403ccac5c2 | ||
![]() |
dcd42b7048 | ||
![]() |
f646639055 | ||
![]() |
a32c0cf0f0 |
2
.flutter
|
@ -1 +1 @@
|
|||
Subproject commit 09de023485e95e6d1225c2baa44b8feb85e0d45f
|
||||
Subproject commit d8a9f9a52e5af486f80d932e838ee93861ffd863
|
4
.github/workflows/dependency-review.yml
vendored
|
@ -17,11 +17,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
|
|
13
.github/workflows/quality-check.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
@ -28,6 +28,9 @@ jobs:
|
|||
- name: Get Flutter packages
|
||||
run: ./flutterw pub get
|
||||
|
||||
- name: Generate app localizations
|
||||
run: ./flutterw gen-l10n
|
||||
|
||||
- name: Static analysis.
|
||||
run: ./flutterw analyze
|
||||
|
||||
|
@ -52,14 +55,14 @@ jobs:
|
|||
build-mode: manual
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
# Building relies on the Android Gradle plugin,
|
||||
# which requires a modern Java version (not the default one).
|
||||
- name: Set up JDK for Android Gradle plugin
|
||||
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
@ -69,7 +72,7 @@ jobs:
|
|||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
|
@ -83,6 +86,6 @@ jobs:
|
|||
./flutterw build apk --profile -t lib/main_play.dart --flavor play
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
|
15
.github/workflows/release.yml
vendored
|
@ -18,14 +18,14 @@ jobs:
|
|||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
# Building relies on the Android Gradle plugin,
|
||||
# which requires a modern Java version (not the default one).
|
||||
- name: Set up JDK for Android Gradle plugin
|
||||
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
|
@ -36,6 +36,9 @@ jobs:
|
|||
- name: Get Flutter packages
|
||||
run: ./flutterw pub get
|
||||
|
||||
- name: Generate app localizations
|
||||
run: ./flutterw gen-l10n
|
||||
|
||||
- name: Update Flutter version file
|
||||
run: scripts/update_flutter_version.sh
|
||||
|
||||
|
@ -75,7 +78,7 @@ jobs:
|
|||
AVES_GOOGLE_API_KEY: ${{ secrets.AVES_GOOGLE_API_KEY }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@c074443f1aee8d4aeeae555aebba3282517141b2 # v2.2.3
|
||||
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
|
||||
with:
|
||||
subject-path: 'outputs/*'
|
||||
|
||||
|
@ -87,7 +90,7 @@ jobs:
|
|||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload app bundle
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: appbundle
|
||||
path: outputs/app-play-release.aab
|
||||
|
@ -98,7 +101,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
@ -106,7 +109,7 @@ jobs:
|
|||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Get appbundle from artifacts
|
||||
uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: appbundle
|
||||
|
||||
|
|
8
.github/workflows/scorecards.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
||||
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
@ -41,7 +41,7 @@ jobs:
|
|||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
||||
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
|
@ -63,7 +63,7 @@ jobs:
|
|||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
|
@ -71,6 +71,6 @@ jobs:
|
|||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
|
3
.gitignore
vendored
|
@ -47,3 +47,6 @@ app.*.map.json
|
|||
# screenshot generation
|
||||
/test_driver/assets/screenshots/
|
||||
/screenshots/
|
||||
|
||||
# generated files
|
||||
/lib/l10ngen/app_localizations*
|
||||
|
|
29
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "aves (main play)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main_play.dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"play"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "aves (main play) [profile]",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main_play.dart",
|
||||
"args": [
|
||||
"--flavor",
|
||||
"play"
|
||||
],
|
||||
"flutterMode": "profile"
|
||||
}
|
||||
]
|
||||
}
|
95
CHANGELOG.md
|
@ -4,6 +4,101 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
## <a id="unreleased"></a>[Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Info: show matching dynamic albums
|
||||
|
||||
### Fixed
|
||||
|
||||
- crash when decoding some large thumbnails
|
||||
|
||||
## <a id="v1.13.2"></a>[v1.13.2] - 2025-06-02
|
||||
|
||||
### Changed
|
||||
|
||||
- downgraded Flutter to stable v3.27.4
|
||||
- prevent display orientation flip when device rotation is locked
|
||||
|
||||
### Fixed
|
||||
|
||||
- moved file losing its extension and no longer being detected as media in some cases
|
||||
- opening home when launching app as media picker
|
||||
- removing groups with obsolete albums
|
||||
- loading group custom covers
|
||||
- crash when parsing some large media with trailing thumbnail
|
||||
|
||||
## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14
|
||||
|
||||
### Fixed
|
||||
|
||||
- albums: show groups to move/copy/export items
|
||||
- albums: hide grouped albums containing hidden items only
|
||||
|
||||
## <a id="v1.13.0"></a>[v1.13.0] - 2025-05-12
|
||||
|
||||
### Added
|
||||
|
||||
- Albums: groups
|
||||
- Collection: sort by storage path
|
||||
- Search: week day filters
|
||||
|
||||
### Changed
|
||||
|
||||
- revert to Skia rendering engine
|
||||
|
||||
## <a id="v1.12.10"></a>[v1.12.10] - 2025-04-16
|
||||
|
||||
### Added
|
||||
|
||||
- Search: format filters
|
||||
- Albums: sort by path
|
||||
|
||||
### Changed
|
||||
|
||||
- upgraded Flutter to stable v3.29.3
|
||||
|
||||
### Fixed
|
||||
|
||||
- region decoding failing to access decoder pool
|
||||
|
||||
## <a id="v1.12.9"></a>[v1.12.9] - 2025-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- Kannada translation (thanks Chethan, Prasannakumar T Bhat)
|
||||
|
||||
### Changed
|
||||
|
||||
- enable Impeller rendering engine
|
||||
|
||||
### Fixed
|
||||
|
||||
- memory pressure during browsing
|
||||
|
||||
## <a id="v1.12.8"></a>[v1.12.8] - 2025-03-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- swiping images for some combinations of screen size, device pixel ratio, and image size
|
||||
|
||||
## <a id="v1.12.7"></a>[v1.12.7] - 2025-03-16
|
||||
|
||||
### Added
|
||||
|
||||
- handle launch error to report and export DB
|
||||
|
||||
### Changed
|
||||
|
||||
- DB post-upgrade sanitization
|
||||
- upgraded Flutter to stable v3.29.2
|
||||
|
||||
## <a id="v1.12.6"></a>[v1.12.6] - 2025-03-11
|
||||
|
||||
### Fixed
|
||||
|
||||
- data loss when editing metadata of items with incorrect mime types
|
||||
- metadata inconsistency in the DB due to v1.12.4 upgrade
|
||||
|
||||
## <a id="v1.12.5"></a>[v1.12.5] - 2025-03-07
|
||||
|
||||
### Added
|
||||
|
|
85
README.md
|
@ -111,17 +111,96 @@ Some users have expressed the wish to financially support the project. Thanks!
|
|||
|
||||
## Project Setup
|
||||
|
||||
### Install dependencies
|
||||
|
||||
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 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
|
||||
[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-dynamic: false
|
||||
|
||||
# cf https://github.com/dart-lang/dart_style/wiki/Configuration
|
||||
formatter:
|
||||
page_width: 240
|
||||
trailing_commas: preserve
|
||||
|
||||
linter:
|
||||
rules:
|
||||
# from 'flutter_lints', excluded
|
||||
|
|
2
android/.gitignore
vendored
|
@ -6,6 +6,8 @@ gradle-wrapper.jar
|
|||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
.kotlin/
|
||||
/build/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
|
|
|
@ -33,13 +33,13 @@ kotlin {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace 'deckers.thibault.aves'
|
||||
compileSdk 35
|
||||
namespace = 'deckers.thibault.aves'
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId packageName
|
||||
minSdk flutter.minSdkVersion
|
||||
targetSdk 35
|
||||
targetSdk 36
|
||||
versionCode flutter.versionCode
|
||||
versionName flutter.versionName
|
||||
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
|
||||
|
@ -134,14 +134,14 @@ flutter {
|
|||
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
url = 'https://jitpack.io'
|
||||
content {
|
||||
includeGroup "com.github.deckerst"
|
||||
includeGroup "com.github.deckerst.mp4parser"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url 'https://s3.amazonaws.com/repo.commonsware.com'
|
||||
url = 'https://s3.amazonaws.com/repo.commonsware.com'
|
||||
content {
|
||||
excludeGroupByRegex "com\\.github\\.deckerst.*"
|
||||
}
|
||||
|
@ -149,36 +149,36 @@ repositories {
|
|||
}
|
||||
|
||||
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.core:core-ktx:1.15.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
|
||||
implementation "androidx.appcompat:appcompat:1.7.1"
|
||||
implementation 'androidx.core:core-ktx:1.16.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.9.1'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.10.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0-beta01'
|
||||
implementation 'androidx.work:work-runtime-ktx:2.10.1'
|
||||
|
||||
implementation 'com.commonsware.cwac:document:0.5.0'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
// SLF4J implementation for `mp4parser`
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.16'
|
||||
implementation 'org.slf4j:slf4j-simple:2.0.17'
|
||||
|
||||
// forked, built by JitPack:
|
||||
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
|
||||
// - https://jitpack.io/p/deckerst/androidsvg
|
||||
// - https://jitpack.io/p/deckerst/mp4parser
|
||||
// - https://jitpack.io/p/deckerst/pixymeta-android
|
||||
implementation 'com.github.deckerst:Android-TiffBitmapFactory:3ed067f021'
|
||||
implementation 'com.github.deckerst:androidsvg:cc9d59a88f'
|
||||
implementation 'com.github.deckerst.mp4parser:isoparser:d5caf7a3dd'
|
||||
implementation 'com.github.deckerst.mp4parser:muxer:d5caf7a3dd'
|
||||
implementation 'com.github.deckerst:pixymeta-android:71eee77dc4'
|
||||
implementation 'com.github.deckerst:Android-TiffBitmapFactory:d6b2b0aa4f'
|
||||
implementation 'com.github.deckerst:androidsvg:67db933051'
|
||||
implementation 'com.github.deckerst.mp4parser:isoparser:c2898f1832'
|
||||
implementation 'com.github.deckerst.mp4parser:muxer:c2898f1832'
|
||||
implementation 'com.github.deckerst:pixymeta-android:cb1cdc932e'
|
||||
implementation project(':exifinterface')
|
||||
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.4'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.1'
|
||||
|
||||
kapt 'androidx.annotation:annotation:1.9.1'
|
||||
ksp "com.github.bumptech.glide:ksp:$glide_version"
|
||||
|
|
|
@ -329,10 +329,6 @@
|
|||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
<!--
|
||||
Screenshot driver scenario is not supported by Impeller: "Compressed screenshots not supported for Impeller".
|
||||
As of Flutter v3.29.0, switching pages with alpha transition yields artifacts when Impeller is enabled.
|
||||
-->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.EnableImpeller"
|
||||
android:value="false" />
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.work.ForegroundInfo
|
|||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import app.loup.streams_channel.StreamsChannel
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
|
||||
import deckers.thibault.aves.channel.calls.DeviceHandler
|
||||
import deckers.thibault.aves.channel.calls.GeocodingHandler
|
||||
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
|
||||
|
@ -44,11 +45,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
private var backgroundChannel: MethodChannel? = null
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
Log.i(LOG_TAG, "Start analysis worker $id")
|
||||
defaultScope.launch {
|
||||
// prevent ANR triggered by slow operations in main thread
|
||||
createNotificationChannel()
|
||||
setForeground(createForegroundInfo())
|
||||
}
|
||||
}.join()
|
||||
suspendCoroutine { cont ->
|
||||
workCont = cont
|
||||
onStart()
|
||||
|
@ -68,7 +70,6 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
}
|
||||
|
||||
private fun onStart() {
|
||||
Log.i(LOG_TAG, "Start analysis worker $id")
|
||||
runBlocking {
|
||||
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, PREF_CALLBACK_HANDLE_KEY) {
|
||||
flutterEngine = it
|
||||
|
@ -132,12 +133,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
result.success(null)
|
||||
}
|
||||
|
||||
"updateNotification" -> {
|
||||
val title = call.argument<String>("title")
|
||||
val message = call.argument<String>("message")
|
||||
setForegroundAsync(createForegroundInfo(title, message))
|
||||
result.success(null)
|
||||
}
|
||||
"updateNotification" -> defaultScope.launch { safeSuspend(call, result, ::updateNotification) }
|
||||
|
||||
"stop" -> {
|
||||
workCont?.resume(null)
|
||||
|
@ -180,17 +176,22 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
|
|||
.setContentIntent(openAppIntent)
|
||||
.addAction(stopAction)
|
||||
.build()
|
||||
return if (Build.VERSION.SDK_INT == 34) {
|
||||
// from Android 14 (API 34), foreground service type is mandatory for long-running workers:
|
||||
// https://developer.android.com/guide/background/persistent/how-to/long-running
|
||||
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else if (Build.VERSION.SDK_INT >= 35) {
|
||||
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
|
||||
} else {
|
||||
ForegroundInfo(NOTIFICATION_ID, notification)
|
||||
// from Android 14 (API 34), foreground service type is mandatory for long-running workers:
|
||||
// https://developer.android.com/guide/background/persistent/how-to/long-running
|
||||
return when {
|
||||
Build.VERSION.SDK_INT >= 35 -> ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
|
||||
Build.VERSION.SDK_INT == 34 -> ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
else -> ForegroundInfo(NOTIFICATION_ID, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateNotification(call: MethodCall, result: MethodChannel.Result) {
|
||||
val title = call.argument<String>("title")
|
||||
val message = call.argument<String>("message")
|
||||
setForeground(createForegroundInfo(title, message))
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<AnalysisWorker>()
|
||||
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"
|
||||
|
|
|
@ -2,6 +2,7 @@ package deckers.thibault.aves.channel.calls
|
|||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
|
@ -18,7 +19,6 @@ import kotlinx.coroutines.SupervisorJob
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
|
||||
class AnalysisHandler(private val activity: FlutterFragmentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
|
||||
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)
|
||||
with(preferences.edit()) {
|
||||
preferences.edit {
|
||||
putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
apply()
|
||||
}
|
||||
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
|
||||
// so we save the possibly long list of entry IDs to shared preferences
|
||||
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())
|
||||
apply()
|
||||
}
|
||||
|
||||
val workData = workDataOf(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.LocaleConfig
|
||||
import android.app.LocaleManager
|
||||
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) {
|
||||
@SuppressLint("WrongConstant")
|
||||
val lm = context.getSystemService(Context.LOCALE_SERVICE) as? LocaleManager
|
||||
lm?.overrideLocaleConfig = LocaleConfig(LocaleList.forLanguageTags(locales.joinToString(",")))
|
||||
}
|
||||
|
|
|
@ -311,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
embeddedByteStream: InputStream,
|
||||
embeddedByteLength: Long,
|
||||
) {
|
||||
val extension = extensionFor(mimeType)
|
||||
val extension = extensionFor(mimeType, defaultExtension = null)
|
||||
val targetFile = StorageUtils.createTempFile(context, extension).apply {
|
||||
transferFrom(embeddedByteStream, embeddedByteLength)
|
||||
}
|
||||
|
@ -319,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
|
|||
val authority = "${context.applicationContext.packageName}.file_provider"
|
||||
val uri = if (displayName != null) {
|
||||
// add extension to ease type identification when sharing this content
|
||||
val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) {
|
||||
val displayNameWithExtension = if (displayName.endsWith(extension, ignoreCase = true)) {
|
||||
displayName
|
||||
} else {
|
||||
"$displayName$extension"
|
||||
|
|
|
@ -31,7 +31,7 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
|
|||
private fun getAddress(call: MethodCall, result: MethodChannel.Result) {
|
||||
val latitude = call.argument<Number>("latitude")?.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
|
||||
if (latitude == null || longitude == null) {
|
||||
result.error("getAddress-args", "missing arguments", null)
|
||||
|
@ -43,11 +43,8 @@ class GeocodingHandler(private val context: Context) : MethodCallHandler {
|
|||
return
|
||||
}
|
||||
|
||||
geocoder = geocoder ?: if (localeString != null) {
|
||||
val split = localeString.split("_")
|
||||
val language = split[0]
|
||||
val country = if (split.size > 1) split[1] else ""
|
||||
Geocoder(context, Locale(language, country))
|
||||
geocoder = geocoder ?: if (localeLanguageTag != null) {
|
||||
Geocoder(context, Locale.forLanguageTag(localeLanguageTag))
|
||||
} else {
|
||||
Geocoder(context)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import deckers.thibault.aves.SearchSuggestionsProvider
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
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)
|
||||
with(preferences.edit()) {
|
||||
preferences.edit {
|
||||
putLong(SearchSuggestionsProvider.CALLBACK_HANDLE_KEY, callbackHandle)
|
||||
apply()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.net.toUri
|
||||
import com.bumptech.glide.Glide
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
|
@ -21,7 +23,8 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
|
|||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"getEntry" -> ioScope.launch { safe(call, result, ::getEntry) }
|
||||
"clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) }
|
||||
"clearImageDiskCache" -> ioScope.launch { safe(call, result, ::clearImageDiskCache) }
|
||||
"clearImageMemoryCache" -> ioScope.launch { safe(call, result, ::clearImageMemoryCache) }
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
@ -47,11 +50,18 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
|
|||
})
|
||||
}
|
||||
|
||||
private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
private fun clearImageDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
Glide.get(context).clearDiskCache()
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
private fun clearImageMemoryCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Glide.get(context).clearMemory()
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL = "deckers.thibault/aves/media_fetch_object"
|
||||
}
|
||||
|
|
|
@ -541,14 +541,18 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
|
||||
// fallback to MP4 `loci` box for location
|
||||
if (!metadataMap.contains(KEY_LATITUDE) || !metadataMap.contains(KEY_LONGITUDE)) {
|
||||
Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { userDataBox ->
|
||||
Path.getPath<LocationInformationBox>(userDataBox, LocationInformationBox.TYPE)?.let { locationBox ->
|
||||
if (!locationBox.isParsed) {
|
||||
locationBox.parseDetails()
|
||||
try {
|
||||
Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { userDataBox ->
|
||||
Path.getPath<LocationInformationBox>(userDataBox, LocationInformationBox.TYPE)?.let { locationBox ->
|
||||
if (!locationBox.isParsed) {
|
||||
locationBox.parseDetails()
|
||||
}
|
||||
metadataMap[KEY_LATITUDE] = locationBox.latitude
|
||||
metadataMap[KEY_LONGITUDE] = locationBox.longitude
|
||||
}
|
||||
metadataMap[KEY_LATITUDE] = locationBox.latitude
|
||||
metadataMap[KEY_LONGITUDE] = locationBox.longitude
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, "failed to get Location Information box by MP4 parser for mimeType=$mimeType uri=$uri", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1003,7 +1007,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(metadataMap)
|
||||
}
|
||||
|
||||
// return description from these fields (by precedence):
|
||||
// returns description from these fields (by precedence):
|
||||
// - XMP / dc:description
|
||||
// - IPTC / caption-abstract
|
||||
// - Exif / UserComment
|
||||
|
@ -1188,8 +1192,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
|||
result.success(null)
|
||||
}
|
||||
|
||||
// return XMP components
|
||||
// return an empty list if there is no XMP
|
||||
// returns XMP components
|
||||
// returns an empty list if there is no XMP
|
||||
private fun getXmp(call: MethodCall, result: MethodChannel.Result) {
|
||||
val mimeType = call.argument<String>("mimeType")
|
||||
val uri = call.argument<String>("uri")?.toUri()
|
||||
|
|
|
@ -2,6 +2,7 @@ package deckers.thibault.aves.channel.calls
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKey
|
||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||
|
@ -45,7 +46,7 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
|
||||
val preferences = getStore()
|
||||
with(preferences.edit()) {
|
||||
preferences.edit {
|
||||
when (value) {
|
||||
is Boolean -> putBoolean(key, value)
|
||||
is Float -> putFloat(key, value)
|
||||
|
@ -58,7 +59,6 @@ class SecurityHandler(private val context: Context) : MethodCallHandler {
|
|||
return
|
||||
}
|
||||
}
|
||||
apply()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import deckers.thibault.aves.utils.MemoryUtils
|
|||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
@ -29,11 +31,7 @@ import kotlin.math.roundToInt
|
|||
class RegionFetcher internal constructor(
|
||||
private val context: Context,
|
||||
) {
|
||||
private var lastDecoderRef: LastDecoderRef? = null
|
||||
|
||||
private val exportUris = HashMap<Pair<Uri, Int?>, Uri>()
|
||||
|
||||
// return decoded bytes in ARGB_8888, with trailer bytes:
|
||||
// returns decoded bytes in ARGB_8888, with trailer bytes:
|
||||
// - width (int32)
|
||||
// - height (int32)
|
||||
fun fetch(
|
||||
|
@ -63,24 +61,12 @@ class RegionFetcher internal constructor(
|
|||
return
|
||||
}
|
||||
|
||||
var currentDecoderRef = lastDecoderRef
|
||||
if (currentDecoderRef != null && currentDecoderRef.requestKey != requestKey) {
|
||||
currentDecoderRef = null
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentDecoderRef == null) {
|
||||
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||
BitmapRegionDecoderCompat.newInstance(input)
|
||||
}
|
||||
if (newDecoder == null) {
|
||||
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
currentDecoderRef = LastDecoderRef(requestKey, newDecoder)
|
||||
val decoder = getOrCreateDecoder(context, uri, requestKey)
|
||||
if (decoder == null) {
|
||||
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
val decoder = currentDecoderRef.decoder
|
||||
lastDecoderRef = currentDecoderRef
|
||||
|
||||
// with raw images, the known image size may not match the decoded image size
|
||||
// so we scale the requested region accordingly
|
||||
|
@ -180,7 +166,7 @@ class RegionFetcher internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private data class LastDecoderRef(
|
||||
private data class DecoderRef(
|
||||
val requestKey: Pair<Uri, Int?>,
|
||||
val decoder: BitmapRegionDecoder,
|
||||
)
|
||||
|
@ -188,5 +174,32 @@ class RegionFetcher internal constructor(
|
|||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<RegionFetcher>()
|
||||
private val PREFERRED_CONFIG = Bitmap.Config.ARGB_8888
|
||||
private const val DECODER_POOL_SIZE = 3
|
||||
private val decoderPool = ArrayList<DecoderRef>()
|
||||
private val exportUris = HashMap<Pair<Uri, Int?>, Uri>()
|
||||
|
||||
private val poolLock = ReentrantLock()
|
||||
|
||||
private fun getOrCreateDecoder(context: Context, uri: Uri, requestKey: Pair<Uri, Int?>): BitmapRegionDecoder? {
|
||||
poolLock.withLock {
|
||||
var decoderRef = decoderPool.firstOrNull { it.requestKey == requestKey }
|
||||
if (decoderRef == null) {
|
||||
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||
BitmapRegionDecoderCompat.newInstance(input)
|
||||
}
|
||||
if (newDecoder == null) {
|
||||
return null
|
||||
}
|
||||
decoderRef = DecoderRef(requestKey, newDecoder)
|
||||
} else {
|
||||
decoderPool.remove(decoderRef)
|
||||
}
|
||||
decoderPool.add(0, decoderRef)
|
||||
while (decoderPool.size > DECODER_POOL_SIZE) {
|
||||
decoderPool.removeAt(decoderPool.size - 1)
|
||||
}
|
||||
return decoderRef.decoder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,13 +17,13 @@ import deckers.thibault.aves.utils.BitmapUtils
|
|||
import deckers.thibault.aves.utils.MemoryUtils
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.math.ceil
|
||||
|
||||
class SvgRegionFetcher internal constructor(
|
||||
private val context: Context,
|
||||
) {
|
||||
private var lastSvgRef: LastSvgRef? = null
|
||||
|
||||
fun fetch(
|
||||
uri: Uri,
|
||||
sizeBytes: Long?,
|
||||
|
@ -39,32 +39,12 @@ class SvgRegionFetcher internal constructor(
|
|||
return
|
||||
}
|
||||
|
||||
var currentSvgRef = lastSvgRef
|
||||
if (currentSvgRef != null && currentSvgRef.uri != uri) {
|
||||
currentSvgRef = null
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentSvgRef == null) {
|
||||
val newSvg = StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||
try {
|
||||
SVG.getFromInputStream(SVGParserBufferedInputStream(input))
|
||||
} catch (ex: SVGParseException) {
|
||||
result.error("fetch-parse", "failed to parse SVG for uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (newSvg == null) {
|
||||
result.error("fetch-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
|
||||
newSvg.normalizeSize()
|
||||
currentSvgRef = LastSvgRef(uri, newSvg)
|
||||
val svg = getOrCreateDecoder(context, uri)
|
||||
if (svg == null) {
|
||||
result.error("fetch-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
|
||||
return
|
||||
}
|
||||
val svg = currentSvgRef.svg
|
||||
lastSvgRef = currentSvgRef
|
||||
|
||||
// we scale the requested region accordingly to the viewbox size
|
||||
val viewBox = svg.documentViewBox
|
||||
|
@ -110,17 +90,46 @@ class SvgRegionFetcher internal constructor(
|
|||
bitmap = Bitmap.createBitmap(bitmap, bleedX, bleedY, targetBitmapWidth, targetBitmapHeight)
|
||||
val bytes = BitmapUtils.getRawBytes(bitmap, recycle = true)
|
||||
result.success(bytes)
|
||||
} catch (e: SVGParseException) {
|
||||
result.error("fetch-parse", "failed to parse SVG for uri=$uri regionRect=$regionRect", null)
|
||||
} catch (e: Exception) {
|
||||
result.error("fetch-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private data class LastSvgRef(
|
||||
private data class DecoderRef(
|
||||
val uri: Uri,
|
||||
val svg: SVG,
|
||||
val decoder: SVG,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val PREFERRED_CONFIG = Bitmap.Config.ARGB_8888
|
||||
private const val DECODER_POOL_SIZE = 3
|
||||
private val decoderPool = ArrayList<DecoderRef>()
|
||||
|
||||
private val poolLock = ReentrantLock()
|
||||
|
||||
private fun getOrCreateDecoder(context: Context, uri: Uri): SVG? {
|
||||
poolLock.withLock {
|
||||
var decoderRef = decoderPool.firstOrNull { it.uri == uri }
|
||||
if (decoderRef == null) {
|
||||
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
|
||||
SVG.getFromInputStream(SVGParserBufferedInputStream(input))
|
||||
}
|
||||
if (newDecoder == null) {
|
||||
return null
|
||||
}
|
||||
newDecoder.normalizeSize()
|
||||
decoderRef = DecoderRef(uri, newDecoder)
|
||||
} else {
|
||||
decoderPool.remove(decoderRef)
|
||||
}
|
||||
decoderPool.add(0, decoderRef)
|
||||
while (decoderPool.size > DECODER_POOL_SIZE) {
|
||||
decoderPool.removeAt(decoderPool.size - 1)
|
||||
}
|
||||
return decoderRef.decoder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ import android.graphics.Bitmap
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.util.Size
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.graphics.scale
|
||||
import androidx.core.net.toUri
|
||||
import com.bumptech.glide.Glide
|
||||
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.utils.BitmapUtils
|
||||
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.SVG
|
||||
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.UriUtils.tryParseId
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ThumbnailFetcher internal constructor(
|
||||
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
|
||||
val recycle = false
|
||||
val bytes = BitmapUtils.getRawBytes(bitmap, recycle = recycle)
|
||||
|
@ -144,4 +172,9 @@ class ThumbnailFetcher internal constructor(
|
|||
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
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
|
||||
"createFile" -> ioScope.launch { createFile() }
|
||||
"openFile" -> ioScope.launch { openFile() }
|
||||
"copyFile" -> ioScope.launch { copyFile() }
|
||||
"edit" -> edit()
|
||||
"pickCollectionFilters" -> pickCollectionFilters()
|
||||
else -> endOfStream()
|
||||
|
@ -181,6 +182,49 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
|
|||
safeStartActivityForStorageAccessResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
|
||||
}
|
||||
|
||||
private suspend fun copyFile() {
|
||||
val name = args["name"] as String?
|
||||
val mimeType = args["mimeType"] as String?
|
||||
val sourceUri = (args["sourceUri"] as String?)?.toUri()
|
||||
if (name == null || mimeType == null || sourceUri == null) {
|
||||
error("copyFile-args", "missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
fun onGranted(uri: Uri) {
|
||||
ioScope.launch {
|
||||
try {
|
||||
StorageUtils.openInputStream(activity, sourceUri)?.use { input ->
|
||||
// truncate is necessary when overwriting a longer file
|
||||
activity.contentResolver.openOutputStream(uri, "wt")?.use { output ->
|
||||
val buffer = ByteArray(BUFFER_SIZE)
|
||||
var len: Int
|
||||
while (input.read(buffer).also { len = it } != -1) {
|
||||
output.write(buffer, 0, len)
|
||||
}
|
||||
}
|
||||
}
|
||||
success(true)
|
||||
} catch (e: Exception) {
|
||||
error("copyFile-write", "failed to copy file from sourceUri=$sourceUri to uri=$uri", e.message)
|
||||
}
|
||||
endOfStream()
|
||||
}
|
||||
}
|
||||
|
||||
fun onDenied() {
|
||||
success(null)
|
||||
endOfStream()
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = mimeType
|
||||
putExtra(Intent.EXTRA_TITLE, name)
|
||||
}
|
||||
safeStartActivityForStorageAccessResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
|
||||
}
|
||||
|
||||
private fun edit() {
|
||||
val uri = args["uri"] as String?
|
||||
val mimeType = args["mimeType"] as String? // optional
|
||||
|
|
|
@ -31,9 +31,15 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
|
|||
|
||||
init {
|
||||
Log.i(LOG_TAG, "start listening to Media Store")
|
||||
context.contentResolver.apply {
|
||||
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
||||
registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
||||
try {
|
||||
context.contentResolver.apply {
|
||||
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
||||
registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// Trying to register an observer may yield a security exception with this message:
|
||||
// "Failed to find provider media for user 0; expected to find a valid ContentProvider for this authority"
|
||||
Log.w(LOG_TAG, "failed to register content observer", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package deckers.thibault.aves.decoder
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
|
@ -10,9 +11,17 @@ import com.bumptech.glide.annotation.GlideModule
|
|||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.ImageHeaderParser
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool
|
||||
import com.bumptech.glide.load.engine.cache.DiskCache
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.engine.cache.LruResourceCache
|
||||
import com.bumptech.glide.load.engine.cache.MemorySizeCalculator
|
||||
import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import deckers.thibault.aves.utils.LogUtils
|
||||
import deckers.thibault.aves.utils.MimeTypes
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
|
@ -23,6 +32,30 @@ class AvesAppGlideModule : AppGlideModule() {
|
|||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
// hide noisy warning (e.g. for images that can't be decoded)
|
||||
builder.setLogLevel(Log.ERROR)
|
||||
|
||||
// sizing
|
||||
val memorySizeCalculator = MemorySizeCalculator.Builder(context).build()
|
||||
builder.setMemorySizeCalculator(memorySizeCalculator)
|
||||
val size: Int = memorySizeCalculator.bitmapPoolSize
|
||||
if (size > 0) {
|
||||
builder.setBitmapPool(LruBitmapPool(size.toLong()))
|
||||
} else {
|
||||
builder.setBitmapPool(BitmapPoolAdapter())
|
||||
}
|
||||
builder.setArrayPool(LruArrayPool(memorySizeCalculator.arrayPoolSizeInBytes))
|
||||
builder.setMemoryCache(LruResourceCache(memorySizeCalculator.memoryCacheSize.toLong()))
|
||||
|
||||
val diskCacheSize = DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE
|
||||
val internalCacheDiskCacheFactory = InternalCacheDiskCacheFactory(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize.toLong())
|
||||
builder.setDiskCache(internalCacheDiskCacheFactory)
|
||||
|
||||
fun toMb(bytes: Int) = Formatter.formatFileSize(context, bytes.toLong())
|
||||
Log.d(
|
||||
LOG_TAG, "Glide disk cache size=${toMb(diskCacheSize)}" +
|
||||
", memory cache size=${toMb(memorySizeCalculator.memoryCacheSize)}" +
|
||||
", bitmap pool size=${toMb(memorySizeCalculator.bitmapPoolSize)}" +
|
||||
", array pool size=${toMb(memorySizeCalculator.arrayPoolSizeInBytes)}"
|
||||
)
|
||||
}
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
|
@ -34,6 +67,8 @@ class AvesAppGlideModule : AppGlideModule() {
|
|||
override fun isManifestParsingEnabled(): Boolean = false
|
||||
|
||||
companion object {
|
||||
private val LOG_TAG = LogUtils.createTag<AvesAppGlideModule>()
|
||||
|
||||
// request a fresh image with the highest quality format
|
||||
val uncachedFullImageOptions = RequestOptions()
|
||||
.format(DecodeFormat.PREFER_ARGB_8888)
|
||||
|
|
|
@ -81,17 +81,18 @@ object PixyMetaHelper {
|
|||
output: OutputStream,
|
||||
iptcDataList: List<FieldMap>?,
|
||||
) {
|
||||
val iptc = iptcDataList?.flatMap {
|
||||
val iptc: List<IPTCDataSet> = iptcDataList?.flatMap {
|
||||
val record = it["record"] as Int
|
||||
val tag = it["tag"] as Int
|
||||
val values = it["values"] as List<*>
|
||||
values.map { data -> IPTCDataSet(IPTCRecord.fromRecordNumber(record), tag, data as ByteArray) }
|
||||
} ?: ArrayList<IPTCDataSet>()
|
||||
} ?: ArrayList()
|
||||
Metadata.insertIPTC(input, output, iptc)
|
||||
}
|
||||
|
||||
fun getXmp(input: InputStream): XMP? = Metadata.readMetadata(input)[MetadataType.XMP] as XMP?
|
||||
|
||||
// PixyMeta may fail with just a log, and write nothing to the output
|
||||
fun setXmp(
|
||||
input: InputStream,
|
||||
output: OutputStream,
|
||||
|
|
|
@ -142,16 +142,18 @@ abstract class ImageProvider {
|
|||
|
||||
val oldFile = File(sourcePath)
|
||||
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
|
||||
val defaultExtension = oldFile.extension
|
||||
oldFile.parent?.let { dir ->
|
||||
val resolution = resolveTargetFileNameWithoutExtension(
|
||||
contextWrapper = activity,
|
||||
dir = dir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = mimeType,
|
||||
defaultExtension = defaultExtension,
|
||||
conflictStrategy = NameConflictStrategy.RENAME,
|
||||
)
|
||||
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
|
||||
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}"
|
||||
val newFile = File(dir, targetFileName)
|
||||
if (oldFile != newFile) {
|
||||
newFields = renameSingle(
|
||||
|
@ -277,11 +279,17 @@ abstract class ImageProvider {
|
|||
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
|
||||
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
|
||||
}
|
||||
|
||||
// there is no benefit providing input extension
|
||||
// for known output MIME type
|
||||
val defaultExtension = null
|
||||
|
||||
val resolution = resolveTargetFileNameWithoutExtension(
|
||||
contextWrapper = activity,
|
||||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = exportMimeType,
|
||||
defaultExtension = defaultExtension,
|
||||
conflictStrategy = nameConflictStrategy,
|
||||
)
|
||||
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
||||
|
@ -358,6 +366,7 @@ abstract class ImageProvider {
|
|||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
defaultExtension = defaultExtension,
|
||||
write = write,
|
||||
)
|
||||
|
||||
|
@ -465,6 +474,7 @@ abstract class ImageProvider {
|
|||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = captureMimeType,
|
||||
defaultExtension = null,
|
||||
conflictStrategy = nameConflictStrategy,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
@ -571,13 +581,14 @@ abstract class ImageProvider {
|
|||
dir: String,
|
||||
desiredNameWithoutExtension: String,
|
||||
mimeType: String,
|
||||
defaultExtension: String?,
|
||||
conflictStrategy: NameConflictStrategy,
|
||||
): NameConflictResolution {
|
||||
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
|
||||
var resolvedName: String? = sanitizedNameWithoutExtension
|
||||
var replacementFile: File? = null
|
||||
|
||||
val extension = extensionFor(mimeType)
|
||||
val extension = extensionFor(mimeType, defaultExtension)
|
||||
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
|
||||
when (conflictStrategy) {
|
||||
NameConflictStrategy.RENAME -> {
|
||||
|
@ -680,18 +691,19 @@ abstract class ImageProvider {
|
|||
try {
|
||||
edit(ExifInterface(editableFile))
|
||||
|
||||
if (editableFile.length() == 0L) {
|
||||
callback.onFailure(Exception("editing Exif yielded an empty file"))
|
||||
return false
|
||||
}
|
||||
|
||||
val editedMimeType = detectMimeType(context, Uri.fromFile(editableFile), mimeType)
|
||||
if (editedMimeType != mimeType) {
|
||||
throw Exception("editing Exif changes mimeType=$mimeType -> $editedMimeType for uri=$uri path=$path")
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
// 1) as of androidx.exifinterface:exifinterface:1.3.6, editing some specific WEBP
|
||||
// makes them undecodable by some decoders (including Android's and Chrome's)
|
||||
// even though `BitmapFactory` successfully decodes their bounds,
|
||||
// editing may corrupt the file for various reasons,
|
||||
// so we check whether decoding it throws an exception
|
||||
// 2) some users have reported corruption when editing JPEG as well,
|
||||
// but conditions are unknown (specific image, custom ROM, low storage, race condition, etc.)
|
||||
ImageDecoder.decodeBitmap(ImageDecoder.createSource(editableFile))
|
||||
}
|
||||
|
||||
|
@ -781,6 +793,11 @@ abstract class ImageProvider {
|
|||
}
|
||||
}
|
||||
|
||||
if (editableFile.length() == 0L) {
|
||||
callback.onFailure(Exception("editing IPTC yielded an empty file"))
|
||||
return false
|
||||
}
|
||||
|
||||
if (trailerVideoBytes != null) {
|
||||
// append trailer video, if any
|
||||
editableFile.appendBytes(trailerVideoBytes!!)
|
||||
|
@ -917,6 +934,11 @@ abstract class ImageProvider {
|
|||
}
|
||||
}
|
||||
|
||||
if (editableFile.length() == 0L) {
|
||||
callback.onFailure(Exception("editing XMP yielded an empty file"))
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// copy the edited temporary file back to the original
|
||||
editableFile.transferTo(outputStream(context, mimeType, uri, path))
|
||||
|
@ -1330,6 +1352,11 @@ abstract class ImageProvider {
|
|||
}
|
||||
}
|
||||
|
||||
if (editableFile.length() == 0L) {
|
||||
callback.onFailure(Exception("removing metadata yielded an empty file"))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// copy the edited temporary file back to the original
|
||||
editableFile.transferTo(outputStream(context, mimeType, uri, path))
|
||||
|
|
|
@ -557,6 +557,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
toBin: Boolean,
|
||||
): FieldMap {
|
||||
val sourcePath = sourceFile?.path
|
||||
val sourceExtension = sourceFile?.extension
|
||||
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
|
||||
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
|
||||
// nothing to do unless it's a renamed copy
|
||||
|
@ -569,6 +570,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
dir = targetDir,
|
||||
desiredNameWithoutExtension = desiredNameWithoutExtension,
|
||||
mimeType = mimeType,
|
||||
defaultExtension = sourceExtension,
|
||||
conflictStrategy = nameConflictStrategy,
|
||||
)
|
||||
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
|
||||
|
@ -580,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
defaultExtension = sourceExtension,
|
||||
) { output: OutputStream ->
|
||||
try {
|
||||
sourceDocFile.copyTo(output)
|
||||
|
@ -615,12 +618,13 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
targetNameWithoutExtension: String,
|
||||
defaultExtension: String?,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
if (StorageUtils.isInVault(activity, targetDir)) {
|
||||
return insertByFile(
|
||||
targetDir = targetDir,
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
|
@ -630,7 +634,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
return insertByMediaStore(
|
||||
activity = activity,
|
||||
targetDir = targetDir,
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
|
||||
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
|
@ -642,6 +646,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetDir = targetDir,
|
||||
targetDirDocFile = targetDirDocFile,
|
||||
targetNameWithoutExtension = targetNameWithoutExtension,
|
||||
defaultExtension = defaultExtension,
|
||||
write = write,
|
||||
)
|
||||
}
|
||||
|
@ -700,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
targetDir: String,
|
||||
targetDirDocFile: DocumentFileCompat?,
|
||||
targetNameWithoutExtension: String,
|
||||
defaultExtension: String?,
|
||||
write: (OutputStream) -> Unit,
|
||||
): String {
|
||||
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
|
||||
|
@ -708,8 +714,22 @@ class MediaStoreImageProvider : ImageProvider() {
|
|||
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
|
||||
// through a document URI, not a tree URI
|
||||
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
|
||||
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
|
||||
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
|
||||
var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
|
||||
// providing a display name and a MIME type does not guarantee
|
||||
// that the created document will be backed by a file with a valid media extension,
|
||||
// but having an extension is essential for media detection by Android,
|
||||
// so we retry with a display name that includes the extension
|
||||
if ((targetDocFile.extension == null || targetDocFile.extension.isEmpty() || targetDocFile.extension == "bin") && defaultExtension != null) {
|
||||
if (targetDocFile.exists()) {
|
||||
targetDocFile.delete()
|
||||
}
|
||||
|
||||
val extension = if (defaultExtension.startsWith(".")) defaultExtension else ".$defaultExtension"
|
||||
targetTreeFile = targetDirDocFile.createFile(mimeType, "$targetNameWithoutExtension$extension")
|
||||
targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
|
||||
}
|
||||
|
||||
try {
|
||||
targetDocFile.openOutputStream().use(write)
|
||||
|
|
|
@ -5,5 +5,5 @@ import kotlin.math.pow
|
|||
|
||||
object MathUtils {
|
||||
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:
|
||||
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
|
||||
fun extensionFor(mimeType: String): String? = when (mimeType) {
|
||||
fun extensionFor(mimeType: String, defaultExtension: String?): String = when (mimeType) {
|
||||
AVI, AVI_VND -> ".avi"
|
||||
DNG, DNG_ADOBE -> ".dng"
|
||||
HEIC, HEIF -> ".heif"
|
||||
MP2T, MP2TS -> ".m2ts"
|
||||
PSD_VND, PSD_X -> ".psd"
|
||||
else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
|
||||
else -> {
|
||||
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: defaultExtension
|
||||
if (ext != null) {
|
||||
// fallback to provided extension when available,
|
||||
// typically the original file extension when moving/renaming
|
||||
if (ext.startsWith(".")) ext else ".$ext"
|
||||
} else {
|
||||
// fallback to generic extensions,
|
||||
// as incorrect file extensions are better than none for media detection
|
||||
if (isVideo(mimeType)) ".mp4" else ".jpg"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
<string name="analysis_channel_name">Σάρωση πολυμέσων</string>
|
||||
<string name="analysis_notification_default_title">Σάρωση στοιχείων</string>
|
||||
<string name="analysis_notification_action_stop">Διακοπή</string>
|
||||
</resources>
|
||||
<string name="map_shortcut_short_label">Χάρτης</string>
|
||||
</resources>
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
<string name="analysis_notification_action_stop">توقف کردن</string>
|
||||
<string name="app_widget_label">قاب عکس</string>
|
||||
<string name="app_name">Aves</string>
|
||||
</resources>
|
||||
<string name="map_shortcut_short_label">نقشه</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">אייבז</string>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_widget_label">מסגרת תמונה</string>
|
||||
<string name="wallpaper">טפט</string>
|
||||
<string name="search_shortcut_short_label">חיפוש</string>
|
||||
|
@ -9,4 +9,4 @@
|
|||
<string name="analysis_notification_default_title">סורק מדיה</string>
|
||||
<string name="analysis_notification_action_stop">הפסק</string>
|
||||
<string name="map_shortcut_short_label">מפה</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
<string name="app_name">ಎವೀಸ್</string>
|
||||
<string name="app_widget_label">ಫೋಟೋ ಫ್ರೇಮ್</string>
|
||||
<string name="wallpaper">ವಾಲ್ಪೇಪರ್</string>
|
||||
<string name="videos_shortcut_short_label">ವೀಡಿಯೊಗಳು</string>
|
||||
|
@ -8,4 +8,5 @@
|
|||
<string name="analysis_notification_default_title">ಮೀಡಿಯಾ ಸ್ಕ್ಯಾನ್ ಮಾಡಲಾಗುತ್ತಿದೆ</string>
|
||||
<string name="analysis_notification_action_stop">ನಿಲ್ಲಿಸಿ</string>
|
||||
<string name="search_shortcut_short_label">ಹುಡುಕಿ</string>
|
||||
</resources>
|
||||
<string name="map_shortcut_short_label">ನಕ್ಷೆ</string>
|
||||
</resources>
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
<string name="videos_shortcut_short_label">ဗီဒီယိုများ</string>
|
||||
<string name="analysis_notification_default_title">မီဒီယာ ကိုစကင်ဖတ်နေသည်</string>
|
||||
<string name="analysis_notification_action_stop">ရပ်ရန်</string>
|
||||
</resources>
|
||||
<string name="map_shortcut_short_label">မြေပုံ</string>
|
||||
</resources>
|
||||
|
|
4
android/app/src/main/res/values-ne/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">एभस</string>
|
||||
</resources>
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="search_shortcut_short_label">ସନ୍ଧାନ</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
4
android/app/src/main/res/values-ur/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Aves</string>
|
||||
</resources>
|
|
@ -8,4 +8,5 @@
|
|||
<string name="search_shortcut_short_label">搜尋</string>
|
||||
<string name="analysis_channel_name">媒體掃描</string>
|
||||
<string name="analysis_notification_action_stop">停止</string>
|
||||
</resources>
|
||||
<string name="map_shortcut_short_label">地圖</string>
|
||||
</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.parseSubSeconds;
|
||||
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
|
||||
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import static java.nio.ByteOrder.BIG_ENDIAN;
|
||||
import static java.nio.ByteOrder.LITTLE_ENDIAN;
|
||||
|
@ -91,7 +90,7 @@ import java.util.regex.Pattern;
|
|||
import java.util.zip.CRC32;
|
||||
|
||||
/*
|
||||
* Forked from 'androidx.exifinterface:exifinterface:1.4.0'
|
||||
* Forked from 'androidx.exifinterface:exifinterface:1.4.1'
|
||||
* Named differently to let ExifInterface be loaded as subdependency.
|
||||
* cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface
|
||||
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
|
||||
|
@ -139,6 +138,12 @@ public class ExifInterfaceFork {
|
|||
// TLAD threshold for safer Exif attribute parsing
|
||||
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
|
||||
|
||||
// TLAD available heap size, to check allocations
|
||||
private long getAvailableHeapSize() {
|
||||
final Runtime runtime = Runtime.getRuntime();
|
||||
return runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory());
|
||||
}
|
||||
|
||||
private static final String TAG = "ExifInterface";
|
||||
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||||
|
||||
|
@ -4553,7 +4558,7 @@ public class ExifInterfaceFork {
|
|||
&& (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
|
||||
|| (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
|
||||
&& !containsTiff700Xmp)) {
|
||||
mXmpFromSeparateMarker = ExifAttribute.createByte(value);
|
||||
mXmpFromSeparateMarker = value != null ? ExifAttribute.createByte(value) : null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -6558,8 +6563,9 @@ public class ExifInterfaceFork {
|
|||
// Exif data in WebP images (e.g.
|
||||
// https://github.com/ImageMagick/ImageMagick/issues/3140)
|
||||
if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
|
||||
payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length,
|
||||
payload.length);
|
||||
payload =
|
||||
Arrays.copyOfRange(
|
||||
payload, IDENTIFIER_EXIF_APP1.length, payload.length);
|
||||
}
|
||||
|
||||
// Save offset to EXIF data for handling thumbnail and attribute offsets.
|
||||
|
@ -6722,8 +6728,11 @@ public class ExifInterfaceFork {
|
|||
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
|
||||
|
||||
boolean needToWriteExif = true;
|
||||
boolean needToWriteXmp = mXmpFromSeparateMarker != null;
|
||||
while (needToWriteExif || needToWriteXmp) {
|
||||
// Either there's some XMP data to write, or it has been cleared locally but was present in
|
||||
// the file when it was read (and so needs to be removed).
|
||||
boolean needToHandleXmpChunk =
|
||||
mXmpFromSeparateMarker != null || mFileOnDiskContainsSeparateXmpMarker;
|
||||
while (needToWriteExif || needToHandleXmpChunk) {
|
||||
int chunkLength = dataInputStream.readInt();
|
||||
int chunkType = dataInputStream.readInt();
|
||||
if (chunkType == PNG_CHUNK_TYPE_IHDR) {
|
||||
|
@ -6738,7 +6747,7 @@ public class ExifInterfaceFork {
|
|||
}
|
||||
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
|
||||
writePngXmpItxtChunk(dataOutputStream);
|
||||
needToWriteXmp = false;
|
||||
needToHandleXmpChunk = false;
|
||||
}
|
||||
continue;
|
||||
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
|
||||
|
@ -6746,10 +6755,25 @@ public class ExifInterfaceFork {
|
|||
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
|
||||
needToWriteExif = false;
|
||||
continue;
|
||||
} else if (chunkType == PNG_CHUNK_TYPE_ITXT && needToWriteXmp) {
|
||||
writePngXmpItxtChunk(dataOutputStream);
|
||||
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
|
||||
needToWriteXmp = false;
|
||||
} else if (chunkType == PNG_CHUNK_TYPE_ITXT
|
||||
&& chunkLength >= PNG_ITXT_XMP_KEYWORD.length) {
|
||||
// Read the 17 byte keyword and 5 expected null bytes.
|
||||
byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length];
|
||||
dataInputStream.readFully(keyword);
|
||||
int remainingChunkBytes = chunkLength - keyword.length + PNG_CHUNK_CRC_BYTE_LENGTH;
|
||||
if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) {
|
||||
if (mXmpFromSeparateMarker != null) {
|
||||
writePngXmpItxtChunk(dataOutputStream);
|
||||
}
|
||||
dataInputStream.skipFully(remainingChunkBytes);
|
||||
needToHandleXmpChunk = false;
|
||||
} else {
|
||||
// This is a non-XMP iTXt chunk, so just copy it to the output and continue.
|
||||
dataOutputStream.writeInt(chunkLength);
|
||||
dataOutputStream.writeInt(chunkType);
|
||||
dataOutputStream.write(keyword);
|
||||
copy(dataInputStream, dataOutputStream, remainingChunkBytes);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
dataOutputStream.writeInt(chunkLength);
|
||||
|
@ -7536,6 +7560,13 @@ public class ExifInterfaceFork {
|
|||
Log.d(TAG, "Invalid strip offset value");
|
||||
return;
|
||||
}
|
||||
|
||||
// TLAD start
|
||||
if (bytesToSkip > getAvailableHeapSize()) {
|
||||
throw new IOException("cannot allocate " + bytesToSkip + " bytes to skip to retrieve thumbnail");
|
||||
}
|
||||
// TLAD end
|
||||
|
||||
try {
|
||||
in.skipFully(bytesToSkip);
|
||||
} catch (EOFException e) {
|
||||
|
|
|
@ -31,7 +31,7 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
|
||||
/*
|
||||
* Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
|
||||
* Forked from 'androidx.exifinterface:exifinterface:1.4.1'
|
||||
* Named differently to let ExifInterface be loaded as subdependency.
|
||||
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
|
||||
*/
|
||||
|
|
|
@ -18,10 +18,10 @@ pluginManagement {
|
|||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.8.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
|
||||
id("com.google.devtools.ksp") version "2.1.10-1.0.29" apply false
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
|
||||
id("com.android.application") version "8.10.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.21" apply false
|
||||
id("com.google.devtools.ksp") version "2.1.21-2.0.1" apply false
|
||||
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
|
35
assets/terms.txt
Normal file
|
@ -0,0 +1,35 @@
|
|||
Terms of Service
|
||||
================
|
||||
|
||||
“Aves Gallery” is an open-source gallery and metadata explorer app allowing you to access and manage your local photos and videos.
|
||||
|
||||
The app is designed for legal, authorized and acceptable purposes.
|
||||
|
||||
|
||||
|
||||
Disclaimer
|
||||
==========
|
||||
|
||||
The app is released “as-is”, without any warranty, responsibility or liability. Use of the app is at your own risk.
|
||||
|
||||
|
||||
|
||||
Privacy Policy
|
||||
==============
|
||||
|
||||
The app does not collect any personal data. We never have access to your photos and videos. This also means that we cannot get them back for you if you delete them without backing them up.
|
||||
|
||||
Optionally, with your consent, the app accesses the inventory of installed apps to improve album display.
|
||||
|
||||
Optionally, with your consent, the app collects anonymous error and diagnostic data to improve the app quality. We use Firebase Crashlytics, and the anonymous data are stored on their servers. Please note that those are anonymous data, there is absolutely nothing personal about those data.
|
||||
|
||||
|
||||
|
||||
Contact
|
||||
=======
|
||||
|
||||
Developer: Thibault Deckers
|
||||
|
||||
Email: gallery.aves@gmail.com
|
||||
|
||||
Website: https://github.com/deckerst/aves
|
|
@ -1,3 +1,4 @@
|
|||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
- provider: true
|
|
@ -2,4 +2,4 @@
|
|||
|
||||
<b>Navigation und Suche</b> ist ein wichtiger Bestandteil von <i>Aves</i>. Das Ziel besteht darin, dass Benutzer problemlos von Alben zu Fotos zu Tags zu Karten usw. wechseln können.
|
||||
|
||||
<i>Aves</i> lässt sich mit Android mit Funktionen wie <b>App-Verknüpfungen</b> und <b>globaler Suche</b> integrieren. Es funktioniert auch als <b>Medienbetrachter und -auswahl</b>.
|
||||
<i>Aves</i> integriert sich in Android (einschließlich Android TV) mit Funktionen wie <b>Widgets</b>, <b>App-Shortcuts</b>, <b>Bildschirmschoner</b> und der <b>globalen Suche</b> integrieren. Sie funktioniert auch als <b>Medienbetrachter und -Picker</b>.
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
In v1.12.0:
|
||||
- save your filtered collection as dynamic albums
|
||||
- enjoy the app in Tamil, Bulgarian and Estonian
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +0,0 @@
|
|||
In v1.12.0:
|
||||
- save your filtered collection as dynamic albums
|
||||
- enjoy the app in Tamil, Bulgarian and Estonian
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.12.1:
|
||||
- enjoy the app in Danish
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.12.1:
|
||||
- enjoy the app in Danish
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.12.2:
|
||||
- enjoy the app in Danish
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.12.2:
|
||||
- enjoy the app in Danish
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.12.3:
|
||||
- edit locations via GPX tracks
|
||||
Full changelog available on GitHub
|
|
@ -1,3 +0,0 @@
|
|||
In v1.12.3:
|
||||
- edit locations via GPX tracks
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +1,4 @@
|
|||
In v1.12.4:
|
||||
In v1.12.6:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +1,4 @@
|
|||
In v1.12.4:
|
||||
In v1.12.6:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +1,4 @@
|
|||
In v1.12.5:
|
||||
In v1.12.7:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician
|
||||
Full changelog available on GitHub
|
|
@ -1,4 +1,4 @@
|
|||
In v1.12.5:
|
||||
In v1.12.7:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/148.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.12.8:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/14801.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.12.8:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/149.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.12.9:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician and Kannada
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/14901.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.12.9:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician and Kannada
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/150.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.12.10:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician and Kannada
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/15001.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.12.10:
|
||||
- play more kinds of motion photos
|
||||
- enjoy the app in Galician and Kannada
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/151.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.13.0:
|
||||
- group albums
|
||||
- filter by day of the week
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/15101.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.13.0:
|
||||
- group albums
|
||||
- filter by day of the week
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/152.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.13.1:
|
||||
- group albums
|
||||
- filter by day of the week
|
||||
Full changelog available on GitHub
|
4
fastlane/metadata/android/en-US/changelogs/15201.txt
Normal file
|
@ -0,0 +1,4 @@
|
|||
In v1.13.1:
|
||||
- group albums
|
||||
- filter by day of the week
|
||||
Full changelog available on GitHub
|
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
|
@ -0,0 +1,4 @@
|
|||
In v1.13.2:
|
||||
- group albums
|
||||
- filter by day of the week
|
||||
Full changelog available on GitHub
|
|
@ -1,5 +1,5 @@
|
|||
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||
<i>Aves</i> יכול להתמודד עם כל מיני תמונות וסרטונים, כולל קובצי JPEG ו-MP4 הטיפוסיים שלך, אבל גם דברים אקזוטיים יותר כמו <b>TIFF מרובי עמודים, SVGs, AVI ישנים ועוד</b>! הוא סורק את אוסף המדיה שלך כדי לזהות <b>תמונות תנועה</b>, <b>פנורמות</b> (הידוע גם בתמונות פנורמיות), <b>סרטוני 360°</b>, וכן קבצי <b>GeoTIFF</b>.
|
||||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
<b>ניווט וחיפוש</b> הם חלק חשוב ב-<i>Aves</i>. המטרה היא שהמשתמשים יזרמו בקלות מאלבומים לתמונות לתגים למפות וכו'.
|
||||
|
||||
<i>Aves</i> integrates with Android (including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
||||
<i>Aves</i> משתלב עם Android (כולל Android TV) עם תכונות כגון <b>ווידג'טים</b>, <b>קיצורי אפליקציות</b>, <b>שומר מסך</b> וטיפול ב<b>חיפוש גלובלי</b>. הוא פועל גם כ<b>מציג ובוחר מדיה</b>.
|
||||
|
|
|
@ -1 +1 @@
|
|||
Gallery and metadata explorer
|
||||
סייר גלריה ומטא נתונים
|
|
@ -1,5 +1,5 @@
|
|||
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||
<i>ಏವೀಸ್</i> ನಿಮ್ಮ JPEG ಗಳು ಮತ್ತು MP4 ಗಳನ್ನು ಒಳಗೊಂಡಂತೆ ಎಲ್ಲಾ ರೀತಿಯ ಚಿತ್ರಗಳು ಮತ್ತು ವೀಡಿಯೊಗಳನ್ನು ನಿಭಾಯಿಸಬಲ್ಲದು, ಅಲ್ಲದೆ ವಿಶಿಷ್ಟವಾದ <b>ಬಹು-ಪುಟ TIFFಗಳು, SVGಗಳು, ಹಳೆಯ AVIಗಳು ಮತ್ತು ಹಲವು ಪ್ರಕಾರಗಳನ್ನು ಕೂಡ ಬೆಂಬಲಿಸುತ್ತದೆ</b> ಇದು <b>ಚಲನೆಯ ಫೋಟೋಗಳು</b>, <b>ಪನೋರಮಾಗಳು</b> (ಫೋಟೋ ಗೋಳಗಳು) <b>360° ವೀಡಿಯೊಗಳು</b>, ಹಾಗೆಯೇ <b>GeoTIFF</b> ಕಡತಗಳನ್ನು ಗುರುತಿಸಲು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಸಂಗ್ರಹವನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.
|
||||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
<b>ನ್ಯಾವಿಗೇಷನ್ ಮತ್ತು ಹುಡುಕಾಟ</b> <i>ಏವೀಸ್</i>ನ ಒಂದು ಪ್ರಮುಖ ಭಾಗವಾಗಿದೆ. ಬಳಕೆದಾರರು ಆಲ್ಬಮ್ಗಳಿಂದ ಫೋಟೋಗಳಿಂದ ಟ್ಯಾಗ್ಗಳಿಗೆ ನಕ್ಷೆಗಳಿಗೆ ಸುಲಭವಾಗಿ ಹರಿಯುವುದು ಗುರಿಯಾಗಿದೆ.
|
||||
|
||||
<i>Aves</i> integrates with Android (including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
||||
<i>ಎವೀಸ್</i> ಆಂಡ್ರಾಯ್ಡ್ (ಟಿವಿ ಸೇರಿದಂತೆ) ನೊಂದಿಗೆ ಸಂಯೋಜಿಸುತ್ತದೆ, ಉದಾಹರಣೆಗೆ <b>ವಿಜೆಟ್ಗಳು</b>, <b>ಆ್ಯಪ್ ಶಾರ್ಟ್ಕಟ್ಗಳು</b>, <b>ಸ್ಕ್ರೀನ್ ಸೇವರ್</b> ಮತ್ತು <b>ಜಾಗತಿಕ ಹುಡುಕಾಟ</b> ನಿರ್ವಹಣೆ. ಇದು <b>ಮೀಡಿಯಾ ವೀಕ್ಷಕ ಮತ್ತು ಪಿಕ್ಕರ್</b> ಆಗಿಯೂ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತದೆ.
|
||||
|
|
BIN
fastlane/metadata/android/kn/images/featureGraphic.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
fastlane/metadata/android/kn/images/phoneScreenshots/1.png
Normal file
After Width: | Height: | Size: 308 KiB |
BIN
fastlane/metadata/android/kn/images/phoneScreenshots/2.png
Normal file
After Width: | Height: | Size: 556 KiB |
BIN
fastlane/metadata/android/kn/images/phoneScreenshots/3.png
Normal file
After Width: | Height: | Size: 150 KiB |
BIN
fastlane/metadata/android/kn/images/phoneScreenshots/4.png
Normal file
After Width: | Height: | Size: 113 KiB |
BIN
fastlane/metadata/android/kn/images/phoneScreenshots/5.png
Normal file
After Width: | Height: | Size: 93 KiB |
BIN
fastlane/metadata/android/kn/images/phoneScreenshots/6.png
Normal file
After Width: | Height: | Size: 376 KiB |
BIN
fastlane/metadata/android/kn/images/phoneScreenshots/7.png
Normal file
After Width: | Height: | Size: 381 KiB |
|
@ -1 +1 @@
|
|||
Gallery and metadata explorer
|
||||
ಗ್ಯಾಲರಿ ಮತ್ತು ಮೆಟಾಡೇಟಾ ಎಕ್ಸ್ಪ್ಲೋರರ್
|
5
fastlane/metadata/android/ne/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
<i>Aves</i> integrates with Android (including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
1
fastlane/metadata/android/ne/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Gallery and metadata explorer
|
5
fastlane/metadata/android/ur/full_description.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
<i>Aves</i> can handle all sorts of images and videos, including your typical JPEGs and MP4s, but also more exotic things like <b>multi-page TIFFs, SVGs, old AVIs and more</b>! It scans your media collection to identify <b>motion photos</b>, <b>panoramas</b> (aka photo spheres), <b>360° videos</b>, as well as <b>GeoTIFF</b> files.
|
||||
|
||||
<b>Navigation and search</b> is an important part of <i>Aves</i>. The goal is for users to easily flow from albums to photos to tags to maps, etc.
|
||||
|
||||
<i>Aves</i> integrates with Android (including Android TV) with features such as <b>widgets</b>, <b>app shortcuts</b>, <b>screen saver</b> and <b>global search</b> handling. It also works as a <b>media viewer and picker</b>.
|
1
fastlane/metadata/android/ur/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Gallery and metadata explorer
|
|
@ -10,7 +10,7 @@ import 'package:flutter/foundation.dart';
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@immutable
|
||||
class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
||||
class FullImage extends ImageProvider<FullImage> with EquatableMixin {
|
||||
final String uri, mimeType;
|
||||
final int? pageId, rotationDegrees, sizeBytes;
|
||||
final bool isFlipped, isAnimated;
|
||||
|
@ -19,7 +19,7 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
@override
|
||||
List<Object?> get props => [uri, pageId, rotationDegrees, isFlipped, isAnimated, scale];
|
||||
|
||||
const UriImage({
|
||||
const FullImage({
|
||||
required this.uri,
|
||||
required this.mimeType,
|
||||
required this.pageId,
|
||||
|
@ -31,12 +31,12 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
});
|
||||
|
||||
@override
|
||||
Future<UriImage> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<UriImage>(this);
|
||||
Future<FullImage> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<FullImage>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(UriImage key, ImageDecoderCallback decode) {
|
||||
ImageStreamCompleter loadImage(FullImage key, ImageDecoderCallback decode) {
|
||||
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||
|
||||
return MultiFrameImageStreamCompleter(
|
||||
|
@ -59,11 +59,11 @@ class UriImage extends ImageProvider<UriImage> with EquatableMixin {
|
|||
case MimeTypes.svg:
|
||||
return false;
|
||||
default:
|
||||
return !isAnimated;
|
||||
return !isAnimated && !MimeTypes.isVideo(mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(UriImage key, ImageDecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
|
||||
Future<ui.Codec> _loadAsync(FullImage key, ImageDecoderCallback decode, StreamController<ImageChunkEvent> chunkEvents) async {
|
||||
assert(key == this);
|
||||
|
||||
final request = ImageRequest(
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"filePickerDoNotShowHiddenFiles": "عدم إظهار الملفات المخفية",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"tagPlaceholderPlace": "المكان",
|
||||
"@tagPlaceholderPlace": {},
|
||||
"tagPlaceholderCountry": "البلد",
|
||||
|
@ -9,12 +7,6 @@
|
|||
"@sourceViewerPageTitle": {},
|
||||
"panoramaDisableSensorControl": "تعطيل التحكم في المستشعر",
|
||||
"@panoramaDisableSensorControl": {},
|
||||
"filePickerNoItems": "لا توجد عناصر",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerOpenFrom": "فتح من",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerShowHiddenFiles": "إظهار الملفات المخفية",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"panoramaEnableSensorControl": "تمكين التحكم في المستشعر",
|
||||
"@panoramaEnableSensorControl": {},
|
||||
"saveTooltip": "حفظ",
|
||||
|
@ -63,8 +55,6 @@
|
|||
"@tagEditorSectionRecent": {},
|
||||
"tagEditorSectionPlaceholders": "العناصر النائبة",
|
||||
"@tagEditorSectionPlaceholders": {},
|
||||
"filePickerUseThisFolder": "إستخدام هذا المجلد",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"hideTooltip": "إخفاء",
|
||||
"@hideTooltip": {},
|
||||
"tagEditorPageAddTagTooltip": "إضافة علامة",
|
||||
|
@ -503,8 +493,6 @@
|
|||
"@viewerSetWallpaperButtonLabel": {},
|
||||
"settingsVideoResumptionModeTile": "استئناف التشغيل",
|
||||
"@settingsVideoResumptionModeTile": {},
|
||||
"collectionGroupNone": "لا تجمع",
|
||||
"@collectionGroupNone": {},
|
||||
"searchRatingSectionTitle": "التقييمات",
|
||||
"@searchRatingSectionTitle": {},
|
||||
"vaultBinUsageDialogMessage": "تستخدم بعض الخزائن سلة المحذوفات.",
|
||||
|
@ -611,7 +599,7 @@
|
|||
"@settingsLanguagePageTitle": {},
|
||||
"rootDirectoryDescription": "دليل الجذر",
|
||||
"@rootDirectoryDescription": {},
|
||||
"viewDialogGroupSectionTitle": "مجموعة",
|
||||
"viewDialogGroupSectionTitle": "الأقسام",
|
||||
"@viewDialogGroupSectionTitle": {},
|
||||
"maxBrightnessAlways": "دائماً",
|
||||
"@maxBrightnessAlways": {},
|
||||
|
@ -1031,8 +1019,6 @@
|
|||
"@entryActionSetAs": {},
|
||||
"sortOrderLowestFirst": "الأدنى أولاً",
|
||||
"@sortOrderLowestFirst": {},
|
||||
"albumGroupNone": "لا تجمع",
|
||||
"@albumGroupNone": {},
|
||||
"statsTopStatesSectionTitle": "أهم الولايات",
|
||||
"@statsTopStatesSectionTitle": {},
|
||||
"settingsViewerQuickActionEditorDisplayedButtonsSectionTitle": "الأزرار المعروضة",
|
||||
|
@ -1463,7 +1449,7 @@
|
|||
"@binPageTitle": {},
|
||||
"tagPlaceholderState": "الولاية",
|
||||
"@tagPlaceholderState": {},
|
||||
"sortByAlbumFileName": "حسب الألبوم واسم الملف",
|
||||
"sortByAlbumFileName": "حسب عنوان الألبوم والعنصر",
|
||||
"@sortByAlbumFileName": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {
|
||||
|
@ -1608,5 +1594,33 @@
|
|||
"editEntryLocationDialogTimeShift": "التحول الزمني",
|
||||
"@editEntryLocationDialogTimeShift": {},
|
||||
"removeEntryMetadataDialogAll": "الكل",
|
||||
"@removeEntryMetadataDialogAll": {}
|
||||
"@removeEntryMetadataDialogAll": {},
|
||||
"sortByPath": "حسب المسار",
|
||||
"@sortByPath": {},
|
||||
"searchFormatSectionTitle": "التنسيقات",
|
||||
"@searchFormatSectionTitle": {},
|
||||
"chipActionGroup": "تغيير التجميع",
|
||||
"@chipActionGroup": {},
|
||||
"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": {},
|
||||
"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": {}
|
||||
}
|
||||
|
|
|
@ -561,14 +561,6 @@
|
|||
"@viewerInfoPageTitle": {},
|
||||
"viewerErrorDoesNotExist": "Файл больш не існуе.",
|
||||
"@viewerErrorDoesNotExist": {},
|
||||
"filePickerUseThisFolder": "Выкарыстоўваць гэтую тэчку",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"filePickerNoItems": "Няма элементаў",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerOpenFrom": "Адкрыць з",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerShowHiddenFiles": "Паказаць схаваныя файлы",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"sourceViewerPageTitle": "Крыніца",
|
||||
"@sourceViewerPageTitle": {},
|
||||
"panoramaDisableSensorControl": "Адключыць сэнсарнае кіраванне",
|
||||
|
@ -631,8 +623,6 @@
|
|||
"@tagEditorDiscardDialogMessage": {},
|
||||
"tagPlaceholderCountry": "Краіна",
|
||||
"@tagPlaceholderCountry": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Не паказваць схаваныя файлы",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"viewerInfoOpenEmbeddedFailureFeedback": "Не ўдалося атрымаць убудаваныя даныя",
|
||||
"@viewerInfoOpenEmbeddedFailureFeedback": {},
|
||||
"mapAttributionOsmHot": "Пліткі ад [HOT](https://www.hotosm.org/) • Арганізаваны [OSM France](https://openstreetmap.fr/)",
|
||||
|
@ -927,8 +917,6 @@
|
|||
"@settingsActionImport": {},
|
||||
"locationPickerUseThisLocationButton": "Выкарыстоўваць гэтае месцазнаходжанне",
|
||||
"@locationPickerUseThisLocationButton": {},
|
||||
"collectionGroupNone": "Не групаваць",
|
||||
"@collectionGroupNone": {},
|
||||
"searchRatingSectionTitle": "Рэйтынгі",
|
||||
"@searchRatingSectionTitle": {},
|
||||
"settingsDisabled": "Адкл.",
|
||||
|
@ -1043,8 +1031,6 @@
|
|||
"@aboutLinkPolicy": {},
|
||||
"sortOrderLowestFirst": "Спачатку з нізкім",
|
||||
"@sortOrderLowestFirst": {},
|
||||
"albumGroupNone": "Не групаваць",
|
||||
"@albumGroupNone": {},
|
||||
"countryPageTitle": "Краіны",
|
||||
"@countryPageTitle": {},
|
||||
"albumGroupType": "Па тыпу",
|
||||
|
|
|
@ -266,7 +266,7 @@
|
|||
"@welcomeMessage": {},
|
||||
"welcomeOptional": "Опционално",
|
||||
"@welcomeOptional": {},
|
||||
"itemCount": "{count, plural, =1{{count} обект} few{{count} обекта} other{{count} обекта}}",
|
||||
"itemCount": "{count, plural, =1{{count} елемент} few{{count} елемента} other{{count} елемента}}",
|
||||
"@itemCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
|
@ -663,7 +663,7 @@
|
|||
"@videoStartOverButtonLabel": {},
|
||||
"videoResumeButtonLabel": "ПРОДЪЛЖИ",
|
||||
"@videoResumeButtonLabel": {},
|
||||
"setCoverDialogLatest": "Последен обект",
|
||||
"setCoverDialogLatest": "Последен елемент",
|
||||
"@setCoverDialogLatest": {},
|
||||
"setCoverDialogCustom": "Персонален",
|
||||
"@setCoverDialogCustom": {},
|
||||
|
@ -725,7 +725,7 @@
|
|||
"@exportEntryDialogWriteMetadata": {},
|
||||
"renameEntryDialogLabel": "Ново име",
|
||||
"@renameEntryDialogLabel": {},
|
||||
"editEntryDialogCopyFromItem": "Копиране от друг обект",
|
||||
"editEntryDialogCopyFromItem": "Копиране от друг елемент",
|
||||
"@editEntryDialogCopyFromItem": {},
|
||||
"editEntryDialogTargetFieldsHeader": "Полета за промяна",
|
||||
"@editEntryDialogTargetFieldsHeader": {},
|
||||
|
@ -873,8 +873,6 @@
|
|||
"@collectionGroupMonth": {},
|
||||
"collectionGroupDay": "По дни",
|
||||
"@collectionGroupDay": {},
|
||||
"collectionGroupNone": "Не групирай",
|
||||
"@collectionGroupNone": {},
|
||||
"sectionUnknown": "Неизвестно",
|
||||
"@sectionUnknown": {},
|
||||
"dateToday": "Днес",
|
||||
|
@ -978,8 +976,6 @@
|
|||
"@albumGroupType": {},
|
||||
"albumGroupVolume": "По обем на съхранение",
|
||||
"@albumGroupVolume": {},
|
||||
"albumGroupNone": "Без групиране",
|
||||
"@albumGroupNone": {},
|
||||
"albumMimeTypeMixed": "Разни",
|
||||
"@albumMimeTypeMixed": {},
|
||||
"albumPickPageTitleCopy": "Копирай в албум",
|
||||
|
@ -1196,9 +1192,9 @@
|
|||
"@settingsNavigationDrawerAddAlbum": {},
|
||||
"settingsThumbnailSectionTitle": "Миниатюри",
|
||||
"@settingsThumbnailSectionTitle": {},
|
||||
"settingsThumbnailOverlayTile": "Наслагване",
|
||||
"settingsThumbnailOverlayTile": "Повече информация",
|
||||
"@settingsThumbnailOverlayTile": {},
|
||||
"settingsThumbnailOverlayPageTitle": "Наслагване",
|
||||
"settingsThumbnailOverlayPageTitle": "Повече информация",
|
||||
"@settingsThumbnailOverlayPageTitle": {},
|
||||
"settingsThumbnailShowHdrIcon": "Показване на HDR икона",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
|
@ -1258,7 +1254,7 @@
|
|||
"@settingsViewerMaximumBrightness": {},
|
||||
"settingsMotionPhotoAutoPlay": "Автоматично възпроизвеждане на снимки с движение",
|
||||
"@settingsMotionPhotoAutoPlay": {},
|
||||
"settingsViewerOverlayPageTitle": "Наслагване",
|
||||
"settingsViewerOverlayPageTitle": "Повече информация",
|
||||
"@settingsViewerOverlayPageTitle": {},
|
||||
"settingsViewerShowHistogram": "Показвай хистограма",
|
||||
"@settingsViewerShowHistogram": {},
|
||||
|
@ -1272,7 +1268,7 @@
|
|||
"@settingsViewerQuickActionEditorAvailableButtonsSectionTitle": {},
|
||||
"settingsViewerQuickActionEmpty": "Без бутони",
|
||||
"@settingsViewerQuickActionEmpty": {},
|
||||
"settingsViewerOverlayTile": "Наслагване",
|
||||
"settingsViewerOverlayTile": "Повече информация",
|
||||
"@settingsViewerOverlayTile": {},
|
||||
"settingsViewerShowRatingTags": "Показване на рейтинг и тагове",
|
||||
"@settingsViewerShowRatingTags": {},
|
||||
|
@ -1322,7 +1318,7 @@
|
|||
"@settingsStorageAccessTile": {},
|
||||
"settingsAccessibilityShowPinchGestureAlternatives": "Показване на алтернативи за жестове с мултитъч",
|
||||
"@settingsAccessibilityShowPinchGestureAlternatives": {},
|
||||
"settingsDisplaySectionTitle": "Изобразяване",
|
||||
"settingsDisplaySectionTitle": "Изглед",
|
||||
"@settingsDisplaySectionTitle": {},
|
||||
"settingsThemeBrightnessTile": "Тема",
|
||||
"@settingsThemeBrightnessTile": {},
|
||||
|
@ -1384,8 +1380,6 @@
|
|||
"@tagPlaceholderState": {},
|
||||
"tagPlaceholderPlace": "Локация",
|
||||
"@tagPlaceholderPlace": {},
|
||||
"filePickerShowHiddenFiles": "Показване на скритите файлове",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"chipActionRemove": "Премахване",
|
||||
"@chipActionRemove": {},
|
||||
"albumTierDynamic": "Динамични",
|
||||
|
@ -1454,7 +1448,7 @@
|
|||
"@settingsAllowErrorReporting": {},
|
||||
"settingsEnableBin": "Използвайте кошчето",
|
||||
"@settingsEnableBin": {},
|
||||
"settingsEnableBinSubtitle": "Съхранявайте изтритите обекти за 30 дни",
|
||||
"settingsEnableBinSubtitle": "Съхранявайте изтритите елементи за 30 дни",
|
||||
"@settingsEnableBinSubtitle": {},
|
||||
"settingsAllowMediaManagement": "Разрешаване на управление на медиите",
|
||||
"@settingsAllowMediaManagement": {},
|
||||
|
@ -1571,10 +1565,6 @@
|
|||
"@tagEditorSectionPlaceholders": {},
|
||||
"tagPlaceholderCountry": "Държава",
|
||||
"@tagPlaceholderCountry": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Не показвай скритите файлове",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"filePickerOpenFrom": "Отворете от",
|
||||
"@filePickerOpenFrom": {},
|
||||
"settingsVideoGestureSideDoubleTapSeek": "Докоснете два пъти краищата на екрана, за превъртане назад/напред",
|
||||
"@settingsVideoGestureSideDoubleTapSeek": {},
|
||||
"settingsSaveSearchHistory": "Запазване на историята на търсенето",
|
||||
|
@ -1617,14 +1607,10 @@
|
|||
"@viewerErrorDoesNotExist": {},
|
||||
"mapAttributionOsmData": "Данни карта © [OpenStreetMap](https://www.openstreetmap.org/copyright) участници",
|
||||
"@mapAttributionOsmData": {},
|
||||
"filePickerNoItems": "Не откривам нищо",
|
||||
"@filePickerNoItems": {},
|
||||
"newDynamicAlbumDialogTitle": "Нов динамичен албум",
|
||||
"@newDynamicAlbumDialogTitle": {},
|
||||
"tagEditorDiscardDialogMessage": "Искате ли да отхвърлите промените?",
|
||||
"@tagEditorDiscardDialogMessage": {},
|
||||
"filePickerUseThisFolder": "Използвай тази папка",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"chipActionDecompose": "Раздели",
|
||||
"@chipActionDecompose": {},
|
||||
"coordinateFormatDdm": "Градуси, десетични минути",
|
||||
|
@ -1634,5 +1620,23 @@
|
|||
"editEntryLocationDialogTimeShift": "Изместване на времето",
|
||||
"@editEntryLocationDialogTimeShift": {},
|
||||
"removeEntryMetadataDialogAll": "Всички",
|
||||
"@removeEntryMetadataDialogAll": {}
|
||||
"@removeEntryMetadataDialogAll": {},
|
||||
"sortByPath": "Според пътя",
|
||||
"@sortByPath": {},
|
||||
"searchFormatSectionTitle": "Формати",
|
||||
"@searchFormatSectionTitle": {},
|
||||
"chipActionCreateGroup": "Създайте група",
|
||||
"@chipActionCreateGroup": {},
|
||||
"chipActionGroup": "Групиране",
|
||||
"@chipActionGroup": {},
|
||||
"newGroupDialogTitle": "Нова Група",
|
||||
"@newGroupDialogTitle": {},
|
||||
"groupAlreadyExists": "Групата вече съществува",
|
||||
"@groupAlreadyExists": {},
|
||||
"albumTierGroups": "Групи",
|
||||
"@albumTierGroups": {},
|
||||
"groupPickerUseThisGroupButton": "Използвайте тази група",
|
||||
"@groupPickerUseThisGroupButton": {},
|
||||
"newGroupDialogNameLabel": "Име на групата",
|
||||
"@newGroupDialogNameLabel": {}
|
||||
}
|
||||
|
|
|
@ -839,8 +839,6 @@
|
|||
"@collectionGroupAlbum": {},
|
||||
"collectionGroupMonth": "Per mes",
|
||||
"@collectionGroupMonth": {},
|
||||
"collectionGroupNone": "No per grup",
|
||||
"@collectionGroupNone": {},
|
||||
"collectionGroupDay": "Per dia",
|
||||
"@collectionGroupDay": {},
|
||||
"dateToday": "Avui",
|
||||
|
@ -949,8 +947,6 @@
|
|||
"@albumGroupType": {},
|
||||
"albumGroupVolume": "Per volum d’emmagatzematge",
|
||||
"@albumGroupVolume": {},
|
||||
"albumGroupNone": "No agrupar",
|
||||
"@albumGroupNone": {},
|
||||
"albumMimeTypeMixed": "Barrejat",
|
||||
"@albumMimeTypeMixed": {},
|
||||
"albumPickPageTitleCopy": "Copiar a Àlbum",
|
||||
|
@ -1345,16 +1341,6 @@
|
|||
"@panoramaDisableSensorControl": {},
|
||||
"sourceViewerPageTitle": "Font",
|
||||
"@sourceViewerPageTitle": {},
|
||||
"filePickerShowHiddenFiles": "Mostra arxius amagats",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"filePickerDoNotShowHiddenFiles": "No mostris arxius amagats",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"filePickerOpenFrom": "Obrir des de",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "Sense element",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerUseThisFolder": "Utilitza aquesta carpeta",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"settingsVideoControlsPageTitle": "Controls",
|
||||
"@settingsVideoControlsPageTitle": {},
|
||||
"settingsSubtitleThemeTextAlignmentDialogTitle": "Ajustament de Text",
|
||||
|
@ -1608,5 +1594,9 @@
|
|||
"dynamicAlbumAlreadyExists": "L’àlbum dinàmic ja existeix",
|
||||
"@dynamicAlbumAlreadyExists": {},
|
||||
"sortOrderShortestFirst": "El més curt primer",
|
||||
"@sortOrderShortestFirst": {}
|
||||
"@sortOrderShortestFirst": {},
|
||||
"sortByPath": "Per ruta",
|
||||
"@sortByPath": {},
|
||||
"searchFormatSectionTitle": "Formats",
|
||||
"@searchFormatSectionTitle": {}
|
||||
}
|
||||
|
|
|
@ -923,16 +923,6 @@
|
|||
"@panoramaEnableSensorControl": {},
|
||||
"sourceViewerPageTitle": "Zdroj",
|
||||
"@sourceViewerPageTitle": {},
|
||||
"filePickerShowHiddenFiles": "Zobrazit skryté soubory",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Nezobrazovat skryté soubory",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"filePickerOpenFrom": "Otevřít z",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "Žádné položky",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerUseThisFolder": "Použít tuto složku",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"pickTooltip": "Vybrat",
|
||||
"@pickTooltip": {},
|
||||
"doubleBackExitMessage": "Pro ukončení klepněte znovu na „zpět“.",
|
||||
|
@ -1041,8 +1031,6 @@
|
|||
"@drawerCollectionSphericalVideos": {},
|
||||
"aboutLicensesBanner": "Tato aplikace využívá tyto open-source baličky a knihovny.",
|
||||
"@aboutLicensesBanner": {},
|
||||
"collectionGroupNone": "Neseskupovat",
|
||||
"@collectionGroupNone": {},
|
||||
"aboutLicensesSectionTitle": "Licence open-source",
|
||||
"@aboutLicensesSectionTitle": {},
|
||||
"collectionActionHideTitleSearch": "Skrýt filtr dle názvu",
|
||||
|
@ -1219,8 +1207,6 @@
|
|||
"@sortOrderLargestFirst": {},
|
||||
"sortOrderSmallestFirst": "Od nejužšího",
|
||||
"@sortOrderSmallestFirst": {},
|
||||
"albumGroupNone": "Neseskupovat",
|
||||
"@albumGroupNone": {},
|
||||
"albumVideoCaptures": "Snímky videa",
|
||||
"@albumVideoCaptures": {},
|
||||
"countryPageTitle": "Země",
|
||||
|
|
|
@ -555,12 +555,6 @@
|
|||
"@tagEditorDiscardDialogMessage": {},
|
||||
"panoramaEnableSensorControl": "Aktivér sensorstyring",
|
||||
"@panoramaEnableSensorControl": {},
|
||||
"filePickerOpenFrom": "Åbn fra",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "Ingen elementer",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerUseThisFolder": "Brug denne mappe",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"authenticateToUnlockVault": "Godkend for at oplåse boks",
|
||||
"@authenticateToUnlockVault": {},
|
||||
"exportEntryDialogFormat": "Format:",
|
||||
|
@ -841,8 +835,6 @@
|
|||
"@collectionGroupMonth": {},
|
||||
"collectionGroupDay": "Efter dag",
|
||||
"@collectionGroupDay": {},
|
||||
"collectionGroupNone": "Gruppér ikke",
|
||||
"@collectionGroupNone": {},
|
||||
"collectionDeleteFailureFeedback": "{count, plural, =1{Kunne ikke slette 1 element} other{Kunne ikke slette {count} elementer}}",
|
||||
"@collectionDeleteFailureFeedback": {
|
||||
"placeholders": {
|
||||
|
@ -858,7 +850,7 @@
|
|||
"@drawerCollectionRaws": {},
|
||||
"sortByRating": "Efter bedømmelse",
|
||||
"@sortByRating": {},
|
||||
"sortByAlbumFileName": "Efter album og filnavn",
|
||||
"sortByAlbumFileName": "Efter album og elementtitel",
|
||||
"@sortByAlbumFileName": {},
|
||||
"albumGroupVolume": "Efter lagervolume",
|
||||
"@albumGroupVolume": {},
|
||||
|
@ -1288,7 +1280,7 @@
|
|||
"@tileLayoutGrid": {},
|
||||
"removeEntryMetadataMotionPhotoXmpWarningDialogMessage": "XMP er påkrævet for at afspille videoen i et bevægelsesfoto.\n\nEr du sikker på, at du vil fjerne den?",
|
||||
"@removeEntryMetadataMotionPhotoXmpWarningDialogMessage": {},
|
||||
"viewDialogGroupSectionTitle": "Gruppér",
|
||||
"viewDialogGroupSectionTitle": "Sektioner",
|
||||
"@viewDialogGroupSectionTitle": {},
|
||||
"hideFilterConfirmationDialogMessage": "Matchende fotos og videoer skjules fra din samling. Du kan vise dem igen i indstillingerne “Privatliv”.\n\nEr du sikker på, at du vil skjule dem?",
|
||||
"@hideFilterConfirmationDialogMessage": {},
|
||||
|
@ -1423,20 +1415,14 @@
|
|||
"@mapAttributionOsmData": {},
|
||||
"viewerInfoSearchSuggestionRights": "Rettigheder",
|
||||
"@viewerInfoSearchSuggestionRights": {},
|
||||
"filePickerShowHiddenFiles": "Vis skjulte filer",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"viewerInfoOpenEmbeddedFailureFeedback": "Kunne ikke udtrække indlejrede data",
|
||||
"@viewerInfoOpenEmbeddedFailureFeedback": {},
|
||||
"tagPlaceholderPlace": "Sted",
|
||||
"@tagPlaceholderPlace": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Vis ikke skjulte filer",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"exportEntryDialogWriteMetadata": "Skriv metadata",
|
||||
"@exportEntryDialogWriteMetadata": {},
|
||||
"editEntryDateDialogCopyField": "Kopiér fra en anden dato",
|
||||
"@editEntryDateDialogCopyField": {},
|
||||
"albumGroupNone": "Gruppér ikke",
|
||||
"@albumGroupNone": {},
|
||||
"albumEmpty": "Ingen album",
|
||||
"@albumEmpty": {},
|
||||
"albumPageTitle": "Album",
|
||||
|
@ -1552,5 +1538,115 @@
|
|||
"settingsViewerQuickActionEmpty": "Ingen knapper",
|
||||
"@settingsViewerQuickActionEmpty": {},
|
||||
"chipActionFilterOut": "Filtrer ud",
|
||||
"@chipActionFilterOut": {}
|
||||
"@chipActionFilterOut": {},
|
||||
"mapStyleOsmHot": "Humanitært OSM",
|
||||
"@mapStyleOsmHot": {},
|
||||
"collectionDeselectSectionTooltip": "Fravælg sektion",
|
||||
"@collectionDeselectSectionTooltip": {},
|
||||
"editEntryLocationDialogImportGpx": "Importér GPX",
|
||||
"@editEntryLocationDialogImportGpx": {},
|
||||
"editEntryLocationDialogTimeShift": "Tidsskift",
|
||||
"@editEntryLocationDialogTimeShift": {},
|
||||
"videoStreamSelectionDialogTrack": "Spor",
|
||||
"@videoStreamSelectionDialogTrack": {},
|
||||
"albumGroupTier": "Efter kategori",
|
||||
"@albumGroupTier": {},
|
||||
"settingsVideoEnableHardwareAcceleration": "Hardwareacceleration",
|
||||
"@settingsVideoEnableHardwareAcceleration": {},
|
||||
"settingsViewerSectionTitle": "Fremviser",
|
||||
"@settingsViewerSectionTitle": {},
|
||||
"openMapPageTooltip": "Se på kortside",
|
||||
"@openMapPageTooltip": {},
|
||||
"settingsCollectionBurstPatternsTile": "Filnavnmønstre",
|
||||
"@settingsCollectionBurstPatternsTile": {},
|
||||
"wallpaperUseScrollEffect": "Brug rulleeffekt på startside",
|
||||
"@wallpaperUseScrollEffect": {},
|
||||
"editEntryDateDialogSourceFileModifiedDate": "Filens ændringsdato",
|
||||
"@editEntryDateDialogSourceFileModifiedDate": {},
|
||||
"editEntryDateDialogShift": "Skift",
|
||||
"@editEntryDateDialogShift": {},
|
||||
"chipActionDecompose": "Split",
|
||||
"@chipActionDecompose": {},
|
||||
"coordinateFormatDdm": "DDM",
|
||||
"@coordinateFormatDdm": {},
|
||||
"videoActionShowNextFrame": "Vis næste frame",
|
||||
"@videoActionShowNextFrame": {},
|
||||
"mapStyleStamenWatercolor": "Stamen Watercolor",
|
||||
"@mapStyleStamenWatercolor": {},
|
||||
"drawerCollectionAll": "Alle samlinger",
|
||||
"@drawerCollectionAll": {},
|
||||
"settingsThumbnailShowVideoDuration": "Vis videovarighed",
|
||||
"@settingsThumbnailShowVideoDuration": {},
|
||||
"settingsThemeColorHighlights": "Farvemarkeringer",
|
||||
"@settingsThemeColorHighlights": {},
|
||||
"viewerInfoSearchEmpty": "Ingen matchende nøgler",
|
||||
"@viewerInfoSearchEmpty": {},
|
||||
"removeEntryMetadataDialogAll": "Alle",
|
||||
"@removeEntryMetadataDialogAll": {},
|
||||
"aboutCreditsSectionTitle": "Kreditering",
|
||||
"@aboutCreditsSectionTitle": {},
|
||||
"settingsCollectionQuickActionTabSelecting": "Valg",
|
||||
"@settingsCollectionQuickActionTabSelecting": {},
|
||||
"settingsCollectionQuickActionTabBrowsing": "Browsing",
|
||||
"@settingsCollectionQuickActionTabBrowsing": {},
|
||||
"settingsViewerQuickActionEditorBanner": "Tryk og hold for at flytte knapper og vælge, hvilke handlinger der vises i fremviseren.",
|
||||
"@settingsViewerQuickActionEditorBanner": {},
|
||||
"settingsAllowInstalledAppAccess": "Tillad adgang til app-lager",
|
||||
"@settingsAllowInstalledAppAccess": {},
|
||||
"viewerInfoBackToViewerTooltip": "Tilbage til fremviser",
|
||||
"@viewerInfoBackToViewerTooltip": {},
|
||||
"videoActionShowPreviousFrame": "Vis forrige frame",
|
||||
"@videoActionShowPreviousFrame": {},
|
||||
"collectionSelectSectionTooltip": "Vælg sektion",
|
||||
"@collectionSelectSectionTooltip": {},
|
||||
"videoStreamSelectionDialogNoSelection": "Der er ingen andre spor.",
|
||||
"@videoStreamSelectionDialogNoSelection": {},
|
||||
"addShortcutDialogLabel": "Genvejsetiket",
|
||||
"@addShortcutDialogLabel": {},
|
||||
"moveUndatedConfirmationDialogMessage": "Gem elementdatoer, før du fortsætter?",
|
||||
"@moveUndatedConfirmationDialogMessage": {},
|
||||
"albumMimeTypeMixed": "Blandet",
|
||||
"@albumMimeTypeMixed": {},
|
||||
"videoActionCaptureFrame": "Tag billede af frame",
|
||||
"@videoActionCaptureFrame": {},
|
||||
"videoActionSelectStreams": "Vælg spor",
|
||||
"@videoActionSelectStreams": {},
|
||||
"videoActionABRepeat": "A-B gentagelse",
|
||||
"@videoActionABRepeat": {},
|
||||
"viewerActionLock": "Lås fremviser",
|
||||
"@viewerActionLock": {},
|
||||
"viewerActionUnlock": "Oplås fremviser",
|
||||
"@viewerActionUnlock": {},
|
||||
"keepScreenOnViewerOnly": "Kun fremvisningsside",
|
||||
"@keepScreenOnViewerOnly": {},
|
||||
"widgetOpenPageViewer": "Åbn fremviser",
|
||||
"@widgetOpenPageViewer": {},
|
||||
"sortByPath": "Efter sti",
|
||||
"@sortByPath": {},
|
||||
"searchFormatSectionTitle": "Formater",
|
||||
"@searchFormatSectionTitle": {},
|
||||
"createButtonLabel": "OPRET",
|
||||
"@createButtonLabel": {},
|
||||
"chipActionGroup": "Ændr gruppering",
|
||||
"@chipActionGroup": {},
|
||||
"chipActionCreateGroup": "Opret gruppe",
|
||||
"@chipActionCreateGroup": {},
|
||||
"albumTierGroups": "Grupper",
|
||||
"@albumTierGroups": {},
|
||||
"newGroupDialogTitle": "Ny gruppe",
|
||||
"@newGroupDialogTitle": {},
|
||||
"newGroupDialogNameLabel": "Gruppenavn",
|
||||
"@newGroupDialogNameLabel": {},
|
||||
"groupAlreadyExists": "Gruppen findes allerede",
|
||||
"@groupAlreadyExists": {},
|
||||
"groupEmpty": "Ingen grupper",
|
||||
"@groupEmpty": {},
|
||||
"groupPickerTitle": "Vælg gruppe",
|
||||
"@groupPickerTitle": {},
|
||||
"groupPickerUseThisGroupButton": "Brug denne gruppe",
|
||||
"@groupPickerUseThisGroupButton": {},
|
||||
"ungrouped": "Gruppe opløst",
|
||||
"@ungrouped": {},
|
||||
"sectionNone": "Ingen sektioner",
|
||||
"@sectionNone": {}
|
||||
}
|
||||
|
|
|
@ -463,7 +463,7 @@
|
|||
"@menuActionStats": {},
|
||||
"viewDialogSortSectionTitle": "Sortieren",
|
||||
"@viewDialogSortSectionTitle": {},
|
||||
"viewDialogGroupSectionTitle": "Gruppe",
|
||||
"viewDialogGroupSectionTitle": "Abschnitte",
|
||||
"@viewDialogGroupSectionTitle": {},
|
||||
"viewDialogLayoutSectionTitle": "Layout",
|
||||
"@viewDialogLayoutSectionTitle": {},
|
||||
|
@ -557,8 +557,6 @@
|
|||
"@collectionGroupMonth": {},
|
||||
"collectionGroupDay": "Nach Tag",
|
||||
"@collectionGroupDay": {},
|
||||
"collectionGroupNone": "Nicht gruppieren",
|
||||
"@collectionGroupNone": {},
|
||||
"sectionUnknown": "Unbekannt",
|
||||
"@sectionUnknown": {},
|
||||
"dateToday": "Heute",
|
||||
|
@ -661,8 +659,6 @@
|
|||
"@albumGroupType": {},
|
||||
"albumGroupVolume": "Nach Speichervolumen",
|
||||
"@albumGroupVolume": {},
|
||||
"albumGroupNone": "Nicht gruppieren",
|
||||
"@albumGroupNone": {},
|
||||
"albumMimeTypeMixed": "Gemischt",
|
||||
"@albumMimeTypeMixed": {},
|
||||
"albumPickPageTitleCopy": "In Album kopieren",
|
||||
|
@ -1111,16 +1107,6 @@
|
|||
"@panoramaDisableSensorControl": {},
|
||||
"sourceViewerPageTitle": "Quelle",
|
||||
"@sourceViewerPageTitle": {},
|
||||
"filePickerShowHiddenFiles": "Versteckte Dateien anzeigen",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Versteckte Dateien nicht anzeigen",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"filePickerOpenFrom": "Öffnen von",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "Keine Elemente",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerUseThisFolder": "Diesen Ordner verwenden",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"widgetOpenPageCollection": "Sammlung öffnen",
|
||||
"@widgetOpenPageCollection": {},
|
||||
"durationDialogSeconds": "Sekunden",
|
||||
|
@ -1410,5 +1396,39 @@
|
|||
"chipActionDecompose": "Aufschlüsseln",
|
||||
"@chipActionDecompose": {},
|
||||
"editEntryLocationDialogImportGpx": "GPX importieren",
|
||||
"@editEntryLocationDialogImportGpx": {}
|
||||
"@editEntryLocationDialogImportGpx": {},
|
||||
"editEntryLocationDialogTimeShift": "Zeitverschiebung",
|
||||
"@editEntryLocationDialogTimeShift": {},
|
||||
"removeEntryMetadataDialogAll": "Alle",
|
||||
"@removeEntryMetadataDialogAll": {},
|
||||
"searchFormatSectionTitle": "Formate",
|
||||
"@searchFormatSectionTitle": {},
|
||||
"sortByPath": "Nach Pfad",
|
||||
"@sortByPath": {},
|
||||
"groupEmpty": "Keine Gruppen",
|
||||
"@groupEmpty": {},
|
||||
"newGroupDialogTitle": "Neue Gruppe",
|
||||
"@newGroupDialogTitle": {},
|
||||
"coordinateFormatDdm": "DDM",
|
||||
"@coordinateFormatDdm": {},
|
||||
"createButtonLabel": "ERSTELLEN",
|
||||
"@createButtonLabel": {},
|
||||
"chipActionGroup": "Gruppe",
|
||||
"@chipActionGroup": {},
|
||||
"chipActionCreateGroup": "Gruppe erstellen",
|
||||
"@chipActionCreateGroup": {},
|
||||
"albumTierGroups": "Gruppen",
|
||||
"@albumTierGroups": {},
|
||||
"sectionNone": "Keine Abschnitte",
|
||||
"@sectionNone": {},
|
||||
"newGroupDialogNameLabel": "Gruppenname",
|
||||
"@newGroupDialogNameLabel": {},
|
||||
"groupAlreadyExists": "Gruppe existiert bereits",
|
||||
"@groupAlreadyExists": {},
|
||||
"ungrouped": "Nicht gruppiert",
|
||||
"@ungrouped": {},
|
||||
"groupPickerTitle": "Gruppe auswählen",
|
||||
"@groupPickerTitle": {},
|
||||
"groupPickerUseThisGroupButton": "Diese Gruppe verwenden",
|
||||
"@groupPickerUseThisGroupButton": {}
|
||||
}
|
||||
|
|
|
@ -377,9 +377,9 @@
|
|||
"@renameProcessorCounter": {},
|
||||
"renameProcessorName": "Όνομα",
|
||||
"@renameProcessorName": {},
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Διαγραφή αυτού του άλμπουμ και του περιεχομένου του;} other{Διαγράψτε αυτό το άλμπουμ και όλα τα {count} στοιχεία του;}}",
|
||||
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Διαγράψτε αυτό το άλμπουμ και το αντικείμενο σε αυτό;} other{Διαγράψτε αυτό το άλμπουμ και τα {count} αντικείμενα σε αυτό;}}",
|
||||
"@deleteSingleAlbumConfirmationDialogMessage": {},
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Διαγράψτε αυτά τα άλμπουμ και το περιεχόμενό τους;} other{Διαγράψτε αυτά τα άλμπουμ και όλα τα {count} στοιχεία τους;}}",
|
||||
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Διαγράψτε αυτά τα άλμπουμ και τα αντικείμενα σε αυτά;} other{Διαγράψτε αυτά τα άλμπουμ και τα {count} αντικείμενα σε αυτά;}}",
|
||||
"@deleteMultiAlbumConfirmationDialogMessage": {},
|
||||
"exportEntryDialogFormat": "Μορφή:",
|
||||
"@exportEntryDialogFormat": {},
|
||||
|
@ -463,7 +463,7 @@
|
|||
"@menuActionStats": {},
|
||||
"viewDialogSortSectionTitle": "Ταξινομηση",
|
||||
"@viewDialogSortSectionTitle": {},
|
||||
"viewDialogGroupSectionTitle": "Ομαδοποιηση",
|
||||
"viewDialogGroupSectionTitle": "Τμήματα",
|
||||
"@viewDialogGroupSectionTitle": {},
|
||||
"viewDialogLayoutSectionTitle": "Διαταξη",
|
||||
"@viewDialogLayoutSectionTitle": {},
|
||||
|
@ -557,8 +557,6 @@
|
|||
"@collectionGroupMonth": {},
|
||||
"collectionGroupDay": "Ανά ημέρα",
|
||||
"@collectionGroupDay": {},
|
||||
"collectionGroupNone": "Να μην γίνει ομαδοποίηση",
|
||||
"@collectionGroupNone": {},
|
||||
"sectionUnknown": "Χωρίς λεπτομέρειες",
|
||||
"@sectionUnknown": {},
|
||||
"dateToday": "Σήμερα",
|
||||
|
@ -661,8 +659,6 @@
|
|||
"@albumGroupType": {},
|
||||
"albumGroupVolume": "Ανά αποθηκευτική μονάδα",
|
||||
"@albumGroupVolume": {},
|
||||
"albumGroupNone": "Να μην γίνει ομαδοποίηση",
|
||||
"@albumGroupNone": {},
|
||||
"albumMimeTypeMixed": "Μικτα",
|
||||
"@albumMimeTypeMixed": {},
|
||||
"albumPickPageTitleCopy": "Αντιγραφή στο άλμπουμ",
|
||||
|
@ -873,7 +869,7 @@
|
|||
"@settingsSlideshowAnimatedZoomEffect": {},
|
||||
"settingsSlideshowTransitionTile": "Μετάβαση",
|
||||
"@settingsSlideshowTransitionTile": {},
|
||||
"settingsSlideshowIntervalTile": "Διάρκεια",
|
||||
"settingsSlideshowIntervalTile": "Διάστημα",
|
||||
"@settingsSlideshowIntervalTile": {},
|
||||
"settingsSlideshowVideoPlaybackTile": "Αναπαραγωγή βίντεο",
|
||||
"@settingsSlideshowVideoPlaybackTile": {},
|
||||
|
@ -1107,16 +1103,6 @@
|
|||
"@panoramaDisableSensorControl": {},
|
||||
"sourceViewerPageTitle": "Πηγη",
|
||||
"@sourceViewerPageTitle": {},
|
||||
"filePickerShowHiddenFiles": "Εμφάνιση κρυφών αρχείων",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"filePickerDoNotShowHiddenFiles": "Να μην εμφανίζονται τα κρυφά αρχεία",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"filePickerOpenFrom": "Άνοιγμα από",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "Κανένα στοιχείο",
|
||||
"@filePickerNoItems": {},
|
||||
"filePickerUseThisFolder": "Χρησιμοποιήστε αυτόν τον φάκελο",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"widgetOpenPageCollection": "Ανοιχτή συλλογή",
|
||||
"@widgetOpenPageCollection": {},
|
||||
"durationDialogSeconds": "Δευτερόλεπτα",
|
||||
|
@ -1320,5 +1306,129 @@
|
|||
"overlayHistogramLuminance": "Φωτεινότητα",
|
||||
"@overlayHistogramLuminance": {},
|
||||
"chipActionShowCollection": "Εμφάνιση στη Συλλογή",
|
||||
"@chipActionShowCollection": {}
|
||||
"@chipActionShowCollection": {},
|
||||
"mapAttributionOsmHot": "Πλακάκια από [HOT](https://www.hotosm.org/) • Φιλοξενείται από [OSM France](https://openstreetmap.fr/)",
|
||||
"@mapAttributionOsmHot": {},
|
||||
"mapAttributionStamen": "Πλακάκια από [Stamen Design](https://stamen.com), [CC BY 3.0](https://creativecommons.org/licenses/by/3.0)",
|
||||
"@mapAttributionStamen": {},
|
||||
"editEntryLocationDialogImportGpx": "Εισαγωγή GPX",
|
||||
"@editEntryLocationDialogImportGpx": {},
|
||||
"editEntryLocationDialogTimeShift": "Χρονική μετατόπιση",
|
||||
"@editEntryLocationDialogTimeShift": {},
|
||||
"aboutDataUsageData": "Δεδομένα",
|
||||
"@aboutDataUsageData": {},
|
||||
"collectionActionAddDynamicAlbum": "Προσθήκη δυναμικού άλμπουμ",
|
||||
"@collectionActionAddDynamicAlbum": {},
|
||||
"collectionActionSetHome": "Ορίστε ως σπίτι",
|
||||
"@collectionActionSetHome": {},
|
||||
"sortByPath": "Από τη διαδρομή",
|
||||
"@sortByPath": {},
|
||||
"explorerPageTitle": "Εξερεύνηση",
|
||||
"@explorerPageTitle": {},
|
||||
"searchFormatSectionTitle": "Μορφές",
|
||||
"@searchFormatSectionTitle": {},
|
||||
"settingsViewerShowHistogram": "Εμφάνιση ιστογράμματος",
|
||||
"@settingsViewerShowHistogram": {},
|
||||
"settingsForceWesternArabicNumeralsTile": "Δύναμη αραβικών αριθμών",
|
||||
"@settingsForceWesternArabicNumeralsTile": {},
|
||||
"mapAttributionOsmData": "Δεδομένα χάρτη © [OpenStreetMap](https://www.openstreetmap.org/copyright) συνεισφέροντες",
|
||||
"@mapAttributionOsmData": {},
|
||||
"mapAttributionOsmLiberty": "Πλακάκια από [OpenMapTiles](https://www.openmaptiles.org/), [CC BY](http://creativecommons.org/licenses/by/4.0) • Φιλοξενείται από [OSM Americana](https://tile.ourmap.us)",
|
||||
"@mapAttributionOsmLiberty": {},
|
||||
"aboutDataUsageMisc": "Διάφορα στοιχεία",
|
||||
"@aboutDataUsageMisc": {},
|
||||
"selectStorageVolumeDialogTitle": "Επιλέξτε Αποθήκευση",
|
||||
"@selectStorageVolumeDialogTitle": {},
|
||||
"renameProcessorHash": "Κατακερματισμός",
|
||||
"@renameProcessorHash": {},
|
||||
"aboutDataUsageSectionTitle": "Χρήση δεδομένων",
|
||||
"@aboutDataUsageSectionTitle": {},
|
||||
"chipActionDecompose": "Διάσπαση",
|
||||
"@chipActionDecompose": {},
|
||||
"entryActionCast": "Εκτέλεση",
|
||||
"@entryActionCast": {},
|
||||
"videoActionShowPreviousFrame": "Εμφάνιση προηγούμενου καρέ",
|
||||
"@videoActionShowPreviousFrame": {},
|
||||
"videoActionABRepeat": "Επανάληψη A-B",
|
||||
"@videoActionABRepeat": {},
|
||||
"videoRepeatActionSetStart": "Ορισμός έναρξης",
|
||||
"@videoRepeatActionSetStart": {},
|
||||
"videoRepeatActionSetEnd": "Ορισμός τέλους",
|
||||
"@videoRepeatActionSetEnd": {},
|
||||
"albumTierDynamic": "Δυναμικό",
|
||||
"@albumTierDynamic": {},
|
||||
"newDynamicAlbumDialogTitle": "Νέο δυναμικό άλμπουμ",
|
||||
"@newDynamicAlbumDialogTitle": {},
|
||||
"dynamicAlbumAlreadyExists": "Το δυναμικό άλμπουμ υπάρχει ήδη",
|
||||
"@dynamicAlbumAlreadyExists": {},
|
||||
"setHomeCustom": "Προσαρμοσμένο",
|
||||
"@setHomeCustom": {},
|
||||
"settingsThumbnailShowHdrIcon": "Εμφάνιση εικονιδίου HDR",
|
||||
"@settingsThumbnailShowHdrIcon": {},
|
||||
"coordinateFormatDdm": "DDM",
|
||||
"@coordinateFormatDdm": {},
|
||||
"chipActionRemove": "Αφαίρεση",
|
||||
"@chipActionRemove": {},
|
||||
"newAlbumDialogAlbumAlreadyExistsHelper": "Το άλμπουμ υπάρχει ήδη",
|
||||
"@newAlbumDialogAlbumAlreadyExistsHelper": {},
|
||||
"explorerActionSelectStorageVolume": "Επιλέξτε αποθηκευτικό χώρο",
|
||||
"@explorerActionSelectStorageVolume": {},
|
||||
"removeEntryMetadataDialogAll": "Όλα",
|
||||
"@removeEntryMetadataDialogAll": {},
|
||||
"aboutDataUsageCache": "Κρυφή μνήμη",
|
||||
"@aboutDataUsageCache": {},
|
||||
"castDialogTitle": "Συσκευές Απεικόνισης",
|
||||
"@castDialogTitle": {},
|
||||
"aboutDataUsageDatabase": "Βάση δεδομένων",
|
||||
"@aboutDataUsageDatabase": {},
|
||||
"sortByDuration": "Με διάρκεια",
|
||||
"@sortByDuration": {},
|
||||
"appExportDynamicAlbums": "Δυναμικά άλμπουμ",
|
||||
"@appExportDynamicAlbums": {},
|
||||
"mapStyleOsmLiberty": "OSM Ελευθερία",
|
||||
"@mapStyleOsmLiberty": {},
|
||||
"mapStyleOpenTopoMap": "OpenTopoMap",
|
||||
"@mapStyleOpenTopoMap": {},
|
||||
"aboutDataUsageInternal": "Εσωτερικά",
|
||||
"@aboutDataUsageInternal": {},
|
||||
"aboutDataUsageExternal": "Εξωτερικά",
|
||||
"@aboutDataUsageExternal": {},
|
||||
"aboutDataUsageClearCache": "Εκκαθάριση μνήμης",
|
||||
"@aboutDataUsageClearCache": {},
|
||||
"videoActionShowNextFrame": "Εμφάνιση επόμενου καρέ",
|
||||
"@videoActionShowNextFrame": {},
|
||||
"mapAttributionOpenTopoMap": "[SRTM](https://www.earthdata.nasa.gov/sensors/srtm) | Πλακάκια από [OpenTopoMap](https://opentopomap.org/), [CC BY-SA](https://creativecommons.org/licenses/by-sa/3.0/)",
|
||||
"@mapAttributionOpenTopoMap": {},
|
||||
"sortOrderShortestFirst": "Ο συντομότερος πρώτος",
|
||||
"@sortOrderShortestFirst": {},
|
||||
"sortOrderLongestFirst": "Μακρύτερο πρώτο",
|
||||
"@sortOrderLongestFirst": {},
|
||||
"stopTooltip": "Σταμάτησε",
|
||||
"@stopTooltip": {},
|
||||
"chipActionGoToExplorerPage": "Εμφάνιση στην Εξερεύνηση",
|
||||
"@chipActionGoToExplorerPage": {},
|
||||
"chipActionCreateGroup": "Δημιουργία ομάδας",
|
||||
"@chipActionCreateGroup": {},
|
||||
"newGroupDialogNameLabel": "Όνομα ομάδας",
|
||||
"@newGroupDialogNameLabel": {},
|
||||
"groupPickerUseThisGroupButton": "Χρησιμοποιήστε αυτή την ομάδα",
|
||||
"@groupPickerUseThisGroupButton": {},
|
||||
"sectionNone": "Όχι τμήματα",
|
||||
"@sectionNone": {},
|
||||
"createButtonLabel": "ΔΗΜΙΟΥΡΓΙΑ",
|
||||
"@createButtonLabel": {},
|
||||
"chipActionGroup": "Ομάδα",
|
||||
"@chipActionGroup": {},
|
||||
"albumTierGroups": "Ομάδες",
|
||||
"@albumTierGroups": {},
|
||||
"newGroupDialogTitle": "Νέα Ομάδα",
|
||||
"@newGroupDialogTitle": {},
|
||||
"groupAlreadyExists": "Η ομάδα υπάρχει ήδη",
|
||||
"@groupAlreadyExists": {},
|
||||
"groupEmpty": "Όχι ομάδες",
|
||||
"@groupEmpty": {},
|
||||
"ungrouped": "Μη ομαδοποιημένο",
|
||||
"@ungrouped": {},
|
||||
"groupPickerTitle": "Επιλογή ομάδας",
|
||||
"@groupPickerTitle": {}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
},
|
||||
|
||||
"applyButtonLabel": "APPLY",
|
||||
"createButtonLabel": "CREATE",
|
||||
"deleteButtonLabel": "DELETE",
|
||||
"nextButtonLabel": "NEXT",
|
||||
"showButtonLabel": "SHOW",
|
||||
|
@ -104,9 +105,11 @@
|
|||
"chipActionLock": "Lock",
|
||||
"chipActionPin": "Pin to top",
|
||||
"chipActionUnpin": "Unpin from top",
|
||||
"chipActionGroup": "Change grouping",
|
||||
"chipActionRename": "Rename",
|
||||
"chipActionSetCover": "Set cover",
|
||||
"chipActionShowCountryStates": "Show states",
|
||||
"chipActionCreateGroup": "Create group",
|
||||
"chipActionCreateAlbum": "Create album",
|
||||
"chipActionCreateVault": "Create vault",
|
||||
"chipActionConfigureVault": "Configure vault",
|
||||
|
@ -208,6 +211,7 @@
|
|||
|
||||
"albumTierNew": "New",
|
||||
"albumTierPinned": "Pinned",
|
||||
"albumTierGroups": "Groups",
|
||||
"albumTierSpecial": "Common",
|
||||
"albumTierApps": "Apps",
|
||||
"albumTierVaults": "Vaults",
|
||||
|
@ -443,6 +447,14 @@
|
|||
"newDynamicAlbumDialogTitle": "New Dynamic Album",
|
||||
"dynamicAlbumAlreadyExists": "Dynamic album already exists",
|
||||
|
||||
"newGroupDialogTitle": "New Group",
|
||||
"newGroupDialogNameLabel": "Group name",
|
||||
"groupAlreadyExists": "Group already exists",
|
||||
"groupEmpty": "No groups",
|
||||
"ungrouped": "Ungrouped",
|
||||
"groupPickerTitle": "Pick Group",
|
||||
"groupPickerUseThisGroupButton": "Use this group",
|
||||
|
||||
"newVaultWarningDialogMessage": "Items in vaults are only available to this app and no others.\n\nIf you uninstall this app, or clear this app data, you will lose all these items.",
|
||||
"newVaultDialogTitle": "New Vault",
|
||||
"configureVaultDialogTitle": "Configure Vault",
|
||||
|
@ -558,7 +570,7 @@
|
|||
"menuActionStats": "Stats",
|
||||
|
||||
"viewDialogSortSectionTitle": "Sort",
|
||||
"viewDialogGroupSectionTitle": "Group",
|
||||
"viewDialogGroupSectionTitle": "Sections",
|
||||
"viewDialogLayoutSectionTitle": "Layout",
|
||||
"viewDialogReverseSortOrder": "Reverse sort order",
|
||||
|
||||
|
@ -630,8 +642,8 @@
|
|||
"collectionGroupAlbum": "By album",
|
||||
"collectionGroupMonth": "By month",
|
||||
"collectionGroupDay": "By day",
|
||||
"collectionGroupNone": "Do not group",
|
||||
|
||||
"sectionNone": "No sections",
|
||||
"sectionUnknown": "Unknown",
|
||||
"dateToday": "Today",
|
||||
"dateYesterday": "Yesterday",
|
||||
|
@ -755,9 +767,10 @@
|
|||
"sortByName": "By name",
|
||||
"sortByItemCount": "By item count",
|
||||
"sortBySize": "By size",
|
||||
"sortByAlbumFileName": "By album & file name",
|
||||
"sortByAlbumFileName": "By album & item title",
|
||||
"sortByRating": "By rating",
|
||||
"sortByDuration": "By duration",
|
||||
"sortByPath": "By path",
|
||||
|
||||
"sortOrderNewestFirst": "Newest first",
|
||||
"sortOrderOldestFirst": "Oldest first",
|
||||
|
@ -773,7 +786,6 @@
|
|||
"albumGroupTier": "By tier",
|
||||
"albumGroupType": "By type",
|
||||
"albumGroupVolume": "By storage volume",
|
||||
"albumGroupNone": "Do not group",
|
||||
|
||||
"albumMimeTypeMixed": "Mixed",
|
||||
|
||||
|
@ -815,6 +827,7 @@
|
|||
"searchCollectionFieldHint": "Search collection",
|
||||
"searchRecentSectionTitle": "Recent",
|
||||
"searchDateSectionTitle": "Date",
|
||||
"searchFormatSectionTitle": "Formats",
|
||||
"searchAlbumsSectionTitle": "Albums",
|
||||
"searchCountriesSectionTitle": "Countries",
|
||||
"searchStatesSectionTitle": "States",
|
||||
|
@ -1099,11 +1112,5 @@
|
|||
"panoramaEnableSensorControl": "Enable sensor control",
|
||||
"panoramaDisableSensorControl": "Disable sensor control",
|
||||
|
||||
"sourceViewerPageTitle": "Source",
|
||||
|
||||
"filePickerShowHiddenFiles": "Show hidden files",
|
||||
"filePickerDoNotShowHiddenFiles": "Don’t show hidden files",
|
||||
"filePickerOpenFrom": "Open from",
|
||||
"filePickerNoItems": "No items",
|
||||
"filePickerUseThisFolder": "Use this folder"
|
||||
"sourceViewerPageTitle": "Source"
|
||||
}
|
||||
|
|
|
@ -921,8 +921,6 @@
|
|||
"@collectionGroupMonth": {},
|
||||
"collectionGroupDay": "𐑚𐑲 𐑛𐑱",
|
||||
"@collectionGroupDay": {},
|
||||
"collectionGroupNone": "𐑛𐑵 𐑯𐑪𐑑 𐑜𐑮𐑵𐑐",
|
||||
"@collectionGroupNone": {},
|
||||
"sectionUnknown": "𐑳𐑯𐑯𐑴𐑯",
|
||||
"@sectionUnknown": {},
|
||||
"dateToday": "𐑑𐑫𐑛𐑱",
|
||||
|
@ -1085,8 +1083,6 @@
|
|||
"@albumGroupType": {},
|
||||
"albumGroupVolume": "𐑚𐑲 𐑕𐑑𐑹𐑦𐑡 𐑝𐑪𐑤𐑿𐑥",
|
||||
"@albumGroupVolume": {},
|
||||
"albumGroupNone": "𐑛𐑵 𐑯𐑪𐑑 𐑜𐑮𐑵𐑐",
|
||||
"@albumGroupNone": {},
|
||||
"albumMimeTypeMixed": "𐑥𐑦𐑒𐑕𐑑",
|
||||
"@albumMimeTypeMixed": {},
|
||||
"albumPickPageTitleCopy": "𐑒𐑪𐑐𐑦 𐑑 𐑨𐑤𐑚𐑩𐑥",
|
||||
|
@ -1279,8 +1275,6 @@
|
|||
"@settingsViewerShowMinimap": {},
|
||||
"settingsViewerShowInformation": "𐑖𐑴 𐑦𐑯𐑓𐑼𐑥𐑱𐑖𐑩𐑯",
|
||||
"@settingsViewerShowInformation": {},
|
||||
"filePickerUseThisFolder": "𐑿𐑟 𐑞𐑦𐑕 𐑓𐑴𐑤𐑛𐑼",
|
||||
"@filePickerUseThisFolder": {},
|
||||
"settingsViewerShowInformationSubtitle": "𐑖𐑴 𐑑𐑲𐑑𐑩𐑤, 𐑛𐑱𐑑, 𐑤𐑴𐑒𐑱𐑖𐑩𐑯, 𐑯𐑯𐑯",
|
||||
"@settingsViewerShowInformationSubtitle": {},
|
||||
"settingsViewerShowOverlayThumbnails": "𐑖𐑴 𐑔𐑳𐑥𐑯𐑱𐑤𐑟",
|
||||
|
@ -1600,14 +1594,6 @@
|
|||
"@panoramaDisableSensorControl": {},
|
||||
"sourceViewerPageTitle": "𐑕𐑹𐑕",
|
||||
"@sourceViewerPageTitle": {},
|
||||
"filePickerShowHiddenFiles": "𐑖𐑴 𐑣𐑦𐑛𐑩𐑯 𐑓𐑲𐑤𐑟",
|
||||
"@filePickerShowHiddenFiles": {},
|
||||
"filePickerDoNotShowHiddenFiles": "𐑛𐑴𐑯'𐑑 𐑖𐑴 𐑣𐑦𐑛𐑩𐑯 𐑓𐑲𐑤𐑟",
|
||||
"@filePickerDoNotShowHiddenFiles": {},
|
||||
"filePickerOpenFrom": "𐑴𐑐𐑩𐑯 𐑓𐑮𐑪𐑥",
|
||||
"@filePickerOpenFrom": {},
|
||||
"filePickerNoItems": "𐑯𐑴 𐑲𐑑𐑩𐑥𐑟",
|
||||
"@filePickerNoItems": {},
|
||||
"videoActionShowPreviousFrame": "𐑖𐑴 𐑐𐑮𐑰𐑝𐑾𐑕 𐑓𐑮𐑱𐑥",
|
||||
"@videoActionShowPreviousFrame": {},
|
||||
"videoActionShowNextFrame": "𐑖𐑴 𐑯𐑧𐑒𐑕𐑑 𐑓𐑮𐑱𐑥",
|
||||
|
|