Compare commits

..

858 commits

Author SHA1 Message Date
9037f8e610 Aggiorna README.md
Some checks failed
Quality check / Flutter analysis (push) Has been cancelled
Quality check / CodeQL analysis (java-kotlin) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
2025-08-14 17:41:59 +08:00
05bb77a793 Aggiorna README.md
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
2025-08-14 16:51:39 +08:00
2f0f3da2fa Aggiorna README.md
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
2025-08-14 16:44:51 +08:00
31f85c3e01 Aggiorna README.md
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
2025-08-14 16:41:04 +08:00
84a822022a Aggiorna README.md
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
2025-08-14 16:07:15 +08:00
94c83914a4 Aggiorna README.md
Some checks failed
Quality check / Flutter analysis (push) Has been cancelled
Quality check / CodeQL analysis (java-kotlin) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
2025-06-11 16:40:56 +08:00
a461e2c55f Aggiorna README.md
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
2025-06-11 16:40:17 +08:00
99c9f85eaf Aggiorna README.md
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
2025-06-11 16:39:24 +08:00
848ad5220e Aggiorna README.md
Some checks are pending
Quality check / Flutter analysis (push) Waiting to run
Quality check / CodeQL analysis (java-kotlin) (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
2025-06-11 16:38:06 +08:00
Thibault Deckers
7577466978 rescale large thumbnails decoded as is;
check thumbnail bitmap size before getting raw bytes
2025-06-10 20:45:59 +02:00
Thibault Deckers
dfcaf4d35a upgrades 2025-06-09 18:47:11 +02:00
Thibault Deckers
171394056f #1608 query bar unfocus when navigating away 2025-06-07 20:12:54 +02:00
dependabot[bot]
60211545e1
Bump github/codeql-action from 3.28.18 to 3.28.19 (#1604)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.18 to 3.28.19.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](ff0a06e83c...fca7ace96b)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-08 01:03:54 +09:00
Thibault Deckers
edbf9744f5 #1612 info: show matching dynamic albums 2025-06-07 18:03:32 +02:00
Thibault Deckers
d272c82454 version bump 2025-06-02 23:52:21 +02:00
Thibault Deckers
20b4f10b62 l10n 2025-06-02 23:48:57 +02:00
Weblate (bot)
3db0478be2
l10n by weblate (#1587)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Jamil Farajov <jamilfarajov@gmail.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Miquel Martí <miquelmarti111@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Murcielago <weblate.j9bmx@slmail.me>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thit Lwin <thitlwincoder@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: WMatheist <wmatheist@protonmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Yurt Page <yurtpage@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: vm <varga.m007@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-06-03 06:44:03 +09:00
Thibault Deckers
c9fd71056f removed obsolete plugin 2025-06-02 22:38:14 +02:00
Thibault Deckers
ca2d2c2026 format 2025-06-02 22:25:42 +02:00
Thibault Deckers
2e775b3906 prevent crash from security exception in media content listening 2025-06-02 22:19:51 +02:00
Thibault Deckers
ea3cb3c063 fixed crash when parsing some large media with trailing thumbnail 2025-06-02 19:45:11 +02:00
dependabot[bot]
340ed6a6d9
Bump ossf/scorecard-action from 2.4.1 to 2.4.2 (#1597)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](f49aabe0b5...05b42c6244)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-03 02:26:28 +09:00
Thibault Deckers
5e0f0b59d8 prevent display orientation flip when device rotation is locked 2025-06-02 19:26:13 +02:00
Thibault Deckers
a0163001bd CI: ensure l10n files are generated 2025-06-02 00:03:47 +02:00
Thibault Deckers
1222a711e0 downgraded Flutter to stable v3.27.4 2025-06-01 20:57:46 +02:00
Thibault Deckers
8c3d0f1b83 fixed file extension loss on move via tree doc 2025-05-31 20:20:07 +02:00
Thibault Deckers
43cb2cd101 android: dependency upgrades 2025-05-31 18:28:50 +02:00
Thibault Deckers
81a2b84c9f app bar layout padding review 2025-05-30 18:03:29 +02:00
Thibault Deckers
bae6d2b7c4 #268 fixed loading group custom covers 2025-05-27 22:00:02 +02:00
Thibault Deckers
9a377ed7bc #268 albums: fixed hiding groups/dynamics when they are explicitly hidden 2025-05-27 19:50:28 +02:00
Thibault Deckers
1119fa1407 #268 groups: listening to source/dynamics to remove groups with obsolete content 2025-05-27 00:28:41 +02:00
Thibault Deckers
7b0f72d6ee chip cover detail layout fixes 2025-05-26 23:40:37 +02:00
Thibault Deckers
6f9a581d99 minor 2025-05-26 23:39:43 +02:00
Thibault Deckers
b6faf36671 l10n 2025-05-26 19:20:32 +02:00
dependabot[bot]
17f3ec437c
Bump github/codeql-action from 3.28.17 to 3.28.18 (#1561)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.17 to 3.28.18.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](60168efe1c...ff0a06e83c)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 02:18:56 +09:00
Weblate (bot)
3ec5b96bc9
l10n by weblate (#1559)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Miquel Martí <miquelmarti111@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Murcielago <weblate.j9bmx@slmail.me>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thit Lwin <thitlwincoder@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: WMatheist <wmatheist@protonmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Yurt Page <yurtpage@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: vm <varga.m007@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-05-27 02:18:12 +09:00
Thibault Deckers
540fbbc2b4 #268 clearer action name 2025-05-26 18:40:24 +02:00
Thibault Deckers
f355efefc1 fixed dependency 2025-05-23 19:40:34 +02:00
Thibault Deckers
3bcaab9a4b minor 2025-05-23 18:59:49 +02:00
Thibault Deckers
ef091b9932 #1576 fixed title sort wording 2025-05-20 18:54:26 +02:00
Thibault Deckers
33667e7e6e #1575 fixed home page when launching app as media picker 2025-05-19 22:53:22 +02:00
Thibault Deckers
2a3cce422b replaced flutter_markdown by flutter_markdown_plus 2025-05-14 23:01:11 +02:00
Thibault Deckers
e0af21f098 version bump 2025-05-14 22:18:09 +02:00
Weblate (bot)
8636e4b73e
l10n by weblate (#1556)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Miquel Martí <miquelmarti111@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Murcielago <weblate.j9bmx@slmail.me>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Yurt Page <yurtpage@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-05-15 05:12:01 +09:00
Thibault Deckers
0ad4b2f16f albums: fixed positioning on group change 2025-05-14 22:10:51 +02:00
Thibault Deckers
df63f06897 albums: hide grouped albums containing hidden items only 2025-05-14 22:01:45 +02:00
Thibault Deckers
09df269ee0 albums: show groups to move/copy/export items 2025-05-14 21:52:21 +02:00
dependabot[bot]
39d7587ac9
Bump actions/dependency-review-action from 4.7.0 to 4.7.1 (#1557)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.7.0 to 4.7.1.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](38ecb5b593...da24556b54)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: 4.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-15 04:38:18 +09:00
Thibault Deckers
0f6a8230d8 version bump 2025-05-12 23:09:29 +02:00
Thibault Deckers
a6c1fd52a6 #1554 Collection: sort by storage path 2025-05-12 23:04:14 +02:00
Weblate (bot)
eaa4fe3317
l10n by weblate (#1555)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Miquel Martí <miquelmarti111@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Murcielago <weblate.j9bmx@slmail.me>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Yurt Page <yurtpage@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-05-13 05:43:39 +09:00
Thibault Deckers
5358a38bd8 groups: scroll to top on group change 2025-05-12 21:22:29 +02:00
Thibault Deckers
244c1a293d groups: fixed default view selection when in group 2025-05-12 20:12:20 +02:00
Thibault Deckers
2ef03f1592 search: fixed query suggestions 2025-05-12 19:39:30 +02:00
Thibault Deckers
f2ef5c6f32 minor 2025-05-11 23:47:10 +02:00
Weblate (bot)
b582dbb3b2
l10n by weblate (#1551)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Miquel Martí <miquelmarti111@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Murcielago <weblate.j9bmx@slmail.me>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Yurt Page <yurtpage@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-05-12 06:45:40 +09:00
dependabot[bot]
b864a4dae3
Bump actions/dependency-review-action from 4.6.0 to 4.7.0 (#1553)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](ce3cf9537a...38ecb5b593)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 06:18:47 +09:00
Thibault Deckers
2b01fb41e2 #268 groups: import 2025-05-11 23:17:34 +02:00
Thibault Deckers
244217417b #268 groups: serialization 2025-05-11 23:10:31 +02:00
Thibault Deckers
651b5926dc #268 cover/pins/bookmarks sub to dynamics/groups; dynamics sub to groups;
container filter mixin;
debug: cover/dynamics dump;
2025-05-11 21:57:38 +02:00
Thibault Deckers
27879a900d group fab; prevent empty filter grid scroll 2025-05-06 22:45:03 +02:00
Thibault Deckers
4b87717cd2 #1549 week day filters 2025-05-05 23:12:23 +02:00
Thibault Deckers
91cfe01af3 l10n 2025-05-05 22:33:54 +02:00
Weblate (bot)
cb6ccab6ca
l10n by weblate (#1550)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Miquel Martí <miquelmarti111@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Yurt Page <yurtpage@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-05-06 05:32:11 +09:00
Thibault Deckers
f04c55e901 ignore l10n gen files 2025-05-05 21:35:45 +02:00
Thibault Deckers
8e0c69cd66 removed l10n gen files from git 2025-05-05 21:28:20 +02:00
Thibault Deckers
c31b64535d #268 album grouping (WIP) 2025-05-05 19:14:40 +02:00
Thibault Deckers
89dee8d508 l10n 2025-05-05 19:10:20 +02:00
Weblate (bot)
93c2c7d34d
l10n by weblate (#1542)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Miquel Martí <miquelmarti111@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-05-06 02:07:30 +09:00
dependabot[bot]
573b7c4593
Bump github/codeql-action from 3.28.16 to 3.28.17 (#1547)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.16 to 3.28.17.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](28deaeda66...60168efe1c)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 01:54:06 +09:00
Thibault Deckers
f9f21fbe76 l10n 2025-04-28 19:38:56 +02:00
dependabot[bot]
9cfb4436fa
Bump actions/attest-build-provenance from 2.2.3 to 2.3.0 (#1541)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.2.3 to 2.3.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](c074443f1a...db473fddc0)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-29 02:32:26 +09:00
Weblate (bot)
925998b709
l10n by weblate (#1529)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ur/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Feliks-WR <aq.1428@tuta.io>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slabs37 <p84haghi@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-04-29 02:27:28 +09:00
Thibault Deckers
a8bb2eb69f #1539 locked: review, prevent rename / metadata export 2025-04-27 23:42:40 +02:00
Thibault Deckers
bb6c2c341b revert to Skia rendering engine 2025-04-24 21:41:19 +02:00
dependabot[bot]
cbaaf2fd87
Bump actions/download-artifact from 4.2.1 to 4.3.0 (#1537)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](95815c38cf...d3f86a106a)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 02:29:44 +09:00
dependabot[bot]
e1fad28411
Bump github/codeql-action from 3.28.15 to 3.28.16 (#1536)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.15 to 3.28.16.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](45775bd823...28deaeda66)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 02:29:34 +09:00
dependabot[bot]
bb26b18017
Bump step-security/harden-runner from 2.11.1 to 2.12.0 (#1534)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.11.1 to 2.12.0.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](c6295a65d1...0634a2670c)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-version: 2.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 02:29:23 +09:00
Thibault Deckers
3a6ad33ea1 minor 2025-04-20 19:24:34 +02:00
Thibault Deckers
75421faf46 minor fix 2025-04-20 19:19:04 +02:00
Thibault Deckers
4df4738dd3 #1507 mime type normalization 2025-04-20 18:33:28 +02:00
Thibault Deckers
e8eae7e9db version bump 2025-04-16 23:37:06 +02:00
Thibault Deckers
f47cd57692 l10n 2025-04-16 23:34:30 +02:00
Weblate (bot)
bb332d5adf
l10n by weblate (#1523)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bora Atıcı <boratici.acc@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: VertekPlus <vertekplus@users.noreply.hosted.weblate.org>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-04-17 06:30:17 +09:00
Thibault Deckers
7f54befb72 #1527 fixed concurrent region decoder pool access 2025-04-16 21:08:09 +02:00
Thibault Deckers
af4ca96da8 #1513 albums: sort by path 2025-04-15 21:55:55 +02:00
Thibault Deckers
90d0256bf7 upgraded Flutter to stable v3.29.3 2025-04-14 20:29:56 +02:00
Thibault Deckers
353cde0ee8 #1493 analysis service: ensure init sequence, safer notification update 2025-04-13 22:14:05 +02:00
Thibault Deckers
63130de577 #1507 search: add mime type filters 2025-04-13 19:31:16 +02:00
Thibault Deckers
ad5a9c848d #1507 syntax for extension filtering in title filter 2025-04-13 19:02:29 +02:00
Thibault Deckers
be75d5a284 static analysis fixes 2025-04-13 19:01:31 +02:00
Thibault Deckers
0142ed7f4f l10n 2025-04-13 18:50:34 +02:00
Weblate (bot)
6c2db18af2
l10n by weblate (#1517)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ne/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/he/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Over Barrow <rawixo6748@insfou.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: elid <shopisrael12@gmail.com>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-04-14 01:45:56 +09:00
dependabot[bot]
93e2b1d310
Bump github/codeql-action from 3.28.13 to 3.28.15 (#1519)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.13 to 3.28.15.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](1b549b9259...45775bd823)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 01:43:05 +09:00
dependabot[bot]
9baa4a0441
Bump actions/setup-java from 4.7.0 to 4.7.1 (#1520)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.7.0 to 4.7.1.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](3a4f6e1af5...c5195efecf)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 4.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 01:42:47 +09:00
Thibault Deckers
f123faeee8 use material symbols over material icons 2025-04-13 18:42:17 +02:00
Thibault Deckers
e3ece7425f version bump 2025-04-06 23:13:12 +02:00
Thibault Deckers
ff4c49718e removed DB upgrade temporary code for v1.12.4 users 2025-04-06 23:10:16 +02:00
Thibault Deckers
5598b0a69d l10n: kn 2025-04-06 23:02:40 +02:00
Thibault Deckers
dac91a2d1d l10n: kn 2025-04-06 21:44:41 +02:00
Weblate (bot)
759f666085
l10n by weblate (#1488)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/kn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Prasannakumar T Bhat <pbhat99@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-04-07 02:17:09 +09:00
Thibault Deckers
c89441bb78 enable Impeller rendering engine 2025-04-06 17:56:52 +02:00
Thibault Deckers
969187444b viewer: improved video controller disposing 2025-04-03 22:59:48 +02:00
Thibault Deckers
1fd3d77bf9 fetch encoded bytes for video cover 2025-04-03 22:58:23 +02:00
Thibault Deckers
32202cc603 viewer: reduced tile area overflow 2025-04-02 23:31:16 +02:00
Thibault Deckers
bce7009ab0 upgrades 2025-04-02 23:28:51 +02:00
dependabot[bot]
b00b17a473
Bump step-security/harden-runner from 2.11.0 to 2.11.1 (#1508)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.11.0 to 2.11.1.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](4d991eb9b9...c6295a65d1)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 06:14:53 +09:00
dependabot[bot]
8be5b8d9c5
Bump actions/dependency-review-action from 4.5.0 to 4.6.0 (#1506)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](3b139cfc5f...ce3cf9537a)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-03 06:14:38 +09:00
Thibault Deckers
503757da0e svg decoder pool 2025-04-02 23:14:09 +02:00
Thibault Deckers
85a1b33e83 debug: glide sizing, memory cache clear 2025-04-02 22:30:40 +02:00
Thibault Deckers
022ad0334e debug: custom flutter image cache size 2025-04-01 23:39:11 +02:00
Thibault Deckers
e09b3e4440 region decoder pool 2025-04-01 23:38:41 +02:00
Thibault Deckers
409d80df4e terms txt version for play store 2025-03-27 23:18:00 +01:00
Thibault Deckers
a6ae2fd4cb terms html version for play store 2025-03-27 23:10:02 +01:00
Thibault Deckers
59aa75e46c upgrades 2025-03-26 18:17:49 +01:00
Thibault Deckers
1df2154f85 version bump 2025-03-25 23:37:43 +01:00
Thibault Deckers
326e74c04c l10n 2025-03-25 23:33:01 +01:00
Weblate (bot)
e1aee40f0c
l10n by weblate (#1457)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: hanyang cheng <cinxiafortis@tutanota.de>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-03-26 07:25:20 +09:00
Thibault Deckers
8193c48234 #1485 fixed magnifier edge detection 2025-03-25 23:23:08 +01:00
Thibault Deckers
9c641e0f49 debug: memory overlay 2025-03-25 22:58:51 +01:00
dependabot[bot]
988d9b2c8d
Bump actions/download-artifact from 4.1.9 to 4.2.1 (#1482)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.9 to 4.2.1.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](cc20338598...95815c38cf)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 04:11:15 +09:00
dependabot[bot]
305dcb4528
Bump actions/upload-artifact from 4.6.1 to 4.6.2 (#1484)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](4cec3d8aa0...ea165f8d65)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 04:10:49 +09:00
dependabot[bot]
a13da39af2
Bump github/codeql-action from 3.28.11 to 3.28.13 (#1487)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.11 to 3.28.13.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](6bb031afdd...1b549b9259)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-26 04:10:30 +09:00
Thibault Deckers
9d7e6bb9ff #1476 launch error handling reload;
DB: table existence check via sqlite_master
2025-03-25 19:49:33 +01:00
Thibault Deckers
509c402227 debug: leak tracking for GCed late and not GCed 2025-03-18 23:41:39 +01:00
Thibault Deckers
77443202aa vs code launch config 2025-03-18 21:56:17 +01:00
Thibault Deckers
508e5ae739 minor 2025-03-18 20:23:39 +01:00
Thibault Deckers
f38178ab7e deprecation prep 2025-03-18 20:18:40 +01:00
Thibault Deckers
a670b683e5 minor fix 2025-03-17 21:59:14 +01:00
Thibault Deckers
9a4c567c2b version bump 2025-03-16 19:44:08 +01:00
Thibault Deckers
672b6fd2dc minor fix 2025-03-16 18:29:09 +01:00
Thibault Deckers
b8e9786f4d minor fixes 2025-03-16 18:05:24 +01:00
Thibault Deckers
cb067aa1ac #1476 launch error handling;
DB: table existence check in v13+ upgrades
2025-03-16 17:17:45 +01:00
Thibault Deckers
cf74e75d58 mediakit upgrade 2025-03-14 18:53:36 +01:00
Thibault Deckers
9e33db5b4d upgraded Flutter to stable v3.29.2 2025-03-14 18:43:28 +01:00
Thibault Deckers
8e6a995c4e Update .gitignore 2025-03-11 23:59:53 +01:00
Thibault Deckers
62d666fb34 minor 2025-03-11 23:30:41 +01:00
Thibault Deckers
403ccac5c2 version bump 2025-03-11 00:30:57 +01:00
Thibault Deckers
dcd42b7048 #1471 check file length after metadata editing via PixyMeta 2025-03-11 00:24:03 +01:00
Thibault Deckers
f646639055 #1471 allow rescan of trashed items 2025-03-10 23:09:32 +01:00
Thibault Deckers
a32c0cf0f0 #1471 DB sanitizing to mitigate v1.12.4 upgrade issue 2025-03-10 23:08:23 +01:00
Thibault Deckers
3f6ef0cdaf version bump 2025-03-07 20:11:18 +01:00
Thibault Deckers
32bda0d9a7 #1463 fixed DB migration from v13; upgraded Flutter to stable v3.29.1 2025-03-07 20:07:34 +01:00
dependabot[bot]
4e10d882c9
Bump github/codeql-action from 3.28.10 to 3.28.11 (#1465)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.10 to 3.28.11.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](b56ba49b26...6bb031afdd)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-08 02:03:43 +09:00
dependabot[bot]
9eadf61109
Bump actions/attest-build-provenance from 2.2.2 to 2.2.3 (#1460)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](bd77c07785...c074443f1a)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-08 02:03:08 +09:00
Thibault Deckers
7a84a80736 upgrades 2025-03-05 23:37:17 +01:00
Thibault Deckers
efc6ed2a01 version bump 2025-03-05 23:29:35 +01:00
Thibault Deckers
967e192b4c minor 2025-03-05 23:24:35 +01:00
Thibault Deckers
a3ce840d02 l10n 2025-03-05 23:21:41 +01:00
Weblate (bot)
ee748d8920
l10n by weblate (#1454)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-03-06 07:20:15 +09:00
Thibault Deckers
15fe378107 fallback for decoding large panorama 2025-03-05 23:17:22 +01:00
Thibault Deckers
2325501f3f decoding fixes 2025-03-04 00:58:00 +01:00
Thibault Deckers
f02108fbcd decoding: RGBA_F16 to ARGB_8888 conversion 2025-03-03 20:54:05 +01:00
Thibault Deckers
b224709c5d full decoding: use raw image descriptor in Flutter on decoded bytes from Android 2025-03-02 23:29:00 +01:00
Thibault Deckers
152b942f57 exif thumbnail decoding: use raw image descriptor in Flutter on decoded bytes from Android 2025-03-02 19:47:32 +01:00
Thibault Deckers
c1a99d9be5 app icon decoding: use raw image descriptor in Flutter on decoded bytes from Android 2025-03-02 18:48:48 +01:00
Thibault Deckers
d4791df333 thumbnail decoding: use raw image descriptor in Flutter on decoded bytes from Android 2025-03-02 18:05:44 +01:00
Thibault Deckers
1f95506abe color mode setting for wide gamut 2025-03-02 17:47:39 +01:00
Thibault Deckers
f850178afd region decoding: use raw image descriptor in Flutter on decoded bytes from Android 2025-03-02 13:06:58 +01:00
Thibault Deckers
5805bb2b5b fastlane: screenshot update 2025-03-01 11:29:37 +01:00
Thibault Deckers
731e82028c minor 2025-03-01 01:51:46 +01:00
Thibault Deckers
fe53d488b5 l10n 2025-03-01 00:55:56 +01:00
Weblate (bot)
eed280e840
l10n by weblate (#1452)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-03-01 08:49:20 +09:00
dependabot[bot]
f0580d8724
Bump actions/attest-build-provenance from 2.2.0 to 2.2.2 (#1453)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.2.0 to 2.2.2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](520d128f16...bd77c07785)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-01 08:47:35 +09:00
Thibault Deckers
83f24374e5 l10n: gl 2025-02-27 20:31:20 +01:00
Thibault Deckers
6c055dd24c driver: screenshot fixes 2025-02-27 01:16:00 +01:00
Thibault Deckers
bf99f751bb l10n: gl 2025-02-26 20:14:38 +01:00
Weblate (bot)
8577eba448
l10n by weblate (#1450)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-02-27 04:08:08 +09:00
dependabot[bot]
12672984d7
Bump actions/download-artifact from 4.1.8 to 4.1.9 (#1451)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4.1.8 to 4.1.9.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](fa0a91b85d...cc20338598)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-27 04:07:00 +09:00
Thibault Deckers
1d2aedf4c3 upgrades 2025-02-25 20:49:02 +01:00
Thibault Deckers
bb401e3410 l10n 2025-02-25 20:42:21 +01:00
Weblate (bot)
699c56e2f5
l10n by weblate (#1449)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-02-26 04:40:49 +09:00
Weblate (bot)
64cc59eae9
l10n by weblate (#1431)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Josep M. Ferrer <txemaq@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: pitroig <ona@riseup.net>
Co-authored-by: rcasl <rcasl@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-02-26 04:21:30 +09:00
Thibault Deckers
76a9cf3cb3 #1448 display home tile in side drawer when customized 2025-02-25 19:59:28 +01:00
dependabot[bot]
5c94d2eece
Bump ncipollo/release-action from 1.15.0 to 1.16.0 (#1446)
Bumps [ncipollo/release-action](https://github.com/ncipollo/release-action) from 1.15.0 to 1.16.0.
- [Release notes](https://github.com/ncipollo/release-action/releases)
- [Commits](cdcc88a9ac...440c8c1cb0)

---
updated-dependencies:
- dependency-name: ncipollo/release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 03:50:31 +09:00
dependabot[bot]
6c7dc7a0e3
Bump ossf/scorecard-action from 2.4.0 to 2.4.1 (#1445)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](62b2cac7ed...f49aabe0b5)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 03:50:19 +09:00
dependabot[bot]
b52d2242a4
Bump actions/upload-artifact from 4.6.0 to 4.6.1 (#1444)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](65c4c4a1dd...4cec3d8aa0)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 03:50:09 +09:00
Thibault Deckers
f02363592f #1441 fallback decoding of images packed in RGBA_1010102 config 2025-02-24 23:37:41 +01:00
dependabot[bot]
a3f6cd7a32
Bump github/codeql-action from 3.28.9 to 3.28.10 (#1440)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.9 to 3.28.10.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](9e8d0789d4...b56ba49b26)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 06:07:07 +09:00
dependabot[bot]
a608e122b1
Bump step-security/harden-runner from 2.10.4 to 2.11.0 (#1437)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.10.4 to 2.11.0.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](cb605e52c2...4d991eb9b9)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 06:06:55 +09:00
Thibault Deckers
3424631f5e editor: wip 2025-02-23 22:06:23 +01:00
Thibault Deckers
5c297c1daf editor: dynamic padding 2025-02-18 23:50:08 +01:00
Thibault Deckers
8fe267d345 downgrade dart to 3.6.0 because of incoherent dartfmt from 3.7.0 2025-02-18 23:44:41 +01:00
Thibault Deckers
f4108c244b #1436 search more broadly in Samsung SEFD box for motion photo video 2025-02-17 19:32:27 +01:00
Thibault Deckers
5769593799 minor 2025-02-16 22:40:40 +01:00
Thibault Deckers
ec9cb234a8 #1434 print: do not rotate image to fit page 2025-02-16 22:24:07 +01:00
Thibault Deckers
5f26cfbbf3 #1427 increased precision of file modified date to milliseconds 2025-02-16 20:32:34 +01:00
Thibault Deckers
9280b4a6a7 #1433 stack RAW with developed HEIC 2025-02-15 16:41:31 +01:00
Thibault Deckers
728b8018c4 #1428 switch parser when encountering !ATTLIST in SVG 2025-02-15 16:16:20 +01:00
Thibault Deckers
98537339bd backdrop filter grouping 2025-02-15 00:01:40 +01:00
Thibault Deckers
ae9e2977b4 shaders for flutter v3.29.0, disabled impeller 2025-02-15 00:01:03 +01:00
Thibault Deckers
61de0ffc4c flutter sdk version, removed android ndk version 2025-02-14 21:43:43 +01:00
Thibault Deckers
421e5d1522 gitignore updates, plugin ref fix 2025-02-14 20:13:34 +01:00
Thibault Deckers
5a505344ef upgraded Flutter to stable v3.29.0 2025-02-14 19:37:18 +01:00
Thibault Deckers
7b25d37616 l10n 2025-02-14 00:38:21 +01:00
dependabot[bot]
8641de4d5b
Bump github/codeql-action from 3.28.8 to 3.28.9 (#1422)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.8 to 3.28.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](dd746615b3...9e8d0789d4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-14 08:37:28 +09:00
Weblate (bot)
af0b79b07d
l10n by weblate (#1419)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <AJ07@users.noreply.hosted.weblate.org>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-02-14 08:37:13 +09:00
Thibault Deckers
983f50a814 minor 2025-02-14 00:01:46 +01:00
Thibault Deckers
79840b098f #1011 updated androidsvg to latest master 2025-02-13 22:03:55 +01:00
Thibault Deckers
95e0272afb editor: keep area on pan to edge 2025-02-12 19:25:44 +01:00
Thibault Deckers
b89db3bc9b editor: improved scale on resize 2025-02-10 23:41:08 +01:00
Thibault Deckers
8353064945 identify video location from Apple QuickTime metadata, and 3GPP loci atom 2025-02-09 19:47:35 +01:00
Thibault Deckers
de0cfb1431 #1424 fixed opening embedded video when video track is not the first one 2025-02-09 12:42:26 +01:00
Thibault Deckers
026cfebd49 #1423 support for Samsung HEIC motion photos embedding video in sefd box 2025-02-09 11:32:27 +01:00
Thibault Deckers
e2e0ee706f magnifier: init fix 2025-02-08 19:47:01 +01:00
Thibault Deckers
d11bd21d89 magnifier: scale boundaries padding;
editor: pan fixes
2025-02-08 19:32:35 +01:00
Thibault Deckers
62952de907
Update README.md 2025-02-06 23:03:43 +01:00
Thibault Deckers
881882a117 version bump 2025-02-06 17:18:19 +01:00
Weblate (bot)
578abe3d5a
l10n by weblate (#1415)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-02-07 01:13:37 +09:00
Thibault Deckers
fd9a118bb0 upgrades 2025-02-06 16:54:33 +01:00
Thibault Deckers
b7b57ca15b minor fix 2025-02-06 16:02:42 +01:00
Thibault Deckers
670b4b1830 minor fix 2025-02-06 14:56:10 +01:00
Thibault Deckers
f143f86732 minor fix 2025-02-06 14:50:31 +01:00
Thibault Deckers
71b37a9d77 upgraded Flutter to stable v3.27.4 2025-02-06 14:20:21 +01:00
Thibault Deckers
22f984bb72 improved handling of motion photo with incorrect video offset 2025-02-05 20:30:06 +01:00
Thibault Deckers
bad11564c6 #1417 do not trust video size and orientation reported by media store 2025-02-04 21:02:12 +01:00
Thibault Deckers
bbd819df19 minor 2025-02-04 20:35:46 +01:00
Thibault Deckers
34e22cd486 minor 2025-02-04 19:48:38 +01:00
Thibault Deckers
4b4d444884 minor 2025-02-04 19:30:06 +01:00
Thibault Deckers
cdfd7808dd minor 2025-02-04 18:56:12 +01:00
Thibault Deckers
ffa23f445d minor 2025-02-04 18:09:34 +01:00
Thibault Deckers
b2e4c291e8 l10n 2025-02-03 19:47:04 +01:00
Weblate (bot)
377601422e
l10n by weblate (#1404)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-02-04 03:43:55 +09:00
dependabot[bot]
eeb2f912b5
Bump actions/setup-java from 4.6.0 to 4.7.0 (#1407)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](7a6d8a8234...3a4f6e1af5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-04 03:42:38 +09:00
dependabot[bot]
e266dc3c28
Bump github/codeql-action from 3.28.6 to 3.28.8 (#1409)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.6 to 3.28.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](17a820bf2e...dd746615b3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-04 03:42:14 +09:00
Thibault Deckers
e14a1a0bee #1075 metadata: toggle for all types in removal dialog 2025-02-03 19:29:43 +01:00
Thibault Deckers
598b705b36 #1323 preserve favourite status when converting items 2025-02-03 19:12:46 +01:00
Thibault Deckers
0a3a792a7e #1201 keep selection when action on several items is interrupted before processing 2025-02-02 22:49:07 +01:00
Thibault Deckers
16da0ec3f5 #1331 ignore attempts to move file to its current folder 2025-02-02 20:59:17 +01:00
Thibault Deckers
892e64ef28 #1384 improved subsampling and filter quality strategy 2025-02-01 00:08:36 +01:00
Weblate (bot)
b54ed21c93
l10n by weblate (#1395)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-01-29 03:59:17 +09:00
dependabot[bot]
8ef2c09856
Bump github/codeql-action from 3.28.2 to 3.28.6 (#1403)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.2 to 3.28.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](d68b2d4edb...17a820bf2e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-29 03:58:07 +09:00
Thibault Deckers
0301269171 #1397 edit location via GPX 2025-01-27 22:22:12 +01:00
Thibault Deckers
a99f4877ce minor 2025-01-23 00:41:03 +01:00
dependabot[bot]
405794467b
Bump actions/attest-build-provenance from 2.1.0 to 2.2.0 (#1399)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.1.0 to 2.2.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](7668571508...520d128f16)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-23 06:14:17 +09:00
dependabot[bot]
2610de09ae
Bump github/codeql-action from 3.28.1 to 3.28.2 (#1398)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.1 to 3.28.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](b6a472f63d...d68b2d4edb)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-23 06:13:54 +09:00
Thibault Deckers
2be1b8b538 l10n 2025-01-21 22:26:45 +01:00
Weblate (bot)
aea76c6c1a
l10n by weblate (#1381)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Ricky Tigg <ricky.tigg@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-01-22 06:25:39 +09:00
Thibault Deckers
dbbef98ac3 upgraded Flutter to stable v3.27.3 2025-01-21 22:21:09 +01:00
Thibault Deckers
43b832680c #1388 info: generate palette in another isolate 2025-01-21 20:27:28 +01:00
Thibault Deckers
b282d0a250 debug: viewer tile display flag 2025-01-21 18:14:25 +01:00
Thibault Deckers
0575a6cce6 #1391 region decoding fallback to jpeg export 2025-01-21 18:01:04 +01:00
Thibault Deckers
8e5d971a6f ExifInterface upgrade 2025-01-21 14:31:33 +01:00
Thibault Deckers
a0f7af96e0 upgraded pixy meta 2025-01-21 00:07:10 +01:00
dependabot[bot]
2d65efdcb4
Bump step-security/harden-runner from 2.10.3 to 2.10.4 (#1392)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.10.3 to 2.10.4.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](c95a14d0e5...cb605e52c2)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-20 18:13:37 +01:00
Thibault Deckers
456486b846 upgraded tiff lib 2025-01-20 00:31:50 +01:00
Thibault Deckers
1b404e0ee8 gradle upgrades 2025-01-16 23:18:03 +01:00
Thibault Deckers
aad89c2255 about: added display details to system info 2025-01-14 22:52:42 +01:00
Thibault Deckers
9faa9b7c26 Merge branch 'develop' of https://github.com/deckerst/aves.git 2025-01-14 22:38:54 +01:00
Thibault Deckers
cfe87422aa upgraded Flutter to stable v3.27.2 2025-01-14 22:38:37 +01:00
Thibault Deckers
8b7a14d916 about: added display details to system info 2025-01-14 22:26:25 +01:00
dependabot[bot]
e1934510b5
Bump github/codeql-action from 3.28.0 to 3.28.1 (#1386)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.0 to 3.28.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](48ab28a6f5...b6a472f63d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 03:11:34 +09:00
dependabot[bot]
4067d89075
Bump ncipollo/release-action from 1.14.0 to 1.15.0 (#1387)
Bumps [ncipollo/release-action](https://github.com/ncipollo/release-action) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/ncipollo/release-action/releases)
- [Commits](2c591bcc8e...cdcc88a9ac)

---
updated-dependencies:
- dependency-name: ncipollo/release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 03:11:22 +09:00
Thibault Deckers
41ab5d8f90 version bump 2025-01-13 10:43:41 +01:00
Thibault Deckers
6a5b0770e0 #1382 DDM coordinate format 2025-01-13 10:40:23 +01:00
Weblate (bot)
f108103a4b
l10n by weblate (#1372)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Short description

Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
2025-01-13 07:16:02 +09:00
Thibault Deckers
4d50f258e4 PiP: fixed trigger state, aspect ratio clamp 2025-01-12 23:13:36 +01:00
Thibault Deckers
bb5bbcc069 #1378 accessibility: apply system "touch and hold delay" setting 2025-01-12 19:09:50 +01:00
dependabot[bot]
8c11a7bbd4
Bump actions/upload-artifact from 4.5.0 to 4.6.0 (#1377)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](6f51ac03b9...65c4c4a1dd)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-11 01:57:07 +09:00
dependabot[bot]
dc01b46fd0
Bump step-security/harden-runner from 2.10.2 to 2.10.3 (#1376)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.10.2 to 2.10.3.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](0080882f6c...c95a14d0e5)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-11 01:56:47 +09:00
Thibault Deckers
43521e6268 removed ffmpeg-kit dependency 2025-01-10 17:56:19 +01:00
Thibault Deckers
550c72e994 #1368 crashfix, using media-kit instead of ffmpeg-kit for video metadata fetch;
info: show video chapters
2025-01-10 00:35:08 +01:00
Thibault Deckers
07f253d587 version bump 2025-01-05 16:35:12 +01:00
Thibault Deckers
162900091e #1365 fixed displaying neighbour items when the initial item of a view intent is a new one 2025-01-05 16:30:57 +01:00
Thibault Deckers
ecbbd3b459 l10n: da 2025-01-05 15:08:32 +01:00
Thibault Deckers
0685bc2a95 l10n 2025-01-05 14:42:14 +01:00
Weblate (bot)
d1ab33e64d
l10n by weblate (#1369)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/sat/
Translation: Aves/App - Main
Translation: Aves/Store - Short description

Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
2025-01-05 22:40:58 +09:00
Thibault Deckers
b87dff0fb0 upgrades 2025-01-05 14:29:28 +01:00
Thibault Deckers
2d1fbbd4d3 search: fixed dynamic album name filtering 2025-01-03 12:08:26 +01:00
Thibault Deckers
9bdb3171d9 #1370 dynamic albums: decompose action 2025-01-01 19:11:27 +01:00
Thibault Deckers
43a42ad1dc reporting fix 2024-12-31 10:59:40 +01:00
Thibault Deckers
799c263202 l10n test 2024-12-30 17:10:49 +01:00
Thibault Deckers
54b5d5d377 l10n test 2024-12-30 17:08:43 +01:00
Thibault Deckers
68a285b7fa fixed app lifecycle state init 2024-12-30 16:18:57 +01:00
Thibault Deckers
cfb546f056 l10n 2024-12-30 10:35:02 +01:00
Weblate (bot)
89f177539e
l10n by weblate (#1353)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/et/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ta/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: Dr Hieu <v7pvas7m@anonaddy.com>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <32201227+glemco@users.noreply.github.com>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-30 18:22:47 +09:00
dependabot[bot]
73b3c96fe2
Bump github/codeql-action from 3.27.9 to 3.28.0 (#1356)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.9 to 3.28.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](df409f7d92...48ab28a6f5)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-30 18:21:18 +09:00
Thibault Deckers
487b364c09 upgrades 2024-12-19 21:58:01 +01:00
Thibault Deckers
dd36ecb7e1 version bump 2024-12-19 21:55:05 +01:00
Weblate (bot)
ef21280721
l10n by weblate (#1350)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/et/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ta/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <32201227+glemco@users.noreply.github.com>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-20 05:51:13 +09:00
Thibault Deckers
e80f6645f1 l10n: et 2024-12-19 21:46:07 +01:00
Thibault Deckers
67bdc50758 countries: show states for Mexico 2024-12-19 21:31:00 +01:00
Thibault Deckers
4685708d49 l10n 2024-12-18 23:26:39 +01:00
Weblate (bot)
784ee82b35
l10n by weblate (#1340)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/da/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/et/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ta/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: bovirus <roberto.boriotti@canon.it>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <32201227+glemco@users.noreply.github.com>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: Максим Горпиніч <mgorpinic2005@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-19 07:24:10 +09:00
dependabot[bot]
17c2c9c044
Bump actions/upload-artifact from 4.4.3 to 4.5.0 (#1349)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.3 to 4.5.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](b4b15b8c7c...6f51ac03b9)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 06:55:48 +09:00
dependabot[bot]
4bd8ebed79
Bump actions/setup-java from 4.5.0 to 4.6.0 (#1348)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](8df1039502...7a6d8a8234)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 06:55:33 +09:00
Thibault Deckers
e30e65a2cc upgraded Flutter to stable v3.27.1 2024-12-17 23:17:19 +01:00
dependabot[bot]
bb37664df0
Bump github/codeql-action from 3.27.7 to 3.27.9 (#1341)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.7 to 3.27.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](babb554ede...df409f7d92)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 06:01:27 +09:00
Thibault Deckers
939895599d #1342 slice entries when inserting in DB, to prevent OOM 2024-12-16 22:00:16 +01:00
Thibault Deckers
b027e9a9a8 upgraded Flutter to stable v3.27.0 2024-12-16 21:59:00 +01:00
Thibault Deckers
96d43a4ab0 version bump 2024-12-11 21:50:47 +01:00
Weblate (bot)
14e8f702ea
Translations update from Hosted Weblate (#1338)
* l10n by weblate

Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/et/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ta/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

* Translated using Weblate (Italian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/

* Translated using Weblate (Russian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/

* Translated using Weblate (French)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/

* Translated using Weblate (Korean)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/

* Translated using Weblate (Persian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fa/

* Translated using Weblate (Portuguese)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/

* Translated using Weblate (Galician)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/

* Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/

* Translated using Weblate (Czech)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/

* Translated using Weblate (Basque)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/

* Translated using Weblate (Slovak)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/

* Translated using Weblate (Belarusian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/

* Translated using Weblate (Burmese)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/

* Translated using Weblate (Bengali)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/

* Translated using Weblate (Vietnamese)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/

* Translated using Weblate (Icelandic)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/

* Translated using Weblate (Swedish)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/

* Translated using Weblate (Santali)

Currently translated at 0.0% (0 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sat/

* Translated using Weblate (Danish)

Currently translated at 0.0% (0 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/da/

* Translated using Weblate (Catalan)

Currently translated at 0.0% (0 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ca/

* Translated using Weblate (English (Shavian))

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/en_Shaw/

* Translated using Weblate (Estonian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/et/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/

* Translated using Weblate (German)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/

* Translated using Weblate (Greek)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/

* Translated using Weblate (Japanese)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/

* Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/

* Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/

* Translated using Weblate (Polish)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/

* Translated using Weblate (Romanian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/

* Translated using Weblate (Lithuanian)

Currently translated at 100.0% (3 of 3 strings)

Translation: Aves/Store - Full description
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/

---------

Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-12 05:39:57 +09:00
Thibault Deckers
9b71593743 Update full_description.txt 2024-12-11 19:48:19 +01:00
Thibault Deckers
c0a821a44f l10n: bg 2024-12-10 23:42:43 +01:00
Thibault Deckers
29d5fab00d l10n: ta 2024-12-10 22:47:47 +01:00
Thibault Deckers
202273b669 upgrades 2024-12-10 21:56:45 +01:00
Weblate (bot)
7b375bb6a8
l10n by weblate (#1333)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/he/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/et/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ta/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-11 05:56:00 +09:00
dependabot[bot]
966275923c
Bump actions/attest-build-provenance from 2.0.1 to 2.1.0 (#1336)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.0.1 to 2.1.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](c4fbc64884...7668571508)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 02:55:09 +09:00
dependabot[bot]
4e0677512f
Bump github/codeql-action from 3.27.6 to 3.27.7 (#1337)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.6 to 3.27.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](aa57810251...babb554ede)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 02:55:02 +09:00
Thibault Deckers
2f5831cf4a l10n 2024-12-09 23:07:45 +01:00
dependabot[bot]
c15595887a
Bump actions/attest-build-provenance from 2.0.0 to 2.0.1 (#1328)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](619dbb2e03...c4fbc64884)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 07:01:22 +09:00
Weblate (bot)
2d70dde7cd
l10n by weblate (#1327)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/et/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Thomas Di Cristofaro <hostedweblate.8347@tdc.akamail.it>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-10 07:01:00 +09:00
Thibault Deckers
ebb00923bf l10n 2024-12-06 00:06:31 +01:00
Weblate (bot)
d2a27e979b
l10n by weblate (#1326)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ta/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-06 07:47:10 +09:00
Weblate (bot)
07f6419bb5
l10n by weblate (#1318)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bg/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Petrov <i_v_c@mail.ru>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-12-05 06:42:16 +09:00
dependabot[bot]
bbfbc509c6
Bump github/codeql-action from 3.27.5 to 3.27.6 (#1324)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.5 to 3.27.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](f09c1c0a94...aa57810251)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 02:51:36 +09:00
dependabot[bot]
4d823acded
Bump actions/attest-build-provenance from 1.4.4 to 2.0.0 (#1325)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1.4.4 to 2.0.0.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](ef244123eb...619dbb2e03)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 02:50:46 +09:00
Thibault Deckers
303425e699 #1107 dynamic albums 2024-12-03 00:25:12 +01:00
Thibault Deckers
76f0764d27 #1319 guard against uninitialized system brightness in some environments 2024-11-30 21:54:22 +01:00
Thibault Deckers
d02e6cc693 l10n 2024-11-28 00:13:56 +01:00
Weblate (bot)
cdd7aca33c
l10n by weblate (#1314)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bg/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bg/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Petrov <i_v_c@mail.ru>
2024-11-28 06:30:19 +09:00
Thibault Deckers
066bff3faf log fix 2024-11-27 22:04:48 +01:00
Thibault Deckers
801cdf2897 upgrades 2024-11-24 23:30:32 +01:00
Thibault Deckers
cc58f919f7 version bump 2024-11-24 23:16:18 +01:00
Weblate (bot)
8ff44186f6
l10n by weblate (#1304)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/et/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Short description

Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Cleverson Cândido <optimuspraimu@gmail.com>
Co-authored-by: Hasan Kara <hasanbeytullahkara@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: wanzh <wanzh@users.noreply.hosted.weblate.org>
2024-11-25 07:11:34 +09:00
Thibault Deckers
fc9c2dfcc5 minor fix 2024-11-24 23:09:47 +01:00
Thibault Deckers
1a78fdfd0b #1292 remember title filter visibility by page 2024-11-24 01:15:36 +01:00
Thibault Deckers
a3024fdf4e minor 2024-11-24 00:52:29 +01:00
Thibault Deckers
5b6eb44c05 trim new tags 2024-11-24 00:08:26 +01:00
Thibault Deckers
ffbf0bd8f2 improved error reporting 2024-11-23 23:24:41 +01:00
Thibault Deckers
ec7e4ac2f2 simplified time zone offset check 2024-11-23 22:33:01 +01:00
dependabot[bot]
66f0e2b1e9
Bump actions/dependency-review-action from 4.4.0 to 4.5.0 (#1310)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](4081bf99e2...3b139cfc5f)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 07:48:17 +09:00
dependabot[bot]
b50c305204
Bump github/codeql-action from 3.27.4 to 3.27.5 (#1308)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.4 to 3.27.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](ea9e4e3799...f09c1c0a94)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 07:48:09 +09:00
dependabot[bot]
1f679ec11c
Bump step-security/harden-runner from 2.10.1 to 2.10.2 (#1306)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.10.1 to 2.10.2.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](91182cccc0...0080882f6c)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 07:47:57 +09:00
Thibault Deckers
2f5e959fb0 integrate with OS app language settings on Android >=14 2024-11-21 23:47:39 +01:00
Thibault Deckers
0dee00dbca version bump 2024-11-18 23:46:12 +01:00
Weblate (bot)
0c55c7eab0
l10n by weblate (#1302)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/et/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Short description

Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Cleverson Cândido <optimuspraimu@gmail.com>
Co-authored-by: Hasan Kara <hasanbeytullahkara@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: wanzh <wanzh@users.noreply.hosted.weblate.org>
2024-11-19 07:41:45 +09:00
Thibault Deckers
46374dfcc1 do not try to start analysis service when app goes to background 2024-11-18 23:41:13 +01:00
Thibault Deckers
58e3912f86 #1269 fixed cataloguing images with wrong MPF offsets 2024-11-18 21:51:20 +01:00
Thibault Deckers
89c4fdd854 info: show avif metadata as video 2024-11-17 18:27:20 +01:00
Thibault Deckers
039de32faa avif: fixed decoding of rotated still, fixed sizing during cataloguing
info: show avif metadata via exifinterface
2024-11-17 18:18:40 +01:00
Thibault Deckers
ad214b3f56 ExifInterface upgrade 2024-11-17 18:15:53 +01:00
Thibault Deckers
a25f117e1b l10n 2024-11-17 15:23:00 +01:00
Weblate (bot)
b16bb12749
l10n by weblate (#1288)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/et/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/et/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Short description

Co-authored-by: Cleverson Cândido <optimuspraimu@gmail.com>
Co-authored-by: Hasan Kara <hasanbeytullahkara@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: wanzh <wanzh@users.noreply.hosted.weblate.org>
2024-11-17 23:20:25 +09:00
dependabot[bot]
925845a130
Bump github/codeql-action from 3.27.2 to 3.27.4 (#1298)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.2 to 3.27.4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](9278e42166...ea9e4e3799)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-17 22:42:58 +09:00
Thibault Deckers
d7f5eabc34 upgraded Flutter to stable v3.24.5 2024-11-17 14:41:31 +01:00
dependabot[bot]
d5fef14e9a
Bump github/codeql-action from 3.27.1 to 3.27.2 (#1290)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.1 to 3.27.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](4f3212b617...9278e42166)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-13 02:58:57 +09:00
Thibault Deckers
e7ec41b8d5 #1280 print: ignore undecodable pages 2024-11-12 18:58:41 +01:00
Thibault Deckers
d83537f774 #1253 fastlane: changed Shavian tag 2024-11-11 22:07:46 +01:00
Weblate (bot)
e3eda2d330
l10n by weblate (#1282)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: rehork <cooky@e.email>
2024-11-12 06:04:56 +09:00
Thibault Deckers
88df125f5e #1271 upgrade media-kit 2024-11-11 13:56:14 +01:00
Thibault Deckers
339a039dd6 #1272 albums: improved album creation feedback 2024-11-08 23:29:51 +01:00
Thibault Deckers
e071ff299a #1253 changed fastlane tag for Shavian as workaround for f-droid broken resolution 2024-11-08 19:38:17 +01:00
Thibault Deckers
a65c0fe3b1 #1279 changed Shavian resource tag to prevent bug on Android <7 2024-11-08 19:30:50 +01:00
Weblate (bot)
1c61aa7d14
l10n by weblate (#1270)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/en_SHAW/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/az/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/en_Shaw/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: 5FeetUnder <15950507+5FeetUnder@users.noreply.github.com>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Noah Kenzie Rodriguez-Beus <noahbeus@protonmail.com>
Co-authored-by: Olli <ollinen@ollit.dev>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Your Average Code <neumeiersi91358@th-nuernberg.de>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: elea11 <p.manuel.warnecke@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
2024-11-09 03:20:37 +09:00
dependabot[bot]
b5e6ce9c94
Bump github/codeql-action from 3.27.0 to 3.27.1 (#1281)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.27.0 to 3.27.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](662472033e...4f3212b617)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-09 02:51:59 +09:00
dependabot[bot]
4384d4dcf3
Bump actions/attest-build-provenance from 1.4.3 to 1.4.4 (#1278)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1.4.3 to 1.4.4.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](1c608d11d6...ef244123eb)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-09 02:51:50 +09:00
Thibault Deckers
be1e96270b version bump 2024-10-30 23:13:44 +01:00
Thibault Deckers
63b9b1015a l10n 2024-10-30 21:48:54 +01:00
Weblate (bot)
382c7e5b1f
l10n by weblate (#1265)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/en_SHAW/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/en_Shaw/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Noah Kenzie Rodriguez-Beus <noahbeus@protonmail.com>
Co-authored-by: Olli <ollinen@ollit.dev>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Your Average Code <neumeiersi91358@th-nuernberg.de>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
2024-10-31 05:47:45 +09:00
Thibault Deckers
71efe696c1 settings: video controls preview 2024-10-30 21:37:14 +01:00
Thibault Deckers
da959874fc media-kit update 2024-10-30 20:33:58 +01:00
Thibault Deckers
e65f23d248 #1086 video: control buttons 2024-10-30 20:06:29 +01:00
Thibault Deckers
ee6b34ad21 logs 2024-10-30 20:05:27 +01:00
Thibault Deckers
e0b3f92b65 video: custom playback buttons 2024-10-30 11:40:07 +01:00
Thibault Deckers
9e64da8940 minor 2024-10-30 10:50:07 +01:00
Thibault Deckers
dbb19e1f31 video controls prep 2024-10-30 10:22:20 +01:00
Thibault Deckers
b97000e8e4 #1172 viewer: hero pop/push diversion animation issue workaround 2024-10-30 01:43:30 +01:00
Thibault Deckers
c9663fea19 lint 2024-10-30 01:33:03 +01:00
Thibault Deckers
520499d33e stats: reversed histogram gradient 2024-10-29 23:11:57 +01:00
Thibault Deckers
b5fea82fce loader/completer review 2024-10-29 23:11:35 +01:00
Thibault Deckers
f0e048e340 lint 2024-10-29 19:44:01 +01:00
Thibault Deckers
104403948c #1238 tagging while vaults are unlocked does not yield recent tags visible when vaults are locked 2024-10-29 19:29:06 +01:00
dependabot[bot]
00a4890fb1
Bump actions/dependency-review-action from 4.3.5 to 4.4.0 (#1268)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.3.5 to 4.4.0.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](a6993e2c61...4081bf99e2)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 02:38:06 +09:00
Thibault Deckers
d4bb360902 fixed shortcut thumbnail for content URI 2024-10-29 18:37:50 +01:00
Thibault Deckers
ae22a25a13 #1248 fixed new album name resolution to allow same name as existing empty directory 2024-10-29 02:02:42 +01:00
Thibault Deckers
c6ec5afba1 #1249 fixed copying content URI items 2024-10-29 01:06:46 +01:00
Thibault Deckers
ccbca7c506 minor fix 2024-10-29 00:05:33 +01:00
Thibault Deckers
ea3619d7f5 disable impeller, minor fix 2024-10-29 00:05:24 +01:00
Thibault Deckers
33ffb1cd1a source init scope review 2024-10-29 00:04:53 +01:00
Thibault Deckers
687ca5eb41 l10n: en-Shaw 2024-10-28 00:13:20 +01:00
Thibault Deckers
1ab663ba70 flutter vNext prep 2024-10-27 23:39:39 +01:00
Thibault Deckers
cce5b1fced l10n: en-Shaw 2024-10-27 23:22:17 +01:00
Thibault Deckers
1c828c3cf6 media-kit package ref fix 2024-10-27 21:14:53 +01:00
Thibault Deckers
5985a89f85 leaks: overlay entry review, change notifier mixin creation event 2024-10-26 01:27:02 +02:00
Thibault Deckers
3a63533dd9 leak tracking setup 2024-10-26 00:58:52 +02:00
Thibault Deckers
8de8d26756 deprecation: whereNotNull 2024-10-26 00:19:43 +02:00
Thibault Deckers
884baae602 deprecation: opacity 2024-10-26 00:10:40 +02:00
Thibault Deckers
e039798463 l10n: en-Shaw 2024-10-25 21:10:30 +02:00
Weblate (bot)
6c8fb28e09
l10n by weblate (#1261)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/en_SHAW/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/en_Shaw/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: Your Average Code <neumeiersi91358@th-nuernberg.de>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
2024-10-26 03:22:27 +09:00
Thibault Deckers
0af94eee1a enable impeller 2024-10-25 19:34:15 +02:00
Thibault Deckers
f1b143ac95 upgraded Flutter to stable v3.24.4 2024-10-25 19:09:26 +02:00
Thibault Deckers
0b614e8ce5 use MPV commands directly 2024-10-25 18:50:37 +02:00
Thibault Deckers
86b06bff7a #1086 frame stepping with latest media-kit 2024-10-25 01:16:19 +02:00
StepSecurity Bot
0ec2875736
[StepSecurity] ci: Harden GitHub Actions (#1264)
Signed-off-by: StepSecurity Bot <bot@stepsecurity.io>
2024-10-25 04:52:59 +09:00
dependabot[bot]
fc30db2f82
Bump actions/setup-java from 4.4.0 to 4.5.0 (#1263)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](b36c23c0d9...8df1039502)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-25 04:48:32 +09:00
Thibault Deckers
d96f9768f9 map: hero fixes 2024-10-24 20:53:29 +02:00
Thibault Deckers
9aeb0a1fc3 fixed silent crash when exiting after cancelled large transaction from item picking 2024-10-24 00:30:59 +02:00
Thibault Deckers
3448a060db l10n 2024-10-23 20:26:59 +02:00
Thibault Deckers
50d151ccc0 Revert "Translations update from Hosted Weblate (#1260)"
This reverts commit d93022591a.
2024-10-23 20:04:47 +02:00
Weblate (bot)
d93022591a
Translations update from Hosted Weblate (#1260)
* l10n by weblate

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/en_SHAW/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main

* Deleted translation using Weblate (English (en_SHAW))

* Deleted translation using Weblate (English (en_SHAW))

---------

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
2024-10-24 03:02:59 +09:00
Thibault Deckers
1522e5c5c4 Revert "Translations update from Hosted Weblate (#1259)"
This reverts commit 1eecd224e5.
2024-10-23 19:41:27 +02:00
Weblate (bot)
1eecd224e5
Translations update from Hosted Weblate (#1259)
* l10n by weblate

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/en_SHAW/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main

* Deleted translation using Weblate (English (en_SHAW))

---------

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
2024-10-24 02:38:09 +09:00
Thibault Deckers
ee116c49f4 Update app_en_SHAW.arb 2024-10-23 19:32:31 +02:00
Weblate (bot)
25d9349cdc
l10n by weblate (#1257)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/en_SHAW/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/en_Shaw/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Paranoid Android <f.cherdzhiev@innopolis.university>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
2024-10-24 02:27:54 +09:00
dependabot[bot]
9983f3800c
Bump actions/checkout from 4.2.1 to 4.2.2 (#1258)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](eef61447b9...11bd71901b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-24 02:25:06 +09:00
Thibault Deckers
df9d9733fa l10n: added files for English (Shavian) locale 2024-10-23 01:22:00 +02:00
Thibault Deckers
b9327db44b map: create shortcut to custom region and filters 2024-10-23 01:09:17 +02:00
Weblate (bot)
5ce8bef9cc
Translations update from Hosted Weblate (#1246)
* l10n by weblate

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main

* Deleted translation using Weblate (English (en_SHAW))

* Deleted translation using Weblate (English (en_SHAW))

* Deleted translation using Weblate (English (Shavian) (en-Shaw))

* Deleted translation using Weblate (English (Shavian) (en-Shaw))

* Deleted translation using Weblate (English (Shavian))

* Delete android/app/src/main/res/values-b+en+Shaw/strings.xml

---------

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Whoever4976 <wolffjonas47@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
2024-10-23 03:28:55 +09:00
dependabot[bot]
51868287d4
Bump github/codeql-action from 3.26.13 to 3.27.0 (#1256)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.13 to 3.27.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](f779452ac5...662472033e)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 02:47:56 +09:00
dependabot[bot]
67616ad077
Bump actions/dependency-review-action from 4.3.4 to 4.3.5 (#1255)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.3.4 to 4.3.5.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](5a2ce3f5b9...a6993e2c61)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-23 02:47:43 +09:00
Thibault Deckers
a3b613662a map: vector tile cache on external storage 2024-10-20 20:04:42 +02:00
Thibault Deckers
5f80fab460 minor 2024-10-20 11:21:59 +02:00
Thibault Deckers
fdf9aa7ef4 Update CHANGELOG.md 2024-10-18 22:15:14 +02:00
Thibault Deckers
76c3244074 db: use cursor 2024-10-18 20:16:24 +02:00
Thibault Deckers
7982c536e4 map: icon, density, transition fixes 2024-10-18 00:35:29 +02:00
Thibault Deckers
95c279088a minor 2024-10-14 20:59:53 +02:00
Thibault Deckers
deb9f4af87 upgrades 2024-10-14 20:55:25 +02:00
Thibault Deckers
342894c360 l10n 2024-10-14 20:39:54 +02:00
Weblate (bot)
a7c117a942
l10n by weblate (#1235)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: João Palmeiro <joaommpalmeiro@gmail.com>
Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
2024-10-15 03:31:35 +09:00
dependabot[bot]
b47e5bf3f4
Bump github/codeql-action from 3.26.12 to 3.26.13 (#1245)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.12 to 3.26.13.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](c36620d31a...f779452ac5)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-15 03:29:26 +09:00
Thibault Deckers
0a99dc24b6 fixed kotlin jvm and source compatibility versions 2024-10-14 19:28:33 +02:00
Thibault Deckers
78a28e6762 minor 2024-10-14 00:00:00 +02:00
Thibault Deckers
ee4dd38bbe check whether app is in foreground before starting service 2024-10-13 23:33:46 +02:00
Thibault Deckers
bb89756815 minor 2024-10-13 23:03:25 +02:00
Thibault Deckers
1058aba262 picker: prevent security exception 2024-10-13 22:04:01 +02:00
Thibault Deckers
2932e0b80f release: generate artifact attestation 2024-10-10 20:41:00 +02:00
Thibault Deckers
a8ad8b9ee6 version bump 2024-10-10 19:10:02 +02:00
Thibault Deckers
148a120e4c minor fixes 2024-10-10 19:07:50 +02:00
Thibault Deckers
fb0a9436db l10n 2024-10-10 18:43:19 +02:00
Weblate (bot)
5d1e59ca0f
l10n by weblate (#1229)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: splice11 <trenchedgrandpa@protonmail.com>
2024-10-11 01:42:40 +09:00
dependabot[bot]
ef33cd7902
Bump actions/upload-artifact from 4.4.2 to 4.4.3 (#1234)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.2 to 4.4.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](84480863f2...b4b15b8c7c)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-11 01:38:57 +09:00
dependabot[bot]
1c51767de4
Bump actions/upload-artifact from 4.4.1 to 4.4.2 (#1232)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](604373da63...84480863f2)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-11 01:16:31 +09:00
Thibault Deckers
9067d75959 fixed case-insensitive access to restricted directories 2024-10-10 18:14:26 +02:00
Thibault Deckers
dde21abbac version bump 2024-10-09 01:04:02 +02:00
Thibault Deckers
9401dd2874 version bump 2024-10-09 00:09:43 +02:00
Thibault Deckers
7da501ad2e upgrades 2024-10-08 23:58:00 +02:00
Thibault Deckers
01e2bcc1b4 fixed download directory access when not using reference case 2024-10-08 23:48:39 +02:00
Weblate (bot)
83f273f76e
l10n by weblate (#1225)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translation: Aves/App - Main

Co-authored-by: LE NHUT BINH <125377511+lenhutbinh@users.noreply.github.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: rehork <cooky@e.email>
2024-10-09 06:07:56 +09:00
dependabot[bot]
802516a82d
Bump github/codeql-action from 3.26.11 to 3.26.12 (#1228)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.11 to 3.26.12.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](6db8d6351f...c36620d31a)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 05:18:52 +09:00
dependabot[bot]
44ea51e1f2
Bump actions/checkout from 4.2.0 to 4.2.1 (#1227)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](d632683dd7...eef61447b9)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-09 05:18:41 +09:00
Thibault Deckers
763eebac7d viewer: fixed video progress bar layout 2024-10-07 23:56:48 +02:00
Thibault Deckers
eb68b8eba6 fixed section header widget span layout 2024-10-07 23:36:21 +02:00
Thibault Deckers
77cea1b7cb info: hide empty XMP 2024-10-07 23:24:03 +02:00
Thibault Deckers
211f803afe source: disable analysis for widget, screen saver; disabling analysis also disables entry discovery 2024-10-07 22:24:58 +02:00
Thibault Deckers
618b63bfc0 source: dismiss changes if only loaded for specific directory 2024-10-07 19:28:20 +02:00
dependabot[bot]
ada3c2908b
Bump actions/upload-artifact from 4.4.0 to 4.4.1 (#1226)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](50769540e7...604373da63)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-08 01:58:35 +09:00
Thibault Deckers
f833689961 prevent ANR by moving Binder call off main thread 2024-10-07 18:58:17 +02:00
Thibault Deckers
0f5d0a42a3 #1203 keep full file to fetch metadata from large PNG 2024-10-07 17:09:12 +02:00
Thibault Deckers
8f431a5426 explorer: hide path action 2024-10-07 16:06:53 +02:00
Thibault Deckers
eb1efe10e5 #1185 relaxed Apple HDR image check conditions 2024-10-07 15:50:13 +02:00
Thibault Deckers
306456967e #1222 cataloguing: fixed MPF offset lookup 2024-10-07 15:19:41 +02:00
Weblate (bot)
2f4d1f5689
l10n by weblate (#1217)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translation: Aves/App - Main

Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
2024-10-07 07:30:24 +09:00
dependabot[bot]
c6265940ab
Bump github/codeql-action from 3.26.10 to 3.26.11 (#1220)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.10 to 3.26.11.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](e2b3eafc8d...6db8d6351f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-07 07:24:37 +09:00
Thibault Deckers
c2a8aa7919 Merge remote-tracking branch 'refs/remotes/weblate/develop'
Conflicts:
	lib/l10n/app_es.arb
	lib/l10n/app_eu.arb
	lib/l10n/app_fr.arb
	lib/l10n/app_hu.arb
	lib/l10n/app_ko.arb
	lib/l10n/app_nl.arb
	lib/l10n/app_pl.arb
	lib/l10n/app_sv.arb
	lib/l10n/app_tr.arb
2024-10-06 23:31:04 +02:00
Shift18
c7eb9d3c91
Translated using Weblate (Swedish)
Currently translated at 100.0% (694 of 694 strings)

Translation: Aves/App - Main
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
2024-10-05 20:15:41 +02:00
Shift18
a7c2a2bb41
Translated using Weblate (Swedish)
Currently translated at 100.0% (9 of 9 strings)

Translation: Aves/App - Android
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
2024-10-05 20:15:40 +02:00
rehork
c2de228a5d
Translated using Weblate (Polish)
Currently translated at 100.0% (694 of 694 strings)

Translation: Aves/App - Main
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
2024-10-05 20:15:39 +02:00
rehork
05dd44e3ce
Translated using Weblate (Polish)
Currently translated at 100.0% (9 of 9 strings)

Translation: Aves/App - Android
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pl/
2024-10-05 20:15:39 +02:00
Oğuz Ersen
b800ed97da
Translated using Weblate (Turkish)
Currently translated at 100.0% (694 of 694 strings)

Translation: Aves/App - Main
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
2024-10-05 20:15:38 +02:00
Thibault Deckers
a2260f995f l10n: map attribution key reuse 2024-10-04 15:38:09 +02:00
Hosted Weblate
e6a2d65168
l10n by weblate
Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Aitor Salaberria <amento@ni.eus>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Anurag Samota <anuragsamotasamota@gmail.com>
Co-authored-by: Enenra <nnra2210@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Leo Aaua Felix <g00g7el@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Taufan <taufanxxx@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Victor M <victormorita@tuta.io>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
2024-10-04 12:44:31 +00:00
Thibault Deckers
7fabb059b7 map: fixed collection refresh when source initializes 2024-10-04 14:44:09 +02:00
Thibault Deckers
1864f77866 upgrades, lints 2024-10-03 23:50:03 +02:00
Thibault Deckers
fcdbbc9854 l10n 2024-10-03 23:17:56 +02:00
Weblate (bot)
a1ddcf37d8
l10n by weblate (#1194)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Anurag Samota <anuragsamotasamota@gmail.com>
Co-authored-by: Enenra <nnra2210@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Leo Aaua Felix <g00g7el@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Taufan <taufanxxx@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Victor M <victormorita@tuta.io>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
2024-10-04 06:13:04 +09:00
Thibault Deckers
a4edacc2d4 Update CHANGELOG.md 2024-10-03 23:02:09 +02:00
Thibault Deckers
c07dc36d26 #1216 settings: hidden path filters are merged with others and can be toggled 2024-10-03 23:00:19 +02:00
Thibault Deckers
d859887319 #1211 receiving geo: uri while editing location fills in coordinates 2024-10-03 00:36:40 +02:00
Thibault Deckers
ec59e348c5 map shortcut, geo: uri handling 2024-10-02 01:11:02 +02:00
Thibault Deckers
fb0b4dcab2 Merge branch 'develop' 2024-09-30 23:54:51 +02:00
Thibault Deckers
34ff91ccea fix 2024-09-30 23:54:18 +02:00
Thibault Deckers
7a63164891 Merge branch 'develop' 2024-09-30 23:41:43 +02:00
dependabot[bot]
c982811748
Bump github/codeql-action from 3.26.9 to 3.26.10 (#1214)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.9 to 3.26.10.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](461ef6c76d...e2b3eafc8d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 06:35:10 +09:00
Thibault Deckers
1e58ae94b9 enterprise: user/work profile switch from drawer 2024-09-30 23:34:46 +02:00
Thibault Deckers
c7e5b1ab7b android: app build cleanup 2024-09-26 23:11:49 +02:00
Thibault Deckers
00d928fa47 Merge branch 'develop' 2024-09-26 20:28:52 +02:00
Thibault Deckers
da8f6cdf7f agp upgrade 2024-09-26 20:28:34 +02:00
dependabot[bot]
9163059a16
Bump actions/checkout from 4.1.7 to 4.2.0 (#1210)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](692973e3d9...d632683dd7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 03:28:12 +09:00
Thibault Deckers
615af106d8 improved overhead 2024-09-26 00:09:12 +02:00
Thibault Deckers
d0d9783b78 americana / osm liberty merge 2024-09-25 23:23:05 +02:00
dependabot[bot]
c85d97b598
Bump github/codeql-action from 3.26.8 to 3.26.9 (#1206)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.8 to 3.26.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](294a9d9291...461ef6c76d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-26 02:19:02 +09:00
dependabot[bot]
178229b283
Bump actions/setup-java from 4.3.0 to 4.4.0 (#1204)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.3.0 to 4.4.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](2dfa2011c5...b36c23c0d9)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 05:04:58 +09:00
dependabot[bot]
c2353de3b4
Bump github/codeql-action from 3.26.7 to 3.26.8 (#1199)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.7 to 3.26.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8214744c54...294a9d9291)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-25 05:04:19 +09:00
Thibault Deckers
c48687b77c added OSM Americana vector tile provider 2024-09-24 00:13:46 +02:00
Thibault Deckers
33bdc41e7b map: added OpenTopoMap layer 2024-09-22 21:19:33 +02:00
Thibault Deckers
1793da1262 version bump 2024-09-17 20:22:13 +02:00
Thibault Deckers
ebdbf0324e source init context logs 2024-09-17 20:19:21 +02:00
Thibault Deckers
9093cfab69 source init context logs 2024-09-17 20:03:38 +02:00
Thibault Deckers
832a683607 minor fixes 2024-09-17 19:41:41 +02:00
Thibault Deckers
9f7fd2b049 fixed crash on view intent on Android API <27 2024-09-17 18:32:32 +02:00
Thibault Deckers
f2275d5785 version bump 2024-09-16 20:02:28 +02:00
Thibault Deckers
4851b997cf #1196 fix access to app from lock screen 2024-09-16 19:52:51 +02:00
Thibault Deckers
fc04a1cf89
Update README.md 2024-09-16 07:50:43 +09:00
Thibault Deckers
2a70cc0fb8
Update release.yml 2024-09-16 07:16:12 +09:00
Thibault Deckers
e1e6f353e0 version bump 2024-09-16 00:12:44 +02:00
Weblate (bot)
6ed79bf5a5
l10n by weblate (#1190)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Anurag Samota <anuragsamotasamota@gmail.com>
Co-authored-by: Enenra <nnra2210@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Leo Aaua Felix <g00g7el@gmail.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Taufan <taufanxxx@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Victor M <victormorita@tuta.io>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
2024-09-16 07:10:37 +09:00
Thibault Deckers
f4e5018b78 #1174 custom placeholder handling for collection-viewer hero 2024-09-16 00:07:14 +02:00
Thibault Deckers
cc3b4f661b route transition duration renaming 2024-09-15 23:50:55 +02:00
Thibault Deckers
748f8b9270 #1192 fixed path filter 2024-09-14 23:02:57 +02:00
StepSecurity Bot
652215fc13
[StepSecurity] ci: Harden GitHub Actions (#1191)
Signed-off-by: StepSecurity Bot <bot@stepsecurity.io>
2024-09-15 02:34:20 +09:00
Thibault Deckers
3c460c5aa6 workflows: job/step names 2024-09-14 19:33:18 +02:00
Thibault Deckers
f6eb97434c Update quality-check.yml 2024-09-14 18:49:00 +02:00
Thibault Deckers
fad079b718 Update check.yml, codeql.yml, and 2 more files... 2024-09-14 18:32:47 +02:00
Thibault Deckers
759b719f8a
Create codeql.yml 2024-09-14 09:32:41 +09:00
Thibault Deckers
6b8790c4a3 fixed gitignore android executables in plugins 2024-09-14 01:29:41 +02:00
Thibault Deckers
5d598bb421
Update release.yml 2024-09-14 08:22:14 +09:00
Thibault Deckers
ba0d91a1ff entry hero review 2024-09-14 00:47:15 +02:00
Thibault Deckers
535d4c0d00 #1177 slideshow: fixed bottom overlay layout on inset transition 2024-09-13 21:30:35 +02:00
Thibault Deckers
013b2631aa l10n 2024-09-13 20:33:02 +02:00
Weblate (bot)
07357c29cc
l10n by weblate (#1159)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Anurag Samota <anuragsamotasamota@gmail.com>
Co-authored-by: Enenra <nnra2210@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Leo Aaua Felix <g00g7el@gmail.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Taufan <taufanxxx@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Victor M <victormorita@tuta.io>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
2024-09-14 03:18:55 +09:00
dependabot[bot]
4ec0893687
Bump github/codeql-action from 3.26.6 to 3.26.7 (#1189)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.6 to 3.26.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](4dd16135b6...8214744c54)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-14 03:18:23 +09:00
dependabot[bot]
0ed2ee44f8
Bump step-security/harden-runner from 2.9.1 to 2.10.1 (#1181)
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.9.1 to 2.10.1.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](5c7944e73c...91182cccc0)

---
updated-dependencies:
- dependency-name: step-security/harden-runner
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-13 07:02:56 +09:00
Thibault Deckers
7b982cfdd9 upgraded Flutter to stable v3.24.3 2024-09-13 00:02:29 +02:00
Thibault Deckers
f9bfbd5bea #1160 secure view: prevent access to export actions 2024-09-12 23:29:25 +02:00
Thibault Deckers
31f4737d3c Update CHANGELOG.md 2024-09-12 00:09:02 +02:00
Thibault Deckers
ef9bcebff2 fixed OOM when cataloguing malformed MP4 files 2024-09-12 00:08:08 +02:00
Thibault Deckers
590c353fcd minor fixes 2024-09-10 20:27:06 +02:00
Thibault Deckers
1b39bb6ab4 #1173 always display launch icon white background 2024-09-09 22:30:21 +02:00
dependabot[bot]
af764c1b42
Bump actions/setup-java from 4.2.2 to 4.3.0 (#1178)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4.2.2 to 4.3.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](6a0805fcef...2dfa2011c5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-10 01:56:49 +09:00
dependabot[bot]
62f99e7d0e
Bump actions/upload-artifact from 4.3.6 to 4.4.0 (#1163)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.3.6 to 4.4.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](834a144ee9...50769540e7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-10 01:56:26 +09:00
Thibault Deckers
af689edae6 upgraded Flutter to stable v3.24.2 2024-09-04 19:05:30 +02:00
Thibault Deckers
61ccb04caa improved app mode init, fallback 2024-09-03 23:47:38 +02:00
Thibault Deckers
877fde1b26 #1160 support opening from the lock screen 2024-09-03 22:17:19 +02:00
Thibault Deckers
467093e283 improved report log for source init/refresh 2024-09-02 23:40:57 +02:00
Thibault Deckers
6869839b44 version bump 2024-09-01 01:31:12 +02:00
Thibault Deckers
9b1694cc36 request notification permission when launching scanning service 2024-09-01 01:08:07 +02:00
Thibault Deckers
5a273737fb l10n 2024-09-01 00:49:06 +02:00
Weblate (bot)
b6b8feecc4
l10n by weblate (#1155)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Leo Aaua Felix <g00g7el@gmail.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Taufan <taufanxxx@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
2024-09-01 07:44:04 +09:00
dependabot[bot]
f5ac09f083
Bump github/codeql-action from 3.26.5 to 3.26.6 (#1158)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.5 to 3.26.6.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](2c779ab0d0...4dd16135b6)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-01 07:39:02 +09:00
Thibault Deckers
c3ef0255fd #971 workaround to remove and report duplicates as they appear 2024-09-01 00:38:43 +02:00
Thibault Deckers
1ff1269e0f test fix 2024-08-30 19:36:46 +02:00
Thibault Deckers
306025380f #971 duplicate detection 2024-08-30 19:21:06 +02:00
Thibault Deckers
2c41e82444 albums: looser app folder matching 2024-08-29 19:19:05 +02:00
Thibault Deckers
98c39c2e7b prevent OOM when cataloguing some RAW files 2024-08-28 19:49:48 +02:00
Thibault Deckers
0f9fc2881a l10n 2024-08-28 17:35:35 +02:00
Weblate (bot)
aee082c950
l10n by weblate (#1139)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Taufan <taufanxxx@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
2024-08-29 00:20:05 +09:00
dependabot[bot]
66a7d30fce
Bump github/codeql-action from 3.26.2 to 3.26.5 (#1152)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.26.2 to 3.26.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](429e197704...2c779ab0d0)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-29 00:17:08 +09:00
Thibault Deckers
015d1ec32d upgraded Flutter to stable v3.24.1 2024-08-28 16:59:02 +02:00
Thibault Deckers
030133d274 l10n: sv 2024-08-18 20:51:00 +02:00
Weblate (bot)
5a5ddab301
l10n by weblate (#1117)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sv/
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: Scorza9999 <oliva.scorza@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
2024-08-19 02:26:44 +09:00
dependabot[bot]
5168ad07c8
Bump ossf/scorecard-action from 2.3.3 to 2.4.0 (#1138)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.3.3 to 2.4.0.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](dc50aa9510...62b2cac7ed)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 01:24:50 +09:00
StepSecurity Bot
8d1342cc0e
[StepSecurity] Apply security best practices (#1137)
Signed-off-by: StepSecurity Bot <bot@stepsecurity.io>
2024-08-19 01:06:32 +09:00
Thibault Deckers
5824974d07 github: check on PR,
scripts: libre APKS extraction
2024-08-18 16:47:37 +02:00
Thibault Deckers
cfe4c72e8f version bump 2024-08-07 22:00:28 +02:00
Thibault Deckers
5d5f80d06f version bump 2024-08-07 22:00:13 +02:00
Thibault Deckers
f691983e0a l10n 2024-08-07 20:30:49 +02:00
Weblate (bot)
75188359c4
l10n by weblate (#1112)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Adrien N <adriennathaniel1999@gmail.com>
Co-authored-by: Andreas Håll <ante_skalman@hotmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sartaj <ssaarrttaajj111@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Tung Anh <buihuutunganh2007@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
2024-08-08 03:25:20 +09:00
Thibault Deckers
59de10a1ce explorer: stats 2024-08-07 20:19:52 +02:00
Thibault Deckers
94cb6e2b80 explorer fixes 2024-08-07 20:05:47 +02:00
Thibault Deckers
4b2a7f8abc upgraded Flutter to stable v3.24.0 2024-08-07 00:49:13 +02:00
Thibault Deckers
554e1faf88 #1113 viewer: long descriptions are scrollable when overlay is expanded by tap 2024-08-05 21:01:55 +02:00
Thibault Deckers
867fb95426 upgrades 2024-08-05 19:52:38 +02:00
Thibault Deckers
aeeaf41e3a appbundle release for libre flavour on accrescent 2024-08-05 19:47:23 +02:00
Thibault Deckers
f824eedf63 minor 2024-08-04 20:17:33 +02:00
Thibault Deckers
4d80dfe1d6 #1087 open external map app from map views 2024-08-04 19:41:25 +02:00
Weblate (bot)
2d5a7f6c27
l10n by weblate (#1111)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Adrien N <adriennathaniel1999@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sartaj <ssaarrttaajj111@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Tung Anh <buihuutunganh2007@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-08-05 01:53:36 +09:00
Weblate (bot)
ed6acea591
l10n by weblate (#1099)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <101049867+ChAJ07@users.noreply.github.com>
Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Adrien N <adriennathaniel1999@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Sartaj <ssaarrttaajj111@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Tung Anh <buihuutunganh2007@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-08-05 01:48:00 +09:00
Thibault Deckers
bf987d63bf #1109 collection: sort by duration 2024-08-04 18:23:50 +02:00
Thibault Deckers
374a85ed7d Update CHANGELOG.md 2024-08-04 17:21:58 +02:00
Thibault Deckers
3bad4b6814 #1110 fixed widget setup race 2024-08-04 17:16:26 +02:00
Thibault Deckers
0bbed0ebda minor 2024-08-04 12:38:14 +02:00
Thibault Deckers
05bb2ab350 #1102 accessibility: enable/disable more animations 2024-07-28 20:05:35 +02:00
Thibault Deckers
1d2396193b #1102 accessibility: enable/disable more animations 2024-07-28 18:26:46 +02:00
Thibault Deckers
36c0fd2a3d #1102 accessibility: enable/disable more animations 2024-07-28 00:22:19 +02:00
Thibault Deckers
e3f6644366 #1102 accessibility: enable/disable more animations 2024-07-27 21:20:35 +02:00
Thibault Deckers
fcde32d555 #1102 accessibility: enable/disable more animations 2024-07-27 20:41:22 +02:00
Thibault Deckers
20ca48a5ed #1100 viewer: dismiss feedback when opening popup menu 2024-07-26 22:20:53 +02:00
Thibault Deckers
2a91306532 minor 2024-07-26 20:46:44 +02:00
Thibault Deckers
065a287f42 l10n 2024-07-25 20:03:52 +02:00
Weblate (bot)
ecbd4435fd
l10n by weblate (#1085)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Adrien N <adriennathaniel1999@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Sartaj <ssaarrttaajj111@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Tung Anh <buihuutunganh2007@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-07-26 03:00:32 +09:00
Thibault Deckers
5498b5d4e5 #1097 fixed relaunch intent extras check 2024-07-25 19:40:17 +02:00
Thibault Deckers
39bb9251dc #1095fixed removing deactivated hidden item 2024-07-24 20:12:27 +02:00
Thibault Deckers
afc09e2ab4 #1080 scrolling quick action selector for tag/move/copy 2024-07-24 00:32:42 +02:00
Thibault Deckers
dc061e89e6 minor 2024-07-21 21:53:23 +02:00
Thibault Deckers
8f0ae0e79e removed legacy ijk plugin 2024-07-21 21:53:15 +02:00
Thibault Deckers
ddca1500c9 minor 2024-07-21 21:47:52 +02:00
Thibault Deckers
39ac82adf1 #1089 fixed collection menu action resolution 2024-07-21 12:28:09 +02:00
Thibault Deckers
b5848cf893 version bump 2024-07-19 08:31:34 +02:00
Thibault Deckers
29954510b7 #1088 revert to fragment activity, restoring auth for vaults, disabling predictive back 2024-07-19 08:29:26 +02:00
Thibault Deckers
e51ec1c50a version bump 2024-07-18 23:58:38 +02:00
Thibault Deckers
1eb007e43b fixed google map rendering on API < 23 2024-07-18 23:54:06 +02:00
Thibault Deckers
685d5c6944 fixed launch crash on API < 24 2024-07-18 23:54:00 +02:00
Thibault Deckers
b4c46ae9b2 upgraded Flutter to stable v3.22.3 2024-07-18 20:43:18 +02:00
Thibault Deckers
a2ea0c377a upgrades 2024-07-17 22:55:57 +02:00
Thibault Deckers
736bc881c6 version bump 2024-07-17 22:38:53 +02:00
Thibault Deckers
3a2ca9ea0c l10n 2024-07-17 22:35:50 +02:00
Weblate (bot)
acbfa69368
l10n by weblate (#1069)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nl/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Sartaj <ssaarrttaajj111@gmail.com>
Co-authored-by: Stephan Paternotte <stephan@paternottes.net>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Tung Anh <buihuutunganh2007@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-07-18 05:35:16 +09:00
Thibault Deckers
4cf5a95d89 fix 2024-07-17 21:38:59 +02:00
Thibault Deckers
2d143f139f #1084 crashfix for png with large exif via ExifInterface,
fixed ExifInterface disambiguation,
improved safe mode
2024-07-17 21:11:36 +02:00
Thibault Deckers
a38c5b72ee analysis: do not chain workers, use prefs for data read/write instead 2024-07-16 20:23:46 +02:00
Thibault Deckers
fbd498bee8 explorer: custom home, shortcut 2024-07-15 23:53:46 +02:00
Thibault Deckers
3d424eb82b android 15 / api 35, predictive back 2024-07-13 01:32:30 +02:00
Thibault Deckers
0cb139b41a version bump 2024-07-11 19:03:20 +02:00
Thibault Deckers
dcc91cf5a3 fixed crash when opening app from widget 2024-07-11 18:59:02 +02:00
Thibault Deckers
f4d2fa6089
Update README.md 2024-07-10 07:01:21 +09:00
Weblate (bot)
dbccb34f6a
l10n by weblate (#1068)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translation: Aves/App - Main
2024-07-10 06:58:45 +09:00
Thibault Deckers
eb9a001919 removed huawei flavor 2024-07-09 23:54:10 +02:00
Thibault Deckers
5577657eee upgrades 2024-07-09 01:36:50 +02:00
Thibault Deckers
37475726ed version bump 2024-07-09 00:17:34 +02:00
Thibault Deckers
91e6ac1d88 analysis: ANR test fix 2024-07-08 23:06:51 +02:00
Thibault Deckers
8cb75f96b5 l10n 2024-07-08 20:50:04 +02:00
Weblate (bot)
94f097e042
l10n by weblate (#1044)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/th/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translation: Aves/App - Main

Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jerguš Fonfer <caro.jf@protonmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Olli <ollinen@ollit.dev>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: atilluF <110931720+atilluF@users.noreply.github.com>
Co-authored-by: elfriob <elfriob@ya.ru>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2024-07-09 03:39:54 +09:00
Thibault Deckers
22eee299c5 widget: async update 2024-07-08 20:37:40 +02:00
Thibault Deckers
b743d0de47 widgets: responsive layout 2024-07-06 00:17:58 +02:00
Thibault Deckers
fdb34edf13 widget: use system defined corner radius 2024-07-05 22:26:40 +02:00
Thibault Deckers
f8525db84e minor fix 2024-07-01 23:40:30 +02:00
Thibault Deckers
ead7adb36c about: clearing cache also removes external cache 2024-07-01 22:48:52 +02:00
Thibault Deckers
3e8bd108b0 resized launcher icon 2024-06-30 23:00:45 +02:00
Thibault Deckers
369be3cee2 minor fixes 2024-06-30 22:33:07 +02:00
Thibault Deckers
7777bf1550 fixed collection page app bar layout 2024-06-30 18:22:07 +02:00
Thibault Deckers
27db528e67 fixed handling wallpaper intent without uri
#1052 handle secure review intent
2024-06-29 01:29:22 +02:00
Thibault Deckers
0f1d8ec760 explorer fixes 2024-06-26 22:13:27 +02:00
Thibault Deckers
d78a897326 upgrades 2024-06-25 23:03:27 +02:00
Thibault Deckers
a5c5d5bad6 explorer: page review 2024-06-25 22:17:27 +02:00
Thibault Deckers
b51769e2c6 minor 2024-06-25 22:16:37 +02:00
Thibault Deckers
4a0009f4f0 explorer: source state listening 2024-06-25 00:05:54 +02:00
Thibault Deckers
97e05506a3 go to explorer from path filter 2024-06-24 23:44:08 +02:00
Thibault Deckers
5651d035e7 explorer fixes 2024-06-24 23:08:31 +02:00
Thibault Deckers
01dd1c925a explorer animations 2024-06-24 22:36:31 +02:00
Thibault Deckers
218846117c explorer fixes 2024-06-24 20:53:12 +02:00
Thibault Deckers
ace841212e #592 #910 explorer 2024-06-24 00:47:27 +02:00
Thibault Deckers
930cdf9120 minor 2024-06-23 19:23:54 +02:00
Thibault Deckers
2d56172c96 #1013 fixed handling of app inactive event by screen rotation on API33+ 2024-06-23 18:23:46 +02:00
Thibault Deckers
978c22dc50 #883 bulk converting motion photos to still images 2024-06-23 02:36:44 +02:00
Thibault Deckers
87cfae1e9a ask to rename/replace/skip when converting items with name conflict 2024-06-22 17:21:41 +02:00
Thibault Deckers
d890d9d9ae #1045 stack RAW and JPEG with same file names 2024-06-21 23:26:00 +02:00
Thibault Deckers
44eecd2e55 version bump 2024-06-17 21:40:28 +02:00
Thibault Deckers
4e811cc37c leaks: imageinfo, notifiers 2024-06-17 21:03:55 +02:00
Thibault Deckers
4116a55f8d leaks: curved animations 2024-06-17 19:09:04 +02:00
Thibault Deckers
404bc5471b minor fixes 2024-06-17 17:19:32 +02:00
Thibault Deckers
030afc70f7 #1052 handle MediaStore.ACTION_REVIEW intent 2024-06-15 19:42:36 +02:00
Thibault Deckers
201d458aef version bump 2024-06-11 23:10:52 +02:00
Weblate (bot)
9657501d6e
l10n by weblate (#1035)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/th/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translation: Aves/App - Main

Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: atilluF <110931720+atilluF@users.noreply.github.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
2024-06-12 06:05:53 +09:00
Thibault Deckers
3ccce6a529 viewer: narrower side tap area 2024-06-09 01:09:16 +02:00
Thibault Deckers
9816cc15b3 static analysis 2024-06-09 00:42:13 +02:00
Thibault Deckers
27cf1bf4aa #1008 opening app from launcher shows home page only when exited by back button 2024-06-09 00:37:30 +02:00
Thibault Deckers
1578d2d944 #1041 allow shifting dates by seconds 2024-06-08 20:05:14 +02:00
Thibault Deckers
da4a7ae38f fixed raw preview region decoding sample size 2024-06-08 18:49:39 +02:00
Thibault Deckers
1346e8867b #1037 use truncated preview when opening large DNG with metadata extractor 2024-06-08 01:53:10 +02:00
Thibault Deckers
981727cb1d Merge branch 'develop' 2024-06-08 00:39:05 +02:00
Thibault Deckers
e19b20fb59 upgraded Flutter to stable v3.22.2 2024-06-07 00:27:51 +02:00
Thibault Deckers
681f834e04 provide stream size, fixes 1037 but regression with other files 2024-06-06 22:28:15 +02:00
Thibault Deckers
dacb04c6bd upgrades 2024-06-02 00:01:24 +02:00
Weblate (bot)
71e95ca3f7
l10n by weblate (#1030)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/th/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translation: Aves/App - Main

Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-31 06:54:23 +09:00
Thibault Deckers
fe0c2e345b #1012 show chip selection in collection via or filter 2024-05-30 23:32:13 +02:00
Thibault Deckers
ece28db3f8 #1032 screen saver: black background 2024-05-30 01:34:18 +02:00
Thibault Deckers
171258ba2b upgraded Flutter to stable v3.22.1 2024-05-22 22:59:30 +02:00
Thibault Deckers
c4a7969ed7 android compat fix for old devices 2024-05-22 22:16:42 +02:00
Thibault Deckers
ab92b883b6 minor 2024-05-21 22:54:00 +02:00
Thibault Deckers
17c88b2514 shaders for 3.22.0 2024-05-21 19:44:01 +02:00
Thibault Deckers
d8193584b1 l10n: typography fixes for arabic/persian 2024-05-20 18:04:46 +02:00
Weblate (bot)
b4af217d43
l10n by weblate (#1029)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translation: Aves/App - Main

Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2024-05-21 00:25:10 +09:00
Thibault Deckers
88decb71a2 l10n 2024-05-20 16:03:07 +02:00
Thibault Deckers
5e452bbd4d l10n 2024-05-20 15:58:27 +02:00
Weblate (bot)
7b761a1f41
l10n by weblate (#1017)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ko/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: ID J <tabby4442@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Maxi <maxitendo01@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Owen Elderbroek <o.elderbroek@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: cheese <deanlemans5646@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: randint <lancameb@hotmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wanzh <wanzh66666@gmail.com>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 何意挽秋風 <94283631+RejectVanity@users.noreply.github.com>
2024-05-20 22:52:30 +09:00
Thibault Deckers
1717b1fc90 l10n test 2024-05-20 15:23:57 +02:00
Thibault Deckers
f10abcd670 format review 2024-05-20 15:20:13 +02:00
Thibault Deckers
ff16651ce1 l10n: fixes for RTL 2024-05-20 01:41:00 +02:00
Thibault Deckers
dd6258d8ac l10n: typography fixes for arabic/persian 2024-05-19 01:28:26 +02:00
Thibault Deckers
ea53420c17 l10n: specify plural num pattern to apply locale 2024-05-18 23:59:21 +02:00
Thibault Deckers
56762eea9c upgraded Flutter to stable v3.22.0 2024-05-18 22:43:29 +02:00
Thibault Deckers
9a4657379b upgrades 2024-05-14 00:06:09 +02:00
Thibault Deckers
45bc2e222d l10n 2024-05-09 12:41:28 +02:00
Weblate (bot)
07ca1a9098
l10n by weblate (#1006)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ko/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: ID J <tabby4442@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Owen Elderbroek <o.elderbroek@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: cheese <deanlemans5646@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: randint <lancameb@hotmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wanzh <wanzh66666@gmail.com>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 何意挽秋風 <94283631+RejectVanity@users.noreply.github.com>
2024-05-09 19:16:21 +09:00
Thibault Deckers
8eabed6bd2 l10n 2024-05-03 18:36:01 +02:00
Thibault Deckers
953013ea36 upgrades 2024-05-03 01:00:52 +02:00
Thibault Deckers
9de065a6b4 version bump 2024-05-03 00:43:33 +02:00
Thibault Deckers
2ed9815c38 l10n: fa 2024-05-03 00:38:48 +02:00
Weblate (bot)
c21f89b9e8
l10n by weblate (#998)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ko/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Co-authored-by: ID J <tabby4442@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: randint <lancameb@hotmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wanzh <wanzh66666@gmail.com>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
2024-05-03 06:58:43 +09:00
Thibault Deckers
eb2a49eb49 #1002 adjusted icon offset 2024-05-02 20:52:43 +02:00
Thibault Deckers
4f746c2177 #1000 collection: select all available as quick action 2024-05-02 20:42:05 +02:00
Thibault Deckers
e359eb46f0 version bump 2024-05-01 18:02:30 +02:00
Weblate (bot)
56012bd174
l10n by weblate (#995)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ko/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Co-authored-by: ID J <tabby4442@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: randint <lancameb@hotmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wanzh <wanzh66666@gmail.com>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-05-02 00:57:57 +09:00
Thibault Deckers
bb7f300a43 fixed printing content orientation according to page format 2024-05-01 00:48:01 +02:00
Thibault Deckers
2f8d870811 minor 2024-04-30 23:05:36 +02:00
Thibault Deckers
25598163a5 #874 external subtitle support 2024-04-29 23:20:56 +02:00
Thibault Deckers
8ee213b787 l10n 2024-04-29 00:23:58 +02:00
Weblate (bot)
9020f4ca29
l10n by weblate (#975)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ko/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Evgeniy Khramov <65224669+thejenja@users.noreply.github.com>
Co-authored-by: ID J <tabby4442@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: randint <lancameb@hotmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wanzh <wanzh66666@gmail.com>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2024-04-29 07:16:38 +09:00
Thibault Deckers
67363ae258 changed logo 2024-04-28 23:48:29 +02:00
Thibault Deckers
5b2fbe48d3 info: color palette 2024-04-28 16:43:16 +02:00
Thibault Deckers
fc9d282caa minor fix 2024-04-25 01:17:11 +02:00
Thibault Deckers
a3dcf41786 improved memory usage and check for video thumbnail decoding 2024-04-25 00:37:36 +02:00
Thibault Deckers
99cd9faec7 upgrades 2024-04-24 23:20:02 +02:00
Thibault Deckers
dffa54239f #957 fixed video pause when changing device orientation on Android API33 2024-04-24 19:57:59 +02:00
Thibault Deckers
65e224ef57 #990 renaming: processor for hash (md5/sha1/sha256) 2024-04-22 22:43:21 +02:00
Thibault Deckers
949f9a514f minor fix 2024-04-21 19:42:18 +02:00
Thibault Deckers
2bb52143cd stats: fixed histogram when crossing DST boundaries 2024-04-21 19:35:53 +02:00
Thibault Deckers
94eccbd2f1 #960 option to force using western arabic numerals for dates 2024-04-21 18:59:08 +02:00
Thibault Deckers
569aef5f9f info: show xmp sections when using exifinterface fallback 2024-04-20 20:21:11 +02:00
Thibault Deckers
dd1d56f73a minor fixes 2024-04-20 18:16:01 +02:00
Thibault Deckers
cdd6f2017c static analysis 2024-04-18 23:37:36 +02:00
Thibault Deckers
8776a3cc44 #987 viewer: fixed snack bar bottom margin 2024-04-18 23:25:35 +02:00
Thibault Deckers
66713cb663 upgraded Flutter to stable v3.19.6 2024-04-18 00:30:43 +02:00
Thibault Deckers
342397fb85 #984 rendering of SVG with large header 2024-04-18 00:12:52 +02:00
Thibault Deckers
ce27d47342 #976 identify Apple variant of HDR images 2024-04-16 22:09:57 +02:00
Thibault Deckers
e9d2e3c42e #976 mpf: apply rotation when decoding sub pages 2024-04-16 19:58:45 +02:00
Thibault Deckers
e777c35b1e #976 mpf: use primary rotation for pages 2024-04-15 20:52:28 +02:00
Thibault Deckers
bf8906a9f1 version bump 2024-04-14 23:24:41 +02:00
Thibault Deckers
69f9dbc2bc l10n 2024-04-14 23:20:07 +02:00
Weblate (bot)
f183d41d1a
l10n by weblate (#965)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ko/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: ID J <tabby4442@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: wanzh <wanzh66666@gmail.com>
Co-authored-by: wickdj <wickdj@gmail.com>
2024-04-15 06:15:33 +09:00
Thibault Deckers
0d2427f9eb Update CHANGELOG.md 2024-04-14 22:56:47 +02:00
Thibault Deckers
1b70b389e6 #970 skip source reinitialization when relaunching app from launcher 2024-04-09 20:06:08 +02:00
Thibault Deckers
71e9e07668 #967 pick collection with removable contextual filters to edit date/location 2024-04-08 22:51:15 +02:00
Thibault Deckers
e9b86f9d77 #969 fixed rendering of SVG with viewbox offset 2024-04-08 21:45:14 +02:00
Thibault Deckers
9e806bc5ee upgraded gradle 2024-04-08 21:44:00 +02:00
Thibault Deckers
e81436fecd fixed build for huawei flavor 2024-04-02 00:01:02 +02:00
Thibault Deckers
b47b59ed6a upgrades 2024-04-01 23:20:13 +02:00
Thibault Deckers
4b458e0f04 version bump 2024-04-01 23:10:16 +02:00
Weblate (bot)
9ce6f19fc3
l10n by weblate (#963)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: elea11 <p.manuel.warnecke@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: しいたけ <Shiitake@users.noreply.hosted.weblate.org>
Co-authored-by: 陳義昇(ㄚ昇) <love80312@gmail.com>
2024-04-02 06:03:14 +09:00
Thibault Deckers
034a816704 viewer: fixed snack bar margin on page transition 2024-04-01 23:00:34 +02:00
Thibault Deckers
9f9cdebc9e minor fix 2024-04-01 19:21:58 +02:00
Thibault Deckers
08bd187c7d albums: case insensitive unique names 2024-04-01 17:04:36 +02:00
Thibault Deckers
1bf5a2e315 l10n 2024-03-31 19:14:38 +02:00
Weblate (bot)
bfbf086c54
l10n by weblate (#944)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: elea11 <p.manuel.warnecke@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: しいたけ <Shiitake@users.noreply.hosted.weblate.org>
Co-authored-by: 陳義昇(ㄚ昇) <love80312@gmail.com>
2024-04-01 02:11:13 +09:00
Thibault Deckers
5e875fa526 minor 2024-03-31 18:53:47 +02:00
Thibault Deckers
e1c3bae90b #622 viewer: fixed side gesture precedence 2024-03-31 18:37:25 +02:00
Thibault Deckers
46aef919be #962 video: A-B repeat 2024-03-31 00:17:37 +01:00
Thibault Deckers
8555322460 upgraded Flutter to stable v3.19.5 2024-03-29 19:11:32 +01:00
Thibault Deckers
007c917dca #960 l10n: use dates with western arabic numerals for maghreb arabic locales 2024-03-27 21:15:32 +01:00
Thibault Deckers
5db31476fe #937 viewer: fixed position drift during scale 2024-03-26 23:11:23 +01:00
Thibault Deckers
bd9a89e5d4 #952 renaming: processors for tags/make/model 2024-03-24 00:57:25 +01:00
Thibault Deckers
55c96ad1c1 #935 hidden items can be toggled 2024-03-23 22:26:52 +01:00
Thibault Deckers
b5dbac1e2b #953 opening app from launcher always show home page 2024-03-23 19:42:14 +01:00
Thibault Deckers
38d6ee430a #956 fairphone burst pattern 2024-03-23 14:27:38 +01:00
Thibault Deckers
1cda37573d migrated flutter gradle plugin usage 2024-03-22 00:05:18 +01:00
Thibault Deckers
fe36f7c1bc upgraded gradle 2024-03-21 22:56:17 +01:00
Thibault Deckers
5d1592ac50 upgraded Flutter to stable v3.19.4 2024-03-21 22:34:59 +01:00
Thibault Deckers
c4777aec61 android 15 / api 35 todo review 2024-03-21 21:36:41 +01:00
Thibault Deckers
0cbe3d8b1d prevent OOM when decoding large region 2024-03-18 23:31:33 +01:00
Thibault Deckers
dbead97c37 minor 2024-03-15 00:35:59 +01:00
Thibault Deckers
b8a1878dff minor fix 2024-03-15 00:24:03 +01:00
Thibault Deckers
7ed69c0a0f maybe fix for Attempt to invoke virtual method 'java.lang.String java.io.File.getPath()' on a null object reference 2024-03-13 19:41:04 +01:00
Thibault Deckers
12af258fd5 version bump 2024-03-12 19:54:08 +01:00
Thibault Deckers
63e64c40c4 analysis: add missing channel for recoverable trash 2024-03-12 19:51:26 +01:00
Thibault Deckers
724155a19a
Update README.md 2024-03-13 03:45:17 +09:00
Thibault Deckers
895c495e22 version bump 2024-03-11 23:33:19 +01:00
Thibault Deckers
8a4f7b9299 l10n 2024-03-11 23:28:18 +01:00
Weblate (bot)
e87e64c264
l10n by weblate (#918)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Alireza Rashidi <alirezarashidigoorabi@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: elea11 <p.manuel.warnecke@gmail.com>
Co-authored-by: syu_pf_ssy <syu.pf.ssy@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: 陳義昇(ㄚ昇) <love80312@gmail.com>
2024-03-12 07:24:28 +09:00
Thibault Deckers
e8ccbf7bf8 fixed tests 2024-03-11 23:21:39 +01:00
Thibault Deckers
4055a1c6d3 minor 2024-03-11 23:06:07 +01:00
Thibault Deckers
a9444b61e2 #782 manual GC before/during cataloguing 2024-03-11 23:05:43 +01:00
Thibault Deckers
0b95c80356 #782 dispose flutter engine from analysis worker 2024-03-11 23:02:00 +01:00
Thibault Deckers
01802f8aaa debug: heap size 2024-03-09 20:03:28 +01:00
Thibault Deckers
6192dea599 upgraded Flutter to stable v3.19.3 2024-03-09 18:56:01 +01:00
Thibault Deckers
ffcd749577 upgraded Flutter to stable v3.19.2 2024-02-28 20:21:08 +01:00
Thibault Deckers
219bf1bf3f #838 cataloguing: detect HDR videos 2024-02-25 22:58:48 +01:00
Thibault Deckers
df2d088ecf minor 2024-02-24 19:28:50 +01:00
Thibault Deckers
535936666c a11y: disabling animations also applies to pop up menus 2024-02-24 18:38:15 +01:00
Thibault Deckers
786335ede3 viewer: fixed error background on light theme 2024-02-24 16:40:16 +01:00
Thibault Deckers
efa6e61ff0 test fix 2024-02-24 01:12:03 +01:00
Thibault Deckers
f287dd4c04 #900 check media store changes on app resume 2024-02-24 01:01:10 +01:00
Thibault Deckers
fcd2e493da minor fix 2024-02-23 19:32:02 +01:00
Thibault Deckers
198bdca9c0 github workflow action upgrade 2024-02-22 21:57:43 +01:00
Thibault Deckers
87c80355c9 version bump 2024-02-22 20:24:37 +01:00
Thibault Deckers
a3d6b652d0 l10n 2024-02-22 20:21:36 +01:00
Weblate (bot)
ec29c34dd7
l10n by weblate (#907)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translation: Aves/App - Android
Translation: Aves/App - Main

Co-authored-by: AJ07 <ajaykumarmeena676@gmail.com>
Co-authored-by: Igor Sorocean <sorocean.igor@gmail.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
2024-02-23 04:19:11 +09:00
Thibault Deckers
413794cb74 #913 recover untracked vault items 2024-02-22 20:14:31 +01:00
Thibault Deckers
a0925273bf fix 2024-02-22 18:03:30 +01:00
Thibault Deckers
31b9b633ae recover untracked binned items 2024-02-22 17:49:45 +01:00
Thibault Deckers
6db333f73b upgraded Flutter to stable v3.19.1 2024-02-21 23:53:28 +01:00
Thibault Deckers
8b62d84b84 Update OWNERS and ExampleUnitTest.java 2024-02-21 23:20:47 +01:00
Thibault Deckers
44cc93d892 added exifinterface fork module 2024-02-21 23:20:36 +01:00
Thibault Deckers
d243d3b6f4 upgraded Flutter to stable v3.19.0 2024-02-15 23:23:05 +01:00
Thibault Deckers
3cef268138 #902 widget: outline color options according to device theme 2024-02-13 00:03:25 +01:00
Thibault Deckers
ed250f9ccf minor 2024-02-12 19:50:24 +01:00
Thibault Deckers
a575fec524 #888 viewer: prompt to show newly edited item 2024-02-11 20:09:38 +01:00
Thibault Deckers
28f7819eaf get result uri from edit intent 2024-02-11 19:45:52 +01:00
Thibault Deckers
a0159c9b82 l10n: ca 2024-02-11 00:20:09 +01:00
Weblate (bot)
53d429180a
l10n by weblate (#899)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ca/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/is/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: Grooty12 <Rasmus@rosendahl-kaa.name>
Co-authored-by: Grooty12 <weblate@grooty.site>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Marc Amorós <marquitus99@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: luckris25 <lk1thebestl@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: minh <teaminh@skiff.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-02-11 07:44:49 +09:00
Thibault Deckers
0ab7550bbc upgrades 2024-02-08 23:04:31 +01:00
Thibault Deckers
21cfee88a7 upgrades 2024-02-08 22:53:23 +01:00
Thibault Deckers
6c175cf81e version bump 2024-02-07 22:29:54 +01:00
Thibault Deckers
f32f92470a l10n 2024-02-06 20:37:47 +01:00
Weblate (bot)
917f1fbe40
l10n by weblate (#887)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/da/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/el/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/is/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: Grooty12 <Rasmus@rosendahl-kaa.name>
Co-authored-by: Grooty12 <weblate@grooty.site>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Languages add-on <noreply-addon-languages@weblate.org>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: luckris25 <lk1thebestl@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: minh <teaminh@skiff.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-02-07 04:33:09 +09:00
Thibault Deckers
c65bda181a #898 fixed viewer transition effects for RTL locales 2024-02-06 20:30:00 +01:00
Thibault Deckers
b32f409b35 #898 fixed viewer transition direction for RTL locales 2024-02-06 20:22:13 +01:00
Thibault Deckers
de63663149 #896 get HEIF size from BitmapFactory over unreliable one from Media Store 2024-02-04 23:32:23 +01:00
Thibault Deckers
d060a051ef reporting: prevent noisy reports 2024-02-04 22:43:48 +01:00
Thibault Deckers
e57484d912 #894 fixed motion photo detection for xml variant of google container item 2024-02-04 19:52:56 +01:00
Thibault Deckers
b4a5513fe1 #894 google xmp refactor 2024-02-04 18:57:35 +01:00
Thibault Deckers
29487a1303 version bump 2024-01-29 23:48:33 +01:00
Thibault Deckers
c9a0ae602b convert: fixed target size 2024-01-29 23:21:27 +01:00
Thibault Deckers
b547b5b9b1 fixed aspect ratio when decoding TIFF with custom size 2024-01-29 23:20:42 +01:00
Thibault Deckers
73d99e9687 pixy meta upgrade/cleanup 2024-01-29 00:00:23 +01:00
Thibault Deckers
743f8f72f9 disable color mode change 2024-01-27 22:29:29 +01:00
Thibault Deckers
c778bd6e9e fixed decoding TIFF to specific size for conversion 2024-01-27 22:24:21 +01:00
Thibault Deckers
ec93b3f21d #706 viewer: improved overlay field fetch 2024-01-27 00:37:08 +01:00
Thibault Deckers
baa318f0de viewer: improved display of multiline descriptions 2024-01-26 23:28:16 +01:00
Thibault Deckers
0b5e235053 report fix 2024-01-26 22:10:59 +01:00
Thibault Deckers
d696dbb01d l10n 2024-01-26 19:39:49 +01:00
Weblate (bot)
8ff1f1eb4d
l10n by weblate (#881)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sat/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/is/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: minh <teaminh@skiff.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-01-27 03:28:48 +09:00
Thibault Deckers
8e37490270 upgraded Flutter to stable v3.16.9 2024-01-26 19:25:13 +01:00
Thibault Deckers
55e8427e3d reporting: privent noisy reports 2024-01-26 18:58:47 +01:00
Thibault Deckers
2e13879d77 #706 fixed histogram byte count, aligned luminance formula 2024-01-23 00:13:07 +01:00
Thibault Deckers
da38e4a4ed #706 lift format control for tiling, allowing large DNG tiling if supported 2024-01-22 20:34:49 +01:00
Thibault Deckers
4fada94fb6 l10n 2024-01-22 19:23:05 +01:00
Thibault Deckers
d17b444feb l10n 2024-01-21 19:46:03 +01:00
Thibault Deckers
cd1fd3be69 l10n 2024-01-20 23:45:32 +01:00
Thibault Deckers
4633b38cdf l10n 2024-01-20 23:33:22 +01:00
Weblate (bot)
6a4075cabd
l10n by weblate (#871)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sv/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Shift18 <bribable.lawyer@posteo.net>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: minh <teaminh@skiff.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-01-21 07:25:27 +09:00
Thibault Deckers
3c3da4a412 upgraded Flutter to stable v3.16.8 2024-01-18 00:24:24 +01:00
Thibault Deckers
82e4a6b22a moved region decoding responsibility to platform side 2024-01-18 00:07:21 +01:00
Thibault Deckers
426264d186 fixed searching from drawer on mobile 2024-01-12 20:01:38 +01:00
Thibault Deckers
fe8415fced l10n 2024-01-12 19:28:38 +01:00
Weblate (bot)
7800c86acc
l10n by weblate (#854)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/is/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Eric <zxmegaxqug@hldrive.com>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: v1s7 <v1s7@users.noreply.hosted.weblate.org>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
2024-01-13 03:20:01 +09:00
Thibault Deckers
f1e41b2b32 upgraded Flutter to stable v3.16.7 2024-01-12 18:45:31 +01:00
Thibault Deckers
fb5457672a #706 prevent multiple calls to load viewer overlay metadata 2024-01-09 23:54:36 +01:00
Thibault Deckers
58e12d147d #763 allow setting any filtered collection as home page 2024-01-07 01:04:59 +01:00
Thibault Deckers
694c5941cb #737 keep playing slideshow when losing focus but app is still visible (e.g. split screen) 2024-01-06 23:36:24 +01:00
Thibault Deckers
b57253c251 #861 strip unlocated filter from context collection when editing location via map 2023-12-30 14:33:35 +01:00
Thibault Deckers
e2cce43620 upgrades 2023-12-30 10:37:29 +01:00
Thibault Deckers
49fa3eec96 #838 HDR color mode 2023-12-29 23:57:58 +01:00
Thibault Deckers
445aa2cb06 AGP upgrade 2023-12-29 23:52:18 +01:00
Thibault Deckers
3547787112 #859 thumbnail overlay HDR icon toggle 2023-12-29 15:10:42 +01:00
Thibault Deckers
f76c45e276 version bump 2023-12-24 16:50:59 +01:00
Thibault Deckers
1984e601b0 viewer: keep controls in the lower right corner even with RTL locales 2023-12-24 16:48:19 +01:00
Weblate (bot)
b17bfd196d
l10n by weblate (#850)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
2023-12-25 00:30:19 +09:00
Thibault Deckers
8981900d6b #853 apply scale when decoding SVG 2023-12-24 16:10:26 +01:00
Thibault Deckers
eb004d8eca #853 prevent decoding SVG to large region 2023-12-24 12:01:59 +01:00
Thibault Deckers
b010e2f86e version bump 2023-12-21 18:56:42 +01:00
Thibault Deckers
40862b6b49 l10n 2023-12-21 18:50:48 +01:00
Weblate (bot)
dd3f5a6844
l10n by weblate (#845)
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translation: Aves/App - Main
Translation: Aves/Store - Full description

Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: yangyangdaji <1504305527@qq.com>
2023-12-22 02:48:24 +09:00
Thibault Deckers
f5e7b5275f multipage: hide gain map page from ultra HDR images 2023-12-21 18:46:50 +01:00
Thibault Deckers
72a6af5040 memory leak fixes 2023-12-21 18:33:36 +01:00
Thibault Deckers
929d7c25fd minor fixes 2023-12-21 16:37:41 +01:00
Thibault Deckers
c95b381fad avoid double extraction for embedded media 2023-12-21 13:18:51 +01:00
Thibault Deckers
5c8cd81ff7 upgraded Flutter to stable v3.16.5 2023-12-20 23:28:56 +01:00
Thibault Deckers
372e5e75c9 restored icon font tree shaking 2023-12-20 23:13:09 +01:00
Thibault Deckers
ae5c2f795e MPF: mpf dependent image decoding in viewer 2023-12-20 20:17:12 +01:00
Thibault Deckers
8c5cfc4a87 MPF: exclude thumbnails to detect multipage JPEG 2023-12-20 19:33:48 +01:00
Thibault Deckers
e5b4f648a1 minor fix 2023-12-20 18:50:50 +01:00
Thibault Deckers
bbc5735ec6 upgrades 2023-12-20 16:56:58 +01:00
Thibault Deckers
fabb79f77f fixed media query update 2023-12-20 16:26:27 +01:00
Thibault Deckers
97ffbfa95b fixed text / strut styles 2023-12-20 15:28:02 +01:00
Thibault Deckers
81977ea4d8 fixed text / strut styles 2023-12-20 14:21:06 +01:00
Thibault Deckers
2d5de662ff fixed showing undecodable trashed item as missing from storage 2023-12-19 19:16:40 +01:00
Thibault Deckers
4c6c56e3f8 ui fixes 2023-12-19 19:09:40 +01:00
Thibault Deckers
4dcfc7568f minor fixes 2023-12-19 15:35:40 +01:00
Thibault Deckers
cd624979fb fixed selection for filter actions 2023-12-19 12:36:26 +01:00
Thibault Deckers
b54c8824f0 ui fixes 2023-12-19 12:04:13 +01:00
Weblate (bot)
0a1c5a44e3
l10n by weblate (#844)
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/be/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/de/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/el/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/es/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/gl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/id/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/it/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ja/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/lt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ro/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/zh_Hant/
Translation: Aves/Store - Full description

Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
2023-12-15 07:09:28 +09:00
Thibault Deckers
6c7323e34f full description key change fix 2023-12-14 22:52:11 +01:00
Hosted Weblate
bbfea38b02
l10n by weblate
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translation: Aves/Store - Full description
2023-12-14 22:44:44 +01:00
Thibault Deckers
96eafd09f0 full description key change fix 2023-12-14 22:44:40 +01:00
Thibault Deckers
761fdd1d58 full description key change fix 2023-12-14 22:36:41 +01:00
Hosted Weblate
6477f3a818
Merge remote-tracking branch 'origin/develop' into develop 2023-12-14 20:56:16 +00:00
Thibault Deckers
c69cc59ed5 Revert "Revert "l10n: full description update""
This reverts commit 6a449d3520.
2023-12-14 21:56:10 +01:00
Thibault Deckers
6a449d3520 Revert "l10n: full description update"
This reverts commit d409e5191d.
2023-12-14 21:54:34 +01:00
Hosted Weblate
2eba75e63d
l10n by weblate
Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Alvi Khan <aveenalvi@gmail.com>
Co-authored-by: Diviega Ayulsa <ayulsadiviega@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Htet Oo Hlaing <htetoh2006@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Khant <khant@users.noreply.hosted.weblate.org>
Co-authored-by: Kryštof Černý <cleverline1mc@gmail.com>
Co-authored-by: Le Lan Anh <bebinh2202@gmail.com>
Co-authored-by: Le Nhut Binh <bebinh2202@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Martin Frandel <martinko.fr@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Rasti K5 <rasti.khdhr@gmail.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Skrripy <Skrripy@users.noreply.hosted.weblate.org>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: khant <khant@users.noreply.hosted.weblate.org>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: mimvahedi <vahedi0vahedi@gmail.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slasb37 <p84haghi@gmail.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Åzze <laitinen.jere222@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description
2023-12-14 21:49:19 +01:00
Thibault Deckers
4a095fa70d minor fixes 2023-12-13 23:28:53 +01:00
Thibault Deckers
14579932ae l10n: ar 2023-12-13 22:38:26 +01:00
Thibault Deckers
3bd5586345 upgraded Flutter to stable v3.16.4 2023-12-13 21:50:13 +01:00
Thibault Deckers
5d5e0b551b rtl fixes 2023-12-13 19:56:17 +01:00
Thibault Deckers
643b9ab77c rtl fixes 2023-12-13 18:55:18 +01:00
Thibault Deckers
d409e5191d l10n: full description update 2023-12-12 23:44:52 +01:00
Thibault Deckers
faef8918e3 rtl fixes; rescan requires selection 2023-12-12 23:39:06 +01:00
Weblate (bot)
e7ed279593
l10n by weblate (#840)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Alvi Khan <aveenalvi@gmail.com>
Co-authored-by: Diviega Ayulsa <ayulsadiviega@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Htet Oo Hlaing <htetoh2006@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Khant <khant@users.noreply.hosted.weblate.org>
Co-authored-by: Kryštof Černý <cleverline1mc@gmail.com>
Co-authored-by: Le Lan Anh <bebinh2202@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Martin Frandel <martinko.fr@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Rasti K5 <rasti.khdhr@gmail.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Skrripy <Skrripy@users.noreply.hosted.weblate.org>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: mimvahedi <vahedi0vahedi@gmail.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slasb37 <p84haghi@gmail.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Åzze <laitinen.jere222@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
2023-12-13 07:07:08 +09:00
Thibault Deckers
0cbe77b385 rtl icon fix 2023-12-11 00:08:24 +01:00
Thibault Deckers
059e852ed1 rtl fixes 2023-12-10 23:28:17 +01:00
Thibault Deckers
273ec45a28 l10n: be 2023-12-10 18:52:22 +01:00
Weblate (bot)
e6e503796d
l10n by weblate (#824)
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-android/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/be/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ckb/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/cs/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/de/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/es/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/eu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fa/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/fr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/hu/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/id/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/is/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/it/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ko/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/my/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/or/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pl/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/pt/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/ru/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/sk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/tr/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/uk/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/aves/app-main/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/ar/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-full-description/vi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/bn/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/fi/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/my/
Translate-URL: https://hosted.weblate.org/projects/aves/store-short-description/vi/
Translation: Aves/App - Android
Translation: Aves/App - Main
Translation: Aves/Store - Full description
Translation: Aves/Store - Short description

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Alvi Khan <aveenalvi@gmail.com>
Co-authored-by: Diviega Ayulsa <ayulsadiviega@gmail.com>
Co-authored-by: Fqwe1 <Fqwe1@users.noreply.hosted.weblate.org>
Co-authored-by: Henning Bunk <henningtbunk@gmail.com>
Co-authored-by: Htet Oo Hlaing <htetoh2006@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Khant <khant@users.noreply.hosted.weblate.org>
Co-authored-by: Kryštof Černý <cleverline1mc@gmail.com>
Co-authored-by: Le Lan Anh <bebinh2202@gmail.com>
Co-authored-by: Linerly <linerly@protonmail.com>
Co-authored-by: Martin Frandel <martinko.fr@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Mohamed Zeroug <mzeroug19@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Rasti K5 <rasti.khdhr@gmail.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Skrripy <Skrripy@users.noreply.hosted.weblate.org>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: Subham Jena <subhamjena8465@gmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thibault Deckers <thibault.deckers@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: glemco <glemco@posteo.net>
Co-authored-by: marciozomb13 <marciozomb13@outlook.com>
Co-authored-by: mimvahedi <vahedi0vahedi@gmail.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: slasb37 <p84haghi@gmail.com>
Co-authored-by: vesp <vesp@post.cz>
Co-authored-by: wickdj <wickdj@gmail.com>
Co-authored-by: yangyangdaji <1504305527@qq.com>
Co-authored-by: Åzze <laitinen.jere222@gmail.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
2023-12-11 02:01:47 +09:00
Thibault Deckers
16aa283425 #838 viewer: mpf multipage retrieval / thumbnails 2023-12-10 01:40:02 +01:00
Thibault Deckers
82b4c8aaa1 #838 info: open MPF embedded images 2023-12-09 18:32:26 +01:00
Thibault Deckers
33ae168eda #838 detect/filter Ultra HDR;
info: show MPF metadata
2023-12-09 15:40:57 +01:00
Thibault Deckers
31cd54879d upgraded Flutter to stable v3.16.3 2023-12-06 20:18:26 +01:00
Thibault Deckers
2dbfb1cf91 removed wifi hardware feature constraint 2023-12-04 22:06:10 +01:00
Thibault Deckers
65a820f121 minor 2023-12-02 17:55:26 +01:00
1341 changed files with 59558 additions and 27335 deletions

@ -1 +1 @@
Subproject commit 9e1c857886f07d342cf106f2cd588bcd5e031bb2 Subproject commit d8a9f9a52e5af486f80d932e838ee93861ffd863

6
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily

View file

@ -1,26 +0,0 @@
name: Quality check
on:
push:
branches:
- develop
jobs:
build:
name: Check code quality.
runs-on: ubuntu-latest
steps:
- name: Clone the repository.
uses: actions/checkout@v3
- name: Get packages for the Flutter project.
run: scripts/pub_get_all.sh
- name: Update the flutter version file.
run: scripts/update_flutter_version.sh
- name: Static analysis.
run: ./flutterw analyze
- name: Unit tests.
run: ./flutterw test

27
.github/workflows/dependency-review.yml vendored Normal file
View file

@ -0,0 +1,27 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
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@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1

91
.github/workflows/quality-check.yml vendored Normal file
View file

@ -0,0 +1,91 @@
name: Quality check
on:
push:
branches: [ "develop", "main" ]
pull_request:
branches: [ "develop", "main" ]
types: [ opened, synchronize, reopened ]
schedule:
- cron: '17 8 * * 3'
# Declare default permissions as read only.
permissions: read-all
jobs:
analyze_flutter:
name: Flutter analysis
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get Flutter packages
run: ./flutterw pub get
- name: Generate app localizations
run: ./flutterw gen-l10n
- name: Static analysis.
run: ./flutterw analyze
- name: Unit tests.
run: ./flutterw test
analyze_codeql:
name: CodeQL analysis (${{ matrix.language }})
runs-on: ubuntu-latest
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
strategy:
fail-fast: false
matrix:
include:
- language: java-kotlin
build-mode: manual
steps:
- name: Harden Runner
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@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'temurin'
java-version: '21'
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- if: matrix.build-mode == 'manual'
shell: bash
# build in profile mode, instead of release,
# so that setting up signing environment variables is not required
run: |
scripts/apply_flavor_play.sh
./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with:
category: "/language:${{matrix.language}}"

View file

@ -5,32 +5,44 @@ on:
tags: tags:
- v* - v*
# Declare default permissions as read only.
permissions: read-all
jobs: jobs:
build: release_github:
name: Build and release artifacts. name: GitHub release
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
attestations: write
contents: write
id-token: write
steps: steps:
- uses: actions/setup-java@v3 - name: Harden Runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with: with:
distribution: 'zulu' egress-policy: audit
java-version: '17'
- name: Clone the repository. # Building relies on the Android Gradle plugin,
uses: actions/checkout@v3 # which requires a modern Java version (not the default one).
- name: Set up JDK for Android Gradle plugin
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'temurin'
java-version: '21'
- name: Get packages for the Flutter project. - name: Checkout repository
run: scripts/pub_get_all.sh uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Update the flutter version file. - 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 run: scripts/update_flutter_version.sh
- name: Static analysis. - name: Build signed artifacts
run: ./flutterw analyze
- name: Unit tests.
run: ./flutterw test
- name: Build signed artifacts.
# `KEY_JKS` should contain the result of: # `KEY_JKS` should contain the result of:
# gpg -c --armor keystore.jks # gpg -c --armor keystore.jks
# `KEY_JKS_PASSPHRASE` should contain the passphrase used for the command above # `KEY_JKS_PASSPHRASE` should contain the passphrase used for the command above
@ -49,13 +61,12 @@ jobs:
cp build/app/outputs/bundle/playRelease/*.aab outputs cp build/app/outputs/bundle/playRelease/*.aab outputs
./flutterw build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders.sksl.json ./flutterw build apk -t lib/main_play.dart --flavor play --bundle-sksl-path shaders.sksl.json
cp build/app/outputs/apk/play/release/*.apk outputs cp build/app/outputs/apk/play/release/*.apk outputs
scripts/apply_flavor_huawei.sh
./flutterw build apk -t lib/main_huawei.dart --flavor huawei --bundle-sksl-path shaders.sksl.json
cp build/app/outputs/apk/huawei/release/*.apk outputs
scripts/apply_flavor_izzy.sh scripts/apply_flavor_izzy.sh
./flutterw build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi ./flutterw build apk -t lib/main_izzy.dart --flavor izzy --split-per-abi
cp build/app/outputs/apk/izzy/release/*.apk outputs cp build/app/outputs/apk/izzy/release/*.apk outputs
scripts/apply_flavor_libre.sh scripts/apply_flavor_libre.sh
./flutterw build appbundle -t lib/main_libre.dart --flavor libre
cp build/app/outputs/bundle/libreRelease/*.aab outputs
./flutterw build apk -t lib/main_libre.dart --flavor libre --split-per-abi ./flutterw build apk -t lib/main_libre.dart --flavor libre --split-per-abi
cp build/app/outputs/apk/libre/release/*.apk outputs cp build/app/outputs/apk/libre/release/*.apk outputs
rm $AVES_STORE_FILE rm $AVES_STORE_FILE
@ -65,35 +76,45 @@ jobs:
AVES_KEY_ALIAS: ${{ secrets.AVES_KEY_ALIAS }} AVES_KEY_ALIAS: ${{ secrets.AVES_KEY_ALIAS }}
AVES_KEY_PASSWORD: ${{ secrets.AVES_KEY_PASSWORD }} AVES_KEY_PASSWORD: ${{ secrets.AVES_KEY_PASSWORD }}
AVES_GOOGLE_API_KEY: ${{ secrets.AVES_GOOGLE_API_KEY }} AVES_GOOGLE_API_KEY: ${{ secrets.AVES_GOOGLE_API_KEY }}
AVES_HUAWEI_API_KEY: ${{ secrets.AVES_HUAWEI_API_KEY }}
- name: Create a release with the APK and App Bundle. - name: Generate artifact attestation
uses: ncipollo/release-action@v1 uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
with:
subject-path: 'outputs/*'
- name: Create GitHub release
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
with: with:
artifacts: "outputs/*" artifacts: "outputs/*"
body: "[Changelog](https://github.com/${{ github.repository }}/blob/develop/CHANGELOG.md#${{ github.ref_name }})" body: "[Changelog](https://github.com/${{ github.repository }}/blob/develop/CHANGELOG.md#${{ github.ref_name }})"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload app bundle - name: Upload app bundle
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: appbundle name: appbundle
path: outputs/app-play-release.aab path: outputs/app-play-release.aab
release: release_play:
name: Create beta release on Play Store. name: Play Store beta release
needs: [ build ] needs: [ release_github ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: Harden Runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- name: Get appbundle from artifacts. - name: Checkout repository
uses: actions/download-artifact@v3 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get appbundle from artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: appbundle name: appbundle
- name: Release app to beta channel. - name: Release to beta channel
uses: r0adkll/upload-google-play@v1.1.1 uses: r0adkll/upload-google-play@935ef9c68bb393a8e6116b1575626a7f5be3a7fb # v1.1.3
with: with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }} serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
packageName: deckers.thibault.aves packageName: deckers.thibault.aves

76
.github/workflows/scorecards.yml vendored Normal file
View file

@ -0,0 +1,76 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '20 7 * * 2'
push:
branches: ["develop"]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
contents: read
actions: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecards on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with:
sarif_file: results.sarif

6
.gitignore vendored
View file

@ -5,9 +5,11 @@
*.swp *.swp
.DS_Store .DS_Store
.atom/ .atom/
.build/
.buildlog/ .buildlog/
.history .history
.svn/ .svn/
.swiftpm/
migrate_working_dir/ migrate_working_dir/
# IntelliJ related # IntelliJ related
@ -27,7 +29,6 @@ migrate_working_dir/
.dart_tool/ .dart_tool/
.flutter-plugins .flutter-plugins
.flutter-plugins-dependencies .flutter-plugins-dependencies
.packages
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
@ -46,3 +47,6 @@ app.*.map.json
# screenshot generation # screenshot generation
/test_driver/assets/screenshots/ /test_driver/assets/screenshots/
/screenshots/ /screenshots/
# generated files
/lib/l10ngen/app_localizations*

18
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,18 @@
repos:
- repo: https://github.com/gherynos/pre-commit-java
rev: v0.2.4
hooks:
- id: Checkstyle
- repo: https://github.com/gitleaks/gitleaks
rev: v8.16.3
hooks:
- id: gitleaks
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shellcheck
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace

29
.vscode/launch.json vendored Normal file
View 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"
}
]
}

View file

@ -4,6 +4,543 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
### Added
- Info: show matching dynamic albums
### Fixed
- crash when decoding some large thumbnails
## <a id="v1.13.2"></a>[v1.13.2] - 2025-06-02
### Changed
- downgraded Flutter to stable v3.27.4
- prevent display orientation flip when device rotation is locked
### Fixed
- moved file losing its extension and no longer being detected as media in some cases
- opening home when launching app as media picker
- removing groups with obsolete albums
- loading group custom covers
- crash when parsing some large media with trailing thumbnail
## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14
### Fixed
- albums: show groups to move/copy/export items
- albums: hide grouped albums containing hidden items only
## <a id="v1.13.0"></a>[v1.13.0] - 2025-05-12
### 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
- support for Samsung HEIC motion photos embedding video in sefd box
- Cataloguing: identify video location from Apple QuickTime metadata, and 3GPP `loci` atom
- Collection: stack RAW and HEIC with same file names
- display home tile in side drawer when customized
- Galician translation (thanks Rubén Castiñeiras Lorenzo)
### Changed
- increased precision of file modified date to milliseconds
- upgraded Flutter to stable v3.29.1
### Fixed
- opening motion photo embedded video when video track is not the first one
- some SVG rendering issues
- decoding of SVG containing references to namespaces in !ATTLIST
- fallback decoding of images packed in RGBA_1010102 config
## <a id="v1.12.4"></a>[v1.12.4] - 2025-03-05 [YANKED]
## <a id="v1.12.3"></a>[v1.12.3] - 2025-02-06
### Added
- Metadata: edit location via GPX
- Metadata: toggle for all types in removal dialog
### Changed
- Viewer: improved subsampling and filter quality strategy
- Collection: ignore moving an item to its current directory
- Collection: keep selection when action on several items is interrupted before processing
- Collection: preserve favourite status when converting items
- upgraded Flutter to stable v3.27.4
### Fixed
- editing TIFF metadata increasing file size
- region decoding for some RAW files
- incorrect video size or orientation as reported by Media Store
- corrupting image when removing video from motion photo with incorrect metadata
## <a id="v1.12.2"></a>[v1.12.2] - 2025-01-13
### Added
- DDM coordinate format option
### Changed
- Video: use `media-kit` instead of `ffmpeg-kit` for metadata fetch
- Info: show video chapters
- Accessibility: apply system "touch and hold delay" setting
### Fixed
- crash when cataloguing some videos
- switching to PiP for any inactive app state
## <a id="v1.12.1"></a>[v1.12.1] - 2025-01-05
### Added
- dynamic album decompose action
- Danish translation (thanks Grooty12, Victor M, cat)
### Fixed
- analysis service not triggering because of uninitialized app lifecycle
- Viewer: displaying neighbour items when the initial item of a view intent is a new one
- Search: dynamic album name filtering
## <a id="v1.12.0"></a>[v1.12.0] - 2024-12-19
### Added
- Countries: show states for Mexico
- Estonian translation (thanks Priit Jõerüüt)
### Changed
- upgraded Flutter to stable v3.27.1
### Fixed
- crash when loading many new items on low memory devices
## <a id="v1.11.20"></a>[v1.11.20] - 2024-12-11
### Added
- Albums: dynamic albums from filter sets
- Bulgarian translation (thanks Petrov)
- Tamil translation (thanks தமிழ்நேரம்)
## <a id="v1.11.19"></a>[v1.11.19] - 2024-11-24
### Added
- integrate with OS app language settings on Android >=14
### Changed
- remember title filter visibility by page
## <a id="v1.11.18"></a>[v1.11.18] - 2024-11-18
### Changed
- Albums: improved album creation feedback
- upgraded Flutter to stable v3.24.5
### Fixed
- crash when playing video with DCL restriction enabled
- cataloguing images with wrong MPF offsets
- printing multi-page items containing some unprintable pages
- English (Shavian) locale tags for store listing
## <a id="v1.11.17"></a>[v1.11.17] - 2024-10-30
### Added
- Map: create shortcut to custom region and filters
- Video: frame stepping forward/backward
- Video: custom playback buttons
- English (Shavian) translation (thanks Paranoid Android)
### Changed
- upgraded Flutter to stable v3.24.4
### Fixed
- crash when loading large collection
- Viewer: copying content URI item
- Albums: creating album with same name as existing empty directory
- Privacy: tagging while vaults are unlocked does not yield recent tags visible when vaults are locked
## <a id="v1.11.16"></a>[v1.11.16] - 2024-10-10
### Fixed
- case-insensitive access to restricted directories
## <a id="v1.11.15"></a>[v1.11.15] - 2024-10-09
### Changed
- Enterprise: do not request `INTERACT_ACROSS_PROFILES` permission (Play Store compatibility)
## <a id="v1.11.14"></a>[v1.11.14] - 2024-10-09
### Added
- Map: OpenTopoMap raster layer
- Map: OSM Liberty vector layer (hosted by OSM Americana)
- Interoperability: receiving `geo:` URI generally opens map page at location
- Interoperability: receiving `geo:` URI when editing item location fills in coordinates
- Map basic app shortcut
- Enterprise: support for work profile switching from the drawer
- Settings: hidden path filters are merged with others and can be toggled
### Removed
- `Safe mode` basic app shortcut
### Fixed
- hanging when cataloguing some JPEG MPF images
- Apple HDR image detection
## <a id="v1.11.13"></a>[v1.11.13] - 2024-09-17
### Added
- support opening from the lock screen
### Changed
- upgraded Flutter to stable v3.24.3
### Fixed
- crash when cataloguing some malformed MP4 files
- inconsistent launch screen
## <a id="v1.11.12"></a>[v1.11.12] - 2024-09-16 [YANKED AGAIN!]
## <a id="v1.11.11"></a>[v1.11.11] - 2024-09-16 [YANKED]
## <a id="v1.11.10"></a>[v1.11.10] - 2024-09-01
### Added
- Swedish translation (thanks Shift18, Andreas Håll)
### Changed
- request notification permission when launching scanning service
- upgraded Flutter to stable v3.24.1
### Fixed
- duplicates from new item loading/refreshing
## <a id="v1.11.9"></a>[v1.11.9] - 2024-08-07
### Added
- Viewer: display more items in tag/copy/move quick action choosers
- Viewer: long descriptions are scrollable when overlay is expanded by tap
- Collection: sort by duration
- Map: open external map app from map views
- Explorer: stats
### Changed
- Accessibility: more animations and effects are suppressed when animations are disabled
- upgraded Flutter to stable v3.24.0
### Fixed
- opening app from launcher always showing home page
- collection quick actions not showing in the top bar nor the menu
- multiple widget setup after device reboot
## <a id="v1.11.8"></a>[v1.11.8] - 2024-07-19
### Added
- Explorer: set custom path as home
- Explorer: create shortcut to custom path
### Changed
- target Android 15 (API 35)
- upgraded Flutter to stable v3.22.3
### Fixed
- crash when cataloguing some PNG files
## <a id="v1.11.7"></a>[v1.11.7] - 2024-07-18 [YANKED AGAIN!]
## <a id="v1.11.6"></a>[v1.11.6] - 2024-07-17 [YANKED]
## <a id="v1.11.5"></a>[v1.11.5] - 2024-07-11
### Added
- Collection: stack RAW and JPEG with same file names
- Collection: ask to rename/replace/skip when converting items with name conflict
- Export: bulk converting motion photos to still images
- Explorer: view folder tree and filter paths
### Fixed
- switching to PiP when changing device orientation on Android >=13
- handling wallpaper intent without URI
- sizing widgets with some launchers on Android >=12
### Removed
- `huawei` app flavor
## <a id="v1.11.4"></a>[v1.11.4] - 2024-07-09 [YANKED]
## <a id="v1.11.3"></a>[v1.11.3] - 2024-06-17
### Added
- handle `MediaStore.ACTION_REVIEW` intent
## <a id="v1.11.2"></a>[v1.11.2] - 2024-06-11
### Added
- Albums / Countries / Tags: show selection in Collection
- allow shifting dates by seconds
### Changed
- opening app from launcher shows home page only when exited by back button
- Screen saver: black background, consistent with slideshow
- upgraded Flutter to stable v3.22.2
### Removed
- support for Android KitKat (API 19)
### Fixed
- crash when cataloguing large images
## <a id="v1.11.1"></a>[v1.11.1] - 2024-05-03
### Added
- Cataloguing: identify Apple variant of HDR images
- Collection: `select all` available as quick action
- Collection: allow using hash (md5/sha1/sha256) when bulk renaming
- Info: color palette
- Video: external subtitle support (SRT)
- option to force using western arabic numerals for dates
- Persian translation (thanks امیر جهانگرد, slasb37, mimvahedi, Alireza Rashidi)
### Changed
- logo
- upgraded Flutter to stable v3.19.6
### Fixed
- rendering of SVG with large header
- stopping video playback when changing device orientation on Android >=13
- printing content orientation according to page format
## <a id="v1.11.0"></a>[v1.11.0] - 2024-05-01 [YANKED]
## <a id="v1.10.9"></a>[v1.10.9] - 2024-04-14
### Fixed
- rendering of SVG with viewbox offset
- superfluous media store reinitialization when relaunching app from launcher
## <a id="v1.10.8"></a>[v1.10.8] - 2024-04-01
### Added
- Collection: support for Fairphone burst pattern
- Collection: allow using tags/make/model when bulk renaming
- Video: A-B repeat
- Settings: hidden items can be toggled
### Changed
- opening app from launcher always show home page
- use dates with western arabic numerals for maghreb arabic locales
- album unique names are case insensitive
- upgraded Flutter to stable v3.19.5
### Fixed
- crash when decoding large region
- viewer position drift during scale
- viewer side gesture precedence (next entry by single tap vs zoom by double tap)
## <a id="v1.10.7"></a>[v1.10.7] - 2024-03-12
### Added
- Cataloguing: detect/filter HDR videos
### Changed
- check Media Store changes when resuming app
- disabling animations also applies to pop up menus
- upgraded Flutter to stable v3.19.3
### Fixed
- engine leak from analysis worker
## <a id="v1.10.6"></a>[v1.10.6] - 2024-03-11 [YANKED]
## <a id="v1.10.5"></a>[v1.10.5] - 2024-02-22
### Added
- Viewer: prompt to show newly edited item
- Widget: outline color options according to device theme
- Catalan translation (thanks Marc Amorós)
### Changed
- upgraded Flutter to stable v3.19.1
### Fixed
- untracked binned items recovery
- untracked vault items recovery
## <a id="v1.10.4"></a>[v1.10.4] - 2024-02-07
### Fixed
- motion photo detection for xml variant of google container item
- HEIF size detection for some corrupted files
- viewer transition direction & effects for RTL locales
## <a id="v1.10.3"></a>[v1.10.3] - 2024-01-29
### Added
- Viewer: optional histogram (for real this time)
- Collection: allow hiding thumbnail overlay HDR icon
- Collection: allow setting any filtered collection as home page
### Changed
- Viewer: lift format control for tiling, allowing large DNG tiling if supported
- Info: strip `unlocated` filter from context collection when editing location via map
- Slideshow: keep playing when losing focus but app is still visible (e.g. split screen)
- upgraded Flutter to stable v3.16.9
### Fixed
- crash when loading some large DNG in viewer
- searching from drawer on mobile
- resizing TIFF during conversion
## <a id="v1.10.2"></a>[v1.10.2] - 2023-12-24
### Changed
- Viewer: keep controls in the lower right corner even with RTL locales
### Fixed
- crash when loading SVG defined with large dimensions
## <a id="v1.10.1"></a>[v1.10.1] - 2023-12-21
### Added
- Cataloguing: detect/filter `Ultra HDR`
- Viewer: show JPEG MPF dependent images (except thumbnails and HDR gain maps)
- Info: show metadata from JPEG MPF
- Info: open images embedded via JPEG MPF
- Arabic translation (thanks Mohamed Zeroug)
- Belarusian translation (thanks Макар Разин)
### Changed
- upgraded Flutter to stable v3.16.5
## <a id="v1.10.0"></a>[v1.10.0] - 2023-12-02 ## <a id="v1.10.0"></a>[v1.10.0] - 2023-12-02
### Added ### Added
@ -860,6 +1397,7 @@ All notable changes to this project will be documented in this file.
- app launching on some devices - app launching on some devices
- corrupting motion photo exif editing (e.g. rotation) - corrupting motion photo exif editing (e.g. rotation)
- accessing files in `Download` directory when not using reference case
## [v1.4.9] - 2021-08-20 ## [v1.4.9] - 2021-08-20

View file

@ -12,12 +12,6 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" [<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
alt='Get it on Google Play' alt='Get it on Google Play'
height="80">](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) height="80">](https://play.google.com/store/apps/details?id=deckers.thibault.aves&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1)
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/huawei-appgallery-badge-english-black.png"
alt='Get it on Huawei AppGallery'
height="80">](https://appgallery.huawei.com/app/C106014023)
[<img src="https://raw.githubusercontent.com/deckerst/common/main/assets/amazon-appstore-badge-english-black.png"
alt='Get it on Amazon Appstore'
height="80">](https://www.amazon.com/dp/B09XQHQQ72)
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" [<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png"
alt='Get it on IzzyOnDroid' alt='Get it on IzzyOnDroid'
height="80">](https://apt.izzysoft.de/fdroid/index/apk/deckers.thibault.aves) height="80">](https://apt.izzysoft.de/fdroid/index/apk/deckers.thibault.aves)
@ -41,7 +35,7 @@ It scans your media collection to identify **motion photos**, **panoramas** (aka
**Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc. **Navigation and search** is an important part of Aves. The goal is for users to easily flow from albums to photos to tags to maps, etc.
Aves integrates with Android (from KitKat to Android 13, including Android TV) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**. Aves integrates with Android (including Android TV) with features such as **widgets**, **app shortcuts**, **screen saver** and **global search** handling. It also works as a **media viewer and picker**.
## Screenshots ## Screenshots
@ -117,17 +111,96 @@ Some users have expressed the wish to financially support the project. Thanks!
## Project Setup ## Project Setup
### Install dependencies
Before running or building the app, update the dependencies for the desired flavor: Before running or building the app, update the dependencies for the desired flavor:
``` ```
# scripts/apply_flavor_play.sh scripts/apply_flavor_play.sh
``` ```
To build the project, create a file named `<app dir>/android/key.properties`. It should contain a reference to a keystore for app signing, and other necessary credentials. See [key_template.properties](https://github.com/deckerst/aves/blob/develop/android/key_template.properties) for the expected keys. To build the project, create a file named `<app dir>/android/key.properties`. It should contain a reference to a keystore for app signing, and other necessary credentials. See [key_template.properties](https://github.com/deckerst/aves/blob/develop/android/key_template.properties) for the expected keys.
To run the app: ### To run the app:
``` ```
# ./flutterw run -t lib/main_play.dart --flavor play ./flutterw run -t lib/main_play.dart --flavor play
```
### To build the app:
creare file con le tue credenziali file.keystore
dove YOUR_ALIAS_NAME è il tuo unico alias name
e YOUR_ALIAS_PWD è la password del tuo alias
```sh
keytool -genkey -v -keystore file.keystore -alias YOUR_ALIAS_NAME -storepass YOUR_ALIAS_PWD -keypass YOUR_ALIAS_PWD -keyalg RSA -validity 36500
```
in questo caso ho inserito
```sh
cd android
keytool -genkey -v -keystore file.keystore -alias FabioMich66 -storepass Master66 -keypass Master66 -keyalg RSA -validity 36500
```
se non puoi eseguire keytool perchè non è nel path di sistema cercalo usando
```sh
cd /
sudo find -name keytool
```
compilare il file `<app dir>/android/key.properties`
```
nano android/key.properties
```
questi i miei dati utilizzando il format key_template.properties
```
storeFile=/Users/fabio/flutter_apps/aves/android/file.keystore
storePassword=Master66
keyAlias=FabioMich66
keyPassword=Master66
googleApiKey=<GOOGLE_API_KEY>
```
infine compilare l'apk
```
./flutterw build apk -t lib/main_play.dart --flavor play
``` ```
[Version badge]: https://img.shields.io/github/v/release/deckerst/aves?include_prereleases&sort=semver [Version badge]: https://img.shields.io/github/v/release/deckerst/aves?include_prereleases&sort=semver
[Build badge]: https://img.shields.io/github/actions/workflow/status/deckerst/aves/check.yml?branch=develop [Build badge]: https://img.shields.io/github/actions/workflow/status/deckerst/aves/quality-check.yml?branch=develop
## Android studio
caricare il file da github selezionando le mnù a tendina File-New-project from Version Control
selezionare version control tipo: git
inserire URL di aves
https://github.com/deckerst/aves
flaggare shallow clone with history troncated 1 commits
aprire la console sulla dir aves appena creata e caricare le dipendenze
```
scripts/apply_flavor_izzy.sh
```
in settings - Languages and Framework - Dart inserire il path
```
/home/fabio/flutter/bin/cache/
```
e spuntare project aves
Edit configurations e aggiungere shell script con un nome x es izzi
poi flaggare script text e inserire
./flutterw run -t lib/main_izzy.dart --flavor izzy
la working directory sarà una cosa così
/home/fabio/StudioProjects/aves

View file

@ -9,6 +9,11 @@ analyzer:
# implicit-casts: false # implicit-casts: false
# implicit-dynamic: false # implicit-dynamic: false
# cf https://github.com/dart-lang/dart_style/wiki/Configuration
formatter:
page_width: 240
trailing_commas: preserve
linter: linter:
rules: rules:
# from 'flutter_lints', excluded # from 'flutter_lints', excluded
@ -36,3 +41,8 @@ linter:
prefer_single_quotes: true prefer_single_quotes: true
sort_child_properties_last: true sort_child_properties_last: true
unawaited_futures: true unawaited_futures: true
# `const` related, included
prefer_const_constructors: true
prefer_const_literals_to_create_immutables: true
prefer_const_declarations: true

5
android/.gitignore vendored
View file

@ -5,9 +5,12 @@ gradle-wrapper.jar
/gradlew.bat /gradlew.bat
/local.properties /local.properties
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/
.kotlin/
/build/
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore.
# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app # See https://flutter.dev/to/reference-keystore
key.properties key.properties
**/*.keystore **/*.keystore
**/*.jks **/*.jks

View file

@ -1,31 +1,13 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'com.google.devtools.ksp' version "$ksp_version" id 'com.google.devtools.ksp'
id 'kotlin-android' id 'kotlin-android'
id 'kotlin-kapt' id 'kotlin-kapt'
id 'dev.flutter.flutter-gradle-plugin'
} }
def packageName = "deckers.thibault.aves" def packageName = "deckers.thibault.aves"
// Flutter properties
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionName = localProperties.getProperty('flutter.versionName')
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
// Keys // Keys
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
@ -44,50 +26,23 @@ if (keystorePropertiesFile.exists()) {
keystoreProperties["keyAlias"] = System.getenv("AVES_KEY_ALIAS") ?: "<NONE>" keystoreProperties["keyAlias"] = System.getenv("AVES_KEY_ALIAS") ?: "<NONE>"
keystoreProperties["keyPassword"] = System.getenv("AVES_KEY_PASSWORD") ?: "<NONE>" keystoreProperties["keyPassword"] = System.getenv("AVES_KEY_PASSWORD") ?: "<NONE>"
keystoreProperties["googleApiKey"] = System.getenv("AVES_GOOGLE_API_KEY") ?: "<NONE>" keystoreProperties["googleApiKey"] = System.getenv("AVES_GOOGLE_API_KEY") ?: "<NONE>"
keystoreProperties["huaweiApiKey"] = System.getenv("AVES_HUAWEI_API_KEY") ?: "<NONE>" }
kotlin {
jvmToolchain 17
} }
android { android {
namespace 'deckers.thibault.aves' namespace = 'deckers.thibault.aves'
compileSdk 34 compileSdk = 36
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
checkAllWarnings true
warningsAsErrors true
disable 'InvalidPackage'
}
packagingOptions {
// The Amazon Developer console mistakenly considers the app to not be 64-bit compatible
// if there are some libs in `lib/armeabi-v7a` unmatched by libs in `lib/arm64-v8a`,
// so we exclude the extra `neon` libs bundled by `FFmpegKit`.
exclude 'lib/armeabi-v7a/*_neon.so'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig { defaultConfig {
applicationId packageName applicationId packageName
// minSdk constraints: minSdk flutter.minSdkVersion
// - Flutter & other plugins: 16 targetSdk 36
// - google_maps_flutter v2.1.1: 20 versionCode flutter.versionCode
// - to build XML documents from XMP data, `metadata-extractor` and `PixyMeta` rely on `DocumentBuilder`, versionName flutter.versionName
// which implementation `DocumentBuilderImpl` is provided by the OS and is not customizable on Android, manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
// but the implementation on API <19 is not robust enough and fails to build XMP documents
minSdk 19
targetSdk 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>",
huaweiApiKey: keystoreProperties["huaweiApiKey"] ?: "<NONE>"]
multiDexEnabled true multiDexEnabled true
} }
@ -110,13 +65,6 @@ android {
ext.useNdkAbiFilters = true ext.useNdkAbiFilters = true
} }
huawei {
// Huawei AppGallery
dimension "store"
// generate a universal APK without x86 native libs
ext.useNdkAbiFilters = true
}
izzy { izzy {
// IzzyOnDroid // IzzyOnDroid
// check offending libraries with `scanapk` // check offending libraries with `scanapk`
@ -180,29 +128,20 @@ android {
} }
} }
tasks.withType(KotlinCompile).configureEach {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlin {
jvmToolchain(8)
}
flutter { flutter {
source '../..' source '../..'
} }
repositories { repositories {
maven { maven {
url 'https://jitpack.io' url = 'https://jitpack.io'
content { content {
includeGroup "com.github.deckerst" includeGroup "com.github.deckerst"
includeGroup "com.github.deckerst.mp4parser" includeGroup "com.github.deckerst.mp4parser"
} }
} }
maven { maven {
url 'https://s3.amazonaws.com/repo.commonsware.com' url = 'https://s3.amazonaws.com/repo.commonsware.com'
content { content {
excludeGroupByRegex "com\\.github\\.deckerst.*" excludeGroupByRegex "com\\.github\\.deckerst.*"
} }
@ -210,39 +149,38 @@ repositories {
} }
dependencies { dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat:1.7.1"
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.exifinterface:exifinterface:1.3.6' implementation 'androidx.lifecycle:lifecycle-process:2.9.1'
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06' implementation 'androidx.security:security-crypto:1.1.0-beta01'
implementation 'androidx.work:work-runtime-ktx:2.9.0' implementation 'androidx.work:work-runtime-ktx:2.10.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0' implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.19.0' implementation 'com.drewnoakes:metadata-extractor:2.19.0'
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"
implementation 'com.google.android.material:material:1.12.0'
// SLF4J implementation for `mp4parser` // SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.9' implementation 'org.slf4j:slf4j-simple:2.0.17'
// forked, built by JitPack: // forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory // - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
// - https://jitpack.io/p/deckerst/androidsvg
// - https://jitpack.io/p/deckerst/mp4parser // - https://jitpack.io/p/deckerst/mp4parser
// - https://jitpack.io/p/deckerst/pixymeta-android // - https://jitpack.io/p/deckerst/pixymeta-android
implementation 'com.github.deckerst:Android-TiffBitmapFactory:90c06eebf4' implementation 'com.github.deckerst:Android-TiffBitmapFactory:d6b2b0aa4f'
implementation 'com.github.deckerst.mp4parser:isoparser:4cc0c5d06c' implementation 'com.github.deckerst:androidsvg:67db933051'
implementation 'com.github.deckerst.mp4parser:muxer:4cc0c5d06c' implementation 'com.github.deckerst.mp4parser:isoparser:c2898f1832'
implementation 'com.github.deckerst:pixymeta-android:706bd73d6e' implementation 'com.github.deckerst.mp4parser:muxer:c2898f1832'
implementation 'com.github.deckerst:pixymeta-android:cb1cdc932e'
implementation project(':exifinterface')
// huawei flavor only testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.1'
huaweiImplementation "com.huawei.agconnect:agconnect-core:$huawei_agconnect_version"
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.1' kapt 'androidx.annotation:annotation:1.9.1'
kapt 'androidx.annotation:annotation:1.7.0'
ksp "com.github.bumptech.glide:ksp:$glide_version" ksp "com.github.bumptech.glide:ksp:$glide_version"
compileOnly rootProject.findProject(':streams_channel') compileOnly rootProject.findProject(':streams_channel')
@ -253,8 +191,3 @@ if (useCrashlytics) {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.firebase.crashlytics'
} }
if (useHms) {
println("Building flavor with HMS plugin")
apply plugin: 'com.huawei.agconnect'
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_flavour">#7B1FA2</color> <color name="ic_launcher_flavour">#815AFA</color>
</resources> </resources>

View file

@ -7,14 +7,13 @@
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false" /> android:required="false" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature <uses-feature
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> android:required="false" />
<!--
TODO TLAD [Android 14 (API 34)] request/handle READ_MEDIA_VISUAL_USER_SELECTED permission
cf https://developer.android.com/about/versions/14/changes/partial-photo-video-access
-->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" /> <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
@ -32,9 +31,13 @@
<!-- to access media with original metadata with scoped storage (API >=29) --> <!-- to access media with original metadata with scoped storage (API >=29) -->
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- to provide a foreground service type, as required by Android 14 (API 34) --> <!-- to provide a foreground service type, as required from Android 14 (API 34) -->
<uses-permission <uses-permission
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"
android:maxSdkVersion="34"
tools:ignore="SystemPermissionTypo" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING"
tools:ignore="SystemPermissionTypo" /> tools:ignore="SystemPermissionTypo" />
<!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode --> <!-- TODO TLAD still needed to fetch map tiles / reverse geocoding / else ? check in release mode -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -68,14 +71,12 @@
--> -->
<!-- <!--
allow install on API 19, despite the `minSdk` declared in dependencies: allow install on API 21, despite the `minSdk` declared in dependencies:
- Google Maps is from API 20
- the Security library is from API 21
- FFmpegKit for Flutter is from API 24 (when not LTS) - FFmpegKit for Flutter is from API 24 (when not LTS)
--> -->
<uses-sdk tools:overrideLibrary="io.flutter.plugins.googlemaps, androidx.security:security-crypto, com.arthenica.ffmpegkit.flutter" /> <uses-sdk tools:overrideLibrary="com.arthenica.ffmpegkit.flutter" />
<!-- from Android 11, we should define <queries> to make other apps visible to this app --> <!-- from Android 11 (API 30), we should define <queries> to make other apps visible to this app -->
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -91,7 +92,7 @@
<data android:mimeType="video/*" /> <data android:mimeType="video/*" />
</intent> </intent>
<!-- <!--
from Android 11, `url_launcher` method `canLaunchUrl()` will return false, from Android 11 (API 30), `url_launcher` method `canLaunchUrl()` will return false,
if appropriate intents are not declared, cf https://pub.dev/packages/url_launcher#configuration= if appropriate intents are not declared, cf https://pub.dev/packages/url_launcher#configuration=
--> -->
<!-- to open https URLs --> <!-- to open https URLs -->
@ -102,7 +103,7 @@
</queries> </queries>
<!-- <!--
as of Flutter v3.16.0, predictive back gesture does not work as of Flutter v3.22.2, predictive back gesture does not work
as expected when extending `FlutterFragmentActivity` as expected when extending `FlutterFragmentActivity`
so we disable `enableOnBackInvokedCallback` so we disable `enableOnBackInvokedCallback`
--> -->
@ -118,6 +119,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
tools:targetApi="tiramisu"> tools:targetApi="tiramisu">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@ -139,6 +141,8 @@
<action android:name="android.intent.action.PICK" /> <action android:name="android.intent.action.PICK" />
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" />
<action android:name="android.provider.action.REVIEW_SECURE" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />
@ -158,6 +162,8 @@
<action android:name="android.intent.action.PICK" /> <action android:name="android.intent.action.PICK" />
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.provider.action.REVIEW" />
<action android:name="android.provider.action.REVIEW_SECURE" />
<action android:name="com.android.camera.action.REVIEW" /> <action android:name="com.android.camera.action.REVIEW" />
<action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" /> <action android:name="com.android.camera.action.SPLIT_SCREEN_REVIEW" />
@ -173,6 +179,13 @@
<data android:scheme="content" /> <data android:scheme="content" />
<data android:scheme="file" /> <data android:scheme="file" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="geo" />
</intent-filter>
<!-- <!--
<intent-filter> <intent-filter>
<action android:name="android.intent.action.EDIT" /> <action android:name="android.intent.action.EDIT" />
@ -253,10 +266,14 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- anonymous service for analysis worker is specified here to provide service type --> <!--
anonymous service for analysis worker is specified here to provide service type:
- `dataSync` for Android 14 (API 34)
- `mediaProcessing` from Android 15 (API 35)
-->
<service <service
android:name="androidx.work.impl.foreground.SystemForegroundService" android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync|mediaProcessing"
tools:node="merge" /> tools:node="merge" />
<service <service
@ -304,9 +321,6 @@
<meta-data <meta-data
android:name="com.google.android.geo.API_KEY" android:name="com.google.android.geo.API_KEY"
android:value="${googleApiKey}" /> android:value="${googleApiKey}" />
<meta-data
android:name="deckers.thibault.aves.huawei.API_KEY"
android:value="${huaweiApiKey}" />
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="false" /> android:value="false" />
@ -315,8 +329,6 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<!-- as of Flutter v3.16.0 (stable),
Impeller fails to render videos & platform views, has poor performance -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.EnableImpeller" android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" /> android:value="false" />

View file

@ -14,8 +14,10 @@ import androidx.work.ForegroundInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import app.loup.streams_channel.StreamsChannel 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.DeviceHandler
import deckers.thibault.aves.channel.calls.GeocodingHandler import deckers.thibault.aves.channel.calls.GeocodingHandler
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
import deckers.thibault.aves.channel.calls.MediaStoreHandler import deckers.thibault.aves.channel.calls.MediaStoreHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.calls.StorageHandler import deckers.thibault.aves.channel.calls.StorageHandler
@ -26,6 +28,10 @@ import deckers.thibault.aves.utils.LogUtils
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -33,24 +39,39 @@ import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class AnalysisWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) { class AnalysisWorker(context: Context, parameters: WorkerParameters) : CoroutineWorker(context, parameters) {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var workCont: Continuation<Any?>? = null private var workCont: Continuation<Any?>? = null
private var flutterEngine: FlutterEngine? = null private var flutterEngine: FlutterEngine? = null
private var backgroundChannel: MethodChannel? = null private var backgroundChannel: MethodChannel? = null
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
createNotificationChannel() Log.i(LOG_TAG, "Start analysis worker $id")
setForeground(createForegroundInfo()) defaultScope.launch {
// prevent ANR triggered by slow operations in main thread
createNotificationChannel()
setForeground(createForegroundInfo())
}.join()
suspendCoroutine { cont -> suspendCoroutine { cont ->
workCont = cont workCont = cont
onStart() onStart()
} }
dispose()
return Result.success() return Result.success()
} }
private suspend fun dispose() {
Log.i(LOG_TAG, "Clean analysis worker $id")
flutterEngine?.let {
FlutterUtils.runOnUiThread {
it.destroy()
}
flutterEngine = null
}
}
private fun onStart() { private fun onStart() {
Log.i(LOG_TAG, "Start analysis worker")
runBlocking { runBlocking {
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, CALLBACK_HANDLE_KEY) { FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, PREF_CALLBACK_HANDLE_KEY) {
flutterEngine = it flutterEngine = it
} }
} }
@ -58,14 +79,15 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
try { try {
initChannels(applicationContext) initChannels(applicationContext)
val preferences = applicationContext.getSharedPreferences(SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
val entryIdStrings = preferences.getStringSet(PREF_ENTRY_IDS_KEY, null)
runBlocking { runBlocking {
FlutterUtils.runOnUiThread { FlutterUtils.runOnUiThread {
backgroundChannel?.invokeMethod( backgroundChannel?.invokeMethod(
"start", hashMapOf( "start", hashMapOf(
"entryIds" to inputData.getIntArray(KEY_ENTRY_IDS)?.toList(), "entryIds" to entryIdStrings?.map { Integer.parseUnsignedInt(it) }?.toList(),
"force" to inputData.getBoolean(KEY_FORCE, false), "force" to inputData.getBoolean(KEY_FORCE, false),
"progressTotal" to inputData.getInt(KEY_PROGRESS_TOTAL, 0),
"progressOffset" to inputData.getInt(KEY_PROGRESS_OFFSET, 0),
) )
) )
} }
@ -88,6 +110,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
// - need Context // - need Context
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(context))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context)) MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(context))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(context))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(context))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(context))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(context))
@ -110,12 +133,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
result.success(null) result.success(null)
} }
"updateNotification" -> { "updateNotification" -> defaultScope.launch { safeSuspend(call, result, ::updateNotification) }
val title = call.argument<String>("title")
val message = call.argument<String>("message")
setForegroundAsync(createForegroundInfo(title, message))
result.success(null)
}
"stop" -> { "stop" -> {
workCont?.resume(null) workCont?.resume(null)
@ -148,40 +166,42 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
applicationContext.getString(R.string.analysis_notification_action_stop), applicationContext.getString(R.string.analysis_notification_action_stop),
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id) WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
).build() ).build()
val icon = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) R.drawable.ic_notification else R.mipmap.ic_launcher_round
val contentTitle = title ?: applicationContext.getText(R.string.analysis_notification_default_title) val contentTitle = title ?: applicationContext.getText(R.string.analysis_notification_default_title)
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL) val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL)
.setContentTitle(contentTitle) .setContentTitle(contentTitle)
.setTicker(contentTitle) .setTicker(contentTitle)
.setContentText(message) .setContentText(message)
.setSmallIcon(icon) .setSmallIcon(R.drawable.ic_notification)
.setOngoing(true) .setOngoing(true)
.setContentIntent(openAppIntent) .setContentIntent(openAppIntent)
.addAction(stopAction) .addAction(stopAction)
.build() .build()
return if (Build.VERSION.SDK_INT >= 34) { // from Android 14 (API 34), foreground service type is mandatory for long-running workers:
// from Android 14 (API 34), foreground service type is mandatory // https://developer.android.com/guide/background/persistent/how-to/long-running
// despite the sample code omitting it at: return when {
// https://developer.android.com/guide/background/persistent/how-to/long-running Build.VERSION.SDK_INT >= 35 -> ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
val type = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC Build.VERSION.SDK_INT == 34 -> ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
ForegroundInfo(NOTIFICATION_ID, notification, type) else -> ForegroundInfo(NOTIFICATION_ID, notification)
} 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 { companion object {
private val LOG_TAG = LogUtils.createTag<AnalysisWorker>() private val LOG_TAG = LogUtils.createTag<AnalysisWorker>()
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background" private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"
const val SHARED_PREFERENCES_KEY = "analysis_service" const val SHARED_PREFERENCES_KEY = "analysis_service"
const val CALLBACK_HANDLE_KEY = "callback_handle" const val PREF_CALLBACK_HANDLE_KEY = "callback_handle"
const val PREF_ENTRY_IDS_KEY = "entry_ids"
const val NOTIFICATION_CHANNEL = "analysis" const val NOTIFICATION_CHANNEL = "analysis"
const val NOTIFICATION_ID = 1 const val NOTIFICATION_ID = 1
const val KEY_ENTRY_IDS = "entry_ids"
const val KEY_FORCE = "force" const val KEY_FORCE = "force"
const val KEY_PROGRESS_TOTAL = "progress_total"
const val KEY_PROGRESS_OFFSET = "progress_offset"
} }
} }

View file

@ -3,8 +3,7 @@ package deckers.thibault.aves
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -12,9 +11,6 @@ class HomeWidgetSettingsActivity : MainActivity() {
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
if (FlutterUtils.isSoftwareRenderingRequired()) {
intent.enableSoftwareRendering()
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// cancel if user does not complete widget setup // cancel if user does not complete widget setup
@ -56,7 +52,7 @@ class HomeWidgetSettingsActivity : MainActivity() {
finish() finish()
} }
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> { override fun extractIntentData(intent: Intent?): FieldMap {
return hashMapOf( return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS, INTENT_DATA_KEY_ACTION to INTENT_ACTION_WIDGET_SETTINGS,
INTENT_DATA_KEY_WIDGET_ID to appWidgetId, INTENT_DATA_KEY_WIDGET_ID to appWidgetId,

View file

@ -8,14 +8,22 @@ import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.util.SizeF
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.core.graphics.createBitmap
import androidx.core.net.toUri
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.MediaFetchBytesHandler
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
import deckers.thibault.aves.channel.calls.MediaStoreHandler
import deckers.thibault.aves.channel.calls.StorageHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
@ -25,8 +33,14 @@ import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.nio.ByteBuffer import java.nio.ByteBuffer
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@ -40,12 +54,15 @@ class HomeWidgetProvider : AppWidgetProvider() {
for (widgetId in appWidgetIds) { for (widgetId in appWidgetIds) {
val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId) val widgetInfo = appWidgetManager.getAppWidgetOptions(widgetId)
val pendingResult = goAsync()
defaultScope.launch { defaultScope.launch {
val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false) val backgroundProps = getProps(context, widgetId, widgetInfo, drawEntryImage = false)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, backgroundProps) updateWidgetImage(context, appWidgetManager, widgetId, backgroundProps)
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false) val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = false)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps) updateWidgetImage(context, appWidgetManager, widgetId, imageProps)
pendingResult?.finish()
} }
} }
} }
@ -61,20 +78,32 @@ class HomeWidgetProvider : AppWidgetProvider() {
imageByteFetchJob = defaultScope.launch { imageByteFetchJob = defaultScope.launch {
delay(500) delay(500)
val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true) val imageProps = getProps(context, widgetId, widgetInfo, drawEntryImage = true, reuseEntry = true)
updateWidgetImage(context, appWidgetManager, widgetId, widgetInfo, imageProps) updateWidgetImage(context, appWidgetManager, widgetId, imageProps)
} }
} }
private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density
private fun getWidgetSizePx(context: Context, widgetInfo: Bundle): Pair<Int, Int> { private fun getWidgetSizesDip(context: Context, widgetInfo: Bundle): List<SizeF> {
val devicePixelRatio = getDevicePixelRatio() var sizes: List<SizeF>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF::class.java)
val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT @Suppress("DEPRECATION")
val widthPx = (widgetInfo.getInt(widthKey) * devicePixelRatio).roundToInt() widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES)
val heightPx = (widgetInfo.getInt(heightKey) * devicePixelRatio).roundToInt() } else {
return Pair(widthPx, heightPx) null
}
if (sizes.isNullOrEmpty()) {
val isPortrait = context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
val widthKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH else AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH
val heightKey = if (isPortrait) AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT else AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT
val widthDip = widgetInfo.getInt(widthKey)
val heightDip = widgetInfo.getInt(heightKey)
sizes = listOf(SizeF(widthDip.toFloat(), heightDip.toFloat()))
}
return sizes
} }
private suspend fun getProps( private suspend fun getProps(
@ -84,86 +113,155 @@ class HomeWidgetProvider : AppWidgetProvider() {
drawEntryImage: Boolean, drawEntryImage: Boolean,
reuseEntry: Boolean = false, reuseEntry: Boolean = false,
): FieldMap? { ): FieldMap? {
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) val sizesDip = getWidgetSizesDip(context, widgetInfo)
if (widthPx == 0 || heightPx == 0) return null if (sizesDip.isEmpty()) return null
val sizeDip = sizesDip.first()
if (sizeDip.width == 0f || sizeDip.height == 0f) return null
val sizesDipMap = sizesDip.map { size -> hashMapOf("widthDip" to size.width, "heightDip" to size.height) }
val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
val params = hashMapOf(
"widgetId" to widgetId,
"sizesDip" to sizesDipMap,
"devicePixelRatio" to getDevicePixelRatio(),
"drawEntryImage" to drawEntryImage,
"reuseEntry" to reuseEntry,
"isSystemThemeDark" to isNightModeOn,
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
put("cornerRadiusPx", context.resources.getDimension(android.R.dimen.system_app_widget_background_radius))
}
}
initFlutterEngine(context) initFlutterEngine(context)
val messenger = flutterEngine!!.dartExecutor
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
try { try {
val props = suspendCoroutine<Any?> { cont -> val props = suspendCoroutine { cont ->
defaultScope.launch { defaultScope.launch {
FlutterUtils.runOnUiThread { FlutterUtils.runOnUiThread {
channel.invokeMethod("drawWidget", hashMapOf( tryDrawWidget(params, cont, 0)
"widgetId" to widgetId,
"widthPx" to widthPx,
"heightPx" to heightPx,
"devicePixelRatio" to getDevicePixelRatio(),
"drawEntryImage" to drawEntryImage,
"reuseEntry" to reuseEntry,
), object : MethodChannel.Result {
override fun success(result: Any?) {
cont.resume(result)
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails"))
}
override fun notImplemented() {
cont.resumeWithException(Exception("not implemented"))
}
})
} }
} }
} }
@Suppress("unchecked_cast") @Suppress("unchecked_cast")
return props as FieldMap? return props as FieldMap?
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId widthPx=$widthPx heightPx=$heightPx", e) Log.e(LOG_TAG, "failed to draw widget for widgetId=$widgetId sizesPx=$sizesDip", e)
} }
return null return null
} }
private fun tryDrawWidget(params: HashMap<String, Any>, cont: Continuation<Any?>, drawRetry: Int) {
val messenger = flutterEngine!!.dartExecutor
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
channel.invokeMethod("drawWidget", params, object : MethodChannel.Result {
override fun success(result: Any?) {
cont.resume(result)
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
cont.resumeWithException(Exception("$errorCode: $errorMessage\n$errorDetails"))
}
override fun notImplemented() {
if (drawRetry > DRAW_RETRY_MAX) {
cont.resumeWithException(Exception("not implemented"))
} else {
Handler(Looper.getMainLooper()).postDelayed({
tryDrawWidget(params, cont, drawRetry + 1)
}, 2000L)
}
}
})
}
private fun updateWidgetImage( private fun updateWidgetImage(
context: Context, context: Context,
appWidgetManager: AppWidgetManager, appWidgetManager: AppWidgetManager,
widgetId: Int, widgetId: Int,
widgetInfo: Bundle,
props: FieldMap?, props: FieldMap?,
) { ) {
props ?: return props ?: return
val bytes = props["bytes"] as ByteArray? val bytesBySizeDip = (props["bytesBySizeDip"] as List<*>?)?.mapNotNull {
if (it is Map<*, *>) {
val widthDip = (it["widthDip"] as Number?)?.toFloat()
val heightDip = (it["heightDip"] as Number?)?.toFloat()
val bytes = it["bytes"] as ByteArray?
if (widthDip != null && heightDip != null && bytes != null) {
Pair(SizeF(widthDip, heightDip), bytes)
} else null
} else null
}
val updateOnTap = props["updateOnTap"] as Boolean? val updateOnTap = props["updateOnTap"] as Boolean?
if (bytes == null || updateOnTap == null) { if (bytesBySizeDip == null || updateOnTap == null) {
Log.e(LOG_TAG, "missing arguments") Log.e(LOG_TAG, "missing arguments")
return return
} }
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) if (bytesBySizeDip.isEmpty()) {
if (widthPx == 0 || heightPx == 0) return Log.e(LOG_TAG, "empty image list")
return
}
val bitmaps = ArrayList<Bitmap>()
fun createRemoteViewsForSize(
context: Context,
widgetId: Int,
sizeDip: SizeF,
bytes: ByteArray,
updateOnTap: Boolean,
): RemoteViews? {
val devicePixelRatio = getDevicePixelRatio()
val widthPx = (sizeDip.width * devicePixelRatio).roundToInt()
val heightPx = (sizeDip.height * devicePixelRatio).roundToInt()
try {
val bitmap = createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888).also {
bitmaps.add(it)
it.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
}
val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId)
return RemoteViews(context.packageName, R.layout.app_widget).apply {
setImageViewBitmap(R.id.widget_img, bitmap)
setOnClickPendingIntent(R.id.widget_img, pendingIntent)
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget", e)
}
return null
}
try { try {
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(bytes)) // multiple rendering for all possible sizes
val views = RemoteViews(
val pendingIntent = if (updateOnTap) buildUpdateIntent(context, widgetId) else buildOpenAppIntent(context, widgetId) bytesBySizeDip.associateBy(
{ (sizeDip, _) -> sizeDip },
val views = RemoteViews(context.packageName, R.layout.app_widget).apply { { (sizeDip, bytes) -> createRemoteViewsForSize(context, widgetId, sizeDip, bytes, updateOnTap) },
setImageViewBitmap(R.id.widget_img, bitmap) ).filterValues { it != null }.mapValues { (_, view) -> view!! }
setOnClickPendingIntent(R.id.widget_img, pendingIntent) )
appWidgetManager.updateAppWidget(widgetId, views)
} else {
// single rendering
val (sizeDip, bytes) = bytesBySizeDip.first()
val views = createRemoteViewsForSize(context, widgetId, sizeDip, bytes, updateOnTap)
appWidgetManager.updateAppWidget(widgetId, views)
} }
appWidgetManager.updateAppWidget(widgetId, views)
bitmap.recycle()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(LOG_TAG, "failed to draw widget", e) Log.e(LOG_TAG, "failed to draw widget", e)
} finally {
bitmaps.forEach { it.recycle() }
bitmaps.clear()
} }
} }
private fun buildUpdateIntent(context: Context, widgetId: Int): PendingIntent { private fun buildUpdateIntent(context: Context, widgetId: Int): PendingIntent {
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, Uri.parse("widget://$widgetId"), context, HomeWidgetProvider::class.java) val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, "widget://$widgetId".toUri(), context, HomeWidgetProvider::class.java)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(widgetId)) .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, intArrayOf(widgetId))
return PendingIntent.getBroadcast( return PendingIntent.getBroadcast(
@ -180,7 +278,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
private fun buildOpenAppIntent(context: Context, widgetId: Int): PendingIntent { private fun buildOpenAppIntent(context: Context, widgetId: Int): PendingIntent {
// set a unique URI to prevent the intent (and its extras) from being shared by different widgets // set a unique URI to prevent the intent (and its extras) from being shared by different widgets
val intent = Intent(MainActivity.INTENT_ACTION_WIDGET_OPEN, Uri.parse("widget://$widgetId"), context, MainActivity::class.java) val intent = Intent(MainActivity.INTENT_ACTION_WIDGET_OPEN, "widget://$widgetId".toUri(), context, MainActivity::class.java)
.putExtra(MainActivity.EXTRA_KEY_WIDGET_ID, widgetId) .putExtra(MainActivity.EXTRA_KEY_WIDGET_ID, widgetId)
return PendingIntent.getActivity( return PendingIntent.getActivity(
@ -199,6 +297,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
private val LOG_TAG = LogUtils.createTag<HomeWidgetProvider>() private val LOG_TAG = LogUtils.createTag<HomeWidgetProvider>()
private const val WIDGET_DART_ENTRYPOINT = "widgetMain" private const val WIDGET_DART_ENTRYPOINT = "widgetMain"
private const val WIDGET_DRAW_CHANNEL = "deckers.thibault/aves/widget_draw" private const val WIDGET_DRAW_CHANNEL = "deckers.thibault/aves/widget_draw"
private const val DRAW_RETRY_MAX = 5
private var flutterEngine: FlutterEngine? = null private var flutterEngine: FlutterEngine? = null
private var imageByteFetchJob: Job? = null private var imageByteFetchJob: Job? = null

View file

@ -1,29 +1,63 @@
package deckers.thibault.aves package deckers.thibault.aves
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.KeyguardManager
import android.app.SearchManager import android.app.SearchManager
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.content.ClipData import android.content.ClipData
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.TransactionTooLargeException
import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.AccessibilityHandler
import deckers.thibault.aves.channel.calls.AnalysisHandler
import deckers.thibault.aves.channel.calls.AppAdapterHandler
import deckers.thibault.aves.channel.calls.AppProfileHandler
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.DebugHandler
import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.EmbeddedDataHandler
import deckers.thibault.aves.channel.calls.GeocodingHandler
import deckers.thibault.aves.channel.calls.GlobalSearchHandler
import deckers.thibault.aves.channel.calls.HomeWidgetHandler
import deckers.thibault.aves.channel.calls.MediaEditHandler
import deckers.thibault.aves.channel.calls.MediaFetchBytesHandler
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
import deckers.thibault.aves.channel.calls.MediaSessionHandler
import deckers.thibault.aves.channel.calls.MediaStoreHandler
import deckers.thibault.aves.channel.calls.MetadataEditHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.calls.SecurityHandler
import deckers.thibault.aves.channel.calls.StorageHandler
import deckers.thibault.aves.channel.calls.WallpaperHandler
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.* import deckers.thibault.aves.channel.streams.ActivityResultStreamHandler
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering import deckers.thibault.aves.channel.streams.AnalysisStreamHandler
import deckers.thibault.aves.utils.FlutterUtils.isSoftwareRenderingRequired import deckers.thibault.aves.channel.streams.ErrorStreamHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.ImageOpStreamHandler
import deckers.thibault.aves.channel.streams.IntentStreamHandler
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreChangeStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.channel.streams.SettingsChangeStreamHandler
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.anyCauseIs
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
@ -37,6 +71,7 @@ import kotlinx.coroutines.launch
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
// `FlutterFragmentActivity` because of local auth plugin
open class MainActivity : FlutterFragmentActivity() { open class MainActivity : FlutterFragmentActivity() {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@ -51,13 +86,6 @@ open class MainActivity : FlutterFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Log.i(LOG_TAG, "onCreate intent=$intent") Log.i(LOG_TAG, "onCreate intent=$intent")
if (isSoftwareRenderingRequired()) {
intent.enableSoftwareRendering()
// running the app from Android Studio automatically adds to the intent the `start-paused` flag
// so the IDE can connect to the app, but launching on KitKat emulators fails because of a timeout
intent.removeExtra("start-paused")
}
intent.extras?.takeUnless { it.isEmpty }?.let { intent.extras?.takeUnless { it.isEmpty }?.let {
Log.i(LOG_TAG, "onCreate intent extras=$it") Log.i(LOG_TAG, "onCreate intent extras=$it")
} }
@ -116,7 +144,9 @@ open class MainActivity : FlutterFragmentActivity() {
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this)) MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this)) MethodChannel(messenger, MediaEditHandler.CHANNEL).setMethodCallHandler(MediaEditHandler(this))
MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this)) MethodChannel(messenger, MetadataEditHandler.CHANNEL).setMethodCallHandler(MetadataEditHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity // - need Activity
MethodChannel(messenger, AppProfileHandler.CHANNEL).setMethodCallHandler(AppProfileHandler(this))
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this)) MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart // result streaming: dart -> platform ->->-> dart
@ -149,7 +179,7 @@ open class MainActivity : FlutterFragmentActivity() {
intentDataMap.clear() intentDataMap.clear()
} }
"submitPickedItems" -> submitPickedItems(call) "submitPickedItems" -> safe(call, result, ::submitPickedItems)
"submitPickedCollectionFilters" -> submitPickedCollectionFilters(call) "submitPickedCollectionFilters" -> submitPickedCollectionFilters(call)
} }
} }
@ -167,11 +197,9 @@ open class MainActivity : FlutterFragmentActivity() {
// as of Flutter v3.0.1, the window `viewInsets` and `viewPadding` // as of Flutter v3.0.1, the window `viewInsets` and `viewPadding`
// are incorrect on startup in some environments (e.g. API 29 emulator), // are incorrect on startup in some environments (e.g. API 29 emulator),
// so we manually request to apply the insets to update the window metrics // so we manually request to apply the insets to update the window metrics
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { Handler(Looper.getMainLooper()).postDelayed({
Handler(Looper.getMainLooper()).postDelayed({ window.decorView.requestApplyInsets()
window.decorView.requestApplyInsets() }, 100)
}, 100)
}
} }
override fun onStop() { override fun onStop() {
@ -207,6 +235,7 @@ open class MainActivity : FlutterFragmentActivity() {
intentStreamHandler.notifyNewIntent(extractIntentData(intent)) intentStreamHandler.notifyNewIntent(extractIntentData(intent))
} }
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
when (requestCode) { when (requestCode) {
@ -218,6 +247,7 @@ open class MainActivity : FlutterFragmentActivity() {
OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data) OPEN_FILE_REQUEST -> onStorageAccessResult(requestCode, data?.data)
PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data) PICK_COLLECTION_FILTERS_REQUEST -> onCollectionFiltersPickResult(resultCode, data)
EDIT_REQUEST -> onEditResult(resultCode, data)
} }
} }
@ -226,6 +256,14 @@ open class MainActivity : FlutterFragmentActivity() {
pendingCollectionFilterPickHandler?.let { it(filters) } pendingCollectionFilterPickHandler?.let { it(filters) }
} }
private fun onEditResult(resultCode: Int, intent: Intent?) {
val fields: FieldMap? = if (resultCode == RESULT_OK) hashMapOf(
"uri" to intent?.data?.toString(),
"mimeType" to intent?.type,
) else null
pendingEditIntentHandler?.let { it(fields) }
}
private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) { private fun onDocumentTreeAccessResult(requestCode: Int, resultCode: Int, intent: Intent?) {
val treeUri = intent?.data val treeUri = intent?.data
if (resultCode != RESULT_OK || treeUri == null) { if (resultCode != RESULT_OK || treeUri == null) {
@ -257,35 +295,65 @@ open class MainActivity : FlutterFragmentActivity() {
} }
} }
open fun extractIntentData(intent: Intent?): MutableMap<String, Any?> { open fun extractIntentData(intent: Intent?): FieldMap {
when (val action = intent?.action) { when (val action = intent?.action) {
Intent.ACTION_MAIN -> { Intent.ACTION_MAIN -> {
if (intent.getBooleanExtra(EXTRA_KEY_SAFE_MODE, false)) { return hashMapOf(
return hashMapOf( INTENT_DATA_KEY_PAGE to intent.getStringExtra(EXTRA_KEY_PAGE),
INTENT_DATA_KEY_SAFE_MODE to true, INTENT_DATA_KEY_FILTERS to extractFiltersFromIntent(intent),
) INTENT_DATA_KEY_EXPLORER_PATH to intent.getStringExtra(EXTRA_KEY_EXPLORER_PATH),
} )
intent.getStringExtra(EXTRA_KEY_PAGE)?.let { page ->
val filters = extractFiltersFromIntent(intent)
return hashMapOf(
INTENT_DATA_KEY_PAGE to page,
INTENT_DATA_KEY_FILTERS to filters,
)
}
} }
Intent.ACTION_VIEW, Intent.ACTION_VIEW,
Intent.ACTION_SEND, Intent.ACTION_SEND,
MediaStore.ACTION_REVIEW,
MediaStore.ACTION_REVIEW_SECURE,
"com.android.camera.action.REVIEW", "com.android.camera.action.REVIEW",
"com.android.camera.action.SPLIT_SCREEN_REVIEW" -> { "com.android.camera.action.SPLIT_SCREEN_REVIEW" -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri -> (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
if (uri.scheme == "geo") {
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW_GEO,
INTENT_DATA_KEY_URI to uri.toString(),
INTENT_DATA_KEY_FILTERS to extractFiltersFromIntent(intent),
)
}
// MIME type is optional // MIME type is optional
val type = intent.type ?: intent.resolveType(this) val type = intent.type ?: intent.resolveType(this)
return hashMapOf( val fields = hashMapOf<String, Any?>(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW, INTENT_DATA_KEY_ACTION to INTENT_ACTION_VIEW,
INTENT_DATA_KEY_MIME_TYPE to type, INTENT_DATA_KEY_MIME_TYPE to type,
INTENT_DATA_KEY_URI to uri.toString(), INTENT_DATA_KEY_URI to uri.toString(),
) )
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
val isLocked = keyguardManager.isKeyguardLocked
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(isLocked)
}
if (isLocked) {
// device is locked, so access to content is limited to intent URI by default
fields[INTENT_DATA_KEY_SECURE_URIS] = listOf(uri.toString())
}
if (action == MediaStore.ACTION_REVIEW_SECURE) {
val uris = ArrayList<String>()
intent.clipData?.let { clipData ->
for (i in 0 until clipData.itemCount) {
clipData.getItemAt(i).uri?.let { uris.add(it.toString()) }
}
}
if (uris.isNotEmpty()) {
fields[INTENT_DATA_KEY_SECURE_URIS] = uris
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && intent.hasExtra(MediaStore.EXTRA_BRIGHTNESS)) {
fields[INTENT_DATA_KEY_BRIGHTNESS] = intent.getFloatExtra(MediaStore.EXTRA_BRIGHTNESS, 0f)
}
return fields
} }
} }
@ -305,7 +373,8 @@ open class MainActivity : FlutterFragmentActivity() {
return hashMapOf( return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS, INTENT_DATA_KEY_ACTION to INTENT_ACTION_PICK_ITEMS,
INTENT_DATA_KEY_MIME_TYPE to intent.type, INTENT_DATA_KEY_MIME_TYPE to intent.type,
INTENT_DATA_KEY_ALLOW_MULTIPLE to (intent.extras?.getBoolean(Intent.EXTRA_ALLOW_MULTIPLE) ?: false), INTENT_DATA_KEY_MIME_TYPES to intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES)?.toList(),
INTENT_DATA_KEY_ALLOW_MULTIPLE to intent.getBooleanExtra(Intent.EXTRA_ALLOW_MULTIPLE, false),
) )
} }
@ -365,28 +434,54 @@ open class MainActivity : FlutterFragmentActivity() {
return null return null
} }
private fun submitPickedItems(call: MethodCall) { open fun submitPickedItems(call: MethodCall, result: MethodChannel.Result) {
val pickedUris = call.argument<List<String>>("uris") val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) { if (pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) } setResult(RESULT_CANCELED)
val intent = Intent().apply { // move code triggering `Binder` call off the main thread
val firstUri = toUri(pickedUris.first()) defaultScope.launch { finish() }
if (pickedUris.size == 1) { return
data = firstUri }
} else {
clipData = ClipData.newUri(contentResolver, null, firstUri).apply { val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this@MainActivity, uriString.toUri()) }
pickedUris.drop(1).forEach { val intent = Intent().apply {
addItem(ClipData.Item(toUri(it))) val firstUri = toUri(pickedUris.first())
} if (pickedUris.size == 1) {
data = firstUri
} else {
clipData = ClipData.newUri(contentResolver, null, firstUri).apply {
pickedUris.drop(1).forEach {
addItem(ClipData.Item(toUri(it)))
} }
} }
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
setResult(RESULT_OK, intent) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
} else { }
setResult(RESULT_CANCELED) // move code triggering `Binder` call off the main thread
defaultScope.launch {
submitPickedItemsIntent(intent, result)
}
}
private fun submitPickedItemsIntent(intent: Intent, result: MethodChannel.Result) {
try {
setResult(RESULT_OK, intent)
finish()
} catch (e: Exception) {
setResult(RESULT_CANCELED)
if (e is SecurityException && intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION != 0) {
// in some environments, providing the write flag yields a `SecurityException`:
// "UID XXXX does not have permission to content://XXXX"
// so we retry without it
Log.i(LOG_TAG, "retry submitting picked items without FLAG_GRANT_WRITE_URI_PERMISSION")
intent.flags = intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION.inv()
submitPickedItemsIntent(intent, result)
} else if (e.anyCauseIs<TransactionTooLargeException>()) {
result.error("submitPickedItems-large", "transaction too large with ${intent.clipData?.itemCount} URIs", e)
} else {
result.error("submitPickedItems-exception", "failed to pick ${intent.clipData?.itemCount} URIs", e)
}
} }
finish()
} }
private fun submitPickedCollectionFilters(call: MethodCall) { private fun submitPickedCollectionFilters(call: MethodCall) {
@ -415,7 +510,16 @@ open class MainActivity : FlutterFragmentActivity() {
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_search else R.drawable.ic_shortcut_search)) .setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_search else R.drawable.ic_shortcut_search))
.setIntent( .setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/search") .putExtra(EXTRA_KEY_PAGE, SEARCH_PAGE_ROUTE_NAME)
)
.build()
val map = ShortcutInfoCompat.Builder(this, "map")
.setShortLabel(getString(R.string.map_shortcut_short_label))
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_map else R.drawable.ic_shortcut_map))
.setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, MAP_PAGE_ROUTE_NAME)
) )
.build() .build()
@ -424,21 +528,12 @@ open class MainActivity : FlutterFragmentActivity() {
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_movie else R.drawable.ic_shortcut_movie)) .setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_movie else R.drawable.ic_shortcut_movie))
.setIntent( .setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java) Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, "/collection") .putExtra(EXTRA_KEY_PAGE, COLLECTION_PAGE_ROUTE_NAME)
.putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}")) .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}"))
) )
.build() .build()
val safeMode = ShortcutInfoCompat.Builder(this, "safeMode") val shortcutInfoList = listOf(videos, search, map)
.setShortLabel(getString(R.string.safe_mode_shortcut_short_label))
.setIcon(IconCompat.createWithResource(this, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_safe_mode else R.drawable.ic_shortcut_safe_mode))
.setIntent(
Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
.putExtra(EXTRA_KEY_SAFE_MODE, true)
)
.build()
val shortcutInfoList = listOf(videos, search, safeMode)
ShortcutManagerCompat.setDynamicShortcuts(this, shortcutInfoList) ShortcutManagerCompat.setDynamicShortcuts(this, shortcutInfoList)
Log.i(LOG_TAG, "set shortcuts: ${shortcutInfoList.joinToString(", ") { v -> v.id }}") Log.i(LOG_TAG, "set shortcuts: ${shortcutInfoList.joinToString(", ") { v -> v.id }}")
} }
@ -458,6 +553,7 @@ open class MainActivity : FlutterFragmentActivity() {
const val DELETE_SINGLE_PERMISSION_REQUEST = 5 const val DELETE_SINGLE_PERMISSION_REQUEST = 5
const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6 const val MEDIA_WRITE_BULK_PERMISSION_REQUEST = 6
const val PICK_COLLECTION_FILTERS_REQUEST = 7 const val PICK_COLLECTION_FILTERS_REQUEST = 7
const val EDIT_REQUEST = 8
const val INTENT_ACTION_EDIT = "edit" const val INTENT_ACTION_EDIT = "edit"
const val INTENT_ACTION_PICK_ITEMS = "pick_items" const val INTENT_ACTION_PICK_ITEMS = "pick_items"
@ -467,25 +563,36 @@ open class MainActivity : FlutterFragmentActivity() {
const val INTENT_ACTION_SEARCH = "search" const val INTENT_ACTION_SEARCH = "search"
const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper" const val INTENT_ACTION_SET_WALLPAPER = "set_wallpaper"
const val INTENT_ACTION_VIEW = "view" const val INTENT_ACTION_VIEW = "view"
const val INTENT_ACTION_VIEW_GEO = "view_geo"
const val INTENT_ACTION_WIDGET_OPEN = "widget_open" const val INTENT_ACTION_WIDGET_OPEN = "widget_open"
const val INTENT_ACTION_WIDGET_SETTINGS = "widget_settings" const val INTENT_ACTION_WIDGET_SETTINGS = "widget_settings"
const val INTENT_DATA_KEY_ACTION = "action" const val INTENT_DATA_KEY_ACTION = "action"
const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple" const val INTENT_DATA_KEY_ALLOW_MULTIPLE = "allowMultiple"
const val INTENT_DATA_KEY_BRIGHTNESS = "brightness"
const val INTENT_DATA_KEY_EXPLORER_PATH = "explorerPath"
const val INTENT_DATA_KEY_FILTERS = "filters" const val INTENT_DATA_KEY_FILTERS = "filters"
const val INTENT_DATA_KEY_MIME_TYPE = "mimeType" const val INTENT_DATA_KEY_MIME_TYPE = "mimeType"
const val INTENT_DATA_KEY_MIME_TYPES = "mimeTypes"
const val INTENT_DATA_KEY_PAGE = "page" const val INTENT_DATA_KEY_PAGE = "page"
const val INTENT_DATA_KEY_QUERY = "query" const val INTENT_DATA_KEY_QUERY = "query"
const val INTENT_DATA_KEY_SAFE_MODE = "safeMode" const val INTENT_DATA_KEY_SECURE_URIS = "secureUris"
const val INTENT_DATA_KEY_URI = "uri" const val INTENT_DATA_KEY_URI = "uri"
const val INTENT_DATA_KEY_WIDGET_ID = "widgetId" const val INTENT_DATA_KEY_WIDGET_ID = "widgetId"
const val EXTRA_KEY_PAGE = "page" const val EXTRA_KEY_PAGE = "page"
const val EXTRA_KEY_EXPLORER_PATH = "explorerPath"
const val EXTRA_KEY_FILTERS_ARRAY = "filters" const val EXTRA_KEY_FILTERS_ARRAY = "filters"
const val EXTRA_KEY_FILTERS_STRING = "filtersString" const val EXTRA_KEY_FILTERS_STRING = "filtersString"
const val EXTRA_KEY_SAFE_MODE = "safeMode"
const val EXTRA_KEY_WIDGET_ID = "widgetId" const val EXTRA_KEY_WIDGET_ID = "widgetId"
// dart page routes
const val COLLECTION_PAGE_ROUTE_NAME = "/collection"
const val ENTRY_VIEWER_PAGE_ROUTE_NAME = "/viewer"
const val EXPLORER_PAGE_ROUTE_NAME = "/explorer"
const val MAP_PAGE_ROUTE_NAME = "/map"
const val SEARCH_PAGE_ROUTE_NAME = "/search"
// request code to pending runnable // request code to pending runnable
val pendingStorageAccessResultHandlers = ConcurrentHashMap<Int, PendingStorageAccessResultHandler>() val pendingStorageAccessResultHandlers = ConcurrentHashMap<Int, PendingStorageAccessResultHandler>()
@ -493,6 +600,8 @@ open class MainActivity : FlutterFragmentActivity() {
var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null var pendingCollectionFilterPickHandler: ((filters: List<String>?) -> Unit)? = null
var pendingEditIntentHandler: ((fields: FieldMap?) -> Unit)? = null
private fun onStorageAccessResult(requestCode: Int, uri: Uri?) { private fun onStorageAccessResult(requestCode: Int, uri: Uri?) {
Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri") Log.i(LOG_TAG, "onStorageAccessResult with requestCode=$requestCode, uri=$uri")
val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return val handler = pendingStorageAccessResultHandlers.remove(requestCode) ?: return

View file

@ -5,10 +5,19 @@ import android.util.Log
import android.view.View import android.view.View
import app.loup.streams_channel.StreamsChannel import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.* import deckers.thibault.aves.channel.calls.AccessibilityHandler
import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.EmbeddedDataHandler
import deckers.thibault.aves.channel.calls.MediaFetchBytesHandler
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
import deckers.thibault.aves.channel.calls.MediaSessionHandler
import deckers.thibault.aves.channel.calls.MediaStoreHandler
import deckers.thibault.aves.channel.calls.MetadataFetchHandler
import deckers.thibault.aves.channel.calls.StorageHandler
import deckers.thibault.aves.channel.calls.window.ServiceWindowHandler import deckers.thibault.aves.channel.calls.window.ServiceWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler import deckers.thibault.aves.channel.streams.MediaStoreStreamHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.FlutterInjector import io.flutter.FlutterInjector
@ -18,12 +27,14 @@ import io.flutter.embedding.android.FlutterView
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint
import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister import io.flutter.embedding.engine.plugins.util.GeneratedPluginRegister
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
// for FlutterView-level integration, cf https://docs.flutter.dev/development/add-to-app/android/add-flutter-view // for FlutterView-level integration, cf https://docs.flutter.dev/development/add-to-app/android/add-flutter-view
class ScreenSaverService : DreamService() { class ScreenSaverService : DreamService() {
private var flutterEngine: FlutterEngine? = null private var flutterEngine: FlutterEngine? = null
private var flutterView: FlutterView? = null private var flutterView: FlutterView? = null
private lateinit var mediaSessionHandler: MediaSessionHandler
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
Log.i(LOG_TAG, "onAttachedToWindow") Log.i(LOG_TAG, "onAttachedToWindow")
@ -77,6 +88,7 @@ class ScreenSaverService : DreamService() {
private fun release() { private fun release() {
destroyView() destroyView()
mediaSessionHandler.dispose()
flutterEngine = null flutterEngine = null
flutterView = null flutterView = null
} }
@ -96,12 +108,19 @@ class ScreenSaverService : DreamService() {
private fun initChannels() { private fun initChannels() {
val messenger = flutterEngine!!.dartExecutor val messenger = flutterEngine!!.dartExecutor
// notification: platform -> dart
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
}
// dart -> platform -> dart // dart -> platform -> dart
// - need Context // - need Context
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this)) MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this)) MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this)) MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this)) MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this)) MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this)) MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this)) MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))

View file

@ -1,9 +1,10 @@
package deckers.thibault.aves package deckers.thibault.aves
import android.content.Intent import android.content.Intent
import deckers.thibault.aves.model.FieldMap
class ScreenSaverSettingsActivity : MainActivity() { class ScreenSaverSettingsActivity : MainActivity() {
override fun extractIntentData(intent: Intent?): MutableMap<String, Any?> { override fun extractIntentData(intent: Intent?): FieldMap {
return hashMapOf( return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS, INTENT_DATA_KEY_ACTION to INTENT_ACTION_SCREEN_SAVER_SETTINGS,
) )

View file

@ -16,8 +16,12 @@ import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.* import kotlinx.coroutines.CoroutineScope
import java.util.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.Locale
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@ -34,7 +38,7 @@ class SearchSuggestionsProvider : ContentProvider() {
val columns = arrayOf( val columns = arrayOf(
SearchManager.SUGGEST_COLUMN_INTENT_DATA, SearchManager.SUGGEST_COLUMN_INTENT_DATA,
SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) SearchManager.SUGGEST_COLUMN_CONTENT_TYPE else "mimeType", SearchManager.SUGGEST_COLUMN_CONTENT_TYPE,
SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_1,
SearchManager.SUGGEST_COLUMN_TEXT_2, SearchManager.SUGGEST_COLUMN_TEXT_2,
SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_ICON_1,

View file

@ -2,131 +2,55 @@ package deckers.thibault.aves
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import androidx.core.net.toUri
import android.os.Bundle import deckers.thibault.aves.channel.calls.AppAdapterHandler
import android.os.Handler import deckers.thibault.aves.model.FieldMap
import android.os.Looper
import android.util.Log
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.calls.window.ActivityWindowHandler
import deckers.thibault.aves.channel.calls.window.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler
import deckers.thibault.aves.channel.streams.MediaCommandStreamHandler
import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.FlutterUtils.enableSoftwareRendering
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.getParcelableExtraCompat import deckers.thibault.aves.utils.getParcelableExtraCompat
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class WallpaperActivity : FlutterFragmentActivity() { class WallpaperActivity : MainActivity() {
private lateinit var intentDataMap: MutableMap<String, Any?> private var originalIntent: String? = null
private lateinit var mediaSessionHandler: MediaSessionHandler
override fun onCreate(savedInstanceState: Bundle?) { override fun extractIntentData(intent: Intent?): FieldMap {
if (FlutterUtils.isSoftwareRenderingRequired()) { if (intent != null) {
intent.enableSoftwareRendering() when (intent.action) {
} Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
super.onCreate(savedInstanceState) (intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
INTENT_DATA_KEY_ACTION to INTENT_ACTION_SET_WALLPAPER,
INTENT_DATA_KEY_MIME_TYPE to type,
INTENT_DATA_KEY_URI to uri.toString(),
)
}
Log.i(LOG_TAG, "onCreate intent=$intent") // if the media URI is not provided we need to pick one first
intent.extras?.takeUnless { it.isEmpty }?.let { originalIntent = intent.action
Log.i(LOG_TAG, "onCreate intent extras=$it") intent.action = Intent.ACTION_PICK
}
intentDataMap = extractIntentData(intent)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val messenger = flutterEngine.dartExecutor
// notification: platform -> dart
val mediaCommandStreamHandler = MediaCommandStreamHandler().apply {
EventChannel(messenger, MediaCommandStreamHandler.CHANNEL).setStreamHandler(this)
}
// dart -> platform -> dart
// - need Context
mediaSessionHandler = MediaSessionHandler(this, mediaCommandStreamHandler)
MethodChannel(messenger, DeviceHandler.CHANNEL).setMethodCallHandler(DeviceHandler(this))
MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, MediaFetchBytesHandler.CHANNEL, AvesByteSendingMethodCodec.INSTANCE).setMethodCallHandler(MediaFetchBytesHandler(this))
MethodChannel(messenger, MediaFetchObjectHandler.CHANNEL).setMethodCallHandler(MediaFetchObjectHandler(this))
MethodChannel(messenger, MediaSessionHandler.CHANNEL).setMethodCallHandler(mediaSessionHandler)
MethodChannel(messenger, MetadataFetchHandler.CHANNEL).setMethodCallHandler(MetadataFetchHandler(this))
MethodChannel(messenger, StorageHandler.CHANNEL).setMethodCallHandler(StorageHandler(this))
// - need ContextWrapper
MethodChannel(messenger, AccessibilityHandler.CHANNEL).setMethodCallHandler(AccessibilityHandler(this))
MethodChannel(messenger, WallpaperHandler.CHANNEL).setMethodCallHandler(WallpaperHandler(this))
// - need Activity
MethodChannel(messenger, WindowHandler.CHANNEL).setMethodCallHandler(ActivityWindowHandler(this))
// result streaming: dart -> platform ->->-> dart
// - need Context
StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL).setStreamHandlerFactory { args -> ImageByteStreamHandler(this, args) }
// intent handling
// detail fetch: dart -> platform
MethodChannel(messenger, MainActivity.INTENT_CHANNEL).setMethodCallHandler { call, result -> onMethodCall(call, result) }
}
override fun onStart() {
Log.i(LOG_TAG, "onStart")
super.onStart()
// as of Flutter v3.0.1, the window `viewInsets` and `viewPadding`
// are incorrect on startup in some environments (e.g. API 29 emulator),
// so we manually request to apply the insets to update the window metrics
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
Handler(Looper.getMainLooper()).postDelayed({
window.decorView.requestApplyInsets()
}, 100)
}
}
override fun onDestroy() {
mediaSessionHandler.dispose()
super.onDestroy()
}
private fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
intentDataMap.clear()
}
}
}
private fun extractIntentData(intent: Intent?): MutableMap<String, Any?> {
when (intent?.action) {
Intent.ACTION_ATTACH_DATA, Intent.ACTION_SET_WALLPAPER -> {
(intent.data ?: intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM))?.let { uri ->
// MIME type is optional
val type = intent.type ?: intent.resolveType(this)
return hashMapOf(
MainActivity.INTENT_DATA_KEY_ACTION to MainActivity.INTENT_ACTION_SET_WALLPAPER,
MainActivity.INTENT_DATA_KEY_MIME_TYPE to type,
MainActivity.INTENT_DATA_KEY_URI to uri.toString(),
)
} }
} }
Intent.ACTION_RUN -> {
// flutter run
}
else -> {
Log.w(LOG_TAG, "unhandled intent action=${intent?.action}")
}
} }
return HashMap()
return super.extractIntentData(intent)
} }
companion object { override fun submitPickedItems(call: MethodCall, result: MethodChannel.Result) {
private val LOG_TAG = LogUtils.createTag<WallpaperActivity>() if (originalIntent != null) {
val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, uriString.toUri()) }
onNewIntent(Intent().apply {
action = originalIntent
data = toUri(pickedUris.first())
})
} else {
setResult(RESULT_CANCELED)
finish()
}
} else {
super.submitPickedItems(call, result)
}
} }
} }

View file

@ -21,27 +21,28 @@ class AvesByteSendingMethodCodec private constructor() : MethodCodec {
return STANDARD.encodeMethodCall(methodCall) return STANDARD.encodeMethodCall(methodCall)
} }
override fun encodeErrorEnvelope(errorCode: String, errorMessage: String?, errorDetails: Any?): ByteBuffer {
return STANDARD.encodeErrorEnvelope(errorCode, errorMessage, errorDetails)
}
override fun encodeErrorEnvelopeWithStacktrace(errorCode: String, errorMessage: String?, errorDetails: Any?, errorStacktrace: String?): ByteBuffer {
return STANDARD.encodeErrorEnvelopeWithStacktrace(errorCode, errorMessage, errorDetails, errorStacktrace)
}
// `StandardMethodCodec` writes the result to a `ByteArrayOutputStream`, then writes the stream to a `ByteBuffer`.
// Here we only handle `ByteArray` results, but we avoid the intermediate stream.
override fun encodeSuccessEnvelope(result: Any?): ByteBuffer { override fun encodeSuccessEnvelope(result: Any?): ByteBuffer {
if (result is ByteArray) { if (result is ByteArray) {
val size = result.size return ByteBuffer.allocateDirect(1 + result.size).apply {
return ByteBuffer.allocateDirect(4 + size).apply { // following `StandardMethodCodec`:
// First byte is zero in success case, and non-zero otherwise.
put(0) put(0)
put(result) put(result)
} }
} }
Log.e(LOG_TAG, "encodeSuccessEnvelope failed with result=$result") Log.e(LOG_TAG, "encodeSuccessEnvelope failed with result=$result")
return ByteBuffer.allocateDirect(0) return encodeErrorEnvelope("invalid-result-type", "Called success with a result which is not a `ByteArray`, type=${result?.javaClass}", null)
}
override fun encodeErrorEnvelope(errorCode: String, errorMessage: String?, errorDetails: Any?): ByteBuffer {
Log.e(LOG_TAG, "encodeErrorEnvelope failed with errorCode=$errorCode, errorMessage=$errorMessage, errorDetails=$errorDetails")
return ByteBuffer.allocateDirect(0)
}
override fun encodeErrorEnvelopeWithStacktrace(errorCode: String, errorMessage: String?, errorDetails: Any?, errorStacktrace: String?): ByteBuffer {
Log.e(LOG_TAG, "encodeErrorEnvelopeWithStacktrace failed with errorCode=$errorCode, errorMessage=$errorMessage, errorDetails=$errorDetails, errorStacktrace=$errorStacktrace")
return ByteBuffer.allocateDirect(0)
} }
companion object { companion object {

View file

@ -6,6 +6,7 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
@ -17,6 +18,7 @@ class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodC
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved) "areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
"getLongPressTimeout" -> safe(call, result, ::getLongPressTimeout)
"hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts) "hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts)
"getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis) "getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis)
"shouldUseBoldFont" -> safe(call, result, ::shouldUseBoldFont) "shouldUseBoldFont" -> safe(call, result, ::shouldUseBoldFont)
@ -34,6 +36,10 @@ class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodC
result.success(removed) result.success(removed)
} }
private fun getLongPressTimeout(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(ViewConfiguration.getLongPressTimeout())
}
private fun hasRecommendedTimeouts(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun hasRecommendedTimeouts(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
} }

View file

@ -1,15 +1,16 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.app.ActivityManager
import android.content.Context import android.content.Context
import androidx.core.app.ComponentActivity import androidx.core.content.edit
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.workDataOf import androidx.work.workDataOf
import deckers.thibault.aves.AnalysisWorker import deckers.thibault.aves.AnalysisWorker
import deckers.thibault.aves.utils.FlutterUtils import deckers.thibault.aves.utils.FlutterUtils
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -18,8 +19,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class AnalysisHandler(private val activity: FlutterFragmentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
class AnalysisHandler(private val activity: ComponentActivity, private val onAnalysisCompleted: () -> Unit) : MethodChannel.MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -37,10 +37,10 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
return return
} }
activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
.edit() preferences.edit {
.putLong(AnalysisWorker.CALLBACK_HANDLE_KEY, callbackHandle) putLong(AnalysisWorker.PREF_CALLBACK_HANDLE_KEY, callbackHandle)
.apply() }
result.success(true) result.success(true)
} }
@ -51,35 +51,36 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
return return
} }
val activityManager: ActivityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningAppProcesses = activityManager.runningAppProcesses
if (runningAppProcesses != null) {
val importance = runningAppProcesses[0].importance
if (importance < ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
// the app is in the background
result.error("startAnalysis-background", "app is in the background (process importance=$importance)", null)
return
}
}
// can be null or empty // can be null or empty
val allEntryIds = call.argument<List<Int>>("entryIds") val allEntryIds = call.argument<List<Int>>("entryIds")
val progressTotal = allEntryIds?.size ?: 0
var progressOffset = 0
// work `Data` cannot occupy more than 10240 bytes when serialized // work `Data` cannot occupy more than 10240 bytes when serialized
// so we split it when we have a long list of entry IDs // so we save the possibly long list of entry IDs to shared preferences
val chunked = allEntryIds?.chunked(WORK_DATA_CHUNK_SIZE) ?: listOf(null) val preferences = activity.getSharedPreferences(AnalysisWorker.SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE)
preferences.edit {
fun buildRequest(entryIds: List<Int>?, progressOffset: Int): OneTimeWorkRequest { putStringSet(AnalysisWorker.PREF_ENTRY_IDS_KEY, allEntryIds?.map { it.toString() }?.toSet())
val workData = workDataOf(
AnalysisWorker.KEY_ENTRY_IDS to entryIds?.toIntArray(),
AnalysisWorker.KEY_FORCE to force,
AnalysisWorker.KEY_PROGRESS_TOTAL to progressTotal,
AnalysisWorker.KEY_PROGRESS_OFFSET to progressOffset,
)
return OneTimeWorkRequestBuilder<AnalysisWorker>().apply { setInputData(workData) }.build()
} }
var work = WorkManager.getInstance(activity).beginUniqueWork( val workData = workDataOf(
AnalysisWorker.KEY_FORCE to force,
)
WorkManager.getInstance(activity).beginUniqueWork(
ANALYSIS_WORK_NAME, ANALYSIS_WORK_NAME,
ExistingWorkPolicy.KEEP, ExistingWorkPolicy.KEEP,
buildRequest(chunked.first(), progressOffset), OneTimeWorkRequestBuilder<AnalysisWorker>().apply { setInputData(workData) }.build(),
) ).enqueue()
chunked.drop(1).forEach { entryIds ->
progressOffset += WORK_DATA_CHUNK_SIZE
work = work.then(buildRequest(entryIds, progressOffset))
}
work.enqueue()
attachToActivity() attachToActivity()
result.success(null) result.success(null)
@ -105,6 +106,5 @@ class AnalysisHandler(private val activity: ComponentActivity, private val onAna
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/analysis" const val CHANNEL = "deckers.thibault/aves/analysis"
private const val ANALYSIS_WORK_NAME = "analysis_work" private const val ANALYSIS_WORK_NAME = "analysis_work"
private const val WORK_DATA_CHUNK_SIZE = 1000
} }
} }

View file

@ -1,6 +1,10 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.* import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
@ -15,21 +19,27 @@ import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.COLLECTION_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.ENTRY_VIEWER_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.EXPLORER_PAGE_ROUTE_NAME
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_EXPLORER_PATH
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_ARRAY
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_FILTERS_STRING
import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE import deckers.thibault.aves.MainActivity.Companion.EXTRA_KEY_PAGE
import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR import deckers.thibault.aves.MainActivity.Companion.EXTRA_STRING_ARRAY_SEPARATOR
import deckers.thibault.aves.MainActivity.Companion.MAP_PAGE_ROUTE_NAME
import deckers.thibault.aves.R import deckers.thibault.aves.R
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.anyCauseIs
import deckers.thibault.aves.utils.getApplicationInfoCompat import deckers.thibault.aves.utils.getApplicationInfoCompat
import deckers.thibault.aves.utils.queryIntentActivitiesCompat import deckers.thibault.aves.utils.queryIntentActivitiesCompat
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -41,7 +51,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.* import java.util.Locale
import java.util.UUID
import kotlin.math.roundToInt import kotlin.math.roundToInt
class AppAdapterHandler(private val context: Context) : MethodCallHandler { class AppAdapterHandler(private val context: Context) : MethodCallHandler {
@ -52,7 +63,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
"getPackages" -> ioScope.launch { safe(call, result, ::getPackages) } "getPackages" -> ioScope.launch { safe(call, result, ::getPackages) }
"getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) } "getAppIcon" -> ioScope.launch { safeSuspend(call, result, ::getAppIcon) }
"copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) } "copyToClipboard" -> ioScope.launch { safe(call, result, ::copyToClipboard) }
"edit" -> safe(call, result, ::edit)
"open" -> safe(call, result, ::open) "open" -> safe(call, result, ::open)
"openMap" -> safe(call, result, ::openMap) "openMap" -> safe(call, result, ::openMap)
"setAs" -> safe(call, result, ::setAs) "setAs" -> safe(call, result, ::setAs)
@ -143,7 +153,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter // convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
val density = context.resources.displayMetrics.density val density = context.resources.displayMetrics.density
val size = (sizeDip * density).roundToInt() val size = (sizeDip * density).roundToInt()
var data: ByteArray? = null var bytes: ByteArray? = null
try { try {
val iconResourceId = context.packageManager.getApplicationInfoCompat(packageName, 0).icon val iconResourceId = context.packageManager.getApplicationInfoCompat(packageName, 0).icon
if (iconResourceId != Resources.ID_NULL) { if (iconResourceId != Resources.ID_NULL) {
@ -164,7 +174,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
try { try {
val bitmap = withContext(Dispatchers.IO) { target.get() } val bitmap = withContext(Dispatchers.IO) { target.get() }
data = bitmap?.getBytes(canHaveAlpha = true, recycle = false) // do not recycle bitmaps fetched from `ContentResolver` as their lifecycle is unknown
val recycle = false
bytes = BitmapUtils.getRawBytes(bitmap, recycle = recycle)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e) Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
} }
@ -174,15 +186,15 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to get app info for packageName=$packageName", e) Log.w(LOG_TAG, "failed to get app info for packageName=$packageName", e)
return return
} }
if (data != null) { if (bytes != null) {
result.success(data) result.success(bytes)
} else { } else {
result.error("getAppIcon-null", "failed to get icon for packageName=$packageName", null) result.error("getAppIcon-null", "failed to get icon for packageName=$packageName", null)
} }
} }
private fun copyToClipboard(call: MethodCall, result: MethodChannel.Result) { private fun copyToClipboard(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val label = call.argument<String>("label") val label = call.argument<String>("label")
if (uri == null) { if (uri == null) {
result.error("copyToClipboard-args", "missing arguments", null) result.error("copyToClipboard-args", "missing arguments", null)
@ -207,25 +219,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
} }
private fun edit(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val mimeType = call.argument<String>("mimeType")
if (uri == null) {
result.error("edit-args", "missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_EDIT)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setDataAndType(getShareableUri(context, uri), mimeType)
val started = safeStartActivity(intent)
result.success(started)
}
private fun open(call: MethodCall, result: MethodChannel.Result) { private fun open(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title") val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val forceChooser = call.argument<Boolean>("forceChooser") val forceChooser = call.argument<Boolean>("forceChooser")
if (uri == null || forceChooser == null) { if (uri == null || forceChooser == null) {
@ -242,7 +238,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
} }
private fun openMap(call: MethodCall, result: MethodChannel.Result) { private fun openMap(call: MethodCall, result: MethodChannel.Result) {
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) } val geoUri = call.argument<String>("geoUri")?.toUri()
if (geoUri == null) { if (geoUri == null) {
result.error("openMap-args", "missing arguments", null) result.error("openMap-args", "missing arguments", null)
return return
@ -256,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
private fun setAs(call: MethodCall, result: MethodChannel.Result) { private fun setAs(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title") val title = call.argument<String>("title")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
if (uri == null) { if (uri == null) {
result.error("setAs-args", "missing arguments", null) result.error("setAs-args", "missing arguments", null)
@ -279,7 +275,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
return return
} }
val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { getShareableUri(context, Uri.parse(it)) }) val uriList = ArrayList(urisByMimeType.values.flatten().mapNotNull { getShareableUri(context, it.toUri()) })
val mimeTypes = urisByMimeType.keys.toTypedArray() val mimeTypes = urisByMimeType.keys.toTypedArray()
// simplify share intent for a single item, as some apps can handle one item but not more // simplify share intent for a single item, as some apps can handle one item but not more
@ -314,7 +310,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
val started = safeStartActivityChooser(title, intent) val started = safeStartActivityChooser(title, intent)
result.success(started) result.success(started)
} catch (e: Exception) { } catch (e: Exception) {
if (e is TransactionTooLargeException || e.cause is TransactionTooLargeException) { if (e.anyCauseIs<TransactionTooLargeException>()) {
result.error("share-large", "transaction too large with ${uriList.size} URIs", e) result.error("share-large", "transaction too large with ${uriList.size} URIs", e)
} else { } else {
result.error("share-exception", "failed to share ${uriList.size} URIs", e) result.error("share-exception", "failed to share ${uriList.size} URIs", e)
@ -365,11 +361,17 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// shortcuts // shortcuts
private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) { private fun pinShortcut(call: MethodCall, result: MethodChannel.Result) {
// common arguments
val label = call.argument<String>("label") val label = call.argument<String>("label")
val iconBytes = call.argument<ByteArray>("iconBytes") val iconBytes = call.argument<ByteArray>("iconBytes")
val route = call.argument<String>("route")
// route dependent arguments
val filters = call.argument<List<String>>("filters") val filters = call.argument<List<String>>("filters")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val explorerPath = call.argument<String>("path")
if (label == null || (filters == null && uri == null)) { val viewUri = call.argument<String>("viewUri")?.toUri()
val geoUri = call.argument<String>("geoUri")?.toUri()
if (label == null || route == null) {
result.error("pin-args", "missing arguments", null) result.error("pin-args", "missing arguments", null)
return return
} }
@ -393,19 +395,60 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// so that foreground is rendered at the intended scale // so that foreground is rendered at the intended scale
val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O val supportAdaptiveIcon = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
icon = IconCompat.createWithResource(context, if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection) val resId = when (route) {
MAP_PAGE_ROUTE_NAME -> if (supportAdaptiveIcon) R.mipmap.ic_shortcut_map else R.drawable.ic_shortcut_map
else -> if (supportAdaptiveIcon) R.mipmap.ic_shortcut_collection else R.drawable.ic_shortcut_collection
}
icon = IconCompat.createWithResource(context, resId)
} }
val intent = when { val intent: Intent = when (route) {
uri != null -> Intent(Intent.ACTION_VIEW, uri, context, MainActivity::class.java) COLLECTION_PAGE_ROUTE_NAME -> {
filters != null -> Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java) if (filters == null) {
.putExtra(EXTRA_KEY_PAGE, "/collection") result.error("pin-filters", "collection shortcut requires filters", null)
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray()) return
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut }
// so we use a joined `String` as fallback Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR)) .putExtra(EXTRA_KEY_PAGE, route)
.putExtra(EXTRA_KEY_FILTERS_ARRAY, filters.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
.putExtra(EXTRA_KEY_FILTERS_STRING, filters.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
}
ENTRY_VIEWER_PAGE_ROUTE_NAME -> {
if (viewUri == null) {
result.error("pin-viewUri", "viewer shortcut requires URI", null)
return
}
Intent(Intent.ACTION_VIEW, viewUri, context, MainActivity::class.java)
}
EXPLORER_PAGE_ROUTE_NAME -> {
Intent(Intent.ACTION_MAIN, null, context, MainActivity::class.java)
.putExtra(EXTRA_KEY_PAGE, route)
.putExtra(EXTRA_KEY_EXPLORER_PATH, explorerPath)
}
MAP_PAGE_ROUTE_NAME -> {
if (geoUri == null) {
result.error("pin-geoUri", "map shortcut requires URI", null)
return
}
Intent(Intent.ACTION_VIEW, geoUri, context, MainActivity::class.java).apply {
putExtra(EXTRA_KEY_PAGE, route)
// filters are optional
filters?.let {
putExtra(EXTRA_KEY_FILTERS_ARRAY, it.toTypedArray())
// on API 25, `String[]` or `ArrayList` extras are null when using the shortcut
// so we use a joined `String` as fallback
putExtra(EXTRA_KEY_FILTERS_STRING, it.joinToString(EXTRA_STRING_ARRAY_SEPARATOR))
}
}
}
else -> { else -> {
result.error("pin-intent", "failed to build intent", null) result.error("pin-route", "unsupported shortcut route=$route", null)
return return
} }
} }
@ -434,6 +477,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
FileProvider.getUriForFile(context, authority, File(path)) FileProvider.getUriForFile(context, authority, File(path))
} }
} }
else -> uri else -> uri
} }
} }

View file

@ -0,0 +1,97 @@
package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.content.Context
import android.content.pm.CrossProfileApps
import android.os.Build
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
class AppProfileHandler(private val activity: Activity) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"canInteractAcrossProfiles" -> safe(call, result, ::canInteractAcrossProfiles)
"canRequestInteractAcrossProfiles" -> safe(call, result, ::canRequestInteractAcrossProfiles)
"requestInteractAcrossProfiles" -> safe(call, result, ::requestInteractAcrossProfiles)
"switchProfile" -> safe(call, result, ::switchProfile)
"getProfileSwitchingLabel" -> safe(call, result, ::getProfileSwitchingLabel)
"getTargetUserProfiles" -> safe(call, result, ::getTargetUserProfiles)
else -> result.notImplemented()
}
}
private fun canInteractAcrossProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
result.success(false)
return
}
val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps
result.success(crossProfileApps.canInteractAcrossProfiles())
}
private fun canRequestInteractAcrossProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
result.success(false)
return
}
val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps
result.success(crossProfileApps.canRequestInteractAcrossProfiles())
}
private fun requestInteractAcrossProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
result.success(false)
return
}
val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps
val intent = crossProfileApps.createRequestInteractAcrossProfilesIntent()
val started = activity.startActivity(intent)
result.success(started)
}
private fun switchProfile(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
result.success(false)
return
}
val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps
val userHandles = crossProfileApps.targetUserProfiles
crossProfileApps.startMainActivity(activity.componentName, userHandles.first())
result.success(null)
}
private fun getProfileSwitchingLabel(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
result.success(null)
return
}
val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps
val userHandles = crossProfileApps.targetUserProfiles
val label = if (userHandles.isEmpty()) "" else crossProfileApps.getProfileSwitchingLabel(userHandles.first())
result.success(label)
}
private fun getTargetUserProfiles(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
result.success(false)
return
}
val crossProfileApps = activity.getSystemService(Context.CROSS_PROFILE_APPS_SERVICE) as CrossProfileApps
val userProfiles = crossProfileApps.targetUserProfiles.map { it.toString() }.toList()
result.success(userProfiles)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/app_profile"
}
}

View file

@ -12,11 +12,15 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.core.net.toUri
import com.drew.metadata.file.FileTypeDirectory import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.* import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.Mp4ParserHelper
import deckers.thibault.aves.metadata.Mp4ParserHelper.dumpBoxes import deckers.thibault.aves.metadata.Mp4ParserHelper.dumpBoxes
import deckers.thibault.aves.metadata.PixyMetaHelper
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
@ -40,6 +44,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import org.mp4parser.IsoFile import org.mp4parser.IsoFile
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException import java.io.IOException
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
class DebugHandler(private val context: Context) : MethodCallHandler { class DebugHandler(private val context: Context) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -75,15 +80,9 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
"obbDir" to context.obbDir, "obbDir" to context.obbDir,
"externalCacheDir" to context.externalCacheDir, "externalCacheDir" to context.externalCacheDir,
"externalFilesDir" to context.getExternalFilesDir(null), "externalFilesDir" to context.getExternalFilesDir(null),
"codeCacheDir" to context.codeCacheDir,
"noBackupFilesDir" to context.noBackupFilesDir,
).apply { ).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
putAll(
hashMapOf(
"codeCacheDir" to context.codeCacheDir,
"noBackupFilesDir" to context.noBackupFilesDir,
)
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
put("dataDir", context.dataDir) put("dataDir", context.dataDir)
} }
@ -104,8 +103,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
private fun getCodecs(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getCodecs(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val codecs = ArrayList<FieldMap>()
fun getFields(info: MediaCodecInfo): FieldMap { fun getFields(info: MediaCodecInfo): FieldMap {
val fields: FieldMap = hashMapOf( val fields: FieldMap = hashMapOf(
"name" to info.name, "name" to info.name,
@ -122,18 +119,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
return fields return fields
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val codecs = MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos.map(::getFields).toList()
codecs.addAll(MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos.map(::getFields))
} else {
@Suppress("deprecation")
val count = MediaCodecList.getCodecCount()
for (i in 0 until count) {
@Suppress("deprecation")
val info = MediaCodecList.getCodecInfoAt(i)
codecs.add(getFields(info))
}
}
result.success(codecs) result.success(codecs)
} }
@ -142,7 +128,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) { private fun getBitmapFactoryInfo(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
if (uri == null) { if (uri == null) {
result.error("getBitmapDecoderInfo-args", "missing arguments", null) result.error("getBitmapDecoderInfo-args", "missing arguments", null)
return return
@ -171,7 +157,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getContentResolverMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getContentResolverMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getContentResolverMetadata-args", "missing arguments", null) result.error("getContentResolverMetadata-args", "missing arguments", null)
return return
@ -227,7 +213,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getExifInterfaceMetadata-args", "missing arguments", null) result.error("getExifInterfaceMetadata-args", "missing arguments", null)
@ -254,7 +240,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getMediaMetadataRetrieverMetadata(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
if (uri == null) { if (uri == null) {
result.error("getMediaMetadataRetrieverMetadata-args", "missing arguments", null) result.error("getMediaMetadataRetrieverMetadata-args", "missing arguments", null)
return return
@ -279,7 +265,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getMetadataExtractorSummary(call: MethodCall, result: MethodChannel.Result) { private fun getMetadataExtractorSummary(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getMetadataExtractorSummary-args", "missing arguments", null) result.error("getMetadataExtractorSummary-args", "missing arguments", null)
@ -290,7 +276,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
metadataMap["mimeType"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir -> metadataMap["mimeType"] = metadata.getDirectoriesOfType(FileTypeDirectory::class.java).joinToString { dir ->
if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)) { if (dir.containsTag(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)) {
dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) dir.getString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE)
@ -323,14 +309,14 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getMp4ParserDump(call: MethodCall, result: MethodChannel.Result) { private fun getMp4ParserDump(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getMp4ParserDump-args", "missing arguments", null) result.error("getMp4ParserDump-args", "missing arguments", null)
return return
} }
val sb = StringBuilder() val sb = StringBuilder()
if (mimeType == MimeTypes.MP4) { if (mimeType == MimeTypes.MP4 || MimeTypes.isHeic(mimeType)) {
try { try {
// we can skip uninteresting boxes with a seekable data source // we can skip uninteresting boxes with a seekable data source
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri") val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
@ -353,7 +339,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getPixyMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getPixyMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getPixyMetadata-args", "missing arguments", null) result.error("getPixyMetadata-args", "missing arguments", null)
return return
@ -374,7 +360,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) { private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
if (uri == null) { if (uri == null) {
result.error("getTiffStructure-args", "missing arguments", null) result.error("getTiffStructure-args", "missing arguments", null)
return return

View file

@ -1,16 +1,23 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.annotation.SuppressLint
import android.app.LocaleConfig
import android.app.LocaleManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.location.Geocoder import android.location.Geocoder
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.LocaleList
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.Settings import android.provider.Settings
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.net.toUri
import com.google.android.material.color.DynamicColors
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.MemoryUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -18,7 +25,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.Locale
class DeviceHandler(private val context: Context) : MethodCallHandler { class DeviceHandler(private val context: Context) : MethodCallHandler {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@ -27,11 +34,14 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
when (call.method) { when (call.method) {
"canManageMedia" -> safe(call, result, ::canManageMedia) "canManageMedia" -> safe(call, result, ::canManageMedia)
"getCapabilities" -> defaultScope.launch { safe(call, result, ::getCapabilities) } "getCapabilities" -> defaultScope.launch { safe(call, result, ::getCapabilities) }
"getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis)
"getLocales" -> safe(call, result, ::getLocales) "getLocales" -> safe(call, result, ::getLocales)
"setLocaleConfig" -> safe(call, result, ::setLocaleConfig)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass) "getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
"isLocked" -> safe(call, result, ::isLocked)
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled) "isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
"requestMediaManagePermission" -> safe(call, result, ::requestMediaManagePermission) "requestMediaManagePermission" -> safe(call, result, ::requestMediaManagePermission)
"getAvailableHeapSize" -> safe(call, result, ::getAvailableHeapSize)
"requestGarbageCollection" -> safe(call, result, ::requestGarbageCollection)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -44,36 +54,32 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
val sdkInt = Build.VERSION.SDK_INT val sdkInt = Build.VERSION.SDK_INT
result.success( result.success(
hashMapOf( hashMapOf(
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context), "canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M), "canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
"canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O), "canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S), "canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N), "canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"hasGeocoder" to Geocoder.isPresent(), "hasGeocoder" to Geocoder.isPresent(),
"isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S), "isDynamicColorAvailable" to DynamicColors.isDynamicColorAvailable(),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O), "showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
"supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q), "supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q),
"supportPictureInPicture" to supportPictureInPicture(),
) )
) )
} }
private fun getDefaultTimeZoneRawOffsetMillis(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun supportPictureInPicture(): Boolean {
result.success(TimeZone.getDefault().rawOffset) // minimum version for `PictureInPictureParams.Builder#setAutoEnterEnabled`
val supportPipOnLeave = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
return supportPipOnLeave && context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
} }
private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getLocales(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
fun toMap(locale: Locale): FieldMap { fun toMap(locale: Locale): FieldMap = hashMapOf(
val fields: HashMap<String, Any?> = hashMapOf( "language" to locale.language,
"language" to locale.language, "country" to locale.country,
"country" to locale.country, "script" to locale.script,
) )
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
fields["script"] = locale.script
}
return fields
}
val locales = ArrayList<FieldMap>() val locales = ArrayList<FieldMap>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -89,6 +95,22 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(locales) result.success(locales)
} }
private fun setLocaleConfig(call: MethodCall, result: MethodChannel.Result) {
val locales = call.argument<List<String>>("locales")
if (locales.isNullOrEmpty()) {
result.error("setLocaleConfig-args", "missing arguments", null)
return
}
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(",")))
}
result.success(true)
}
private fun getPerformanceClass(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getPerformanceClass(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val performanceClass = Build.VERSION.MEDIA_PERFORMANCE_CLASS val performanceClass = Build.VERSION.MEDIA_PERFORMANCE_CLASS
@ -100,12 +122,14 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(Build.VERSION.SDK_INT) result.success(Build.VERSION.SDK_INT)
} }
private fun isLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as android.app.KeyguardManager
val isLocked = keyguardManager.isKeyguardLocked
result.success(isLocked)
}
private fun isSystemFilePickerEnabled(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun isSystemFilePickerEnabled(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val enabled = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).resolveActivity(context.packageManager) != null
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).resolveActivity(context.packageManager) != null
} else {
false
}
result.success(enabled) result.success(enabled)
} }
@ -115,11 +139,20 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
return return
} }
val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA, Uri.parse("package:${context.packageName}")) val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA, "package:${context.packageName}".toUri())
context.startActivity(intent) context.startActivity(intent)
result.success(true) result.success(true)
} }
private fun getAvailableHeapSize(@Suppress("unused_parameter") methodCall: MethodCall, result: MethodChannel.Result) {
result.success(MemoryUtils.getAvailableHeapSize())
}
private fun requestGarbageCollection(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
Runtime.getRuntime().gc()
result.success(true)
}
companion object { companion object {
const val CHANNEL = "deckers.thibault/aves/device" const val CHANNEL = "deckers.thibault/aves/device"
} }

View file

@ -1,31 +1,36 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPUtils import com.adobe.internal.xmp.XMPUtils
import com.bumptech.glide.load.resource.bitmap.TransformationUtils import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.metadata.xmp.XmpDirectory import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.* import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.XMP.doesPropPathExist
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.xmp.GoogleDeviceContainer
import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
import deckers.thibault.aves.metadata.xmp.XMPPropName
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ContentImageProvider
import deckers.thibault.aves.model.provider.ImageProvider import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.utils.* import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.FileUtils.transferFrom import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.extensionFor import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -40,8 +45,9 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) } "getExifThumbnails" -> ioScope.launch { safe(call, result, ::getExifThumbnails) }
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) } "extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
"extractJpegMpfItem" -> ioScope.launch { safe(call, result, ::extractJpegMpfItem) }
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) } "extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
"extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) } "extractMotionPhotoVideo" -> ioScope.launch { safe(call, result, ::extractMotionPhotoVideo) }
"extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) } "extractVideoEmbeddedPicture" -> ioScope.launch { safe(call, result, ::extractVideoEmbeddedPicture) }
@ -50,9 +56,9 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
} }
} }
private suspend fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) { private fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getExifThumbnails-args", "missing arguments", null) result.error("getExifThumbnails-args", "missing arguments", null)
@ -67,7 +73,9 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
exif.thumbnailBitmap?.let { bitmap -> exif.thumbnailBitmap?.let { bitmap ->
TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let { TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let {
it.getBytes(canHaveAlpha = false, recycle = false)?.let { bytes -> thumbnails.add(bytes) } // do not recycle bitmaps fetched from `ExifInterface` as their lifecycle is unknown
val recycle = false
BitmapUtils.getRawBytes(it, recycle = recycle)?.let { bytes -> thumbnails.add(bytes) }
} }
} }
} }
@ -81,7 +89,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractGoogleDeviceItem(call: MethodCall, result: MethodChannel.Result) { private fun extractGoogleDeviceItem(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName") val displayName = call.argument<String>("displayName")
val dataUri = call.argument<String>("dataUri") val dataUri = call.argument<String>("dataUri")
@ -95,19 +103,12 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
// data can be large and stored in "Extended XMP", // data can be large and stored in "Extended XMP",
// which is returned as a second XMP directory // which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java) val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
try { try {
container = xmpDirs.firstNotNullOfOrNull { container = xmpDirs.firstNotNullOfOrNull { GoogleXMP.getDeviceContainer(it.xmpMeta) }
val xmpMeta = it.xmpMeta
if (xmpMeta.doesPropPathExist(listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) {
GoogleDeviceContainer().apply { findItems(xmpMeta) }
} else {
null
}
}
} catch (e: XMPException) { } catch (e: XMPException) {
result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message) result.error("extractGoogleDeviceItem-xmp", "failed to read XMP directory for uri=$uri dataUri=$dataUri", e.message)
return return
@ -141,9 +142,43 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
result.error("extractGoogleDeviceItem-empty", "failed to extract item from Google Device XMP at uri=$uri dataUri=$dataUri", null) result.error("extractGoogleDeviceItem-empty", "failed to extract item from Google Device XMP at uri=$uri dataUri=$dataUri", null)
} }
private fun extractJpegMpfItem(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName")
val id = call.argument<Int>("id")
if (mimeType == null || uri == null || sizeBytes == null || id == null) {
result.error("extractJpegMpfItem-args", "missing arguments", null)
return
}
val pageIndex = id - 1
val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes)
if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex]
mpEntry.mimeType?.let { embedMimeType ->
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes)
if (baseOffset != null) {
dataOffset += baseOffset
}
}
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(dataOffset)
copyEmbeddedBytes(result, embedMimeType, displayName, input, mpEntry.size)
}
return
}
}
result.error("extractJpegMpfItem-empty", "failed to extract file index=$id from MPF at uri=$uri", null)
}
private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) { private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName") val displayName = call.argument<String>("displayName")
if (mimeType == null || uri == null || sizeBytes == null) { if (mimeType == null || uri == null || sizeBytes == null) {
@ -151,7 +186,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
return return
} }
MultiPage.getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes -> MultiPage.getTrailerVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
val imageSizeBytes = sizeBytes - videoSizeBytes val imageSizeBytes = sizeBytes - videoSizeBytes
StorageUtils.openInputStream(context, uri)?.let { input -> StorageUtils.openInputStream(context, uri)?.let { input ->
copyEmbeddedBytes(result, mimeType, displayName, input, imageSizeBytes) copyEmbeddedBytes(result, mimeType, displayName, input, imageSizeBytes)
@ -164,7 +199,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractMotionPhotoVideo(call: MethodCall, result: MethodChannel.Result) { private fun extractMotionPhotoVideo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName") val displayName = call.argument<String>("displayName")
if (mimeType == null || uri == null || sizeBytes == null) { if (mimeType == null || uri == null || sizeBytes == null) {
@ -172,11 +207,10 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
return return
} }
MultiPage.getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes -> MultiPage.getMotionPhotoVideoSizing(context, uri, mimeType, sizeBytes)?.let { (videoOffset, videoSize) ->
val videoStartOffset = sizeBytes - videoSizeBytes
StorageUtils.openInputStream(context, uri)?.let { input -> StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(videoStartOffset) input.skip(videoOffset)
copyEmbeddedBytes(result, MimeTypes.MP4, displayName, input, videoSizeBytes) copyEmbeddedBytes(result, MimeTypes.MP4, displayName, input, videoSize)
} }
return return
} }
@ -185,7 +219,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
} }
private fun extractVideoEmbeddedPicture(call: MethodCall, result: MethodChannel.Result) { private fun extractVideoEmbeddedPicture(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val displayName = call.argument<String>("displayName") val displayName = call.argument<String>("displayName")
if (uri == null) { if (uri == null) {
result.error("extractVideoEmbeddedPicture-args", "missing arguments", null) result.error("extractVideoEmbeddedPicture-args", "missing arguments", null)
@ -217,7 +251,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractXmpDataProp(call: MethodCall, result: MethodChannel.Result) { private fun extractXmpDataProp(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val displayName = call.argument<String>("displayName") val displayName = call.argument<String>("displayName")
val dataProp = call.argument<List<Any>>("propPath") val dataProp = call.argument<List<Any>>("propPath")
@ -238,7 +272,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
// data can be large and stored in "Extended XMP", // data can be large and stored in "Extended XMP",
// which is returned as a second XMP directory // which is returned as a second XMP directory
val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java) val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
@ -277,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
embeddedByteStream: InputStream, embeddedByteStream: InputStream,
embeddedByteLength: Long, embeddedByteLength: Long,
) { ) {
val extension = extensionFor(mimeType) val extension = extensionFor(mimeType, defaultExtension = null)
val targetFile = StorageUtils.createTempFile(context, extension).apply { val targetFile = StorageUtils.createTempFile(context, extension).apply {
transferFrom(embeddedByteStream, embeddedByteLength) transferFrom(embeddedByteStream, embeddedByteLength)
} }
@ -285,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val authority = "${context.applicationContext.packageName}.file_provider" val authority = "${context.applicationContext.packageName}.file_provider"
val uri = if (displayName != null) { val uri = if (displayName != null) {
// add extension to ease type identification when sharing this content // add extension to ease type identification when sharing this content
val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) { val displayNameWithExtension = if (displayName.endsWith(extension, ignoreCase = true)) {
displayName displayName
} else { } else {
"$displayName$extension" "$displayName$extension"
@ -295,12 +329,18 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
FileProvider.getUriForFile(context, authority, targetFile) FileProvider.getUriForFile(context, authority, targetFile)
} }
val resultFields: FieldMap = hashMapOf( val resultFields: FieldMap = hashMapOf(
"uri" to uri.toString(), EntryFields.URI to uri.toString(),
"mimeType" to mimeType, EntryFields.MIME_TYPE to mimeType,
) )
if (isImage(mimeType) || isVideo(mimeType)) { if (isImage(mimeType) || isVideo(mimeType)) {
val provider = getProvider(context, uri)
if (provider == null) {
result.error("copyEmbeddedBytes-provider", "failed to find provider for uri=$uri", null)
return
}
ioScope.launch { ioScope.launch {
ContentImageProvider().fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback { provider.fetchSingle(context, uri, mimeType, false, object : ImageProvider.ImageOpCallback {
override fun onSuccess(fields: FieldMap) { override fun onSuccess(fields: FieldMap) {
resultFields.putAll(fields) resultFields.putAll(fields)
result.success(resultFields) result.success(resultFields)

View file

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

View file

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

View file

@ -1,8 +1,8 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri
import android.util.Log import android.util.Log
import androidx.core.net.toUri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
@ -44,7 +44,7 @@ class MediaEditHandler(private val contextWrapper: ContextWrapper) : MethodCallH
} }
private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) { private suspend fun captureFrame(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val desiredName = call.argument<String>("desiredName") val desiredName = call.argument<String>("desiredName")
val exifFields = call.argument<FieldMap>("exif") ?: HashMap() val exifFields = call.argument<FieldMap>("exif") ?: HashMap()
val bytes = call.argument<ByteArray>("bytes") val bytes = call.argument<ByteArray>("bytes")
@ -55,7 +55,7 @@ class MediaEditHandler(private val contextWrapper: ContextWrapper) : MethodCallH
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("captureFrame-provider", "failed to find provider for uri=$uri", null) result.error("captureFrame-provider", "failed to find provider for uri=$uri", null)
return return

View file

@ -2,12 +2,13 @@ package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import androidx.core.net.toUri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -16,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Date
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler { class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
@ -26,25 +28,25 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) } "getThumbnail" -> ioScope.launch { safe(call, result, ::getThumbnail) }
"getRegion" -> ioScope.launch { safeSuspend(call, result, ::getRegion) } "getRegion" -> ioScope.launch { safe(call, result, ::getRegion) }
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) { private fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri") val uri = call.argument<String>(EntryFields.URI)
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>(EntryFields.MIME_TYPE)
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong() val dateModifiedMillis = call.argument<Number>(EntryFields.DATE_MODIFIED_MILLIS)?.toLong()
val rotationDegrees = call.argument<Int>("rotationDegrees") val rotationDegrees = call.argument<Int>(EntryFields.ROTATION_DEGREES)
val isFlipped = call.argument<Boolean>("isFlipped") val isFlipped = call.argument<Boolean>(EntryFields.IS_FLIPPED)
val widthDip = call.argument<Number>("widthDip")?.toDouble() val widthDip = call.argument<Number>("widthDip")?.toDouble()
val heightDip = call.argument<Number>("heightDip")?.toDouble() val heightDip = call.argument<Number>("heightDip")?.toDouble()
val pageId = call.argument<Int>("pageId") val pageId = call.argument<Int>("pageId")
val defaultSizeDip = call.argument<Number>("defaultSizeDip")?.toDouble() val defaultSizeDip = call.argument<Number>("defaultSizeDip")?.toDouble()
val quality = call.argument<Int>("quality") val quality = call.argument<Int>("quality")
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null || quality == null) { if (uri == null || mimeType == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null || quality == null) {
result.error("getThumbnail-args", "missing arguments", null) result.error("getThumbnail-args", "missing arguments", null)
return return
} }
@ -54,7 +56,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
context = context, context = context,
uri = uri, uri = uri,
mimeType = mimeType, mimeType = mimeType,
dateModifiedSecs = dateModifiedSecs, dateModifiedMillis = dateModifiedMillis ?: (Date().time),
rotationDegrees = rotationDegrees, rotationDegrees = rotationDegrees,
isFlipped = isFlipped, isFlipped = isFlipped,
width = (widthDip * density).roundToInt(), width = (widthDip * density).roundToInt(),
@ -66,8 +68,8 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
).fetch() ).fetch()
} }
private suspend fun getRegion(call: MethodCall, result: MethodChannel.Result) { private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val pageId = call.argument<Int>("pageId") val pageId = call.argument<Int>("pageId")
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
@ -89,11 +91,13 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
MimeTypes.SVG -> SvgRegionFetcher(context).fetch( MimeTypes.SVG -> SvgRegionFetcher(context).fetch(
uri = uri, uri = uri,
sizeBytes = sizeBytes, sizeBytes = sizeBytes,
scale = sampleSize,
regionRect = regionRect, regionRect = regionRect,
imageWidth = imageWidth, imageWidth = imageWidth,
imageHeight = imageHeight, imageHeight = imageHeight,
result = result, result = result,
) )
MimeTypes.TIFF -> TiffRegionFetcher(context).fetch( MimeTypes.TIFF -> TiffRegionFetcher(context).fetch(
uri = uri, uri = uri,
page = pageId ?: 0, page = pageId ?: 0,
@ -101,6 +105,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
regionRect = regionRect, regionRect = regionRect,
result = result, result = result,
) )
else -> regionFetcher.fetch( else -> regionFetcher.fetch(
uri = uri, uri = uri,
mimeType = mimeType, mimeType = mimeType,

View file

@ -1,7 +1,9 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import android.net.Uri import android.os.Handler
import android.os.Looper
import androidx.core.net.toUri
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
@ -21,36 +23,45 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"getEntry" -> ioScope.launch { safe(call, result, ::getEntry) } "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() else -> result.notImplemented()
} }
} }
private fun getEntry(call: MethodCall, result: MethodChannel.Result) { private fun getEntry(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") // MIME type is optional val mimeType = call.argument<String>("mimeType") // MIME type is optional
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val allowUnsized = call.argument<Boolean>("allowUnsized") ?: false
if (uri == null) { if (uri == null) {
result.error("getEntry-args", "missing arguments", null) result.error("getEntry-args", "missing arguments", null)
return return
} }
val provider = getProvider(uri) val provider = getProvider(context, uri)
if (provider == null) { if (provider == null) {
result.error("getEntry-provider", "failed to find provider for uri=$uri", null) result.error("getEntry-provider", "failed to find provider for uri=$uri mimeType=$mimeType", null)
return return
} }
provider.fetchSingle(context, uri, mimeType, object : ImageOpCallback { provider.fetchSingle(context, uri, mimeType, allowUnsized, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields) override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message) override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri mimeType=$mimeType", throwable.message)
}) })
} }
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() Glide.get(context).clearDiskCache()
result.success(null) 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 { companion object {
const val CHANNEL = "deckers.thibault/aves/media_fetch_object" const val CHANNEL = "deckers.thibault/aves/media_fetch_object"
} }

View file

@ -1,12 +1,16 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.* import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager import android.media.AudioManager
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.net.toUri
import androidx.media.session.MediaButtonReceiver import androidx.media.session.MediaButtonReceiver
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
@ -59,7 +63,7 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
} }
private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) { private suspend fun updateSession(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val title = call.argument<String>("title") ?: uri?.toString() val title = call.argument<String>("title") ?: uri?.toString()
val durationMillis = call.argument<Number>("durationMillis")?.toLong() val durationMillis = call.argument<Number>("durationMillis")?.toLong()
val stateString = call.argument<String>("state") val stateString = call.argument<String>("state")

View file

@ -3,6 +3,8 @@ package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.provider.MediaStoreImageProvider import deckers.thibault.aves.model.provider.MediaStoreImageProvider
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -20,13 +22,15 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
when (call.method) { when (call.method) {
"checkObsoleteContentIds" -> ioScope.launch { safe(call, result, ::checkObsoleteContentIds) } "checkObsoleteContentIds" -> ioScope.launch { safe(call, result, ::checkObsoleteContentIds) }
"checkObsoletePaths" -> ioScope.launch { safe(call, result, ::checkObsoletePaths) } "checkObsoletePaths" -> ioScope.launch { safe(call, result, ::checkObsoletePaths) }
"getChangedUris" -> ioScope.launch { safe(call, result, ::getChangedUris) }
"getGeneration" -> ioScope.launch { safe(call, result, ::getGeneration) }
"scanFile" -> ioScope.launch { safe(call, result, ::scanFile) } "scanFile" -> ioScope.launch { safe(call, result, ::scanFile) }
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) { private fun checkObsoleteContentIds(call: MethodCall, result: MethodChannel.Result) {
val knownContentIds = call.argument<List<Int?>>("knownContentIds") val knownContentIds = call.argument<List<Number?>>("knownContentIds")?.map { it?.toLong() }
if (knownContentIds == null) { if (knownContentIds == null) {
result.error("checkObsoleteContentIds-args", "missing arguments", null) result.error("checkObsoleteContentIds-args", "missing arguments", null)
return return
@ -35,7 +39,7 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
} }
private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) { private fun checkObsoletePaths(call: MethodCall, result: MethodChannel.Result) {
val knownPathById = call.argument<Map<Int?, String?>>("knownPathById") val knownPathById = call.argument<Map<Number?, String?>>("knownPathById")?.mapKeys { it.key?.toLong() }
if (knownPathById == null) { if (knownPathById == null) {
result.error("checkObsoletePaths-args", "missing arguments", null) result.error("checkObsoletePaths-args", "missing arguments", null)
return return
@ -43,6 +47,32 @@ class MediaStoreHandler(private val context: Context) : MethodCallHandler {
result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById)) result.success(MediaStoreImageProvider().checkObsoletePaths(context, knownPathById))
} }
private fun getChangedUris(call: MethodCall, result: MethodChannel.Result) {
val sinceGeneration = call.argument<Int>("sinceGeneration")
if (sinceGeneration == null) {
result.error("getChangedUris-args", "missing arguments", null)
return
}
val uris = MediaStoreImageProvider().getChangedUris(context, sinceGeneration)
result.success(uris)
}
private fun getGeneration(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val generation = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try {
MediaStore.getGeneration(context, MediaStore.VOLUME_EXTERNAL_PRIMARY)
} catch (e: Exception) {
// may yield `IllegalArgumentException: Volume external_primary not found`
val volumes = MediaStore.getExternalVolumeNames(context).joinToString(", ")
result.error("getGeneration-primary", e.message + " (available volumes are [$volumes])", e)
return
}
} else {
null
}
result.success(generation)
}
private fun scanFile(call: MethodCall, result: MethodChannel.Result) { private fun scanFile(call: MethodCall, result: MethodChannel.Result) {
val path = call.argument<String>("path") val path = call.argument<String>("path")
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import androidx.core.net.toUri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.Mp4TooLargeException import deckers.thibault.aves.metadata.Mp4TooLargeException
import deckers.thibault.aves.model.ExifOrientationOp import deckers.thibault.aves.model.ExifOrientationOp
@ -54,7 +54,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } val uri = (entryMap["uri"] as String?)?.toUri()
val path = entryMap["path"] as String? val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String? val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) { if (uri == null || path == null || mimeType == null) {
@ -62,7 +62,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("editOrientation-provider", "failed to find provider for uri=$uri", null) result.error("editOrientation-provider", "failed to find provider for uri=$uri", null)
return return
@ -74,7 +74,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
private fun editDate(call: MethodCall, result: MethodChannel.Result) { private fun editDate(call: MethodCall, result: MethodChannel.Result) {
val dateMillis = call.argument<Number>("dateMillis")?.toLong() val dateMillis = call.argument<Number>("dateMillis")?.toLong()
val shiftMinutes = call.argument<Number>("shiftMinutes")?.toLong() val shiftSeconds = call.argument<Number>("shiftSeconds")?.toLong()
val fields = call.argument<List<String>>("fields") val fields = call.argument<List<String>>("fields")
val entryMap = call.argument<FieldMap>("entry") val entryMap = call.argument<FieldMap>("entry")
if (entryMap == null || fields == null) { if (entryMap == null || fields == null) {
@ -82,7 +82,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } val uri = (entryMap["uri"] as String?)?.toUri()
val path = entryMap["path"] as String? val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String? val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) { if (uri == null || path == null || mimeType == null) {
@ -90,14 +90,14 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("editDate-provider", "failed to find provider for uri=$uri", null) result.error("editDate-provider", "failed to find provider for uri=$uri", null)
return return
} }
val callback = MetadataOpCallback("editDate", entryMap, result) val callback = MetadataOpCallback("editDate", entryMap, result)
provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftMinutes, fields, callback) provider.editDate(contextWrapper, path, uri, mimeType, dateMillis, shiftSeconds, fields, callback)
} }
private fun editMetadata(call: MethodCall, result: MethodChannel.Result) { private fun editMetadata(call: MethodCall, result: MethodChannel.Result) {
@ -109,7 +109,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } val uri = (entryMap["uri"] as String?)?.toUri()
val path = entryMap["path"] as String? val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String? val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) { if (uri == null || path == null || mimeType == null) {
@ -117,7 +117,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("editMetadata-provider", "failed to find provider for uri=$uri", null) result.error("editMetadata-provider", "failed to find provider for uri=$uri", null)
return return
@ -134,7 +134,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } val uri = (entryMap["uri"] as String?)?.toUri()
val path = entryMap["path"] as String? val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String? val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) { if (uri == null || path == null || mimeType == null) {
@ -142,7 +142,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("removeTrailerVideo-provider", "failed to find provider for uri=$uri", null) result.error("removeTrailerVideo-provider", "failed to find provider for uri=$uri", null)
return return
@ -160,7 +160,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) } val uri = (entryMap["uri"] as String?)?.toUri()
val path = entryMap["path"] as String? val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String? val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) { if (uri == null || path == null || mimeType == null) {
@ -168,7 +168,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("removeTypes-provider", "failed to find provider for uri=$uri", null) result.error("removeTypes-provider", "failed to find provider for uri=$uri", null)
return return

View file

@ -1,11 +1,12 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.Context import android.content.Context
import android.media.MediaFormat
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.core.net.toUri
import com.adobe.internal.xmp.XMPException import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory import com.adobe.internal.xmp.XMPMetaFactory
@ -22,12 +23,14 @@ import com.drew.metadata.exif.GpsDirectory
import com.drew.metadata.file.FileTypeDirectory import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.gif.GifAnimationDirectory import com.drew.metadata.gif.GifAnimationDirectory
import com.drew.metadata.iptc.IptcDirectory import com.drew.metadata.iptc.IptcDirectory
import com.drew.metadata.mov.metadata.QuickTimeMetadataDirectory
import com.drew.metadata.mp4.media.Mp4UuidBoxDirectory import com.drew.metadata.mp4.media.Mp4UuidBoxDirectory
import com.drew.metadata.png.PngDirectory import com.drew.metadata.png.PngDirectory
import com.drew.metadata.webp.WebpDirectory import com.drew.metadata.webp.WebpDirectory
import com.drew.metadata.xmp.XmpDirectory import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifGeoTiffTags import deckers.thibault.aves.metadata.ExifGeoTiffTags
import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble
@ -50,15 +53,6 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper
import deckers.thibault.aves.metadata.MultiPage import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.PixyMetaHelper import deckers.thibault.aves.metadata.PixyMetaHelper
import deckers.thibault.aves.metadata.QuickTimeMetadata import deckers.thibault.aves.metadata.QuickTimeMetadata
import deckers.thibault.aves.metadata.XMP
import deckers.thibault.aves.metadata.XMP.doesPropExist
import deckers.thibault.aves.metadata.XMP.getPropArrayItemValues
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeInt
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.getSafeString
import deckers.thibault.aves.metadata.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_ITXT_DIR_NAME import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_ITXT_DIR_NAME
import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_LAST_MODIFICATION_TIME_FORMAT import deckers.thibault.aves.metadata.metadataextractor.Helper.PNG_LAST_MODIFICATION_TIME_FORMAT
@ -76,8 +70,22 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory import deckers.thibault.aves.metadata.metadataextractor.PngActlDirectory
import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.metadata.xmp.XMP
import deckers.thibault.aves.metadata.xmp.XMP.doesPropExist
import deckers.thibault.aves.metadata.xmp.XMP.getPropArrayItemValues
import deckers.thibault.aves.metadata.xmp.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.xmp.XMP.getSafeInt
import deckers.thibault.aves.metadata.xmp.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.xmp.XMP.hasHdrGainMap
import deckers.thibault.aves.metadata.xmp.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.xmp.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.HashUtils
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN
@ -94,11 +102,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.json.JSONObject import org.json.JSONObject
import org.mp4parser.boxes.threegpp.ts26244.LocationInformationBox
import org.mp4parser.tools.Path
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.ParseException import java.text.ParseException
import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.roundToLong import kotlin.math.roundToLong
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
class MetadataFetchHandler(private val context: Context) : MethodCallHandler { class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -116,14 +128,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) } "hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) } "getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
"getDate" -> ioScope.launch { safe(call, result, ::getDate) } "getDate" -> ioScope.launch { safe(call, result, ::getDate) }
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) } "getFields" -> ioScope.launch { safe(call, result, ::getFields) }
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
private fun getAllMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getAllMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getAllMetadata-args", "missing arguments", null) result.error("getAllMetadata-args", "missing arguments", null)
@ -155,9 +167,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
// remove this stat as it is not actual XMP data // remove this stat as it is not actual XMP data
dirMap.remove(XmpDirectory().getTagName(XmpDirectory.TAG_XMP_VALUE_COUNT)) dirMap.remove(XmpDirectory().getTagName(XmpDirectory.TAG_XMP_VALUE_COUNT))
// add schema prefixes for namespace resolution if (dirMap.isNotEmpty()) {
val prefixes = XMPMetaFactory.getSchemaRegistry().prefixes // add schema prefixes for namespace resolution
dirMap["schemaRegistryPrefixes"] = JSONObject(prefixes).toString() val prefixes = XMPMetaFactory.getSchemaRegistry().prefixes
dirMap["schemaRegistryPrefixes"] = JSONObject(prefixes).toString()
}
} }
val mp4UuidDirCount = HashMap<String, Int>() val mp4UuidDirCount = HashMap<String, Int>()
@ -220,12 +234,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 } foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
foundMp4Uuid = metadata.directories.any { it is Mp4UuidBoxDirectory && it.tagCount > 0 } foundMp4Uuid = metadata.directories.any { it is Mp4UuidBoxDirectory && it.tagCount > 0 }
val dirByName = metadata.directories.filter { val dirByName = metadata.directories.filter {
(it.tagCount > 0 || it.errorCount > 0) (it.tagCount > 0 || it.errorCount > 0 || it is MpEntryDirectory)
&& it !is FileTypeDirectory && it !is FileTypeDirectory
&& it !is AviDirectory && it !is AviDirectory
}.groupBy { dir -> dir.name } }.groupBy { dir -> dir.name }
@ -287,7 +301,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) } byGeoTiff[false]?.map { exifTagMapper(it) }?.let { dirMap.putAll(it) }
} }
mimeType == MimeTypes.DNG -> { mimeType == MimeTypes.DNG || mimeType == MimeTypes.DNG_ADOBE -> {
// split DNG tags in their own directory // split DNG tags in their own directory
val dngDirMap = metadataMap[DIR_DNG] ?: HashMap() val dngDirMap = metadataMap[DIR_DNG] ?: HashMap()
metadataMap[DIR_DNG] = dngDirMap metadataMap[DIR_DNG] = dngDirMap
@ -344,6 +358,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
} }
dir is MpEntryDirectory -> {
dirMap.putAll(dir.describe())
}
else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) }) else -> dirMap.putAll(tags.map { Pair(it.tagName, it.description) })
} }
} }
@ -384,6 +402,21 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// do not overwrite XMP parsed by metadata-extractor // do not overwrite XMP parsed by metadata-extractor
// with raw XMP found by ExifInterface // with raw XMP found by ExifInterface
allTags.remove(Metadata.DIR_XMP) allTags.remove(Metadata.DIR_XMP)
} else {
val xmpTags = allTags[Metadata.DIR_XMP]
if (xmpTags != null) {
val xmpRaw = xmpTags[ExifInterface.TAG_XMP]
if (xmpRaw != null) {
val metadata = com.drew.metadata.Metadata()
val xmpBytes = xmpRaw.toByteArray(Charsets.UTF_8)
SafeXmpReader().extract(xmpBytes, 0, xmpBytes.size, metadata, null)
metadata.getFirstDirectoryOfType(XmpDirectory::class.java)?.let { xmpDir ->
val dirMap = HashMap<String, String>()
processXmp(xmpDir.xmpMeta, dirMap, allowMultiple = true)
allTags[Metadata.DIR_XMP] = dirMap
}
}
}
} }
metadataMap.putAll(allTags.mapValues { it.value.toMutableMap() }) metadataMap.putAll(allTags.mapValues { it.value.toMutableMap() })
} }
@ -419,9 +452,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (isVideo(mimeType)) { if (isVideo(mimeType)) {
// `metadata-extractor` do not extract custom tags in user data box // `metadata-extractor` do not extract custom tags in user data box
val userDataDir = Mp4ParserHelper.getUserData(context, mimeType, uri) Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { box ->
if (userDataDir.isNotEmpty()) { metadataMap[Metadata.DIR_MP4_USER_DATA] = Mp4ParserHelper.extractBoxFields(box)
metadataMap[Metadata.DIR_MP4_USER_DATA] = userDataDir
} }
// this is used as fallback when the video metadata cannot be found on the Dart side // this is used as fallback when the video metadata cannot be found on the Dart side
@ -440,6 +472,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// Android's `MediaExtractor` and `MediaPlayer` cannot be used for details // Android's `MediaExtractor` and `MediaPlayer` cannot be used for details
// about embedded images as they do not list them as separate tracks // about embedded images as they do not list them as separate tracks
// and only identify at most one // and only identify at most one
} else if (isHeic(mimeType)) {
Mp4ParserHelper.getSamsungSefd(context, uri)?.let { (_, bytes) ->
metadataMap[Mp4ParserHelper.SAMSUNG_MAKERNOTE_BOX_TYPE] = hashMapOf(
"Size" to bytes.size.toString(),
)
}
} }
if (metadataMap.isNotEmpty()) { if (metadataMap.isNotEmpty()) {
@ -487,7 +525,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// - XMP / MicrosoftPhoto:Rating // - XMP / MicrosoftPhoto:Rating
private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val path = call.argument<String>("path") val path = call.argument<String>("path")
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
@ -497,8 +535,33 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val metadataMap = HashMap<String, Any>() val metadataMap = HashMap<String, Any>()
getCatalogMetadataByMetadataExtractor(mimeType, uri, path, sizeBytes, metadataMap) getCatalogMetadataByMetadataExtractor(mimeType, uri, path, sizeBytes, metadataMap)
if (isVideo(mimeType) || isHeic(mimeType)) { if (isVideo(mimeType) || isHeic(mimeType)) {
getMultimediaCatalogMetadataByMediaMetadataRetriever(mimeType, uri, metadataMap) getMultimediaCatalogMetadataByMediaMetadataRetriever(mimeType, uri, metadataMap)
// fallback to MP4 `loci` box for location
if (!metadataMap.contains(KEY_LATITUDE) || !metadataMap.contains(KEY_LONGITUDE)) {
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
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get Location Information box by MP4 parser for mimeType=$mimeType uri=$uri", e)
}
}
}
if (isHeic(mimeType)) {
val flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
if ((flags and MASK_IS_MOTION_PHOTO == 0) && MultiPage.isHeicSefdMotionPhoto(context, uri)) {
metadataMap[KEY_FLAGS] = flags or MASK_IS_MULTIPAGE or MASK_IS_MOTION_PHOTO
}
} }
// report success even when empty // report success even when empty
@ -551,6 +614,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (xmpMeta.isMotionPhoto()) { if (xmpMeta.isMotionPhoto()) {
flags = flags or MASK_IS_MULTIPAGE or MASK_IS_MOTION_PHOTO flags = flags or MASK_IS_MULTIPAGE or MASK_IS_MOTION_PHOTO
} }
// identification of embedded gain map
if (xmpMeta.hasHdrGainMap()) {
flags = flags or MASK_IS_HDR
}
} catch (e: XMPException) { } catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e) Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
} }
@ -566,7 +634,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 } foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
foundMp4Uuid = metadata.directories.any { it is Mp4UuidBoxDirectory && it.tagCount > 0 } foundMp4Uuid = metadata.directories.any { it is Mp4UuidBoxDirectory && it.tagCount > 0 }
@ -623,6 +691,15 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
} }
// JPEG Multi-Picture Format
if (metadata.getDirectoriesOfType(MpEntryDirectory::class.java).count { !it.entry.isThumbnail } > 1) {
flags = flags or MASK_IS_MULTIPAGE
if (hasAppleHdrGainMap(uri, sizeBytes)) {
flags = flags or MASK_IS_HDR
}
}
// XMP // XMP
if (!isLargeMp4(mimeType, sizeBytes)) { if (!isLargeMp4(mimeType, sizeBytes)) {
metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach { metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach {
@ -642,6 +719,22 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
} }
if (!metadataMap.containsKey(KEY_LATITUDE) || !metadataMap.containsKey(KEY_LONGITUDE)) {
for (dir in metadata.getDirectoriesOfType(QuickTimeMetadataDirectory::class.java)) {
dir.getSafeString(QuickTimeMetadataDirectory.TAG_LOCATION_ISO6709) { locationString ->
val matcher = Metadata.VIDEO_LOCATION_PATTERN.matcher(locationString)
if (matcher.find() && matcher.groupCount() >= 2) {
val latitude = matcher.group(1)?.toDoubleOrNull()
val longitude = matcher.group(2)?.toDoubleOrNull()
if (latitude != null && longitude != null) {
metadataMap[KEY_LATITUDE] = latitude
metadataMap[KEY_LONGITUDE] = longitude
}
}
}
}
}
when (mimeType) { when (mimeType) {
MimeTypes.PNG -> { MimeTypes.PNG -> {
// date fallback to PNG time chunk // date fallback to PNG time chunk
@ -747,6 +840,31 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
metadataMap[KEY_FLAGS] = flags metadataMap[KEY_FLAGS] = flags
} }
private fun hasAppleHdrGainMap(uri: Uri, sizeBytes: Long?): Boolean {
val mpEntries = MultiPage.getJpegMpfEntries(context, uri, sizeBytes) ?: return false
mpEntries.filter { it.type == MpEntry.TYPE_UNDEFINED }.forEachIndexed { mpIndex, mpEntry ->
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
val baseOffset = MultiPage.getJpegMpfBaseOffset(context, uri, sizeBytes)
if (baseOffset != null) {
dataOffset += baseOffset
}
}
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(dataOffset)
try {
val pageMetadata = Helper.safeRead(input, sizeBytes)
if (pageMetadata.getDirectoriesOfType(XmpDirectory::class.java).any { it.xmpMeta.hasHdrGainMap() }) {
return true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for uri=$uri mpIndex=$mpIndex mpEntry=$mpEntry", e)
}
}
}
return false
}
private fun getMultimediaCatalogMetadataByMediaMetadataRetriever( private fun getMultimediaCatalogMetadataByMediaMetadataRetriever(
mimeType: String, mimeType: String,
uri: Uri, uri: Uri,
@ -761,7 +879,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { metadataMap[KEY_DATE_MILLIS] = it } retriever.getSafeDateMillis(MediaMetadataRetriever.METADATA_KEY_DATE) { metadataMap[KEY_DATE_MILLIS] = it }
} }
if (!metadataMap.containsKey(KEY_LATITUDE)) { if (!metadataMap.containsKey(KEY_LATITUDE) || !metadataMap.containsKey(KEY_LONGITUDE)) {
val locationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION) val locationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
if (locationString != null) { if (locationString != null) {
val matcher = Metadata.VIDEO_LOCATION_PATTERN.matcher(locationString) val matcher = Metadata.VIDEO_LOCATION_PATTERN.matcher(locationString)
@ -782,6 +900,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER) {
if (it == MediaFormat.COLOR_TRANSFER_ST2084 || it == MediaFormat.COLOR_TRANSFER_HLG) {
flags = flags or MASK_IS_HDR
}
}
}
metadataMap[KEY_FLAGS] = flags metadataMap[KEY_FLAGS] = flags
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get catalog metadata by MediaMetadataRetriever for uri=$uri", e) Log.w(LOG_TAG, "failed to get catalog metadata by MediaMetadataRetriever for uri=$uri", e)
@ -793,15 +919,16 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) { private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { val fields = call.argument<List<String>>("fields")
if (mimeType == null || uri == null || fields == null) {
result.error("getOverlayMetadata-args", "missing arguments", null) result.error("getOverlayMetadata-args", "missing arguments", null)
return return
} }
val metadataMap = HashMap<String, Any>() val metadataMap = HashMap<String, Any>()
if (isVideo(mimeType)) { if (fields.isEmpty() || isVideo(mimeType)) {
result.success(metadataMap) result.success(metadataMap)
return return
} }
@ -823,13 +950,24 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) { for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
foundExif = true foundExif = true
dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator } if (fields.contains(KEY_APERTURE)) {
dir.getSafeRational(ExifDirectoryBase.TAG_EXPOSURE_TIME, saveExposureTime) dir.getSafeRational(ExifDirectoryBase.TAG_FNUMBER) { metadataMap[KEY_APERTURE] = it.numerator.toDouble() / it.denominator }
dir.getSafeRational(ExifDirectoryBase.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator } }
dir.getSafeInt(ExifDirectoryBase.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it } if (fields.contains(KEY_DESCRIPTION)) {
getDescriptionByMetadataExtractor(metadata)?.let { metadataMap[KEY_DESCRIPTION] = it }
}
if (fields.contains(KEY_EXPOSURE_TIME)) {
dir.getSafeRational(ExifDirectoryBase.TAG_EXPOSURE_TIME, saveExposureTime)
}
if (fields.contains(KEY_FOCAL_LENGTH)) {
dir.getSafeRational(ExifDirectoryBase.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it.numerator.toDouble() / it.denominator }
}
if (fields.contains(KEY_ISO)) {
dir.getSafeInt(ExifDirectoryBase.TAG_ISO_EQUIVALENT) { metadataMap[KEY_ISO] = it }
}
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -846,10 +984,18 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input) val exif = ExifInterface(input)
exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it } if (fields.contains(KEY_APERTURE)) {
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime) exif.getSafeDouble(ExifInterface.TAG_F_NUMBER) { metadataMap[KEY_APERTURE] = it }
exif.getSafeDouble(ExifInterface.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it } }
exif.getSafeInt(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY) { metadataMap[KEY_ISO] = it } if (fields.contains(KEY_EXPOSURE_TIME)) {
exif.getSafeRational(ExifInterface.TAG_EXPOSURE_TIME, saveExposureTime)
}
if (fields.contains(KEY_FOCAL_LENGTH)) {
exif.getSafeDouble(ExifInterface.TAG_FOCAL_LENGTH) { metadataMap[KEY_FOCAL_LENGTH] = it }
}
if (fields.contains(KEY_ISO)) {
exif.getSafeInt(ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY) { metadataMap[KEY_ISO] = it }
}
} }
} catch (e: Exception) { } catch (e: Exception) {
// ExifInterface initialization can fail with a RuntimeException // ExifInterface initialization can fail with a RuntimeException
@ -861,9 +1007,50 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(metadataMap) result.success(metadataMap)
} }
// returns description from these fields (by precedence):
// - XMP / dc:description
// - IPTC / caption-abstract
// - Exif / UserComment
// - Exif / ImageDescription
private fun getDescriptionByMetadataExtractor(metadata: com.drew.metadata.Metadata): String? {
var description: String? = null
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
val xmpMeta = dir.xmpMeta
try {
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) {
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it }
}
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory", e)
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
// user comment field specifies encoding, unlike other string fields
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
if (string.isNotBlank()) {
description = string
}
}
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = it }
}
}
return description
}
private fun getGeoTiffInfo(call: MethodCall, result: MethodChannel.Result) { private fun getGeoTiffInfo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getGeoTiffInfo-args", "missing arguments", null) result.error("getGeoTiffInfo-args", "missing arguments", null)
@ -873,7 +1060,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
val fields = HashMap<Int, Any?>() val fields = HashMap<Int, Any?>()
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) { for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
if (dir.containsGeoTiffTags()) { if (dir.containsGeoTiffTags()) {
@ -904,7 +1091,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getMultiPageInfo(call: MethodCall, result: MethodChannel.Result) { private fun getMultiPageInfo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val isMotionPhoto = call.argument<Boolean>("isMotionPhoto") val isMotionPhoto = call.argument<Boolean>("isMotionPhoto")
if (mimeType == null || uri == null || sizeBytes == null || isMotionPhoto == null) { if (mimeType == null || uri == null || sizeBytes == null || isMotionPhoto == null) {
@ -913,10 +1100,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
val pages: ArrayList<FieldMap>? = if (isMotionPhoto) { val pages: ArrayList<FieldMap>? = if (isMotionPhoto) {
MultiPage.getMotionPhotoPages(context, uri, mimeType, sizeBytes = sizeBytes) MultiPage.getMotionPhotoPages(context, uri, mimeType, sizeBytes)
} else { } else {
when (mimeType) { when (mimeType) {
MimeTypes.HEIC, MimeTypes.HEIF -> MultiPage.getHeicTracks(context, uri) MimeTypes.HEIC, MimeTypes.HEIF -> MultiPage.getHeicTracks(context, uri)
MimeTypes.JPEG -> MultiPage.getJpegMpfPages(context, uri, sizeBytes)
MimeTypes.TIFF -> MultiPage.getTiffPages(context, uri) MimeTypes.TIFF -> MultiPage.getTiffPages(context, uri)
else -> null else -> null
} }
@ -930,7 +1118,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getPanoramaInfo(call: MethodCall, result: MethodChannel.Result) { private fun getPanoramaInfo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getPanoramaInfo-args", "missing arguments", null) result.error("getPanoramaInfo-args", "missing arguments", null)
@ -943,23 +1131,13 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
fun processXmp(xmpMeta: XMPMeta, allowMultiple: Boolean = false) { fun processXmp(xmpMeta: XMPMeta, allowMultiple: Boolean = false) {
if (foundXmp && !allowMultiple) return if (foundXmp && !allowMultiple) return
foundXmp = true foundXmp = true
try { fields.putAll(GoogleXMP.getPanoramaInfo(xmpMeta))
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
xmpMeta.getSafeInt(XMP.GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
xmpMeta.getSafeInt(XMP.GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
xmpMeta.getSafeString(XMP.GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
}
} }
if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) { if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach { metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach {
processXmp(it, allowMultiple = true) processXmp(it, allowMultiple = true)
} }
@ -985,14 +1163,14 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (fields.isEmpty()) { if (fields.isEmpty()) {
result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null) result.error("getPanoramaInfo-empty", "failed to get info for mimeType=$mimeType uri=$uri", null)
} else { } else {
fields["projectionType"] = fields["projectionType"] ?: XMP.GPANO_PROJECTION_TYPE_DEFAULT fields["projectionType"] = fields["projectionType"] ?: GoogleXMP.GPANO_PROJECTION_TYPE_DEFAULT
result.success(fields) result.success(fields)
} }
} }
private fun getIptc(call: MethodCall, result: MethodChannel.Result) { private fun getIptc(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getIptc-args", "missing arguments", null) result.error("getIptc-args", "missing arguments", null)
return return
@ -1014,11 +1192,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(null) result.success(null)
} }
// return XMP components // returns XMP components
// return an empty list if there is no XMP // returns an empty list if there is no XMP
private fun getXmp(call: MethodCall, result: MethodChannel.Result) { private fun getXmp(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
result.error("getXmp-args", "missing arguments", null) result.error("getXmp-args", "missing arguments", null)
@ -1041,7 +1219,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) { if (canReadWithMetadataExtractor(mimeType) && !isLargeMp4(mimeType, sizeBytes)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach { metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach {
processXmp(it, allowMultiple = true) processXmp(it, allowMultiple = true)
} }
@ -1090,7 +1268,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getContentPropValue(call: MethodCall, result: MethodChannel.Result) { private fun getContentPropValue(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val prop = call.argument<String>("prop") val prop = call.argument<String>("prop")
if (mimeType == null || uri == null || prop == null) { if (mimeType == null || uri == null || prop == null) {
result.error("getContentPropValue-args", "missing arguments", null) result.error("getContentPropValue-args", "missing arguments", null)
@ -1107,7 +1285,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getDate(call: MethodCall, result: MethodChannel.Result) { private fun getDate(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
val field = call.argument<String>("field") val field = call.argument<String>("field")
if (mimeType == null || uri == null || field == null) { if (mimeType == null || uri == null || field == null) {
@ -1119,7 +1297,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
val tag = when (field) { val tag = when (field) {
ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME ExifInterface.TAG_DATETIME -> ExifIFD0Directory.TAG_DATETIME
ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED ExifInterface.TAG_DATETIME_DIGITIZED -> ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED
@ -1174,55 +1352,61 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(dateMillis) result.success(dateMillis)
} }
// return description from these fields (by precedence): private fun getFields(call: MethodCall, result: MethodChannel.Result) {
// - XMP / dc:description
// - IPTC / caption-abstract
// - Exif / UserComment
// - Exif / ImageDescription
private fun getDescription(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong() val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) { val fields = call.argument<List<String>>("fields")
result.error("getDescription-args", "missing arguments", null) if (mimeType == null || uri == null || fields == null) {
result.error("getFields-args", "missing arguments", null)
return return
} }
var description: String? = null val metadataMap = HashMap<String, Any?>()
val hashFields = fields.filter { it.startsWith(HASH_FIELD_PREFIX) }.toSet()
metadataMap.putAll(getHashFields(uri, mimeType, sizeBytes, hashFields))
val exifFields = fields.filterNot { hashFields.contains(it) }.toSet()
metadataMap.putAll(getExifFields(uri, mimeType, sizeBytes, exifFields))
result.success(metadataMap)
}
private fun getHashFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
fields.forEach { field ->
val function = field.substringAfter(HASH_FIELD_PREFIX).lowercase(Locale.ROOT)
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
metadataMap[field] = HashUtils.getHash(input, function)
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get hash for mimeType=$mimeType uri=$uri function=$function", e)
}
}
return metadataMap
}
private fun getExifFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
if (fields.isEmpty() || isVideo(mimeType)) {
return metadataMap
}
var foundExif = false
if (canReadWithMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
for (dir in metadata.getDirectoriesOfType(ExifDirectoryBase::class.java)) {
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) { foundExif = true
val xmpMeta = dir.xmpMeta val allTags = ExifInterfaceHelper.allTags
try { fields.forEach { tag ->
if (xmpMeta.doesPropExist(XMP.DC_DESCRIPTION_PROP_NAME)) { allTags[tag]?.let { mapper ->
xmpMeta.getSafeLocalizedText(XMP.DC_DESCRIPTION_PROP_NAME, acceptBlank = false) { description = it } val tagType = mapper.type
dir.getDescription(tagType)?.let { value -> metadataMap[tag] = value }
} }
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(IptcDirectory::class.java)) {
dir.getSafeString(IptcDirectory.TAG_CAPTION, acceptBlank = false) { description = it }
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifSubIFDDirectory::class.java)) {
// user comment field specifies encoding, unlike other string fields
if (dir.containsTag(ExifSubIFDDirectory.TAG_USER_COMMENT)) {
val string = dir.getDescription(ExifSubIFDDirectory.TAG_USER_COMMENT)
if (string.isNotBlank()) {
description = string
}
}
}
}
if (description == null) {
for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
dir.getSafeString(ExifIFD0Directory.TAG_IMAGE_DESCRIPTION, acceptBlank = false) { description = it }
} }
} }
} }
@ -1235,7 +1419,28 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
} }
result.success(description) if (!foundExif && canReadWithExifInterface(mimeType)) {
// fallback to read EXIF via ExifInterface
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
fields.forEach { tag ->
if (exif.hasAttribute(tag)) {
val value = exif.getAttribute(tag)
if (value != null) {
metadataMap[tag] = value
}
}
}
}
} catch (e: Exception) {
// ExifInterface initialization can fail with a RuntimeException
// caused by an internal MediaMetadataRetriever failure
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for mimeType=$mimeType uri=$uri", e)
}
}
return metadataMap
} }
companion object { companion object {
@ -1297,10 +1502,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private const val MASK_IS_360 = 1 shl 3 private const val MASK_IS_360 = 1 shl 3
private const val MASK_IS_MULTIPAGE = 1 shl 4 private const val MASK_IS_MULTIPAGE = 1 shl 4
private const val MASK_IS_MOTION_PHOTO = 1 shl 5 private const val MASK_IS_MOTION_PHOTO = 1 shl 5
private const val MASK_IS_HDR = 1 shl 6 // for images: embedded HDR gainmap, for videos: HDR color transfer
private const val XMP_SUBJECTS_SEPARATOR = ";" private const val XMP_SUBJECTS_SEPARATOR = ";"
// overlay metadata // overlay metadata
private const val KEY_APERTURE = "aperture" private const val KEY_APERTURE = "aperture"
private const val KEY_DESCRIPTION = "description"
private const val KEY_EXPOSURE_TIME = "exposureTime" private const val KEY_EXPOSURE_TIME = "exposureTime"
private const val KEY_FOCAL_LENGTH = "focalLength" private const val KEY_FOCAL_LENGTH = "focalLength"
private const val KEY_ISO = "iso" private const val KEY_ISO = "iso"
@ -1308,6 +1515,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// additional media key // additional media key
private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture" private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture"
private const val HASH_FIELD_PREFIX = "hash"
private const val VALUE_SKIPPED_DATA = "[skipped]" private const val VALUE_SKIPPED_DATA = "[skipped]"
} }
} }

View file

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

View file

@ -29,6 +29,9 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
when (call.method) { when (call.method) {
"getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) } "getDataUsage" -> ioScope.launch { safe(call, result, ::getDataUsage) }
"getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) } "getStorageVolumes" -> ioScope.launch { safe(call, result, ::getStorageVolumes) }
"getCacheDirectory" -> ioScope.launch { safe(call, result, ::getCacheDirectory) }
"getUntrackedTrashPaths" -> ioScope.launch { safe(call, result, ::getUntrackedTrashPaths) }
"getUntrackedVaultPaths" -> ioScope.launch { safe(call, result, ::getUntrackedVaultPaths) }
"getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) } "getVaultRoot" -> ioScope.launch { safe(call, result, ::getVaultRoot) }
"getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) } "getFreeSpace" -> ioScope.launch { safe(call, result, ::getFreeSpace) }
"getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) } "getGrantedDirectories" -> ioScope.launch { safe(call, result, ::getGrantedDirectories) }
@ -37,6 +40,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
"revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess) "revokeDirectoryAccess" -> safe(call, result, ::revokeDirectoryAccess)
"deleteEmptyDirectories" -> ioScope.launch { safe(call, result, ::deleteEmptyDirectories) } "deleteEmptyDirectories" -> ioScope.launch { safe(call, result, ::deleteEmptyDirectories) }
"deleteTempDirectory" -> ioScope.launch { safe(call, result, ::deleteTempDirectory) } "deleteTempDirectory" -> ioScope.launch { safe(call, result, ::deleteTempDirectory) }
"deleteExternalCache" -> ioScope.launch { safe(call, result, ::deleteExternalCache) }
"canRequestMediaFileBulkAccess" -> safe(call, result, ::canRequestMediaFileBulkAccess) "canRequestMediaFileBulkAccess" -> safe(call, result, ::canRequestMediaFileBulkAccess)
"canInsertMedia" -> safe(call, result, ::canInsertMedia) "canInsertMedia" -> safe(call, result, ::canInsertMedia)
else -> result.notImplemented() else -> result.notImplemented()
@ -45,20 +49,19 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
private fun getDataUsage(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getDataUsage(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
var internalCache = getFolderSize(context.cacheDir) var internalCache = getFolderSize(context.cacheDir)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { internalCache += getFolderSize(context.codeCacheDir)
internalCache += getFolderSize(context.codeCacheDir)
}
val externalCache = context.externalCacheDirs.map(::getFolderSize).sum() val externalCache = context.externalCacheDirs.map(::getFolderSize).sum()
val externalFilesDirs = context.getExternalFilesDirs(null)
val dataDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) context.dataDir else File(context.applicationInfo.dataDir) val dataDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) context.dataDir else File(context.applicationInfo.dataDir)
val database = getFolderSize(File(dataDir, "databases")) val database = getFolderSize(File(dataDir, "databases"))
val flutter = getFolderSize(File(PathUtils.getDataDirectory(context))) val flutter = getFolderSize(File(PathUtils.getDataDirectory(context)))
val vaults = getFolderSize(File(StorageUtils.getVaultRoot(context))) val vaults = getFolderSize(File(StorageUtils.getVaultRoot(context)))
val trash = context.getExternalFilesDirs(null).mapNotNull { StorageUtils.trashDirFor(context, it.path) }.map(::getFolderSize).sum() val trash = externalFilesDirs.mapNotNull { StorageUtils.trashDirFor(context, it.path) }.map(::getFolderSize).sum()
val internalData = getFolderSize(dataDir) - internalCache val internalData = getFolderSize(dataDir) - internalCache
val externalData = context.getExternalFilesDirs(null).map(::getFolderSize).sum() val externalData = externalFilesDirs.map(::getFolderSize).sum()
val miscData = internalData + externalData - (database + flutter + vaults + trash) val miscData = internalData + externalData - (database + flutter + vaults + trash)
result.success( result.success(
@ -103,12 +106,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
val volumeFile = File(volumePath) val volumeFile = File(volumePath)
try { try {
val isPrimary = volumePath == primaryVolumePath val isPrimary = volumePath == primaryVolumePath
val isRemovable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val isRemovable = Environment.isExternalStorageRemovable(volumeFile)
Environment.isExternalStorageRemovable(volumeFile)
} else {
// random guess
!isPrimary
}
volumes.add( volumes.add(
hashMapOf( hashMapOf(
"path" to volumePath, "path" to volumePath,
@ -125,6 +123,47 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
result.success(volumes) result.success(volumes)
} }
private fun getCacheDirectory(call: MethodCall, result: MethodChannel.Result) {
val external = call.argument<Boolean>("external")
if (external == null) {
result.error("getCacheDirectory-args", "missing arguments", null)
return
}
val dir = (if (external) context.externalCacheDir else context.cacheDir)
result.success(dir!!.path)
}
private fun getUntrackedTrashPaths(call: MethodCall, result: MethodChannel.Result) {
val knownPaths = call.argument<List<String>>("knownPaths")
if (knownPaths == null) {
result.error("getUntrackedTrashPaths-args", "missing arguments", null)
return
}
val trashDirs = context.getExternalFilesDirs(null).filterNotNull().mapNotNull { StorageUtils.trashDirFor(context, it.path) }
val trashItemPaths = trashDirs.flatMap { dir -> dir.listFiles()?.filterNotNull()?.mapNotNull { file -> file.path } ?: listOf() }
val untrackedPaths = trashItemPaths.filterNot(knownPaths::contains).toList()
result.success(untrackedPaths)
}
private fun getUntrackedVaultPaths(call: MethodCall, result: MethodChannel.Result) {
val vault = call.argument<String>("vault")
val knownPaths = call.argument<List<String>>("knownPaths")
if (vault == null || knownPaths == null) {
result.error("getUntrackedVaultPaths-args", "missing arguments", null)
return
}
val vaultDir = File(StorageUtils.getVaultRoot(context), vault)
val vaultItemPaths = vaultDir.listFiles()?.mapNotNull { file -> file?.path } ?: listOf()
val untrackedPaths = vaultItemPaths.filterNot(knownPaths::contains).toList()
result.success(untrackedPaths)
}
private fun getVaultRoot(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun getVaultRoot(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(StorageUtils.getVaultRoot(context)) result.success(StorageUtils.getVaultRoot(context))
} }
@ -171,11 +210,6 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
return return
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
result.error("revokeDirectoryAccess-unsupported", "volume access is not allowed before Android Lollipop", null)
return
}
val success = PermissionManager.revokeDirectoryAccess(context, path) val success = PermissionManager.revokeDirectoryAccess(context, path)
result.success(success) result.success(success)
} }
@ -205,6 +239,11 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
result.success(StorageUtils.deleteTempDirectory(context)) result.success(StorageUtils.deleteTempDirectory(context))
} }
private fun deleteExternalCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
context.externalCacheDirs.filter { it.exists() }.forEach { it.deleteRecursively() }
result.success(true)
}
private fun canRequestMediaFileBulkAccess(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) { private fun canRequestMediaFileBulkAccess(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
} }

View file

@ -4,33 +4,37 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder import android.graphics.BitmapRegionDecoder
import android.graphics.ColorSpace
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Build
import android.util.Log
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import deckers.thibault.aves.decoder.AvesAppGlideModule
import com.bumptech.glide.load.engine.DiskCacheStrategy import deckers.thibault.aves.decoder.MultiPageImage
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.MultiTrackImage
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MathUtils
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
// As of Android 14 (API 34), `BitmapRegionDecoder` documentation states
// that "only the JPEG, PNG, WebP and HEIF formats are supported"
// but in practice it successfully decodes some others.
class RegionFetcher internal constructor( class RegionFetcher internal constructor(
private val context: Context, private val context: Context,
) { ) {
private var lastDecoderRef: LastDecoderRef? = null // returns decoded bytes in ARGB_8888, with trailer bytes:
// - width (int32)
private val pageTempUris = HashMap<Pair<Uri, Int>, Uri>() // - height (int32)
fun fetch(
private val multiTrackGlideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
suspend fun fetch(
uri: Uri, uri: Uri,
mimeType: String, mimeType: String,
pageId: Int?, pageId: Int?,
@ -38,78 +42,117 @@ class RegionFetcher internal constructor(
regionRect: Rect, regionRect: Rect,
imageWidth: Int, imageWidth: Int,
imageHeight: Int, imageHeight: Int,
requestKey: Pair<Uri, Int?> = Pair(uri, pageId),
result: MethodChannel.Result, result: MethodChannel.Result,
) { ) {
if (MimeTypes.isHeic(mimeType) && pageId != null) { if (pageId != null && MultiPageImage.isSupported(mimeType)) {
val id = Pair(uri, pageId) // use JPEG export for requested page
fetch( fetch(
uri = pageTempUris.getOrPut(id) { createJpegForPage(uri, pageId) }, uri = exportUris.getOrPut(requestKey) { createTemporaryJpegExport(uri, mimeType, pageId) },
mimeType = MimeTypes.JPEG, mimeType = MimeTypes.JPEG,
pageId = null, pageId = null,
sampleSize = sampleSize, sampleSize = sampleSize,
regionRect = regionRect, regionRect = regionRect,
imageWidth = imageWidth, imageWidth = imageWidth,
imageHeight = imageHeight, imageHeight = imageHeight,
requestKey = requestKey,
result = result, result = result,
) )
return return
} }
val options = BitmapFactory.Options().apply {
inSampleSize = sampleSize
}
var currentDecoderRef = lastDecoderRef
if (currentDecoderRef != null && currentDecoderRef.uri != uri) {
currentDecoderRef = null
}
try { try {
if (currentDecoderRef == null) { val decoder = getOrCreateDecoder(context, uri, requestKey)
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input -> if (decoder == null) {
BitmapRegionDecoderCompat.newInstance(input) result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
} return
if (newDecoder == null) {
result.error("getRegion-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
return
}
currentDecoderRef = LastDecoderRef(uri, newDecoder)
} }
val decoder = currentDecoderRef.decoder
lastDecoderRef = currentDecoderRef
// with raw images, the known image size may not match the decoded image size // with raw images, the known image size may not match the decoded image size
// so we scale the requested region accordingly // so we scale the requested region accordingly
val effectiveRect = if (imageWidth != decoder.width || imageHeight != decoder.height) { var effectiveRect = regionRect
var effectiveSampleSize = sampleSize
if (imageWidth != decoder.width || imageHeight != decoder.height) {
val xf = decoder.width.toDouble() / imageWidth val xf = decoder.width.toDouble() / imageWidth
val yf = decoder.height.toDouble() / imageHeight val yf = decoder.height.toDouble() / imageHeight
Rect( effectiveRect = Rect(
(regionRect.left * xf).roundToInt(), (regionRect.left * xf).roundToInt(),
(regionRect.top * yf).roundToInt(), (regionRect.top * yf).roundToInt(),
(regionRect.right * xf).roundToInt(), (regionRect.right * xf).roundToInt(),
(regionRect.bottom * yf).roundToInt(), (regionRect.bottom * yf).roundToInt(),
) )
} else { val factor = MathUtils.highestPowerOf2((1 / max(xf, yf)).roundToInt())
regionRect if (factor > 1) {
effectiveSampleSize = max(1, effectiveSampleSize / factor)
}
} }
val bitmap = decoder.decodeRegion(effectiveRect, options) val options = BitmapFactory.Options().apply {
if (bitmap != null) { inSampleSize = effectiveSampleSize
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true)) // Specifying preferred config and color space avoids the need for conversion afterwards,
// but may prevent decoding (e.g. from RGBA_1010102 to ARGB_8888 on some devices).
inPreferredConfig = PREFERRED_CONFIG
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
}
}
val pixelCount = effectiveRect.width() * effectiveRect.height() / effectiveSampleSize
val targetBitmapSizeBytes = BitmapUtils.getExpectedImageSize(pixelCount.toLong(), options.inPreferredConfig)
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
// decoding a region that large would yield an OOM when creating the bitmap
result.error("fetch-large-region", "Region too large for uri=$uri regionRect=$regionRect", null)
return
}
var bitmap = decoder.decodeRegion(effectiveRect, options)
if (bitmap == null) {
// retry without specifying config or color space,
// falling back to custom byte conversion afterwards
options.inPreferredConfig = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && options.inPreferredColorSpace != null) {
options.inPreferredColorSpace = null
}
bitmap = decoder.decodeRegion(effectiveRect, options)
}
val bytes = BitmapUtils.getRawBytes(bitmap, recycle = true)
if (bytes != null) {
result.success(bytes)
} else { } else {
result.error("getRegion-null", "failed to decode region for uri=$uri regionRect=$regionRect", null) result.error("fetch-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
} }
} catch (e: Exception) { } catch (e: Exception) {
result.error("getRegion-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message) if (mimeType != MimeTypes.JPEG) {
// retry with JPEG export on failure,
// as some formats are not fully supported by `BitmapRegionDecoder`
fetch(
uri = exportUris.getOrPut(requestKey) { createTemporaryJpegExport(uri, mimeType, pageId) },
mimeType = MimeTypes.JPEG,
pageId = null,
sampleSize = sampleSize,
regionRect = regionRect,
imageWidth = imageWidth,
imageHeight = imageHeight,
requestKey = requestKey,
result = result,
)
return
}
result.error("fetch-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
} }
} }
private fun createJpegForPage(sourceUri: Uri, pageId: Int): Uri { private fun createTemporaryJpegExport(uri: Uri, mimeType: String, pageId: Int?): Uri {
Log.d(LOG_TAG, "create JPEG export for uri=$uri mimeType=$mimeType pageId=$pageId")
val target = Glide.with(context) val target = Glide.with(context)
.asBitmap() .asBitmap()
.apply(multiTrackGlideOptions) .apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(MultiTrackImage(context, sourceUri, pageId)) .load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId))
.submit() .submit()
try { try {
val bitmap = target.get() val bitmap = target.get()
val tempFile = StorageUtils.createTempFile(context).apply { val tempFile = StorageUtils.createTempFile(context).apply {
@ -123,8 +166,40 @@ class RegionFetcher internal constructor(
} }
} }
private data class LastDecoderRef( private data class DecoderRef(
val uri: Uri, val requestKey: Pair<Uri, Int?>,
val decoder: BitmapRegionDecoder, val decoder: BitmapRegionDecoder,
) )
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
}
}
}
} }

View file

@ -6,25 +6,28 @@ import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import android.net.Uri import android.net.Uri
import androidx.core.graphics.createBitmap
import com.caverock.androidsvg.PreserveAspectRatio import com.caverock.androidsvg.PreserveAspectRatio
import com.caverock.androidsvg.RenderOptions import com.caverock.androidsvg.RenderOptions
import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException import com.caverock.androidsvg.SVGParseException
import deckers.thibault.aves.metadata.SVGParserBufferedInputStream
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.ceil import kotlin.math.ceil
class SvgRegionFetcher internal constructor( class SvgRegionFetcher internal constructor(
private val context: Context, private val context: Context,
) { ) {
private var lastSvgRef: LastSvgRef? = null fun fetch(
suspend fun fetch(
uri: Uri, uri: Uri,
sizeBytes: Long?, sizeBytes: Long?,
scale: Int,
regionRect: Rect, regionRect: Rect,
imageWidth: Int, imageWidth: Int,
imageHeight: Int, imageHeight: Int,
@ -32,43 +35,23 @@ class SvgRegionFetcher internal constructor(
) { ) {
if (!MemoryUtils.canAllocate(sizeBytes)) { if (!MemoryUtils.canAllocate(sizeBytes)) {
// opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser` // opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser`
result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, for uri=$uri regionRect=$regionRect", null) result.error("fetch-read-large-file", "SVG too large at $sizeBytes bytes, for uri=$uri regionRect=$regionRect", null)
return return
} }
var currentSvgRef = lastSvgRef
if (currentSvgRef != null && currentSvgRef.uri != uri) {
currentSvgRef = null
}
try { try {
if (currentSvgRef == null) { val svg = getOrCreateDecoder(context, uri)
val newSvg = StorageUtils.openInputStream(context, uri)?.use { input -> if (svg == null) {
try { result.error("fetch-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
SVG.getFromInputStream(input) return
} 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 = currentSvgRef.svg
lastSvgRef = currentSvgRef
// we scale the requested region accordingly to the viewbox size // we scale the requested region accordingly to the viewbox size
val viewBox = svg.documentViewBox val viewBox = svg.documentViewBox
val svgWidth = viewBox.width() val svgWidth = viewBox.width()
val svgHeight = viewBox.height() val svgHeight = viewBox.height()
val xf = imageWidth / ceil(svgWidth) val xf = imageWidth / scale / ceil(svgWidth)
val yf = imageHeight / ceil(svgHeight) val yf = imageHeight / scale / ceil(svgHeight)
// some SVG paths do not respect the rendering viewbox and do not reach its edges // some SVG paths do not respect the rendering viewbox and do not reach its edges
// so we render to a slightly larger bitmap, using a slightly larger viewbox, // so we render to a slightly larger bitmap, using a slightly larger viewbox,
// and crop that bitmap to the target region size // and crop that bitmap to the target region size
@ -80,6 +63,7 @@ class SvgRegionFetcher internal constructor(
(regionRect.right + bleedX) / xf, (regionRect.right + bleedX) / xf,
(regionRect.bottom + bleedY) / yf, (regionRect.bottom + bleedY) / yf,
) )
effectiveRect.offset(viewBox.left, viewBox.top)
val renderOptions = RenderOptions() val renderOptions = RenderOptions()
renderOptions.viewBox(effectiveRect.left, effectiveRect.top, effectiveRect.width(), effectiveRect.height()) renderOptions.viewBox(effectiveRect.left, effectiveRect.top, effectiveRect.width(), effectiveRect.height())
@ -87,23 +71,65 @@ class SvgRegionFetcher internal constructor(
val targetBitmapWidth = regionRect.width() val targetBitmapWidth = regionRect.width()
val targetBitmapHeight = regionRect.height() val targetBitmapHeight = regionRect.height()
var bitmap = Bitmap.createBitmap( val canvasWidth = targetBitmapWidth + bleedX * 2
targetBitmapWidth + bleedX * 2, val canvasHeight = targetBitmapHeight + bleedY * 2
targetBitmapHeight + bleedY * 2,
Bitmap.Config.ARGB_8888 val config = PREFERRED_CONFIG
) val pixelCount = canvasWidth * canvasHeight
val targetBitmapSizeBytes = BitmapUtils.getExpectedImageSize(pixelCount.toLong(), config)
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
// decoding a region that large would yield an OOM when creating the bitmap
result.error("fetch-read-large-region", "SVG region too large for uri=$uri regionRect=$regionRect", null)
return
}
var bitmap = createBitmap(canvasWidth, canvasHeight, config)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
svg.renderToCanvas(canvas, renderOptions) svg.renderToCanvas(canvas, renderOptions)
bitmap = Bitmap.createBitmap(bitmap, bleedX, bleedY, targetBitmapWidth, targetBitmapHeight) bitmap = Bitmap.createBitmap(bitmap, bleedX, bleedY, targetBitmapWidth, targetBitmapHeight)
result.success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) 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) { } catch (e: Exception) {
result.error("fetch-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message) 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 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
}
}
}
} }

View file

@ -5,34 +5,37 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log
import android.util.Size import android.util.Size
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.graphics.scale
import androidx.core.net.toUri
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.decoder.MultiTrackImage import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.SvgImage import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.TiffImage import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.SVG import deckers.thibault.aves.utils.MimeTypes.SVG
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import kotlin.math.min
import kotlin.math.roundToInt
class ThumbnailFetcher internal constructor( class ThumbnailFetcher internal constructor(
private val context: Context, private val context: Context,
uri: String, uri: String,
private val mimeType: String, private val mimeType: String,
private val dateModifiedSecs: Long, private val dateModifiedMillis: Long,
private val rotationDegrees: Int, private val rotationDegrees: Int,
private val isFlipped: Boolean, private val isFlipped: Boolean,
width: Int?, width: Int?,
@ -42,15 +45,15 @@ class ThumbnailFetcher internal constructor(
private val quality: Int, private val quality: Int,
private val result: MethodChannel.Result, private val result: MethodChannel.Result,
) { ) {
private val uri: Uri = Uri.parse(uri) private val uri: Uri = uri.toUri()
private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize private val width: Int = if (width?.takeIf { it > 0 } != null) width else defaultSize
private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize private val height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
private val svgFetch = mimeType == SVG private val svgFetch = mimeType == SVG
private val tiffFetch = mimeType == MimeTypes.TIFF private val tiffFetch = mimeType == MimeTypes.TIFF
private val multiTrackFetch = isHeic(mimeType) && pageId != null private val multiPageFetch = pageId != null && MultiPageImage.isSupported(mimeType)
private val customFetch = svgFetch || tiffFetch || multiTrackFetch private val customFetch = svgFetch || tiffFetch || multiPageFetch
suspend fun fetch() { fun fetch() {
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
var exception: Exception? = null var exception: Exception? = null
@ -80,7 +83,33 @@ class ThumbnailFetcher internal constructor(
} }
if (bitmap != null) { if (bitmap != null) {
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false, quality = quality)) 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)
if (bytes != null) {
result.success(bytes)
} else { } else {
var errorDetails: String? = exception?.message var errorDetails: String? = exception?.message
if (errorDetails?.isNotEmpty() == true) { if (errorDetails?.isNotEmpty() == true) {
@ -121,33 +150,21 @@ class ThumbnailFetcher internal constructor(
// add signature to ignore cache for images which got modified but kept the same URI // add signature to ignore cache for images which got modified but kept the same URI
var options = RequestOptions() var options = RequestOptions()
.format(if (quality == 100) DecodeFormat.PREFER_ARGB_8888 else DecodeFormat.PREFER_RGB_565) .format(if (quality == 100) DecodeFormat.PREFER_ARGB_8888 else DecodeFormat.PREFER_RGB_565)
.signature(ObjectKey("$dateModifiedSecs-$rotationDegrees-$isFlipped-$width-$pageId")) .signature(ObjectKey("$dateModifiedMillis-$rotationDegrees-$isFlipped-$width-$pageId"))
.override(width, height) .override(width, height)
if (isVideo(mimeType)) {
val target = if (isVideo(mimeType)) {
options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE) options = options.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
Glide.with(context)
.asBitmap()
.apply(options)
.load(VideoThumbnail(context, uri))
.submit(width, height)
} else {
val model: Any = when {
svgFetch -> SvgImage(context, uri)
tiffFetch -> TiffImage(context, uri, pageId)
multiTrackFetch -> MultiTrackImage(context, uri, pageId)
else -> StorageUtils.getGlideSafeUri(context, uri, mimeType)
}
Glide.with(context)
.asBitmap()
.apply(options)
.load(model)
.submit(width, height)
} }
val target = Glide.with(context)
.asBitmap()
.apply(options)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId))
.submit(width, height)
return try { return try {
var bitmap = target.get() var bitmap = target.get()
if (needRotationAfterGlide(mimeType)) { if (needRotationAfterGlide(mimeType, pageId)) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
} }
bitmap bitmap
@ -155,4 +172,9 @@ class ThumbnailFetcher internal constructor(
Glide.with(context).clear(target) Glide.with(context).clear(target)
} }
} }
companion object {
private val LOG_TAG = LogUtils.createTag<ThumbnailFetcher>()
private const val BITMAP_SIZE_DANGER_THRESHOLD = 20 * (1 shl 20) // MB
}
} }

View file

@ -1,9 +1,10 @@
package deckers.thibault.aves.channel.calls.fetchers package deckers.thibault.aves.channel.calls.fetchers
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect import android.graphics.Rect
import android.net.Uri import android.net.Uri
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import org.beyka.tiffbitmapfactory.DecodeArea import org.beyka.tiffbitmapfactory.DecodeArea
import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.beyka.tiffbitmapfactory.TiffBitmapFactory
@ -11,7 +12,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
class TiffRegionFetcher internal constructor( class TiffRegionFetcher internal constructor(
private val context: Context, private val context: Context,
) { ) {
suspend fun fetch( fun fetch(
uri: Uri, uri: Uri,
page: Int, page: Int,
sampleSize: Int, sampleSize: Int,
@ -31,9 +32,10 @@ class TiffRegionFetcher internal constructor(
inSampleSize = sampleSize inSampleSize = sampleSize
inDecodeArea = DecodeArea(regionRect.left, regionRect.top, regionRect.width(), regionRect.height()) inDecodeArea = DecodeArea(regionRect.left, regionRect.top, regionRect.width(), regionRect.height())
} }
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) val bitmap: Bitmap? = TiffBitmapFactory.decodeFileDescriptor(fd, options)
if (bitmap != null) { val bytes = BitmapUtils.getRawBytes(bitmap, recycle = true)
result.success(bitmap.getBytes(canHaveAlpha = true, recycle = true)) if (bytes != null) {
result.success(bytes)
} else { } else {
result.error("getRegion-tiff-null", "failed to decode region for uri=$uri page=$page regionRect=$regionRect", null) result.error("getRegion-tiff-null", "failed to decode region for uri=$uri page=$page regionRect=$regionRect", null)
} }

View file

@ -1,6 +1,7 @@
package deckers.thibault.aves.channel.calls.window package deckers.thibault.aves.channel.calls.window
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo
import android.os.Build import android.os.Build
import android.view.WindowManager import android.view.WindowManager
import deckers.thibault.aves.utils.getDisplayCompat import deckers.thibault.aves.utils.getDisplayCompat
@ -75,4 +76,32 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
) )
) )
} }
override fun supportsWideGamut(call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.resources.configuration.isScreenWideColorGamut)
}
override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.resources.configuration.isScreenHdr)
}
override fun setColorMode(call: MethodCall, result: MethodChannel.Result) {
val wideColorGamut = call.argument<Boolean>("wideColorGamut")
val hdr = call.argument<Boolean>("hdr")
if (wideColorGamut == null || hdr == null) {
result.error("setColorMode-args", "missing arguments", null)
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.window.colorMode = if (hdr) {
ActivityInfo.COLOR_MODE_HDR
} else if (wideColorGamut) {
ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
} else {
ActivityInfo.COLOR_MODE_DEFAULT
}
}
result.success(null)
}
} }

View file

@ -28,4 +28,16 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) { override fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) {
result.success(HashMap<String, Any>()) result.success(HashMap<String, Any>())
} }
override fun supportsWideGamut(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) {
result.success(false)
}
override fun setColorMode(call: MethodCall, result: MethodChannel.Result) {
result.success(null)
}
} }

View file

@ -18,6 +18,9 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation) "requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware) "isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
"getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets) "getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
"supportsWideGamut" -> Coresult.safe(call, result, ::supportsWideGamut)
"supportsHdr" -> Coresult.safe(call, result, ::supportsHdr)
"setColorMode" -> Coresult.safe(call, result, ::setColorMode)
else -> result.notImplemented() else -> result.notImplemented()
} }
} }
@ -44,6 +47,12 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result) abstract fun getCutoutInsets(call: MethodCall, result: MethodChannel.Result)
abstract fun supportsWideGamut(call: MethodCall, result: MethodChannel.Result)
abstract fun supportsHdr(call: MethodCall, result: MethodChannel.Result)
abstract fun setColorMode(call: MethodCall, result: MethodChannel.Result)
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>() private val LOG_TAG = LogUtils.createTag<WindowHandler>()
const val CHANNEL = "deckers.thibault/aves/window" const val CHANNEL = "deckers.thibault/aves/window"

View file

@ -7,8 +7,10 @@ import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.net.toUri
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.PendingStorageAccessResultHandler import deckers.thibault.aves.PendingStorageAccessResultHandler
import deckers.thibault.aves.channel.calls.AppAdapterHandler
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.PermissionManager import deckers.thibault.aves.utils.PermissionManager
@ -47,6 +49,8 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() } "requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
"createFile" -> ioScope.launch { createFile() } "createFile" -> ioScope.launch { createFile() }
"openFile" -> ioScope.launch { openFile() } "openFile" -> ioScope.launch { openFile() }
"copyFile" -> ioScope.launch { copyFile() }
"edit" -> edit()
"pickCollectionFilters" -> pickCollectionFilters() "pickCollectionFilters" -> pickCollectionFilters()
else -> endOfStream() else -> endOfStream()
} }
@ -59,11 +63,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
return return
} }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
error("requestDirectoryAccess-unsupported", "directory access is not allowed before Android Lollipop", null)
return
}
PermissionManager.requestDirectoryAccess(activity, ensureTrailingSeparator(path), { PermissionManager.requestDirectoryAccess(activity, ensureTrailingSeparator(path), {
success(true) success(true)
endOfStream() endOfStream()
@ -74,7 +73,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
} }
private fun requestMediaFileAccess() { private fun requestMediaFileAccess() {
val uris = (args["uris"] as List<*>?)?.mapNotNull { if (it is String) Uri.parse(it) else null } val uris = (args["uris"] as List<*>?)?.mapNotNull { if (it is String) it.toUri() else null }
val mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null } val mimeTypes = (args["mimeTypes"] as List<*>?)?.mapNotNull { if (it is String) it else null }
if (uris.isNullOrEmpty() || mimeTypes == null || mimeTypes.size != uris.size) { if (uris.isNullOrEmpty() || mimeTypes == null || mimeTypes.size != uris.size) {
error("requestMediaFileAccess-args", "missing arguments", null) error("requestMediaFileAccess-args", "missing arguments", null)
@ -100,10 +99,13 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
endOfStream() endOfStream()
} }
private suspend fun safeStartActivityForResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) { private suspend fun safeStartActivityForStorageAccessResult(intent: Intent, requestCode: Int, onGranted: (uri: Uri) -> Unit, onDenied: () -> Unit) {
if (intent.resolveActivity(activity.packageManager) != null) { if (intent.resolveActivity(activity.packageManager) != null) {
MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied) MainActivity.pendingStorageAccessResultHandlers[requestCode] = PendingStorageAccessResultHandler(null, onGranted, onDenied)
activity.startActivityForResult(intent, requestCode) if (!safeStartActivityForResult(intent, requestCode)) {
MainActivity.notifyError("failed to start activity for intent=$intent extras=${intent.extras}")
onDenied()
}
} else { } else {
MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}") MainActivity.notifyError("failed to resolve activity for intent=$intent extras=${intent.extras}")
onDenied() onDenied()
@ -144,7 +146,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
type = mimeType type = mimeType
putExtra(Intent.EXTRA_TITLE, name) putExtra(Intent.EXTRA_TITLE, name)
} }
safeStartActivityForResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied) safeStartActivityForStorageAccessResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
} }
private suspend fun openFile() { private suspend fun openFile() {
@ -177,7 +179,76 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
setTypeAndNormalize(mimeType ?: MimeTypes.ANY) setTypeAndNormalize(mimeType ?: MimeTypes.ANY)
} }
safeStartActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied) 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
if (uri == null) {
error("edit-args", "missing arguments", null)
return
}
val intent = Intent(Intent.ACTION_EDIT)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
.setDataAndType(AppAdapterHandler.getShareableUri(activity, uri.toUri()), mimeType)
if (intent.resolveActivity(activity.packageManager) == null) {
error("edit-resolve", "cannot resolve activity for this intent for uri=$uri mimeType=$mimeType", null)
return
}
MainActivity.pendingEditIntentHandler = { fields ->
success(fields)
endOfStream()
}
if (!safeStartActivityForResult(intent, MainActivity.EDIT_REQUEST)) {
error("edit-start", "cannot start activity for this intent for uri=$uri mimeType=$mimeType", null)
}
} }
private fun pickCollectionFilters() { private fun pickCollectionFilters() {
@ -192,6 +263,24 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST) activity.startActivityForResult(intent, MainActivity.PICK_COLLECTION_FILTERS_REQUEST)
} }
private fun safeStartActivityForResult(intent: Intent, requestCode: Int): Boolean {
return try {
activity.startActivityForResult(intent, requestCode)
true
} catch (e: SecurityException) {
if (intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION != 0) {
// in some environments, providing the write flag yields a `SecurityException`:
// "UID XXXX does not have permission to content://XXXX"
// so we retry without it
Log.i(LOG_TAG, "retry intent=$intent without FLAG_GRANT_WRITE_URI_PERMISSION")
intent.flags = intent.flags and Intent.FLAG_GRANT_WRITE_URI_PERMISSION.inv()
safeStartActivityForResult(intent, requestCode)
} else {
false
}
}
}
override fun onCancel(arguments: Any?) { override fun onCancel(arguments: Any?) {
Log.i(LOG_TAG, "onCancel arguments=$arguments") Log.i(LOG_TAG, "onCancel arguments=$arguments")
} }
@ -230,6 +319,6 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ActivityResultStreamHandler>() private val LOG_TAG = LogUtils.createTag<ActivityResultStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/activity_result_stream" const val CHANNEL = "deckers.thibault/aves/activity_result_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB private const val BUFFER_SIZE = 1 shl 18 // 256kB
} }
} }

View file

@ -5,20 +5,15 @@ import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.net.toUri
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat import deckers.thibault.aves.decoder.AvesAppGlideModule
import com.bumptech.glide.load.engine.DiskCacheStrategy import deckers.thibault.aves.utils.BitmapUtils
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.MultiTrackImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
@ -29,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
class ImageByteStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler { class ImageByteStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler {
@ -85,11 +81,13 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
return return
} }
val decoded = arguments["decoded"] as Boolean
val mimeType = arguments["mimeType"] as String? val mimeType = arguments["mimeType"] as String?
val uri = (arguments["uri"] as String?)?.let { Uri.parse(it) } val uri = (arguments["uri"] as String?)?.toUri()
val sizeBytes = (arguments["sizeBytes"] as Number?)?.toLong() val sizeBytes = (arguments["sizeBytes"] as Number?)?.toLong()
val rotationDegrees = arguments["rotationDegrees"] as Int val rotationDegrees = arguments["rotationDegrees"] as Int
val isFlipped = arguments["isFlipped"] as Boolean val isFlipped = arguments["isFlipped"] as Boolean
val isAnimated = arguments["isAnimated"] as Boolean
val pageId = arguments["pageId"] as Int? val pageId = arguments["pageId"] as Int?
if (mimeType == null || uri == null) { if (mimeType == null || uri == null) {
@ -98,19 +96,31 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
return return
} }
if (isVideo(mimeType)) { if (canDecodeWithFlutter(mimeType, isAnimated) && !decoded) {
streamVideoByGlide(uri, mimeType, sizeBytes)
} else if (!canDecodeWithFlutter(mimeType, rotationDegrees, isFlipped)) {
// decode exotic format on platform side, then encode it in portable format for Flutter
streamImageByGlide(uri, pageId, mimeType, sizeBytes, rotationDegrees, isFlipped)
} else {
// to be decoded by Flutter // to be decoded by Flutter
streamImageAsIs(uri, mimeType, sizeBytes) streamOriginalEncodedBytes(uri, mimeType, sizeBytes)
} else if (isVideo(mimeType)) {
streamVideoByGlide(
uri = uri,
mimeType = mimeType,
sizeBytes = sizeBytes,
decoded = decoded,
)
} else {
streamImageByGlide(
uri = uri,
pageId = pageId,
mimeType = mimeType,
sizeBytes = sizeBytes,
rotationDegrees = rotationDegrees,
isFlipped = isFlipped,
decoded = decoded,
)
} }
endOfStream() endOfStream()
} }
private fun streamImageAsIs(uri: Uri, mimeType: String, sizeBytes: Long?) { private fun streamOriginalEncodedBytes(uri: Uri, mimeType: String, sizeBytes: Long?) {
if (!MemoryUtils.canAllocate(sizeBytes)) { if (!MemoryUtils.canAllocate(sizeBytes)) {
error("streamImage-image-read-large", "original image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null) error("streamImage-image-read-large", "original image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
return return
@ -130,29 +140,29 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
sizeBytes: Long?, sizeBytes: Long?,
rotationDegrees: Int, rotationDegrees: Int,
isFlipped: Boolean, isFlipped: Boolean,
decoded: Boolean,
) { ) {
val model: Any = if (isHeic(mimeType) && pageId != null) {
MultiTrackImage(context, uri, pageId)
} else if (mimeType == MimeTypes.TIFF) {
TiffImage(context, uri, pageId)
} else {
StorageUtils.getGlideSafeUri(context, uri, mimeType, sizeBytes)
}
val target = Glide.with(context) val target = Glide.with(context)
.asBitmap() .asBitmap()
.apply(glideOptions) .apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(model) .load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId, sizeBytes))
.submit() .submit()
try { try {
var bitmap = withContext(Dispatchers.IO) { target.get() } var bitmap = withContext(Dispatchers.IO) { target.get() }
if (needRotationAfterGlide(mimeType)) { if (needRotationAfterGlide(mimeType, pageId)) {
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
} }
if (bitmap != null) { if (bitmap != null) {
val bytes = bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false) // do not recycle bitmaps fetched from Glide as their lifecycle is unknown
val recycle = false
val bytes = if (decoded) {
BitmapUtils.getRawBytes(bitmap, recycle = recycle)
} else {
BitmapUtils.getEncodedBytes(bitmap, canHaveAlpha = MimeTypes.canHaveAlpha(mimeType), recycle = recycle)
}
if (MemoryUtils.canAllocate(sizeBytes)) { if (MemoryUtils.canAllocate(sizeBytes)) {
success(bytes) streamBytes(ByteArrayInputStream(bytes))
} else { } else {
error("streamImage-image-decode-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null) error("streamImage-image-decode-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
} }
@ -160,24 +170,31 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
error("streamImage-image-decode-null", "failed to get image for mimeType=$mimeType uri=$uri", null) error("streamImage-image-decode-null", "failed to get image for mimeType=$mimeType uri=$uri", null)
} }
} catch (e: Exception) { } catch (e: Exception) {
error("streamImage-image-decode-exception", "failed to get image for mimeType=$mimeType uri=$uri model=$model", toErrorDetails(e)) error("streamImage-image-decode-exception", "failed to get image for mimeType=$mimeType uri=$uri", toErrorDetails(e))
} finally { } finally {
Glide.with(context).clear(target) Glide.with(context).clear(target)
} }
} }
private suspend fun streamVideoByGlide(uri: Uri, mimeType: String, sizeBytes: Long?) { private suspend fun streamVideoByGlide(uri: Uri, mimeType: String, sizeBytes: Long?, decoded: Boolean) {
val target = Glide.with(context) val target = Glide.with(context)
.asBitmap() .asBitmap()
.apply(glideOptions) .apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(VideoThumbnail(context, uri)) .load(AvesAppGlideModule.getModel(context, uri, mimeType, null, sizeBytes))
.submit() .submit()
try { try {
val bitmap = withContext(Dispatchers.IO) { target.get() } val bitmap = withContext(Dispatchers.IO) { target.get() }
if (bitmap != null) { if (bitmap != null) {
val bytes = bitmap.getBytes(canHaveAlpha = false, recycle = false) // do not recycle bitmaps fetched from Glide as their lifecycle is unknown
val recycle = false
val bytes = if (decoded) {
BitmapUtils.getRawBytes(bitmap, recycle = recycle)
} else {
BitmapUtils.getEncodedBytes(bitmap, canHaveAlpha = false, recycle = recycle)
}
if (MemoryUtils.canAllocate(sizeBytes)) { if (MemoryUtils.canAllocate(sizeBytes)) {
success(bytes) streamBytes(ByteArrayInputStream(bytes))
} else { } else {
error("streamImage-video-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null) error("streamImage-video-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
} }
@ -219,11 +236,5 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
const val CHANNEL = "deckers.thibault/aves/media_byte_stream" const val CHANNEL = "deckers.thibault/aves/media_byte_stream"
private const val BUFFER_SIZE = 2 shl 17 // 256kB private const val BUFFER_SIZE = 2 shl 17 // 256kB
// request a fresh image with the highest quality format
private val glideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
} }
} }

View file

@ -1,10 +1,10 @@
package deckers.thibault.aves.channel.streams package deckers.thibault.aves.channel.streams
import android.net.Uri import android.app.Activity
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.fragment.app.FragmentActivity import androidx.core.net.toUri
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
@ -21,9 +21,8 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
class ImageOpStreamHandler(private val activity: FragmentActivity, private val arguments: Any?) : EventChannel.StreamHandler { class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private lateinit var eventSink: EventSink private lateinit var eventSink: EventSink
private lateinit var handler: Handler private lateinit var handler: Handler
@ -107,7 +106,7 @@ class ImageOpStreamHandler(private val activity: FragmentActivity, private val a
result["skipped"] = true result["skipped"] = true
} else { } else {
result["success"] = false result["success"] = false
getProvider(uri)?.let { provider -> getProvider(activity, uri)?.let { provider ->
try { try {
provider.delete(activity, uri, path, mimeType) provider.delete(activity, uri, path, mimeType)
result["success"] = true result["success"] = true
@ -142,7 +141,7 @@ class ImageOpStreamHandler(private val activity: FragmentActivity, private val a
// assume same provider for all entries // assume same provider for all entries
val firstEntry = entryMapList.first() val firstEntry = entryMapList.first()
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } val provider = (firstEntry["uri"] as String?)?.toUri()?.let { getProvider(activity, it) }
if (provider == null) { if (provider == null) {
error("convert-provider", "failed to find provider for entry=$firstEntry", null) error("convert-provider", "failed to find provider for entry=$firstEntry", null)
return return
@ -231,7 +230,7 @@ class ImageOpStreamHandler(private val activity: FragmentActivity, private val a
entriesToNewName[AvesEntry(rawEntry)] = newName entriesToNewName[AvesEntry(rawEntry)] = newName
} }
val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(kv.key.uri) } val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(activity, kv.key.uri) }
for ((provider, entryList) in byProvider) { for ((provider, entryList) in byProvider) {
if (provider == null) { if (provider == null) {
error("rename-provider", "failed to find provider for entry=${entryList.firstOrNull()}", null) error("rename-provider", "failed to find provider for entry=${entryList.firstOrNull()}", null)

View file

@ -30,12 +30,24 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
} }
init { init {
context.contentResolver.apply { Log.i(LOG_TAG, "start listening to Media Store")
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver) try {
registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver) 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)
} }
} }
fun dispose() {
Log.i(LOG_TAG, "stop listening to Media Store")
context.contentResolver.unregisterContentObserver(contentObserver)
}
override fun onListen(arguments: Any?, eventSink: EventSink) { override fun onListen(arguments: Any?, eventSink: EventSink) {
this.eventSink = eventSink this.eventSink = eventSink
handler = Handler(Looper.getMainLooper()) handler = Handler(Looper.getMainLooper())
@ -45,10 +57,6 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
Log.i(LOG_TAG, "onCancel arguments=$arguments") Log.i(LOG_TAG, "onCancel arguments=$arguments")
} }
fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver)
}
private fun success(uri: String?) { private fun success(uri: String?) {
handler?.post { handler?.post {
try { try {

View file

@ -19,13 +19,13 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E
private lateinit var eventSink: EventSink private lateinit var eventSink: EventSink
private lateinit var handler: Handler private lateinit var handler: Handler
private var knownEntries: Map<Int?, Int?>? = null // knownEntries: map of contentId -> dateModifiedMillis
private var knownEntries: Map<Long?, Long?>? = null
private var directory: String? = null private var directory: String? = null
init { init {
if (arguments is Map<*, *>) { if (arguments is Map<*, *>) {
@Suppress("unchecked_cast") knownEntries = (arguments["knownEntries"] as? Map<*, *>?)?.map { (it.key as Number?)?.toLong() to (it.value as Number?)?.toLong() }?.toMap()
knownEntries = arguments["knownEntries"] as Map<Int?, Int?>?
directory = arguments["directory"] as String? directory = arguments["directory"] as String?
} }
} }

View file

@ -7,6 +7,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.ViewConfiguration
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
@ -21,6 +22,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
private val contentObserver = object : ContentObserver(null) { private val contentObserver = object : ContentObserver(null) {
private var accelerometerRotation: Int = 0 private var accelerometerRotation: Int = 0
private var transitionAnimationScale: Float = 1f private var transitionAnimationScale: Float = 1f
private var longPressTimeoutMillis: Int = 0
init { init {
update() update()
@ -36,6 +38,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
hashMapOf( hashMapOf(
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation, Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale, Settings.Global.TRANSITION_ANIMATION_SCALE to transitionAnimationScale,
KEY_LONG_PRESS_TIMEOUT_MILLIS to longPressTimeoutMillis,
) )
) )
} }
@ -54,6 +57,11 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
transitionAnimationScale = newTransitionAnimationScale transitionAnimationScale = newTransitionAnimationScale
changed = true changed = true
} }
val newLongPressTimeout = ViewConfiguration.getLongPressTimeout()
if (longPressTimeoutMillis != newLongPressTimeout) {
longPressTimeoutMillis = newLongPressTimeout
changed = true
}
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null) Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
} }
@ -62,9 +70,13 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
} }
init { init {
context.contentResolver.apply { Log.i(LOG_TAG, "start listening to system settings")
registerContentObserver(Settings.System.CONTENT_URI, true, contentObserver) context.contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, contentObserver)
} }
fun dispose() {
Log.i(LOG_TAG, "stop listening to system settings")
context.contentResolver.unregisterContentObserver(contentObserver)
} }
override fun onListen(arguments: Any?, eventSink: EventSink) { override fun onListen(arguments: Any?, eventSink: EventSink) {
@ -76,10 +88,6 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
Log.i(LOG_TAG, "onCancel arguments=$arguments") Log.i(LOG_TAG, "onCancel arguments=$arguments")
} }
fun dispose() {
context.contentResolver.unregisterContentObserver(contentObserver)
}
private fun success(settings: FieldMap) { private fun success(settings: FieldMap) {
handler?.post { handler?.post {
try { try {
@ -93,5 +101,8 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<SettingsChangeStreamHandler>() private val LOG_TAG = LogUtils.createTag<SettingsChangeStreamHandler>()
const val CHANNEL = "deckers.thibault/aves/settings_change" const val CHANNEL = "deckers.thibault/aves/settings_change"
// cf `Settings.Secure.LONG_PRESS_TIMEOUT`
const val KEY_LONG_PRESS_TIMEOUT_MILLIS = "long_press_timeout"
} }
} }

View file

@ -1,14 +1,30 @@
package deckers.thibault.aves.decoder package deckers.thibault.aves.decoder
import android.content.Context import android.content.Context
import android.net.Uri
import android.text.format.Formatter
import android.util.Log import android.util.Log
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.ImageHeaderParser 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.load.resource.bitmap.ExifInterfaceImageHeaderParser
import com.bumptech.glide.module.AppGlideModule 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
import deckers.thibault.aves.utils.compatRemoveIf import deckers.thibault.aves.utils.compatRemoveIf
@GlideModule @GlideModule
@ -16,6 +32,30 @@ class AvesAppGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) { override fun applyOptions(context: Context, builder: GlideBuilder) {
// hide noisy warning (e.g. for images that can't be decoded) // hide noisy warning (e.g. for images that can't be decoded)
builder.setLogLevel(Log.ERROR) 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) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
@ -25,4 +65,28 @@ class AvesAppGlideModule : AppGlideModule() {
} }
override fun isManifestParsingEnabled(): Boolean = false 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)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
fun getModel(context: Context, uri: Uri, mimeType: String, pageId: Int?, sizeBytes: Long? = null): Any {
return if (pageId != null && MultiPageImage.isSupported(mimeType)) {
MultiPageImage(context, uri, mimeType, pageId)
} else if (mimeType == MimeTypes.TIFF) {
TiffImage(context, uri, pageId)
} else if (mimeType == MimeTypes.SVG) {
SvgImage(context, uri)
} else if (isVideo(mimeType)) {
VideoThumbnail(context, uri)
} else {
StorageUtils.getGlideSafeUri(context, uri, mimeType, sizeBytes)
}
}
}
} }

View file

@ -17,32 +17,40 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.module.LibraryGlideModule import com.bumptech.glide.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.MultiTrackMedia import deckers.thibault.aves.metadata.MultiTrackMedia
import deckers.thibault.aves.utils.MimeTypes
@GlideModule @GlideModule
class MultiTrackImageGlideModule : LibraryGlideModule() { class MultiPageImageGlideModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.append(MultiTrackImage::class.java, Bitmap::class.java, MultiTrackThumbnailLoader.Factory()) registry.append(MultiPageImage::class.java, Bitmap::class.java, MultiPageThumbnailLoader.Factory())
} }
} }
class MultiTrackImage(val context: Context, val uri: Uri, val trackIndex: Int?) class MultiPageImage(val context: Context, val uri: Uri, val mimeType: String, val pageId: Int?) {
override fun toString(): String = "MultiPageImage#${hashCode()}{uri=$uri, mimeType=$mimeType, pageId=$pageId}"
internal class MultiTrackThumbnailLoader : ModelLoader<MultiTrackImage, Bitmap> { companion object {
override fun buildLoadData(model: MultiTrackImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> { fun isSupported(mimeType: String) = MimeTypes.isHeic(mimeType) || mimeType == MimeTypes.JPEG
return ModelLoader.LoadData(ObjectKey(model.uri), MultiTrackImageFetcher(model, width, height)) }
}
internal class MultiPageThumbnailLoader : ModelLoader<MultiPageImage, Bitmap> {
override fun buildLoadData(model: MultiPageImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
return ModelLoader.LoadData(ObjectKey(model.uri), MultiPageImageFetcher(model, width, height))
} }
override fun handles(model: MultiTrackImage): Boolean = true override fun handles(model: MultiPageImage): Boolean = true
internal class Factory : ModelLoaderFactory<MultiTrackImage, Bitmap> { internal class Factory : ModelLoaderFactory<MultiPageImage, Bitmap> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MultiTrackImage, Bitmap> = MultiTrackThumbnailLoader() override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MultiPageImage, Bitmap> = MultiPageThumbnailLoader()
override fun teardown() {} override fun teardown() {}
} }
} }
internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int, val height: Int) : DataFetcher<Bitmap> { internal class MultiPageImageFetcher(val model: MultiPageImage, val width: Int, val height: Int) : DataFetcher<Bitmap> {
override fun loadData(priority: Priority, callback: DataCallback<in Bitmap>) { override fun loadData(priority: Priority, callback: DataCallback<in Bitmap>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
callback.onLoadFailed(Exception("unsupported Android version")) callback.onLoadFailed(Exception("unsupported Android version"))
@ -51,9 +59,17 @@ internal class MultiTrackImageFetcher(val model: MultiTrackImage, val width: Int
val context = model.context val context = model.context
val uri = model.uri val uri = model.uri
val trackIndex = model.trackIndex val mimeType = model.mimeType
var bitmap: Bitmap? = null
if (MimeTypes.isHeic(mimeType)) {
val trackIndex = model.pageId
bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
} else if (mimeType == MimeTypes.JPEG) {
val pageIndex = model.pageId ?: 0
bitmap = MultiPage.getJpegMpfBitmap(context, uri, pageIndex)
}
val bitmap = MultiTrackMedia.getImage(context, uri, trackIndex)
if (bitmap == null) { if (bitmap == null) {
callback.onLoadFailed(Exception("null bitmap")) callback.onLoadFailed(Exception("null bitmap"))
} else { } else {

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.net.Uri import android.net.Uri
import androidx.core.graphics.createBitmap
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.Registry import com.bumptech.glide.Registry
@ -18,6 +19,7 @@ import com.bumptech.glide.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException import com.caverock.androidsvg.SVGParseException
import deckers.thibault.aves.metadata.SVGParserBufferedInputStream
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import kotlin.math.ceil import kotlin.math.ceil
@ -52,7 +54,7 @@ internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int)
val bitmap: Bitmap? = StorageUtils.openInputStream(context, uri)?.use { input -> val bitmap: Bitmap? = StorageUtils.openInputStream(context, uri)?.use { input ->
try { try {
SVG.getFromInputStream(input)?.let { svg -> SVG.getFromInputStream(SVGParserBufferedInputStream(input))?.let { svg ->
svg.normalizeSize() svg.normalizeSize()
val viewBox = svg.documentViewBox val viewBox = svg.documentViewBox
val svgWidth = viewBox.width() val svgWidth = viewBox.width()
@ -60,14 +62,14 @@ internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int)
val bitmapWidth: Int val bitmapWidth: Int
val bitmapHeight: Int val bitmapHeight: Int
if (width / height > svgWidth / svgHeight) { if (width / height.toFloat() > svgWidth / svgHeight) {
bitmapWidth = ceil(svgWidth * height / svgHeight).toInt() bitmapWidth = ceil(svgWidth * height / svgHeight).toInt()
bitmapHeight = height bitmapHeight = height
} else { } else {
bitmapWidth = width bitmapWidth = width
bitmapHeight = ceil(svgHeight * width / svgWidth).toInt() bitmapHeight = ceil(svgHeight * width / svgWidth).toInt()
} }
val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888) val bitmap = createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
svg.renderToCanvas(canvas) svg.renderToCanvas(canvas)

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.decoder
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import androidx.core.graphics.scale
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.Registry import com.bumptech.glide.Registry
@ -48,7 +49,8 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
val page = model.page ?: 0 val page = model.page ?: 0
var sampleSize = 1 var sampleSize = 1
if (width > 0 && height > 0) { val customSize = width > 0 && height > 0
if (customSize) {
// determine sample size // determine sample size
val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd() val fd = context.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
if (fd == null) { if (fd == null) {
@ -62,8 +64,8 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
TiffBitmapFactory.decodeFileDescriptor(fd, options) TiffBitmapFactory.decodeFileDescriptor(fd, options)
val imageWidth = options.outWidth val imageWidth = options.outWidth
val imageHeight = options.outHeight val imageHeight = options.outHeight
if (imageHeight > height || imageWidth > width) { if (imageWidth > width || imageHeight > height) {
while (imageHeight / (sampleSize * 2) > height && imageWidth / (sampleSize * 2) > width) { while (imageHeight / (sampleSize * 2) >= height && imageWidth / (sampleSize * 2) >= width) {
sampleSize *= 2 sampleSize *= 2
} }
} }
@ -81,9 +83,23 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
inSampleSize = sampleSize inSampleSize = sampleSize
} }
try { try {
val bitmap = TiffBitmapFactory.decodeFileDescriptor(fd, options) val bitmap: Bitmap? = TiffBitmapFactory.decodeFileDescriptor(fd, options)
// calling `TiffBitmapFactory.closeFd(fd)` after decoding yields a segmentation fault
if (bitmap == null) { if (bitmap == null) {
callback.onLoadFailed(Exception("Decoding full TIFF yielded null bitmap")) callback.onLoadFailed(Exception("Decoding full TIFF yielded null bitmap"))
} else if (customSize) {
val dstWidth: Int
val dstHeight: Int
val aspectRatio = bitmap.width.toFloat() / bitmap.height
if (aspectRatio > 1) {
dstWidth = (height * aspectRatio).toInt()
dstHeight = height
} else {
dstWidth = width
dstHeight = (width / aspectRatio).toInt()
}
callback.onDataReady(bitmap.scale(dstWidth, dstHeight))
} else { } else {
callback.onDataReady(bitmap) callback.onDataReady(bitmap)
} }

View file

@ -1,8 +1,12 @@
package deckers.thibault.aves.decoder package deckers.thibault.aves.decoder
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.Registry import com.bumptech.glide.Registry
@ -16,50 +20,62 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.module.LibraryGlideModule import com.bumptech.glide.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.IOException
import kotlin.math.ceil
import kotlin.math.roundToInt
@GlideModule @GlideModule
class VideoThumbnailGlideModule : LibraryGlideModule() { class VideoThumbnailGlideModule : LibraryGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.append(VideoThumbnail::class.java, InputStream::class.java, VideoThumbnailLoader.Factory()) registry.append(VideoThumbnail::class.java, Bitmap::class.java, VideoThumbnailLoader.Factory())
} }
} }
class VideoThumbnail(val context: Context, val uri: Uri) class VideoThumbnail(val context: Context, val uri: Uri)
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> { internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, Bitmap> {
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> { override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model)) return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model, width, height))
} }
override fun handles(model: VideoThumbnail): Boolean = true override fun handles(model: VideoThumbnail): Boolean = true
internal class Factory : ModelLoaderFactory<VideoThumbnail, InputStream> { internal class Factory : ModelLoaderFactory<VideoThumbnail, Bitmap> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<VideoThumbnail, InputStream> = VideoThumbnailLoader() override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<VideoThumbnail, Bitmap> = VideoThumbnailLoader()
override fun teardown() {} override fun teardown() {}
} }
} }
internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher<InputStream> { internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val width: Int, val height: Int) : DataFetcher<Bitmap> {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataCallback<in Bitmap>) {
ioScope.launch { ioScope.launch {
val retriever = openMetadataRetriever(model.context, model.uri) val retriever = openMetadataRetriever(model.context, model.uri)
if (retriever == null) { if (retriever == null) {
callback.onLoadFailed(Exception("failed to initialize MediaMetadataRetriever for uri=${model.uri}")) callback.onLoadFailed(Exception("failed to initialize MediaMetadataRetriever for uri=${model.uri}"))
} else { } else {
try { try {
var bytes = retriever.embeddedPicture var bitmap: Bitmap? = null
if (bytes == null) {
retriever.embeddedPicture?.let { bytes ->
try {
bitmap = BitmapFactory.decodeStream(ByteArrayInputStream(bytes))
} catch (e: IOException) {
// ignore
}
}
if (bitmap == null) {
// there is no consistent strategy across devices to match // there is no consistent strategy across devices to match
// the thumbnails returned by the content resolver / Media Store // the thumbnails returned by the content resolver / Media Store
// so we derive one in an arbitrary way // so we derive one in an arbitrary way
@ -68,18 +84,71 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
if (durationMillis != null) { if (durationMillis != null) {
timeMillis = if (durationMillis < 15000) 0 else 15000 timeMillis = if (durationMillis < 15000) 0 else 15000
} }
val frame = if (timeMillis != null) { val timeMicros = if (timeMillis != null) timeMillis * 1000 else -1
retriever.getFrameAtTime(timeMillis * 1000) val option = MediaMetadataRetriever.OPTION_CLOSEST_SYNC
} else {
retriever.frameAtTime var videoWidth = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toFloatOrNull()
var videoHeight = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toFloatOrNull()
if (videoWidth == null || videoHeight == null) {
throw Exception("failed to get video dimensions")
}
var dstWidth = 0
var dstHeight = 0
if (width > 0 && height > 0) {
val rotationDegrees = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toIntOrNull()
if (rotationDegrees != null) {
val isRotated = rotationDegrees % 180 == 90
if (isRotated) {
val temp = videoWidth
videoWidth = videoHeight
videoHeight = temp
}
// cover fit
val videoAspectRatio = videoWidth / videoHeight
if (videoWidth > width || videoHeight > height) {
if (width / height.toFloat() > videoAspectRatio) {
dstHeight = ceil(videoHeight * width / videoWidth).toInt()
dstWidth = (dstHeight * videoAspectRatio).roundToInt()
} else {
dstWidth = ceil(videoWidth * height / videoHeight).toInt()
dstHeight = (dstWidth / videoAspectRatio).roundToInt()
}
}
}
}
// the returned frame is already rotated according to the video metadata
bitmap = if (dstWidth > 0 && dstHeight > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
val pixelCount = dstWidth * dstHeight
val targetBitmapSizeBytes = BitmapUtils.getExpectedImageSize(pixelCount.toLong(), getPreferredConfig())
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
throw Exception("not enough memory to allocate $targetBitmapSizeBytes bytes for the scaled frame at $dstWidth x $dstHeight")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
retriever.getScaledFrameAtTime(timeMicros, option, dstWidth, dstHeight, getBitmapParams())
} else {
retriever.getScaledFrameAtTime(timeMicros, option, dstWidth, dstHeight)
}
} else {
val pixelCount = videoWidth * videoHeight
val targetBitmapSizeBytes = BitmapUtils.getExpectedImageSize(pixelCount.toLong(), getPreferredConfig())
if (!MemoryUtils.canAllocate(targetBitmapSizeBytes)) {
throw Exception("not enough memory to allocate $targetBitmapSizeBytes bytes for the full frame at $videoWidth x $videoHeight")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
retriever.getFrameAtTime(timeMicros, option, getBitmapParams())
} else {
retriever.getFrameAtTime(timeMicros, option)
}
} }
bytes = frame?.getBytes(canHaveAlpha = false, recycle = false)
} }
if (bytes != null) { if (bitmap == null) {
callback.onDataReady(ByteArrayInputStream(bytes)) callback.onLoadFailed(Exception("failed to get embedded picture or any frame for uri=${model.uri}"))
} else { } else {
callback.onLoadFailed(Exception("failed to get embedded picture or any frame")) callback.onDataReady(bitmap)
} }
} catch (e: Exception) { } catch (e: Exception) {
callback.onLoadFailed(e) callback.onLoadFailed(e)
@ -91,13 +160,30 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe
} }
} }
@RequiresApi(Build.VERSION_CODES.P)
private fun getBitmapParams(): MediaMetadataRetriever.BitmapParams {
val params = MediaMetadataRetriever.BitmapParams()
params.preferredConfig = this.getPreferredConfig()
return params
}
private fun getPreferredConfig(): Bitmap.Config {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// improved precision with the same memory cost as `ARGB_8888` (4 bytes per pixel)
// for wide-gamut and HDR content which does not require alpha blending
Bitmap.Config.RGBA_1010102
} else {
Bitmap.Config.ARGB_8888
}
}
// already cleaned up in loadData and ByteArrayInputStream will be GC'd // already cleaned up in loadData and ByteArrayInputStream will be GC'd
override fun cleanup() {} override fun cleanup() {}
// cannot cancel // cannot cancel
override fun cancel() {} override fun cancel() {}
override fun getDataClass(): Class<InputStream> = InputStream::class.java override fun getDataClass(): Class<Bitmap> = Bitmap::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL override fun getDataSource(): DataSource = DataSource.LOCAL
} }

View file

@ -1,20 +1,24 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.drew.lang.Rational import com.drew.lang.Rational
import com.drew.metadata.Directory import com.drew.metadata.Directory
import com.drew.metadata.exif.* import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.exif.ExifThumbnailDirectory
import com.drew.metadata.exif.GpsDirectory
import com.drew.metadata.exif.PanasonicRawIFD0Directory
import com.drew.metadata.exif.makernotes.OlympusCameraSettingsMakernoteDirectory import com.drew.metadata.exif.makernotes.OlympusCameraSettingsMakernoteDirectory
import com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory import com.drew.metadata.exif.makernotes.OlympusImageProcessingMakernoteDirectory
import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory import com.drew.metadata.exif.makernotes.OlympusMakernoteDirectory
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Locale
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.roundToLong import kotlin.math.roundToLong
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
object ExifInterfaceHelper { object ExifInterfaceHelper {
private val LOG_TAG = LogUtils.createTag<ExifInterfaceHelper>() private val LOG_TAG = LogUtils.createTag<ExifInterfaceHelper>()
@ -22,7 +26,7 @@ object ExifInterfaceHelper {
val GPS_DATE_FORMAT = SimpleDateFormat("yyyy:MM:dd", Locale.ROOT) val GPS_DATE_FORMAT = SimpleDateFormat("yyyy:MM:dd", Locale.ROOT)
val GPS_TIME_FORMAT = SimpleDateFormat("HH:mm:ss", Locale.ROOT) val GPS_TIME_FORMAT = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
private const val precisionErrorTolerance = 1e-10 private const val PRECISION_ERROR_TOLERANCE = 1e-10
// ExifInterface always states it has the following attributes // ExifInterface always states it has the following attributes
// and returns "0" instead of "null" when they are actually missing // and returns "0" instead of "null" when they are actually missing
@ -220,7 +224,7 @@ object ExifInterfaceHelper {
// initialize metadata-extractor directories that we will fill // initialize metadata-extractor directories that we will fill
// by tags converted from the ExifInterface attributes // by tags converted from the ExifInterface attributes
// so that we can rely on metadata-extractor descriptions // so that we can rely on metadata-extractor descriptions
val dirs = DirType.values().associateWith { it.createDirectory() } val dirs = DirType.entries.associateWith { it.createDirectory() }
// exclude Exif directory when it only includes image size // exclude Exif directory when it only includes image size
val isUselessExif = fun(it: Map<String, String>): Boolean { val isUselessExif = fun(it: Map<String, String>): Boolean {
@ -308,7 +312,7 @@ object ExifInterfaceHelper {
val numerator = 1L val numerator = 1L
val f = numerator / d val f = numerator / d
val denominator = f.roundToLong() val denominator = f.roundToLong()
if (abs(f - denominator) < precisionErrorTolerance) { if (abs(f - denominator) < PRECISION_ERROR_TOLERANCE) {
return Rational(numerator, denominator) return Rational(numerator, denominator)
} }
} }

View file

@ -111,20 +111,25 @@ object MediaMetadataRetrieverHelper {
// format // format
MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION, MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION,
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION -> "$value°" MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION -> "$value°"
MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT, MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH, MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT, MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH,
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH -> "$value pixels" MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT, MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH -> "$value pixels"
MediaMetadataRetriever.METADATA_KEY_BITRATE -> { MediaMetadataRetriever.METADATA_KEY_BITRATE -> {
val bitrate = value.toLongOrNull() ?: 0 val bitrate = value.toLongOrNull() ?: 0
if (bitrate > 0) formatBitrate(bitrate) else null if (bitrate > 0) formatBitrate(bitrate) else null
} }
MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE -> { MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE -> {
val framerate = value.toDoubleOrNull() ?: 0.0 val framerate = value.toDoubleOrNull() ?: 0.0
if (framerate > 0.0) "$framerate" else null if (framerate > 0.0) "$framerate" else null
} }
MediaMetadataRetriever.METADATA_KEY_DURATION -> { MediaMetadataRetriever.METADATA_KEY_DURATION -> {
val dateMillis = value.toLongOrNull() ?: 0 val dateMillis = value.toLongOrNull() ?: 0
if (dateMillis > 0) durationFormat.format(Date(dateMillis)) else null if (dateMillis > 0) durationFormat.format(Date(dateMillis)) else null
} }
MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE -> { MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE -> {
when (value.toIntOrNull()) { when (value.toIntOrNull()) {
MediaFormat.COLOR_RANGE_FULL -> "Full" MediaFormat.COLOR_RANGE_FULL -> "Full"
@ -132,6 +137,7 @@ object MediaMetadataRetrieverHelper {
else -> value else -> value
} }
} }
MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD -> { MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD -> {
when (value.toIntOrNull()) { when (value.toIntOrNull()) {
MediaFormat.COLOR_STANDARD_BT709 -> "BT.709" MediaFormat.COLOR_STANDARD_BT709 -> "BT.709"
@ -141,6 +147,7 @@ object MediaMetadataRetrieverHelper {
else -> value else -> value
} }
} }
MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER -> { MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER -> {
when (value.toIntOrNull()) { when (value.toIntOrNull()) {
MediaFormat.COLOR_TRANSFER_LINEAR -> "Linear" MediaFormat.COLOR_TRANSFER_LINEAR -> "Linear"
@ -154,6 +161,7 @@ object MediaMetadataRetrieverHelper {
MediaMetadataRetriever.METADATA_KEY_COMPILATION, MediaMetadataRetriever.METADATA_KEY_COMPILATION,
MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER,
MediaMetadataRetriever.METADATA_KEY_YEAR -> if (value != "0") value else null MediaMetadataRetriever.METADATA_KEY_YEAR -> if (value != "0") value else null
MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER -> if (value != "0/0") value else null MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER -> if (value != "0/0") value else null
MediaMetadataRetriever.METADATA_KEY_DATE -> { MediaMetadataRetriever.METADATA_KEY_DATE -> {
val dateMillis = Metadata.parseVideoMetadataDate(value) val dateMillis = Metadata.parseVideoMetadataDate(value)
@ -168,4 +176,12 @@ object MediaMetadataRetrieverHelper {
}?.let { save(it) } }?.let { save(it) }
} }
} }
fun MediaFormat.getSafeInt(key: String, save: (value: Int) -> Unit) {
if (this.containsKey(key)) save(this.getInteger(key))
}
fun MediaFormat.getSafeLong(key: String, save: (value: Long) -> Unit) {
if (this.containsKey(key)) save(this.getLong(key))
}
} }

View file

@ -2,18 +2,24 @@ package deckers.thibault.aves.metadata
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface import android.util.Log
import deckers.thibault.aves.utils.FileUtils.transferFrom import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.regex.Pattern import java.util.regex.Pattern
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
object Metadata { object Metadata {
private val LOG_TAG = LogUtils.createTag<Metadata>()
const val IPTC_MARKER_BYTE: Byte = 0x1c const val IPTC_MARKER_BYTE: Byte = 0x1c
// Pattern to extract latitude & longitude from a video location tag (cf ISO 6709) // Pattern to extract latitude & longitude from a video location tag (cf ISO 6709)
@ -118,7 +124,7 @@ object Metadata {
return date.time + parseSubSecond(subSecond) return date.time + parseSubSecond(subSecond)
} }
// Opening large PSD/TIFF files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1), // Opening some large files yields an OOM (both with `metadata-extractor` v2.15.0 and `ExifInterface` v1.3.1),
// so we define an arbitrary threshold to avoid a crash on launch. // so we define an arbitrary threshold to avoid a crash on launch.
// It is not clear whether it is because of the file itself or its metadata. // It is not clear whether it is because of the file itself or its metadata.
private const val FILE_SIZE_MAX = 100 * (1 shl 20) // MB private const val FILE_SIZE_MAX = 100 * (1 shl 20) // MB
@ -132,15 +138,19 @@ object Metadata {
private val previewFiles = HashMap<Uri, File>() private val previewFiles = HashMap<Uri, File>()
private fun getSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): Uri { private fun getSafeUri(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): Uri {
// formats known to yield OOM for large files
return when (mimeType) { return when (mimeType) {
// formats known to yield OOM for large files // formats known to yield OOM for large files
MimeTypes.DNG,
MimeTypes.DNG_ADOBE,
MimeTypes.HEIC, MimeTypes.HEIC,
MimeTypes.HEIF, MimeTypes.HEIF,
MimeTypes.MP4, MimeTypes.MP4,
MimeTypes.PSD_VND, MimeTypes.PSD_VND,
MimeTypes.PSD_X, MimeTypes.PSD_X,
MimeTypes.TIFF -> { MimeTypes.TIFF ->
if (isDangerouslyLarge(sizeBytes)) { if (isDangerouslyLarge(sizeBytes)) {
Log.d(LOG_TAG, "Dangerously large file with uri=$uri, mimeType=$mimeType, size=$sizeBytes")
// make a preview from the beginning of the file, // make a preview from the beginning of the file,
// hoping the metadata is accessible in the copied chunk // hoping the metadata is accessible in the copied chunk
var previewFile = previewFiles[uri] var previewFile = previewFiles[uri]
@ -153,15 +163,18 @@ object Metadata {
// small enough to be safe as it is // small enough to be safe as it is
uri uri
} }
}
// *probably* safe else ->
else -> uri // *probably* safe
uri
} }
} }
fun createPreviewFile(context: Context, uri: Uri): File { fun createPreviewFile(context: Context, uri: Uri): File {
val size = PREVIEW_SIZE
Log.d(LOG_TAG, "create preview of size=$size for uri=$uri")
return StorageUtils.createTempFile(context).apply { return StorageUtils.createTempFile(context).apply {
transferFrom(StorageUtils.openInputStream(context, uri), PREVIEW_SIZE) transferFrom(StorageUtils.openInputStream(context, uri), size)
} }
} }

View file

@ -3,12 +3,17 @@ package deckers.thibault.aves.metadata
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import deckers.thibault.aves.metadata.xmp.XMP
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.toByteArray import deckers.thibault.aves.utils.toByteArray
import deckers.thibault.aves.utils.toHex import deckers.thibault.aves.utils.toHex
import org.mp4parser.* import org.mp4parser.BasicContainer
import org.mp4parser.Box
import org.mp4parser.Container
import org.mp4parser.IsoFile
import org.mp4parser.PropertyBoxParserImpl
import org.mp4parser.boxes.UnknownBox import org.mp4parser.boxes.UnknownBox
import org.mp4parser.boxes.UserBox import org.mp4parser.boxes.UserBox
import org.mp4parser.boxes.apple.AppleCoverBox import org.mp4parser.boxes.apple.AppleCoverBox
@ -16,8 +21,18 @@ import org.mp4parser.boxes.apple.AppleGPSCoordinatesBox
import org.mp4parser.boxes.apple.AppleItemListBox import org.mp4parser.boxes.apple.AppleItemListBox
import org.mp4parser.boxes.apple.AppleVariableSignedIntegerBox import org.mp4parser.boxes.apple.AppleVariableSignedIntegerBox
import org.mp4parser.boxes.apple.Utf8AppleDataBox import org.mp4parser.boxes.apple.Utf8AppleDataBox
import org.mp4parser.boxes.iso14496.part12.* import org.mp4parser.boxes.iso14496.part12.FreeBox
import org.mp4parser.boxes.iso14496.part12.HandlerBox
import org.mp4parser.boxes.iso14496.part12.MediaDataBox
import org.mp4parser.boxes.iso14496.part12.MetaBox
import org.mp4parser.boxes.iso14496.part12.MovieBox
import org.mp4parser.boxes.iso14496.part12.MovieFragmentBox
import org.mp4parser.boxes.iso14496.part12.SampleTableBox
import org.mp4parser.boxes.iso14496.part12.SegmentIndexBox
import org.mp4parser.boxes.iso14496.part12.TrackHeaderBox
import org.mp4parser.boxes.iso14496.part12.UserDataBox
import org.mp4parser.boxes.threegpp.ts26244.AuthorBox import org.mp4parser.boxes.threegpp.ts26244.AuthorBox
import org.mp4parser.boxes.threegpp.ts26244.LocationInformationBox
import org.mp4parser.support.AbstractBox import org.mp4parser.support.AbstractBox
import org.mp4parser.support.Matrix import org.mp4parser.support.Matrix
import org.mp4parser.tools.Path import org.mp4parser.tools.Path
@ -31,6 +46,15 @@ object Mp4ParserHelper {
// arbitrary size to detect boxes that may yield an OOM // arbitrary size to detect boxes that may yield an OOM
private const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB private const val BOX_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB
const val SAMSUNG_MAKERNOTE_BOX_TYPE = "sefd"
const val SEFD_MOTION_PHOTO_NAME = "MotionPhoto_Data"
private val largerTypeWhitelist = listOf(
// HEIC motion photo may contain Samsung maker notes in `sefd` box,
// including a video larger than the danger threshold
SAMSUNG_MAKERNOTE_BOX_TYPE,
)
fun computeEdits(context: Context, uri: Uri, modifier: (isoFile: IsoFile) -> Unit): List<Pair<Long, ByteArray>> { fun computeEdits(context: Context, uri: Uri, modifier: (isoFile: IsoFile) -> Unit): List<Pair<Long, ByteArray>> {
// we can skip uninteresting boxes with a seekable data source // we can skip uninteresting boxes with a seekable data source
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri") val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
@ -119,6 +143,35 @@ object Mp4ParserHelper {
return false return false
} }
// returns the offset and data of the Samsung maker notes box
fun getSamsungSefd(context: Context, uri: Uri): Pair<Long, ByteArray>? {
try {
// we can skip uninteresting boxes with a seekable data source
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
pfd.use {
FileInputStream(it.fileDescriptor).use { stream ->
stream.channel.use { channel ->
IsoFile(channel, metadataBoxParser()).use { isoFile ->
var offset = 0L
for (box in isoFile.boxes) {
if (box is UnknownBox && box.type == SAMSUNG_MAKERNOTE_BOX_TYPE) {
if (!box.isParsed) {
box.parseDetails()
}
return Pair(offset + 8, box.data.toByteArray()) // skip 8 bytes for box header
}
offset += box.size
}
}
}
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to read sefd box", e)
}
return null
}
// extensions // extensions
fun IsoFile.updateLocation(locationIso6709: String?) { fun IsoFile.updateLocation(locationIso6709: String?) {
@ -258,18 +311,18 @@ object Mp4ParserHelper {
) )
setBoxSkipper { type, size -> setBoxSkipper { type, size ->
if (skippedTypes.contains(type)) return@setBoxSkipper true if (skippedTypes.contains(type)) return@setBoxSkipper true
if (size > BOX_SIZE_DANGER_THRESHOLD) throw Exception("box (type=$type size=$size) is too large") if (size > BOX_SIZE_DANGER_THRESHOLD && !largerTypeWhitelist.contains(type)) throw Exception("box (type=$type size=$size) is too large")
false false
} }
} }
fun getUserData( fun getUserDataBox(
context: Context, context: Context,
mimeType: String, mimeType: String,
uri: Uri, uri: Uri,
): MutableMap<String, String> { ): UserDataBox? {
val fields = HashMap<String, String>() if (mimeType != MimeTypes.MP4) return null
if (mimeType != MimeTypes.MP4) return fields
try { try {
// we can skip uninteresting boxes with a seekable data source // we can skip uninteresting boxes with a seekable data source
val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri") val pfd = StorageUtils.openInputFileDescriptor(context, uri) ?: throw Exception("failed to open file descriptor for uri=$uri")
@ -278,10 +331,7 @@ object Mp4ParserHelper {
stream.channel.use { channel -> stream.channel.use { channel ->
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device` // creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
IsoFile(channel, metadataBoxParser()).use { isoFile -> IsoFile(channel, metadataBoxParser()).use { isoFile ->
val userDataBox = Path.getPath<UserDataBox>(isoFile.movieBox, UserDataBox.TYPE) return Path.getPath(isoFile.movieBox, UserDataBox.TYPE)
if (userDataBox != null) {
fields.putAll(extractBoxFields(userDataBox))
}
} }
} }
} }
@ -291,10 +341,10 @@ object Mp4ParserHelper {
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get User Data box by MP4 parser for mimeType=$mimeType uri=$uri", e) Log.w(LOG_TAG, "failed to get User Data box by MP4 parser for mimeType=$mimeType uri=$uri", e)
} }
return fields return null
} }
private fun extractBoxFields(container: Container): HashMap<String, String> { fun extractBoxFields(container: Container): HashMap<String, String> {
val fields = HashMap<String, String>() val fields = HashMap<String, String>()
for (box in container.boxes) { for (box in container.boxes) {
if (box is AbstractBox && !box.isParsed) { if (box is AbstractBox && !box.isParsed) {
@ -308,9 +358,20 @@ object Mp4ParserHelper {
is AppleGPSCoordinatesBox -> fields[key] = box.value is AppleGPSCoordinatesBox -> fields[key] = box.value
is AppleItemListBox -> fields.putAll(extractBoxFields(box)) is AppleItemListBox -> fields.putAll(extractBoxFields(box))
is AppleVariableSignedIntegerBox -> fields[key] = box.value.toString() is AppleVariableSignedIntegerBox -> fields[key] = box.value.toString()
is Utf8AppleDataBox -> fields[key] = box.value
is HandlerBox -> {} is HandlerBox -> {}
is LocationInformationBox -> {
hashMapOf<String, String>(
"Language" to box.language,
"Name" to box.name,
"Role" to box.role.toString(),
"Longitude" to box.longitude.toString(),
"Latitude" to box.latitude.toString(),
"Altitude" to box.altitude.toString(),
"Astronomical Body" to box.astronomicalBody,
"Additional Notes" to box.additionalNotes,
).forEach { (k, v) -> fields["$key/$k"] = v }
}
is MetaBox -> { is MetaBox -> {
val handlerBox = Path.getPath<HandlerBox>(box, HandlerBox.TYPE).apply { parseDetails() } val handlerBox = Path.getPath<HandlerBox>(box, HandlerBox.TYPE).apply { parseDetails() }
when (val handlerType = handlerBox?.handlerType ?: MetaBox.TYPE) { when (val handlerType = handlerBox?.handlerType ?: MetaBox.TYPE) {
@ -335,6 +396,8 @@ object Mp4ParserHelper {
} }
} }
is Utf8AppleDataBox -> fields[key] = box.value
else -> fields[key] = box.toString() else -> fields[key] = box.toString()
} }
} }
@ -347,6 +410,7 @@ object Mp4ParserHelper {
"catg" -> "Category" "catg" -> "Category"
"covr" -> "Cover Art" "covr" -> "Cover Art"
"keyw" -> "Keyword" "keyw" -> "Keyword"
"loci" -> "Location"
"mcvr" -> "Preview Image" "mcvr" -> "Preview Image"
"pcst" -> "Podcast" "pcst" -> "Podcast"
"SDLN" -> "Play Mode" "SDLN" -> "Play Mode"

View file

@ -1,6 +1,8 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaExtractor import android.media.MediaExtractor
import android.media.MediaFormat import android.media.MediaFormat
import android.net.Uri import android.net.Uri
@ -8,22 +10,35 @@ import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.xmp.XmpDirectory import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.metadata.XMP.countPropArrayItems import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt
import deckers.thibault.aves.metadata.XMP.doesPropExist import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt
import deckers.thibault.aves.metadata.XMP.getSafeLong import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeLong
import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeInt
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntryDirectory
import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.metadata.xmp.XMP
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.indexOfBytes import deckers.thibault.aves.utils.indexOfBytes
import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.DataInputStream import java.io.DataInputStream
import java.io.EOFException
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
object MultiPage { object MultiPage {
private val LOG_TAG = LogUtils.createTag<MultiPage>() private val LOG_TAG = LogUtils.createTag<MultiPage>()
// TODO TLAD more generic support, (e.g. 0x00000014 + `ftyp` + `qt `)
// atom length (variable, e.g. `0x00000018`) + atom type (`ftyp`) + type (variable, e.g. `mp42`, `qt`)
private val heicMotionPhotoVideoStartIndicator = byteArrayOf(0x00, 0x00, 0x00, 0x18) + "ftypmp42".toByteArray() private val heicMotionPhotoVideoStartIndicator = byteArrayOf(0x00, 0x00, 0x00, 0x18) + "ftypmp42".toByteArray()
// page info // page info
@ -36,24 +51,16 @@ object MultiPage {
private const val KEY_ROTATION_DEGREES = "rotationDegrees" private const val KEY_ROTATION_DEGREES = "rotationDegrees"
fun getHeicTracks(context: Context, uri: Uri): ArrayList<FieldMap> { fun getHeicTracks(context: Context, uri: Uri): ArrayList<FieldMap> {
fun MediaFormat.getSafeInt(key: String, save: (value: Int) -> Unit) {
if (this.containsKey(key)) save(this.getInteger(key))
}
fun MediaFormat.getSafeLong(key: String, save: (value: Long) -> Unit) {
if (this.containsKey(key)) save(this.getLong(key))
}
val tracks = ArrayList<FieldMap>() val tracks = ArrayList<FieldMap>()
val extractor = MediaExtractor() val extractor = MediaExtractor()
extractor.setDataSource(context, uri, null) extractor.setDataSource(context, uri, null)
for (i in 0 until extractor.trackCount) { for (pageIndex in 0 until extractor.trackCount) {
try { try {
val format = extractor.getTrackFormat(i) val format = extractor.getTrackFormat(pageIndex)
format.getString(MediaFormat.KEY_MIME)?.let { mime -> format.getString(MediaFormat.KEY_MIME)?.let { mime ->
val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime
val track: FieldMap = hashMapOf( val track: FieldMap = hashMapOf(
KEY_PAGE to i, KEY_PAGE to pageIndex,
KEY_MIME_TYPE to trackMime, KEY_MIME_TYPE to trackMime,
) )
@ -72,78 +79,226 @@ object MultiPage {
tracks.add(track) tracks.add(track)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get HEIC track information for uri=$uri, track num=$i", e) Log.w(LOG_TAG, "failed to get HEIC track information for uri=$uri, pageIndex=$pageIndex", e)
} }
} }
extractor.release() extractor.release()
return tracks return tracks
} }
fun getMotionPhotoPages(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): ArrayList<FieldMap> { fun isHeicSefdMotionPhoto(context: Context, uri: Uri): Boolean {
fun MediaFormat.getSafeInt(key: String, save: (value: Int) -> Unit) { return getHeicSefdMotionPhotoVideoSizing(context, uri) != null
if (this.containsKey(key)) save(this.getInteger(key)) }
}
fun MediaFormat.getSafeLong(key: String, save: (value: Long) -> Unit) { private fun getHeicSefdMotionPhotoVideoSizing(context: Context, uri: Uri): Pair<Long, Long>? {
if (this.containsKey(key)) save(this.getLong(key)) Mp4ParserHelper.getSamsungSefd(context, uri)?.let { (sefdOffset, sefdBytes) ->
// we could properly parse each tag until we find the "embedded video" tag (0x0a30)
// but it seems that decoding the SEFT trailer is necessary for this,
// so we simply search for the "MotionPhoto_Data" sequence instead
val name = Mp4ParserHelper.SEFD_MOTION_PHOTO_NAME
val index = sefdBytes.indexOfBytes(name.toByteArray(Charsets.UTF_8))
if (index != -1) {
val videoOffset = sefdOffset + index + name.length
val videoSize = sefdBytes.size - (videoOffset - sefdOffset)
return Pair(videoOffset, videoSize)
}
} }
return null
}
val tracks = ArrayList<FieldMap>() private fun getJpegMpfPrimaryRotation(context: Context, uri: Uri, sizeBytes: Long): Int {
val extractor = MediaExtractor() val mimeType = MimeTypes.JPEG
var pfd: ParcelFileDescriptor? = null var rotationDegrees = 0
try {
getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes -> var foundExif = false
val videoStartOffset = sizeBytes - videoSizeBytes if (canReadWithMetadataExtractor(mimeType)) {
pfd = context.contentResolver.openFileDescriptor(uri, "r") try {
pfd?.fileDescriptor?.let { fd -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
extractor.setDataSource(fd, videoStartOffset, videoSizeBytes) val metadata = Helper.safeRead(input, sizeBytes)
// set the original image as the first and default track foundExif = metadata.directories.any { it is ExifDirectoryBase && it.tagCount > 0 }
var trackCount = 0 for (dir in metadata.getDirectoriesOfType(ExifIFD0Directory::class.java)) {
tracks.add( dir.getSafeInt(ExifDirectoryBase.TAG_ORIENTATION) {
hashMapOf( rotationDegrees = Metadata.getRotationDegreesForExifCode(it)
KEY_PAGE to trackCount++,
KEY_MIME_TYPE to mimeType,
KEY_IS_DEFAULT to true,
)
)
// add video tracks from the appended video
if (extractor.trackCount > 0) {
// only consider the first track to represent the appended video
val trackIndex = 0
try {
val format = extractor.getTrackFormat(trackIndex)
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
if (MimeTypes.isVideo(mime)) {
val track: FieldMap = hashMapOf(
KEY_PAGE to trackCount++,
KEY_MIME_TYPE to MimeTypes.MP4,
KEY_IS_DEFAULT to false,
)
format.getSafeInt(MediaFormat.KEY_WIDTH) { track[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { track[KEY_HEIGHT] = it }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
format.getSafeInt(MediaFormat.KEY_ROTATION) { track[KEY_ROTATION_DEGREES] = it }
}
format.getSafeLong(MediaFormat.KEY_DURATION) { track[KEY_DURATION] = it / 1000 }
tracks.add(track)
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get motion photo track information for uri=$uri, track num=$trackIndex", e)
} }
} }
} }
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to read metadata by metadata-extractor for mimeType=$mimeType uri=$uri", e)
}
}
if (!foundExif) {
// fallback to read EXIF via ExifInterface
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
exif.getSafeInt(ExifInterface.TAG_ORIENTATION, acceptZero = false) {
rotationDegrees = exif.rotationDegrees
}
}
} catch (e: Exception) {
// ExifInterface initialization can fail with a RuntimeException
// caused by an internal MediaMetadataRetriever failure
Log.w(LOG_TAG, "failed to get metadata by ExifInterface for mimeType=$mimeType uri=$uri", e)
}
}
return rotationDegrees
}
// starts after `[APP2 marker (1 byte)] [segment size (2 bytes)] [MPF marker (4 bytes)]`
fun getJpegMpfBaseOffset(context: Context, uri: Uri, sizeBytes: Long?): Int? {
val mimeType = MimeTypes.JPEG
val endMarker = 0xFF
val app2Marker = JpegSegmentType.APP2.byteValue
val mpfMarker = "MPF".toByteArray() + 0x00
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
var offset = 0
val marker = ByteArray(4)
while (true) {
// look for APP2 marker (0xFFE2)
var found = false
while (!found) {
var i = input.read()
if (i == -1) throw EOFException()
offset++
if (i == endMarker) {
i = input.read()
if (i == -1) throw EOFException()
offset++
found = i.toByte() == app2Marker
}
}
// skip 2 bytes for segment size
input.skip(2)
offset += 2
input.read(marker, 0, marker.size)
offset += 4
if (marker.contentEquals(mpfMarker)) {
return offset
}
}
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to open motion photo for uri=$uri", e) Log.w(LOG_TAG, "failed to get MPF base offset from uri=$uri", e)
} finally {
extractor.release()
pfd?.close()
} }
return tracks return null
} }
fun getMotionPhotoOffset(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? { fun getJpegMpfEntries(context: Context, uri: Uri, sizeBytes: Long?): List<MpEntry>? {
val mimeType = MimeTypes.JPEG
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input, sizeBytes)
return metadata.getDirectoriesOfType(MpEntryDirectory::class.java).map { it.entry }
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to find MPF entries", e)
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to find MPF entries", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to find MPF entries", e)
}
return null
}
fun getJpegMpfPages(context: Context, uri: Uri, sizeBytes: Long): ArrayList<FieldMap> {
val primaryRotation = getJpegMpfPrimaryRotation(context, uri, sizeBytes)
val pages = ArrayList<FieldMap>()
val baseOffset = getJpegMpfBaseOffset(context, uri, sizeBytes)
val mpEntries = getJpegMpfEntries(context, uri, sizeBytes)
if (mpEntries != null && baseOffset != null) {
for ((pageIndex, mpEntry) in mpEntries.withIndex()) {
mpEntry.mimeType?.let { embedMimeType ->
val page = hashMapOf<String, Any?>(
KEY_PAGE to pageIndex,
KEY_MIME_TYPE to embedMimeType,
KEY_IS_DEFAULT to (pageIndex == 0),
KEY_ROTATION_DEGREES to primaryRotation,
)
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
dataOffset += baseOffset
}
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(dataOffset)
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)
options.outWidth.takeIf { it >= 0 }?.let { page[KEY_WIDTH] = it }
options.outHeight.takeIf { it >= 0 }?.let { page[KEY_HEIGHT] = it }
pages.add(page)
}
}
}
}
return pages
}
fun getJpegMpfBitmap(context: Context, uri: Uri, pageIndex: Int): Bitmap? {
val mpEntries = getJpegMpfEntries(context, uri, null)
if (mpEntries != null && pageIndex < mpEntries.size) {
val mpEntry = mpEntries[pageIndex]
var dataOffset = mpEntry.dataOffset
if (dataOffset > 0) {
val baseOffset = getJpegMpfBaseOffset(context, uri, null)
if (baseOffset != null) {
dataOffset += baseOffset
}
}
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(dataOffset)
return BitmapFactory.decodeStream(input)
}
}
return null
}
fun getMotionPhotoPages(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): ArrayList<FieldMap> {
val pages = ArrayList<FieldMap>()
getMotionPhotoVideoInfo(context, uri, mimeType, sizeBytes)?.let { videoInfo ->
// set the original image as the first and default track
var pageIndex = 0
pages.add(
hashMapOf(
KEY_PAGE to pageIndex++,
KEY_MIME_TYPE to mimeType,
KEY_IS_DEFAULT to true,
)
)
// add video tracks from the appended video
videoInfo.getString(MediaFormat.KEY_MIME)?.let { mime ->
if (MimeTypes.isVideo(mime)) {
val page: FieldMap = hashMapOf(
KEY_PAGE to pageIndex++,
KEY_MIME_TYPE to MimeTypes.MP4,
KEY_IS_DEFAULT to false,
)
videoInfo.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
videoInfo.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
videoInfo.getSafeInt(MediaFormat.KEY_ROTATION) { page[KEY_ROTATION_DEGREES] = it }
}
videoInfo.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 }
pages.add(page)
}
}
}
return pages
}
fun getTrailerVideoSize(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
if (MimeTypes.isHeic(mimeType)) { if (MimeTypes.isHeic(mimeType)) {
// XMP in HEIC motion photos (as taken with a Samsung Camera v12.0.01.50) indicates an `Item:Length` of 68 bytes for the video. // XMP in HEIC motion photos (as taken with a Samsung Camera v12.0.01.50) indicates an `Item:Length` of 68 bytes for the video.
// This item does not contain the video itself, but only some kind of metadata (no doc, no spec), // This item does not contain the video itself, but only some kind of metadata (no doc, no spec),
@ -168,25 +323,12 @@ object MultiPage {
var foundXmp = false var foundXmp = false
fun processXmp(xmpMeta: XMPMeta) { fun processXmp(xmpMeta: XMPMeta) {
if (xmpMeta.doesPropExist(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME)) { offsetFromEnd = offsetFromEnd ?: GoogleXMP.getTrailingVideoOffsetFromEnd(xmpMeta)
// `GCamera` motion photo
xmpMeta.getSafeLong(XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
} else if (xmpMeta.doesPropExist(XMP.GCONTAINER_DIRECTORY_PROP_NAME)) {
// `Container` motion photo
val count = xmpMeta.countPropArrayItems(XMP.GCONTAINER_DIRECTORY_PROP_NAME)
for (i in 1 until count + 1) {
val mime = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = xmpMeta.getSafeStructField(listOf(XMP.GCONTAINER_DIRECTORY_PROP_NAME, i, XMP.GCONTAINER_ITEM_PROP_NAME, XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
if (MimeTypes.isVideo(mime) && length != null) {
offsetFromEnd = length.toLong()
}
}
}
} }
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
foundXmp = metadata.directories.any { it is XmpDirectory && it.tagCount > 0 } foundXmp = metadata.directories.any { it is XmpDirectory && it.tagCount > 0 }
metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach(::processXmp) metadata.getDirectoriesOfType(XmpDirectory::class.java).map { it.xmpMeta }.forEach(::processXmp)
} }
@ -203,10 +345,66 @@ object MultiPage {
return offsetFromEnd return offsetFromEnd
} }
private fun getMotionPhotoVideoInfo(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): MediaFormat? {
getMotionPhotoVideoSizing(context, uri, mimeType, sizeBytes)?.let { (videoOffset, videoSize) ->
return getEmbedVideoInfo(context, uri, videoOffset, videoSize)
}
return null
}
fun getTrailerVideoInfo(context: Context, uri: Uri, fileSize: Long, videoSize: Long): MediaFormat? {
return getEmbedVideoInfo(context, uri, videoOffset = fileSize - videoSize, videoSize = videoSize)
}
private fun getEmbedVideoInfo(context: Context, uri: Uri, videoOffset: Long, videoSize: Long): MediaFormat? {
val extractor = MediaExtractor()
var pfd: ParcelFileDescriptor? = null
try {
pfd = context.contentResolver.openFileDescriptor(uri, "r")
pfd?.fileDescriptor?.let { fd ->
extractor.setDataSource(fd, videoOffset, videoSize)
// video track may be after an audio track
for (trackIndex in 0 until extractor.trackCount) {
try {
val format = extractor.getTrackFormat(trackIndex)
format.getString(MediaFormat.KEY_MIME)?.let {
if (MimeTypes.isVideo(it)) {
return format
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get track information for uri=$uri, track num=$trackIndex", e)
}
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to open motion photo for uri=$uri", e)
} finally {
extractor.release()
pfd?.close()
}
return null
}
fun getMotionPhotoVideoSizing(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Pair<Long, Long>? {
// default to trailer videos
getTrailerVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSize ->
val videoOffset = sizeBytes - videoSize
return Pair(videoOffset, videoSize)
}
if (MimeTypes.isHeic(mimeType)) {
// fallback to video within Samsung SEFD box
return getHeicSefdMotionPhotoVideoSizing(context, uri)
}
return null
}
fun getTiffPages(context: Context, uri: Uri): ArrayList<FieldMap> { fun getTiffPages(context: Context, uri: Uri): ArrayList<FieldMap> {
fun toMap(page: Int, options: TiffBitmapFactory.Options): FieldMap { fun toMap(pageIndex: Int, options: TiffBitmapFactory.Options): FieldMap {
return hashMapOf( return hashMapOf(
KEY_PAGE to page, KEY_PAGE to pageIndex,
KEY_MIME_TYPE to MimeTypes.TIFF, KEY_MIME_TYPE to MimeTypes.TIFF,
KEY_WIDTH to options.outWidth, KEY_WIDTH to options.outWidth,
KEY_HEIGHT to options.outHeight, KEY_HEIGHT to options.outHeight,
@ -217,8 +415,8 @@ object MultiPage {
getTiffPageInfo(context, uri, 0)?.let { first -> getTiffPageInfo(context, uri, 0)?.let { first ->
pages.add(toMap(0, first)) pages.add(toMap(0, first))
val pageCount = first.outDirectoryCount val pageCount = first.outDirectoryCount
for (i in 1 until pageCount) { for (pageIndex in 1 until pageCount) {
getTiffPageInfo(context, uri, i)?.let { pages.add(toMap(i, it)) } getTiffPageInfo(context, uri, pageIndex)?.let { pages.add(toMap(pageIndex, it)) }
} }
} }
return pages return pages

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import android.content.Context
import android.net.Uri
import deckers.thibault.aves.metadata.Metadata.TYPE_COMMENT import deckers.thibault.aves.metadata.Metadata.TYPE_COMMENT
import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF
import deckers.thibault.aves.metadata.Metadata.TYPE_ICC_PROFILE import deckers.thibault.aves.metadata.Metadata.TYPE_ICC_PROFILE
@ -10,6 +12,8 @@ import deckers.thibault.aves.metadata.Metadata.TYPE_JPEG_DUCKY
import deckers.thibault.aves.metadata.Metadata.TYPE_PHOTOSHOP_IRB import deckers.thibault.aves.metadata.Metadata.TYPE_PHOTOSHOP_IRB
import deckers.thibault.aves.metadata.Metadata.TYPE_XMP import deckers.thibault.aves.metadata.Metadata.TYPE_XMP
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import pixy.meta.meta.Metadata import pixy.meta.meta.Metadata
import pixy.meta.meta.MetadataEntry import pixy.meta.meta.MetadataEntry
import pixy.meta.meta.MetadataType import pixy.meta.meta.MetadataType
@ -19,9 +23,9 @@ import pixy.meta.meta.iptc.IPTCRecord
import pixy.meta.meta.jpeg.JPGMeta import pixy.meta.meta.jpeg.JPGMeta
import pixy.meta.meta.xmp.XMP import pixy.meta.meta.xmp.XMP
import pixy.meta.string.XMLUtils import pixy.meta.string.XMLUtils
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.*
object PixyMetaHelper { object PixyMetaHelper {
fun describe(input: InputStream): HashMap<String, String> { fun describe(input: InputStream): HashMap<String, String> {
@ -77,17 +81,18 @@ object PixyMetaHelper {
output: OutputStream, output: OutputStream,
iptcDataList: List<FieldMap>?, iptcDataList: List<FieldMap>?,
) { ) {
val iptc = iptcDataList?.flatMap { val iptc: List<IPTCDataSet> = iptcDataList?.flatMap {
val record = it["record"] as Int val record = it["record"] as Int
val tag = it["tag"] as Int val tag = it["tag"] as Int
val values = it["values"] as List<*> val values = it["values"] as List<*>
values.map { data -> IPTCDataSet(IPTCRecord.fromRecordNumber(record), tag, data as ByteArray) } values.map { data -> IPTCDataSet(IPTCRecord.fromRecordNumber(record), tag, data as ByteArray) }
} ?: ArrayList<IPTCDataSet>() } ?: ArrayList()
Metadata.insertIPTC(input, output, iptc) Metadata.insertIPTC(input, output, iptc)
} }
fun getXmp(input: InputStream): XMP? = Metadata.readMetadata(input)[MetadataType.XMP] as XMP? 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( fun setXmp(
input: InputStream, input: InputStream,
output: OutputStream, output: OutputStream,
@ -105,6 +110,48 @@ object PixyMetaHelper {
fun XMP.extendedXmpDocString(): String = XMLUtils.serializeToString(extendedXmpDocument) fun XMP.extendedXmpDocString(): String = XMLUtils.serializeToString(extendedXmpDocument)
fun copyIptcXmp(
context: Context,
sourceMimeType: String,
sourceUri: Uri,
targetMimeType: String,
targetUri: Uri,
editableFile: File,
) {
var pixyIptc: IPTC? = null
var pixyXmp: XMP? = null
if (MimeTypes.canReadWithPixyMeta(sourceMimeType)) {
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
val metadata = Metadata.readMetadata(input)
if (MimeTypes.canEditIptc(targetMimeType)) {
pixyIptc = metadata[MetadataType.IPTC] as IPTC?
}
if (MimeTypes.canEditXmp(targetMimeType)) {
pixyXmp = metadata[MetadataType.XMP] as XMP?
}
}
}
if (pixyIptc != null || pixyXmp != null) {
editableFile.outputStream().use { output ->
if (pixyIptc != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val iptcs = pixyIptc!!.dataSets.flatMap { it.value }
Metadata.insertIPTC(input, output, iptcs)
}
}
if (pixyXmp != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val xmpString = pixyXmp!!.xmpDocString()
val extendedXmp = if (pixyXmp!!.hasExtendedXmp()) pixyXmp!!.extendedXmpDocString() else null
setXmp(input, output, xmpString, if (targetMimeType == MimeTypes.JPEG) extendedXmp else null)
}
}
}
}
}
fun removeMetadata(input: InputStream, output: OutputStream, metadataTypes: Set<String>) { fun removeMetadata(input: InputStream, output: OutputStream, metadataTypes: Set<String>) {
val types = metadataTypes.map(::toMetadataType).toTypedArray() val types = metadataTypes.map(::toMetadataType).toTypedArray()
Metadata.removeMetadata(input, output, *types) Metadata.removeMetadata(input, output, *types)

View file

@ -3,7 +3,6 @@ package deckers.thibault.aves.metadata
import deckers.thibault.aves.utils.toHex import deckers.thibault.aves.utils.toHex
import java.math.BigInteger import java.math.BigInteger
import java.nio.charset.Charset import java.nio.charset.Charset
import java.util.*
class QuickTimeMetadataBlock(val type: String, val value: String, val language: String) class QuickTimeMetadataBlock(val type: String, val value: String, val language: String)

View file

@ -1,6 +1,9 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata
import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVG
import java.io.BufferedInputStream
import java.io.InputStream
import kotlin.math.max
object SvgHelper { object SvgHelper {
fun SVG.normalizeSize() { fun SVG.normalizeSize() {
@ -10,4 +13,19 @@ object SvgHelper {
setDocumentWidth("100%") setDocumentWidth("100%")
setDocumentHeight("100%") setDocumentHeight("100%")
} }
} }
// As of AndroidSVG v1.4, SVGParser.ENTITY_WATCH_BUFFER_SIZE is set at 4096.
// This constant is not configurable and used for the internal buffer mark read limit.
// Parsing will fail if the SVG header is larger than this value.
// So we define and apply a minimum read limit.
class SVGParserBufferedInputStream(input: InputStream) : BufferedInputStream(input) {
@Synchronized
override fun mark(readlimit: Int) {
super.mark(max(MINIMUM_READ_LIMIT, readlimit))
}
companion object {
private const val MINIMUM_READ_LIMIT = 1 shl 14 // 16kB
}
}

View file

@ -27,6 +27,7 @@ import com.drew.metadata.xmp.XmpReader
import deckers.thibault.aves.metadata.ExifGeoTiffTags import deckers.thibault.aves.metadata.ExifGeoTiffTags
import deckers.thibault.aves.metadata.GeoTiffKeys import deckers.thibault.aves.metadata.GeoTiffKeys
import deckers.thibault.aves.metadata.Metadata import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpfReader
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.IOException import java.io.IOException
@ -58,19 +59,21 @@ object Helper {
// e.g. "exif [...] 134 [...] 4578696600004949[...]" // e.g. "exif [...] 134 [...] 4578696600004949[...]"
private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL) private val PNG_RAW_PROFILE_PATTERN = Regex("^\\n(.*?)\\n\\s*(\\d+)\\n(.*)", RegexOption.DOT_MATCHES_ALL)
// providing the stream length is risky, as it may crash if it is incorrect
private const val safeReadStreamLength = -1L
fun readMimeType(input: InputStream): String? { fun readMimeType(input: InputStream): String? {
val bufferedInputStream = if (input is BufferedInputStream) input else BufferedInputStream(input) val bufferedInputStream = if (input is BufferedInputStream) input else BufferedInputStream(input)
return FileTypeDetector.detectFileType(bufferedInputStream).mimeType return FileTypeDetector.detectFileType(bufferedInputStream).mimeType
} }
@Throws(IOException::class, ImageProcessingException::class) @Throws(IOException::class, ImageProcessingException::class)
fun safeRead(input: InputStream): com.drew.metadata.Metadata { fun safeRead(input: InputStream, @Suppress("unused_parameter") sizeBytes: Long?): com.drew.metadata.Metadata {
val inputStream = if (input is BufferedInputStream) input else BufferedInputStream(input) val inputStream = if (input is BufferedInputStream) input else BufferedInputStream(input)
val fileType = FileTypeDetector.detectFileType(inputStream) val fileType = FileTypeDetector.detectFileType(inputStream)
// Providing the stream length is risky, as it may crash if it is incorrect.
// Not providing the stream length is also risky, as it may lead to OOM
// when `RandomAccessStreamReader` reads the entire stream to validate offsets.
val undefinedStreamLength = -1L
val metadata = when (fileType) { val metadata = when (fileType) {
FileType.Jpeg -> safeReadJpeg(inputStream) FileType.Jpeg -> safeReadJpeg(inputStream)
FileType.Mp4 -> safeReadMp4(inputStream) FileType.Mp4 -> safeReadMp4(inputStream)
@ -81,9 +84,9 @@ object Helper {
FileType.Cr2, FileType.Cr2,
FileType.Nef, FileType.Nef,
FileType.Orf, FileType.Orf,
FileType.Rw2 -> safeReadTiff(inputStream) FileType.Rw2 -> safeReadTiff(inputStream, undefinedStreamLength)
else -> ImageMetadataReader.readMetadata(inputStream, safeReadStreamLength, fileType) else -> ImageMetadataReader.readMetadata(inputStream, undefinedStreamLength, fileType)
} }
metadata.addDirectory(FileTypeDirectory(fileType)) metadata.addDirectory(FileTypeDirectory(fileType))
@ -97,6 +100,7 @@ object Helper {
val readers = ArrayList<JpegSegmentMetadataReader>().apply { val readers = ArrayList<JpegSegmentMetadataReader>().apply {
addAll(JpegMetadataReader.ALL_READERS.filter { it !is XmpReader }) addAll(JpegMetadataReader.ALL_READERS.filter { it !is XmpReader })
add(SafeXmpReader()) add(SafeXmpReader())
add(MpfReader())
} }
val metadata = com.drew.metadata.Metadata() val metadata = com.drew.metadata.Metadata()
@ -113,8 +117,8 @@ object Helper {
} }
@Throws(IOException::class, TiffProcessingException::class) @Throws(IOException::class, TiffProcessingException::class)
fun safeReadTiff(input: InputStream): com.drew.metadata.Metadata { fun safeReadTiff(input: InputStream, streamLength: Long): com.drew.metadata.Metadata {
val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, safeReadStreamLength) val reader = RandomAccessStreamReader(input, RandomAccessStreamReader.DEFAULT_CHUNK_LENGTH, streamLength)
val metadata = com.drew.metadata.Metadata() val metadata = com.drew.metadata.Metadata()
val handler = SafeExifTiffHandler(metadata, null, 0) val handler = SafeExifTiffHandler(metadata, null, 0)
TiffReader().processTiff(reader, handler, 0) TiffReader().processTiff(reader, handler, 0)
@ -164,7 +168,7 @@ object Helper {
// This seems to cover all known Exif and Xmp date strings // This seems to cover all known Exif and Xmp date strings
// Note that " : : : : " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html // Note that " : : : : " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html
private val datePatterns = arrayOf( private val dateFormats = arrayOf(
"yyyy:MM:dd HH:mm:ss", "yyyy:MM:dd HH:mm:ss",
"yyyy:MM:dd HH:mm", "yyyy:MM:dd HH:mm",
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss",
@ -177,7 +181,7 @@ object Helper {
"yyyy-MM", "yyyy-MM",
"yyyyMMdd", // as used in IPTC data "yyyyMMdd", // as used in IPTC data
"yyyy" "yyyy"
) ).map { SimpleDateFormat(it, Locale.ROOT) }.toTypedArray()
private val subsecondPattern = Pattern.compile("(\\d\\d:\\d\\d:\\d\\d)(\\.\\d+)") private val subsecondPattern = Pattern.compile("(\\d\\d:\\d\\d:\\d\\d)(\\.\\d+)")
private val timeZonePattern = Pattern.compile("(Z|[+-]\\d\\d:\\d\\d|[+-]\\d\\d\\d\\d)$") private val timeZonePattern = Pattern.compile("(Z|[+-]\\d\\d:\\d\\d|[+-]\\d\\d\\d\\d)$")
private val calendar: Calendar = GregorianCalendar() private val calendar: Calendar = GregorianCalendar()
@ -208,11 +212,10 @@ object Helper {
effectiveTimeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replace("Z".toRegex(), "")) effectiveTimeZone = TimeZone.getTimeZone("GMT" + timeZoneMatcher.group().replace("Z".toRegex(), ""))
dateString = timeZoneMatcher.replaceAll("") dateString = timeZoneMatcher.replaceAll("")
} }
for (datePattern in datePatterns) { for (dateFormat in dateFormats) {
try { try {
val parsed = SimpleDateFormat(datePattern, Locale.ROOT).apply { dateFormat.timeZone = effectiveTimeZone ?: TimeZone.getTimeZone("GMT") // don't interpret zone time
this.timeZone = effectiveTimeZone ?: TimeZone.getTimeZone("GMT") // don't interpret zone time val parsed = dateFormat.parse(dateString)
}.parse(dateString)
if (parsed != null) { if (parsed != null) {
calendar.time = parsed calendar.time = parsed
if (calendar.get(Calendar.YEAR) < PARSED_DATE_YEAR_MAX) { if (calendar.get(Calendar.YEAR) < PARSED_DATE_YEAR_MAX) {
@ -293,9 +296,7 @@ object Helper {
if (!modelTiePoints && !modelTransformation) return false if (!modelTiePoints && !modelTransformation) return false
val modelPixelScale = this.containsTag(ExifGeoTiffTags.TAG_MODEL_PIXEL_SCALE) val modelPixelScale = this.containsTag(ExifGeoTiffTags.TAG_MODEL_PIXEL_SCALE)
if ((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiePoints)) return false return !((modelTransformation && modelPixelScale) || (modelPixelScale && !modelTiePoints))
return true
} }
// TODO TLAD use `GeoTiffDirectory` from the Java version of `metadata-extractor` when available // TODO TLAD use `GeoTiffDirectory` from the Java version of `metadata-extractor` when available

View file

@ -4,7 +4,7 @@ import com.drew.imaging.mp4.Mp4Handler
import com.drew.metadata.Metadata import com.drew.metadata.Metadata
import com.drew.metadata.mp4.Mp4Context import com.drew.metadata.mp4.Mp4Context
import com.drew.metadata.mp4.media.Mp4UuidBoxHandler import com.drew.metadata.mp4.media.Mp4UuidBoxHandler
import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.xmp.XMP
class SafeMp4UuidBoxHandler(metadata: Metadata) : Mp4UuidBoxHandler(metadata) { class SafeMp4UuidBoxHandler(metadata: Metadata) : Mp4UuidBoxHandler(metadata) {
override fun processBox(type: String?, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*> { override fun processBox(type: String?, payload: ByteArray?, boxSize: Long, context: Mp4Context?): Mp4Handler<*> {

View file

@ -31,6 +31,7 @@ import deckers.thibault.aves.utils.LogUtils
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.Locale
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
import java.util.zip.ZipException import java.util.zip.ZipException
@ -42,7 +43,7 @@ object SafePngMetadataReader {
private val LOG_TAG = LogUtils.createTag<SafePngMetadataReader>() private val LOG_TAG = LogUtils.createTag<SafePngMetadataReader>()
// arbitrary size to detect chunks that may yield an OOM // arbitrary size to detect chunks that may yield an OOM
private const val chunkSizeDangerThreshold = SafeXmpReader.SEGMENT_TYPE_SIZE_DANGER_THRESHOLD private const val CHUNK_SIZE_DANGER_THRESHOLD = SafeXmpReader.SEGMENT_TYPE_SIZE_DANGER_THRESHOLD
private val latin1Encoding = Charsets.ISO_8859_1 private val latin1Encoding = Charsets.ISO_8859_1
private val utf8Encoding = Charsets.UTF_8 private val utf8Encoding = Charsets.UTF_8
@ -85,7 +86,7 @@ object SafePngMetadataReader {
val bytes = chunk.bytes val bytes = chunk.bytes
// TLAD insert start // TLAD insert start
if (bytes.size > chunkSizeDangerThreshold) { if (bytes.size > CHUNK_SIZE_DANGER_THRESHOLD) {
Log.w(LOG_TAG, "PNG chunk $chunkType is too large, with a size of ${bytes.size} B") Log.w(LOG_TAG, "PNG chunk $chunkType is too large, with a size of ${bytes.size} B")
return return
} }
@ -159,7 +160,7 @@ object SafePngMetadataReader {
// Only compression method allowed by the spec is zero: deflate // Only compression method allowed by the spec is zero: deflate
if (compressionMethod.toInt() == 0) { if (compressionMethod.toInt() == 0) {
// bytes left for compressed text is: // bytes left for compressed text is:
// total bytes length - (profilenamebytes length + null byte + compression method byte) // total bytes length - (profileNameBytes length + null byte + compression method byte)
val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1) val bytesLeft = bytes.size - (profileNameBytes.size + 1 + 1)
val compressedProfile = reader.getBytes(bytesLeft) val compressedProfile = reader.getBytes(bytesLeft)
try { try {
@ -290,11 +291,12 @@ object SafePngMetadataReader {
val second = reader.uInt8.toInt() val second = reader.uInt8.toInt()
val directory = PngDirectory(PngChunkType.tIME) val directory = PngDirectory(PngChunkType.tIME)
if (DateUtil.isValidDate(year, month - 1, day) && DateUtil.isValidTime(hour, minute, second)) { if (DateUtil.isValidDate(year, month - 1, day) && DateUtil.isValidTime(hour, minute, second)) {
val dateString = String.format("%04d:%02d:%02d %02d:%02d:%02d", year, month, day, hour, minute, second) val dateString = String.format(Locale.ROOT, "%04d:%02d:%02d %02d:%02d:%02d", year, month, day, hour, minute, second)
directory.setString(PngDirectory.TAG_LAST_MODIFICATION_TIME, dateString) directory.setString(PngDirectory.TAG_LAST_MODIFICATION_TIME, dateString)
} else { } else {
directory.addError( directory.addError(
String.format( String.format(
Locale.ROOT,
"PNG tIME data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d", "PNG tIME data describes an invalid date/time: year=%d month=%d day=%d hour=%d minute=%d second=%d",
year, month, day, hour, minute, second year, month, day, hour, minute, second
) )

View file

@ -16,6 +16,7 @@ import com.drew.metadata.xmp.XmpDirectory
import com.drew.metadata.xmp.XmpReader import com.drew.metadata.xmp.XmpReader
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import java.io.IOException import java.io.IOException
import java.util.Locale
class SafeXmpReader : XmpReader() { class SafeXmpReader : XmpReader() {
// adapted from `XmpReader` to detect and skip large extended XMP // adapted from `XmpReader` to detect and skip large extended XMP
@ -133,7 +134,7 @@ class SafeXmpReader : XmpReader() {
System.arraycopy(segmentBytes, totalOffset, extendedXMPBuffer, chunkOffset, segmentLength - totalOffset) System.arraycopy(segmentBytes, totalOffset, extendedXMPBuffer, chunkOffset, segmentLength - totalOffset)
} else { } else {
val directory = XmpDirectory() val directory = XmpDirectory()
directory.addError(String.format("Inconsistent length for the Extended XMP buffer: %d instead of %d", fullLength, extendedXMPBuffer.size)) directory.addError(String.format(Locale.ROOT, "Inconsistent length for the Extended XMP buffer: %d instead of %d", fullLength, extendedXMPBuffer.size))
metadata.addDirectory(directory) metadata.addDirectory(directory)
} }
} }

View file

@ -0,0 +1,37 @@
package deckers.thibault.aves.metadata.metadataextractor.mpf
import deckers.thibault.aves.utils.MimeTypes
class MpEntry(val flags: Int, val format: Int, val type: Int, val size: Long, val dataOffset: Long, val dep1: Short, val dep2: Short) {
val mimeType: String?
get() = getMimeType(format)
val isThumbnail: Boolean
get() = when (type) {
TYPE_THUMBNAIL_VGA, TYPE_THUMBNAIL_FULL_HD -> true
else -> false
}
override fun toString(): String = "MpEntry#${hashCode()}{flags=$flags, format=$format, type=$type, size=$size, dataOffset=$dataOffset, dep1=$dep1, dep2=$dep2}"
companion object {
const val FLAG_REPRESENTATIVE = 1 shl 2
const val FLAG_DEPENDENT_CHILD = 1 shl 3
const val FLAG_DEPENDENT_PARENT = 1 shl 4
const val TYPE_PRIMARY = 0x030000
const val TYPE_THUMBNAIL_VGA = 0x010001
const val TYPE_THUMBNAIL_FULL_HD = 0x010002
const val TYPE_PANORAMA = 0x020001
const val TYPE_DISPARITY = 0x020002
const val TYPE_MULTI_ANGLE = 0x020003
const val TYPE_UNDEFINED = 0x000000
fun getMimeType(format: Int): String? {
return when (format) {
0 -> MimeTypes.JPEG
else -> null
}
}
}
}

View file

@ -0,0 +1,64 @@
package deckers.thibault.aves.metadata.metadataextractor.mpf
import com.drew.metadata.Directory
import com.drew.metadata.TagDescriptor
class MpEntryDirectory(val id: Int, val entry: MpEntry) : Directory() {
private val descriptor = MpEntryDescriptor(this)
init {
setDescriptor(descriptor)
}
fun describe(): Map<String, String> {
return HashMap<String, String>().apply {
put("Flags", descriptor.getFlagsDescription(entry.flags))
put("Format", descriptor.getFormatDescription(entry.format))
put("Type", descriptor.getTypeDescription(entry.type))
put("Size", "${entry.size} bytes")
put("Offset", "${entry.dataOffset} bytes")
put("Dependent Image 1 Entry Number", "${entry.dep1}")
put("Dependent Image 2 Entry Number", "${entry.dep2}")
}
}
override fun getName(): String {
return "MPF Image #$id"
}
override fun getTagNameMap(): HashMap<Int, String> {
return _tagNameMap
}
companion object {
private val _tagNameMap = HashMap<Int, String>()
}
}
class MpEntryDescriptor(directory: MpEntryDirectory?) : TagDescriptor<MpEntryDirectory>(directory) {
fun getFlagsDescription(flags: Int): String {
val flagStrings = ArrayList<String>().apply {
if (flags and MpEntry.FLAG_REPRESENTATIVE != 0) add("representative image")
if (flags and MpEntry.FLAG_DEPENDENT_CHILD != 0) add("dependent child image")
if (flags and MpEntry.FLAG_DEPENDENT_PARENT != 0) add("dependent parent image")
}
return if (flagStrings.isEmpty()) "none" else flagStrings.joinToString(", ")
}
fun getFormatDescription(format: Int): String {
return MpEntry.getMimeType(format) ?: "Unknown ($format)"
}
fun getTypeDescription(type: Int): String {
return when (type) {
MpEntry.TYPE_PRIMARY -> "Baseline MP Primary Image"
MpEntry.TYPE_THUMBNAIL_VGA -> "Large Thumbnail (VGA equivalent)"
MpEntry.TYPE_THUMBNAIL_FULL_HD -> "Large Thumbnail (full HD equivalent)"
MpEntry.TYPE_PANORAMA -> "Multi-frame Panorama"
MpEntry.TYPE_DISPARITY -> "Multi-frame Disparity"
MpEntry.TYPE_MULTI_ANGLE -> "Multi-angle"
MpEntry.TYPE_UNDEFINED -> "Undefined"
else -> "Unknown ($type)"
}
}
}

View file

@ -0,0 +1,36 @@
package deckers.thibault.aves.metadata.metadataextractor.mpf
import com.drew.metadata.Directory
import com.drew.metadata.TagDescriptor
class MpfDirectory : Directory() {
init {
setDescriptor(MpfDescriptor(this))
}
override fun getName(): String {
return "MPF"
}
override fun getTagNameMap(): HashMap<Int, String> {
return _tagNameMap
}
companion object {
const val TAG_MPF_VERSION = 0xb000
const val TAG_NUMBER_OF_IMAGES = 0xb001
const val TAG_MP_ENTRY = 0xb002
private const val TAG_IMAGE_UID_LIST = 0xb003
private const val TAG_TOTAL_FRAMES = 0xb004
private val _tagNameMap = HashMap<Int, String>().apply {
put(TAG_MPF_VERSION, "MPF Version")
put(TAG_NUMBER_OF_IMAGES, "Number Of Images")
put(TAG_MP_ENTRY, "MP Entry")
put(TAG_IMAGE_UID_LIST, "Image UID List")
put(TAG_TOTAL_FRAMES, "Total Frames")
}
}
}
class MpfDescriptor(directory: MpfDirectory?) : TagDescriptor<MpfDirectory>(directory)

View file

@ -0,0 +1,95 @@
package deckers.thibault.aves.metadata.metadataextractor.mpf
import android.util.Log
import com.drew.imaging.jpeg.JpegSegmentMetadataReader
import com.drew.imaging.jpeg.JpegSegmentType
import com.drew.lang.ByteArrayReader
import com.drew.lang.RandomAccessReader
import com.drew.metadata.Metadata
import com.drew.metadata.MetadataReader
import deckers.thibault.aves.utils.LogUtils
class MpfReader : JpegSegmentMetadataReader, MetadataReader {
override fun getSegmentTypes(): Iterable<JpegSegmentType> {
return listOf(JpegSegmentType.APP2)
}
override fun readJpegSegments(segments: Iterable<ByteArray>, metadata: Metadata, segmentType: JpegSegmentType) {
for (segmentBytes in segments) {
// Skip segments not starting with the required header
if (segmentBytes.size >= PREAMBLE.length && PREAMBLE == String(segmentBytes, 0, PREAMBLE.length)) {
extract(ByteArrayReader(segmentBytes), metadata)
}
}
}
override fun extract(reader: RandomAccessReader, metadata: Metadata) {
val directory = MpfDirectory()
metadata.addDirectory(directory)
val baseOffset = 4
// MP Format Identifier (4Byte)
// MP header
// - MP Endian (4Byte)
val byteOrderIdentifier = reader.getInt16(baseOffset)
if (byteOrderIdentifier.toInt() == 0x4d4d) { // "MM"
reader.isMotorolaByteOrder = true
} else if (byteOrderIdentifier.toInt() == 0x4949) { // "II"
reader.isMotorolaByteOrder = false
}
// - Offset to First IFD (4Byte)
val firstIfdOffset = reader.getInt32(baseOffset + 4)
// [in primary image only] MP Index IFD:
// - Count (2Byte)
var offset = baseOffset + firstIfdOffset
val tagCount = reader.getInt16(offset)
offset += 2
// - MP Index Fields (Overall Structure Info.)
var imageCount = 0
for (tag in 0..<tagCount) {
when (val tagId = reader.getUInt16(offset)) {
MpfDirectory.TAG_MPF_VERSION -> directory.setString(tagId, reader.getString(offset + 8, 4, Charsets.US_ASCII))
MpfDirectory.TAG_NUMBER_OF_IMAGES -> {
imageCount = reader.getInt32(offset + 8)
directory.setInt(tagId, imageCount)
}
MpfDirectory.TAG_MP_ENTRY -> {
var mpEntryOffset = baseOffset + reader.getInt32(offset + 8)
for (index in 0..<imageCount) {
// individual image
val attribute = reader.getUInt32(mpEntryOffset)
val flags = (attribute shr 27 and 0x1f).toInt()
val format = (attribute shr 24 and 0x7).toInt()
val type = (attribute and 0xffffff).toInt()
val size = reader.getUInt32(mpEntryOffset + 4)
val dataOffset = reader.getUInt32(mpEntryOffset + 8)
val dep1 = reader.getInt16(mpEntryOffset + 12)
val dep2 = reader.getInt16(mpEntryOffset + 14)
metadata.addDirectory(MpEntryDirectory(index + 1, MpEntry(flags, format, type, size, dataOffset, dep1, dep2)))
mpEntryOffset += 16
}
}
else -> Log.d(LOG_TAG, "unknown tag=$tagId")
}
offset += 12
}
// - Offset of Next IFD (4Byte)
// Value (MP Index IFD)
// [in primary & other images] MP Attributes IFD:
// - Count (2Byte)
// - MP Attribute Fields (Details of Specific Image Usage)
// - Offset of Next IFD
// Value (MP Attribute IFD)
}
companion object {
private val LOG_TAG = LogUtils.createTag<MpfReader>()
private const val PREAMBLE = "MPF"
}
}

View file

@ -1,10 +1,11 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata.xmp
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.adobe.internal.xmp.XMPMeta import com.adobe.internal.xmp.XMPMeta
import deckers.thibault.aves.metadata.XMP.countPropPathArrayItems import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.XMP.getSafeStructField import deckers.thibault.aves.metadata.xmp.XMP.countPropPathArrayItems
import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
import deckers.thibault.aves.utils.indexOfBytes import deckers.thibault.aves.utils.indexOfBytes
import java.io.DataInputStream import java.io.DataInputStream
@ -15,12 +16,12 @@ class GoogleDeviceContainer {
private val offsets: MutableList<Int> = ArrayList() private val offsets: MutableList<Int> = ArrayList()
fun findItems(xmpMeta: XMPMeta) { fun findItems(xmpMeta: XMPMeta) {
val containerDirectoryPath = listOf(XMP.GDEVICE_CONTAINER_PROP_NAME, XMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME) val containerDirectoryPath = listOf(GoogleXMP.GDEVICE_CONTAINER_PROP_NAME, GoogleXMP.GDEVICE_CONTAINER_DIRECTORY_PROP_NAME)
val count = xmpMeta.countPropPathArrayItems(containerDirectoryPath) val count = xmpMeta.countPropPathArrayItems(containerDirectoryPath)
for (i in 1 until count + 1) { for (i in 1 until count + 1) {
val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value val mimeType = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull() val length = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME))?.value?.toLongOrNull()
val dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, XMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value val dataUri = xmpMeta.getSafeStructField(containerDirectoryPath + listOf(i, GoogleXMP.GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME))?.value
if (mimeType != null && length != null && dataUri != null) { if (mimeType != null && length != null && dataUri != null) {
items.add( items.add(
GoogleDeviceContainerItem( GoogleDeviceContainerItem(

View file

@ -0,0 +1,205 @@
package deckers.thibault.aves.metadata.xmp
import android.util.Log
import com.adobe.internal.xmp.XMPError
import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMeta
import deckers.thibault.aves.metadata.xmp.XMP.countPropArrayItems
import deckers.thibault.aves.metadata.xmp.XMP.doesPropExist
import deckers.thibault.aves.metadata.xmp.XMP.doesPropPathExist
import deckers.thibault.aves.metadata.xmp.XMP.getSafeInt
import deckers.thibault.aves.metadata.xmp.XMP.getSafeLong
import deckers.thibault.aves.metadata.xmp.XMP.getSafeString
import deckers.thibault.aves.metadata.xmp.XMP.getSafeStructField
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
object GoogleXMP {
private val LOG_TAG = LogUtils.createTag<GoogleXMP>()
// namespaces
private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/"
private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/"
private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/"
private const val GDEVICE_CONTAINER_NS_URI = "http://ns.google.com/photos/dd/1.0/container/"
private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
// embedded media data properties
// cf https://developers.google.com/depthmap-metadata
// cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
private val knownDataProps = listOf(
XMPPropName(GAUDIO_NS_URI, "Data"),
XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"),
XMPPropName(GIMAGE_NS_URI, "Data"),
XMPPropName(GDEPTH_NS_URI, "Data"),
XMPPropName(GDEPTH_NS_URI, "Confidence"),
)
fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it }
// google portrait
val GDEVICE_CONTAINER_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container")
val GDEVICE_CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_CONTAINER_NS_URI, "Directory")
val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI")
val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
// container
private val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
private val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
private val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
private val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
private val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
private val GCONTAINER_ITEM_SEMANTIC_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Semantic")
private const val ITEM_SEMANTIC_GAIN_MAP = "GainMap"
// panorama
// cf https://developers.google.com/streetview/spherical-metadata
private val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageHeightPixels")
private val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageWidthPixels")
private val GPANO_CROPPED_AREA_LEFT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaLeftPixels")
private val GPANO_CROPPED_AREA_TOP_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaTopPixels")
private val GPANO_FULL_PANO_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoHeightPixels")
private val GPANO_FULL_PANO_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoWidthPixels")
private val GPANO_PROJECTION_TYPE_PROP_NAME = XMPPropName(GPANO_NS_URI, "ProjectionType")
const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular"
// `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default
// `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode)
private val gpanoRequiredProps = listOf(
GPANO_CROPPED_AREA_HEIGHT_PROP_NAME,
GPANO_CROPPED_AREA_WIDTH_PROP_NAME,
GPANO_CROPPED_AREA_LEFT_PROP_NAME,
GPANO_CROPPED_AREA_TOP_PROP_NAME,
GPANO_FULL_PANO_WIDTH_PROP_NAME,
)
fun isUltraHdPhoto(meta: XMPMeta): Boolean {
if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
for (i in 1 until count + 1) {
val semantic = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_SEMANTIC_PROP_NAME))?.value
if (semantic == ITEM_SEMANTIC_GAIN_MAP) {
return true
}
}
}
return false
}
fun isMotionPhoto(meta: XMPMeta): Boolean {
try {
// GCamera motion photo
if (meta.doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true
// Container motion photo
if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
var hasImage = false
var hasVideo = false
for (i in 1 until count + 1) {
val mime = getContainerItemAttribute(meta, i, GCONTAINER_ITEM_MIME_PROP_NAME)
val length = getContainerItemAttribute(meta, i, GCONTAINER_ITEM_LENGTH_PROP_NAME)
// `length` is not always provided for the image item
hasImage = hasImage || MimeTypes.isImage(mime)
hasVideo = hasVideo || (MimeTypes.isVideo(mime) && length != null)
}
if (hasImage && hasVideo) return true
}
return false
} catch (e: XMPException) {
if (e.errorCode != XMPError.BADSCHEMA) {
// `BADSCHEMA` code is reported when we check a property
// from a non standard namespace, and that namespace is not declared in the XMP
Log.w(LOG_TAG, "failed to check Google motion photo props from XMP", e)
}
}
return false
}
private fun getContainerItemAttribute(meta: XMPMeta, i: Int, attribute: XMPPropName): String? {
// variant of `Container:Item` with `<rdf:li rdf:parseType="Resource">`
val mime = meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, attribute))?.value
// variant of `Container:Item` with `<rdf:li>`
return mime ?: meta.getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, attribute))?.value
}
fun isPanorama(meta: XMPMeta): Boolean {
try {
if (gpanoRequiredProps.all { meta.doesPropExist(it) }) return true
} catch (e: XMPException) {
if (e.errorCode != XMPError.BADSCHEMA) {
// `BADSCHEMA` code is reported when we check a property
// from a non standard namespace, and that namespace is not declared in the XMP
Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e)
}
}
return false
}
fun getPanoramaInfo(meta: XMPMeta): FieldMap {
val fields: FieldMap = hashMapOf()
try {
meta.getSafeInt(GPANO_CROPPED_AREA_LEFT_PROP_NAME) { fields["croppedAreaLeft"] = it }
meta.getSafeInt(GPANO_CROPPED_AREA_TOP_PROP_NAME) { fields["croppedAreaTop"] = it }
meta.getSafeInt(GPANO_CROPPED_AREA_WIDTH_PROP_NAME) { fields["croppedAreaWidth"] = it }
meta.getSafeInt(GPANO_CROPPED_AREA_HEIGHT_PROP_NAME) { fields["croppedAreaHeight"] = it }
meta.getSafeInt(GPANO_FULL_PANO_WIDTH_PROP_NAME) { fields["fullPanoWidth"] = it }
meta.getSafeInt(GPANO_FULL_PANO_HEIGHT_PROP_NAME) { fields["fullPanoHeight"] = it }
meta.getSafeString(GPANO_PROJECTION_TYPE_PROP_NAME) { fields["projectionType"] = it }
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory", e)
}
return fields
}
fun getTrailingVideoOffsetFromEnd(meta: XMPMeta): Long? {
var offsetFromEnd: Long? = null
if (meta.doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) {
// `GCamera` motion photo
meta.getSafeLong(GCAMERA_VIDEO_OFFSET_PROP_NAME) { offsetFromEnd = it }
} else if (meta.doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) {
// `Container` motion photo
val count = meta.countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
for (i in 1 until count + 1) {
val mime = getContainerItemAttribute(meta, i, GCONTAINER_ITEM_MIME_PROP_NAME)
if (MimeTypes.isVideo(mime)) {
getContainerItemAttribute(meta, i, GCONTAINER_ITEM_LENGTH_PROP_NAME)?.let { offsetFromEnd = it.toLong() }
}
}
}
return offsetFromEnd
}
fun updateTrailingVideoOffset(xmp: String, oldOffset: Number, newOffset: Number): String {
return xmp.replace(
// GCamera motion photo
"${GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$oldOffset\"",
"${GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newOffset\"",
).replace(
// Container motion photo
"${GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$oldOffset\"",
"${GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newOffset\"",
)
}
fun getDeviceContainer(meta: XMPMeta): GoogleDeviceContainer? {
return if (meta.doesPropPathExist(listOf(GDEVICE_CONTAINER_PROP_NAME, GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) {
GoogleDeviceContainer().apply { findItems(meta) }
} else {
null
}
}
}

View file

@ -1,4 +1,4 @@
package deckers.thibault.aves.metadata package deckers.thibault.aves.metadata.xmp
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
@ -11,6 +11,7 @@ import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory import com.adobe.internal.xmp.XMPMetaFactory
import com.adobe.internal.xmp.properties.XMPProperty import com.adobe.internal.xmp.properties.XMPProperty
import com.drew.metadata.Directory import com.drew.metadata.Directory
import deckers.thibault.aves.metadata.Mp4ParserHelper
import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler
@ -39,16 +40,8 @@ object XMP {
private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/" private const val XMP_NS_URI = "http://ns.adobe.com/xap/1.0/"
// other namespaces // other namespaces
private const val GAUDIO_NS_URI = "http://ns.google.com/photos/1.0/audio/" private const val APPLE_HDRGM_NS_URI = "http://ns.apple.com/HDRGainMap/1.0/"
private const val GCAMERA_NS_URI = "http://ns.google.com/photos/1.0/camera/" private const val HDRGM_NS_URI = "http://ns.adobe.com/hdr-gain-map/1.0/"
private const val GCONTAINER_NS_URI = "http://ns.google.com/photos/1.0/container/"
private const val GCONTAINER_ITEM_NS_URI = "http://ns.google.com/photos/1.0/container/item/"
private const val GDEPTH_NS_URI = "http://ns.google.com/photos/1.0/depthmap/"
private const val GDEVICE_NS_URI = "http://ns.google.com/photos/dd/1.0/device/"
private const val GDEVICE_CONTAINER_NS_URI = "http://ns.google.com/photos/dd/1.0/container/"
private const val GDEVICE_ITEM_NS_URI = "http://ns.google.com/photos/dd/1.0/item/"
private const val GIMAGE_NS_URI = "http://ns.google.com/photos/1.0/image/"
private const val GPANO_NS_URI = "http://ns.google.com/photos/1.0/panorama/"
private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01" private const val PMTM_NS_URI = "http://www.hdrsoft.com/photomatix_settings01"
val DC_SUBJECT_PROP_NAME = XMPPropName(DC_NS_URI, "subject") val DC_SUBJECT_PROP_NAME = XMPPropName(DC_NS_URI, "subject")
@ -62,59 +55,17 @@ object XMP {
private const val GENERIC_LANG = "" private const val GENERIC_LANG = ""
private const val SPECIFIC_LANG = "en-US" private const val SPECIFIC_LANG = "en-US"
// embedded media data properties fun isDataPath(path: String) = GoogleXMP.isDataPath(path)
// cf https://developers.google.com/depthmap-metadata
// cf https://developers.google.com/vr/reference/cardboard-camera-vr-photo-format
private val knownDataProps = listOf(
XMPPropName(GAUDIO_NS_URI, "Data"),
XMPPropName(GCAMERA_NS_URI, "RelitInputImageData"),
XMPPropName(GIMAGE_NS_URI, "Data"),
XMPPropName(GDEPTH_NS_URI, "Data"),
XMPPropName(GDEPTH_NS_URI, "Confidence"),
)
fun isDataPath(path: String) = knownDataProps.map { it.toString() }.any { path == it } // HDR gain map
// google portrait private val HDRGM_VERSION_PROP_NAME = XMPPropName(HDRGM_NS_URI, "Version")
private val APPLE_HDRGM_VERSION_PROP_NAME = XMPPropName(APPLE_HDRGM_NS_URI, "HDRGainMapVersion")
val GDEVICE_CONTAINER_PROP_NAME = XMPPropName(GDEVICE_NS_URI, "Container")
val GDEVICE_CONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GDEVICE_CONTAINER_NS_URI, "Directory")
val GDEVICE_CONTAINER_ITEM_DATA_URI_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "DataURI")
val GDEVICE_CONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Length")
val GDEVICE_CONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GDEVICE_ITEM_NS_URI, "Mime")
// motion photo
val GCAMERA_VIDEO_OFFSET_PROP_NAME = XMPPropName(GCAMERA_NS_URI, "MicroVideoOffset")
val GCONTAINER_DIRECTORY_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Directory")
val GCONTAINER_ITEM_PROP_NAME = XMPPropName(GCONTAINER_NS_URI, "Item")
val GCONTAINER_ITEM_LENGTH_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Length")
val GCONTAINER_ITEM_MIME_PROP_NAME = XMPPropName(GCONTAINER_ITEM_NS_URI, "Mime")
// panorama // panorama
// cf https://developers.google.com/streetview/spherical-metadata
val GPANO_CROPPED_AREA_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageHeightPixels")
val GPANO_CROPPED_AREA_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaImageWidthPixels")
val GPANO_CROPPED_AREA_LEFT_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaLeftPixels")
val GPANO_CROPPED_AREA_TOP_PROP_NAME = XMPPropName(GPANO_NS_URI, "CroppedAreaTopPixels")
val GPANO_FULL_PANO_HEIGHT_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoHeightPixels")
val GPANO_FULL_PANO_WIDTH_PROP_NAME = XMPPropName(GPANO_NS_URI, "FullPanoWidthPixels")
val GPANO_PROJECTION_TYPE_PROP_NAME = XMPPropName(GPANO_NS_URI, "ProjectionType")
const val GPANO_PROJECTION_TYPE_DEFAULT = "equirectangular"
private val PMTM_IS_PANO360_PROP_NAME = XMPPropName(PMTM_NS_URI, "IsPano360") private val PMTM_IS_PANO360_PROP_NAME = XMPPropName(PMTM_NS_URI, "IsPano360")
// `GPano:ProjectionType` is required by spec but it is sometimes missing, assuming default
// `GPano:FullPanoHeightPixels` is required by spec but it is sometimes missing (e.g. Samsung Camera app panorama mode)
private val gpanoRequiredProps = listOf(
GPANO_CROPPED_AREA_HEIGHT_PROP_NAME,
GPANO_CROPPED_AREA_WIDTH_PROP_NAME,
GPANO_CROPPED_AREA_LEFT_PROP_NAME,
GPANO_CROPPED_AREA_TOP_PROP_NAME,
GPANO_FULL_PANO_WIDTH_PROP_NAME,
)
// as of `metadata-extractor` v2.18.0, XMP is not discovered in HEIC images, // as of `metadata-extractor` v2.18.0, XMP is not discovered in HEIC images,
// so we fall back to the native content resolver, if possible // so we fall back to the native content resolver, if possible
fun checkHeic( fun checkHeic(
@ -180,47 +131,33 @@ object XMP {
// extensions // extensions
fun XMPMeta.isMotionPhoto(): Boolean { fun XMPMeta.hasHdrGainMap(): Boolean {
try { try {
// GCamera motion photo // standard HDR gain map
if (doesPropExist(GCAMERA_VIDEO_OFFSET_PROP_NAME)) return true if (doesPropExist(HDRGM_VERSION_PROP_NAME)) return true
// Container motion photo // `Ultra HDR`
if (doesPropExist(GCONTAINER_DIRECTORY_PROP_NAME)) { if (GoogleXMP.isUltraHdPhoto(this)) return true
val count = countPropArrayItems(GCONTAINER_DIRECTORY_PROP_NAME)
var hasImage = false // Apple HDR gain map
var hasVideo = false if (doesPropExist(APPLE_HDRGM_VERSION_PROP_NAME)) return true
for (i in 1 until count + 1) {
val mime = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_MIME_PROP_NAME))?.value
val length = getSafeStructField(listOf(GCONTAINER_DIRECTORY_PROP_NAME, i, GCONTAINER_ITEM_PROP_NAME, GCONTAINER_ITEM_LENGTH_PROP_NAME))?.value
hasImage = hasImage || MimeTypes.isImage(mime) && length != null
hasVideo = hasVideo || MimeTypes.isVideo(mime) && length != null
}
if (hasImage && hasVideo) return true
}
return false return false
} catch (e: XMPException) { } catch (e: XMPException) {
if (e.errorCode != XMPError.BADSCHEMA) { if (e.errorCode != XMPError.BADSCHEMA) {
// `BADSCHEMA` code is reported when we check a property // `BADSCHEMA` code is reported when we check a property
// from a non standard namespace, and that namespace is not declared in the XMP // from a non standard namespace, and that namespace is not declared in the XMP
Log.w(LOG_TAG, "failed to check Google motion photo props from XMP", e) Log.w(LOG_TAG, "failed to check HDR props from XMP", e)
} }
} }
return false return false
} }
fun XMPMeta.isMotionPhoto() = GoogleXMP.isMotionPhoto(this)
fun XMPMeta.isPanorama(): Boolean { fun XMPMeta.isPanorama(): Boolean {
// Google // Google
try { if (GoogleXMP.isPanorama(this)) return true
if (gpanoRequiredProps.all { doesPropExist(it) }) return true
} catch (e: XMPException) {
if (e.errorCode != XMPError.BADSCHEMA) {
// `BADSCHEMA` code is reported when we check a property
// from a non standard namespace, and that namespace is not declared in the XMP
Log.w(LOG_TAG, "failed to check Google panorama props from XMP", e)
}
}
// Photomatix // Photomatix
try { try {

View file

@ -1,23 +1,30 @@
package deckers.thibault.aves.model package deckers.thibault.aves.model
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
class AvesEntry(map: FieldMap) { class AvesEntry(map: FieldMap) {
val uri: Uri = Uri.parse(map["uri"] as String) // content or file URI val uri: Uri = (map[EntryFields.URI] as String).toUri() // content or file URI
val path = map["path"] as String? // best effort to get local path val path = map[EntryFields.PATH] as String? // best effort to get local path
val pageId = map["pageId"] as Int? // null means the main entry val pageId = map[EntryFields.PAGE_ID] as Int? // null means the main entry
val mimeType = map["mimeType"] as String val mimeType = map[EntryFields.MIME_TYPE] as String
val width = map["width"] as Int val width = map[EntryFields.WIDTH] as Int
val height = map["height"] as Int val height = map[EntryFields.HEIGHT] as Int
val rotationDegrees = map["rotationDegrees"] as Int val rotationDegrees = map[EntryFields.ROTATION_DEGREES] as Int
val isFlipped = map["isFlipped"] as Boolean val isFlipped = map[EntryFields.IS_FLIPPED] as Boolean
val sizeBytes = toLong(map["sizeBytes"]) val sizeBytes = toLong(map[EntryFields.SIZE_BYTES])
val trashed = map["trashed"] as Boolean val trashed = map[EntryFields.TRASHED] as Boolean
val trashPath = map["trashPath"] as String? val trashPath = map[EntryFields.TRASH_PATH] as String?
val isRotated: Boolean private val isRotated: Boolean
get() = rotationDegrees % 180 == 90 get() = rotationDegrees % 180 == 90
val displayWidth: Int
get() = if (isRotated) height else width
val displayHeight: Int
get() = if (isRotated) width else height
companion object { companion object {
// convenience method // convenience method
private fun toLong(o: Any?): Long? = when (o) { private fun toLong(o: Any?): Long? = when (o) {

View file

@ -0,0 +1,29 @@
package deckers.thibault.aves.model
// entry fields exported and imported from/to the platform side
// should match `EntryFields` on Dart side
object EntryFields {
const val ORIGIN = "origin" // int
const val URI = "uri" // string
const val CONTENT_ID = "contentId" // long
const val PATH = "path" // string
const val PAGE_ID = "pageId" // int
const val SOURCE_MIME_TYPE = "sourceMimeType" // string
const val MIME_TYPE = "mimeType" // string
const val WIDTH = "width" // int
const val HEIGHT = "height" // int
const val SOURCE_ROTATION_DEGREES = "sourceRotationDegrees" // int
const val ROTATION_DEGREES = "rotationDegrees" // int
const val IS_FLIPPED = "isFlipped" // boolean
const val DATE_ADDED_SECS = "dateAddedSecs" // long
const val DATE_MODIFIED_MILLIS = "dateModifiedMillis" // long
const val SOURCE_DATE_TAKEN_MILLIS = "sourceDateTakenMillis" // long
const val DURATION_MILLIS = "durationMillis" // long
const val SIZE_BYTES = "sizeBytes" // long
const val TRASHED = "trashed" // boolean
const val TRASH_PATH = "trashPath" // string
const val TITLE = "title" // string
}

View file

@ -1,5 +1,7 @@
package deckers.thibault.aves.model package deckers.thibault.aves.model
import java.io.File
enum class NameConflictStrategy { enum class NameConflictStrategy {
RENAME, REPLACE, SKIP; RENAME, REPLACE, SKIP;
@ -9,4 +11,6 @@ enum class NameConflictStrategy {
return valueOf(name.uppercase()) return valueOf(name.uppercase())
} }
} }
} }
class NameConflictResolution(var nameWithoutExtension: String?, var replacementFile: File?)

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import androidx.exifinterface.media.ExifInterface import androidx.core.net.toUri
import com.drew.metadata.avi.AviDirectory import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory import com.drew.metadata.jpeg.JpegDirectory
@ -29,6 +29,7 @@ import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId import deckers.thibault.aves.utils.UriUtils.tryParseId
import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.IOException import java.io.IOException
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
class SourceEntry { class SourceEntry {
private val origin: Int private val origin: Int
@ -41,7 +42,7 @@ class SourceEntry {
private var sourceRotationDegrees: Int? = null private var sourceRotationDegrees: Int? = null
private var sizeBytes: Long? = null private var sizeBytes: Long? = null
private var dateAddedSecs: Long? = null private var dateAddedSecs: Long? = null
private var dateModifiedSecs: Long? = null private var dateModifiedMillis: Long? = null
private var sourceDateTakenMillis: Long? = null private var sourceDateTakenMillis: Long? = null
private var durationMillis: Long? = null private var durationMillis: Long? = null
@ -54,45 +55,45 @@ class SourceEntry {
} }
constructor(map: FieldMap) { constructor(map: FieldMap) {
origin = map["origin"] as Int origin = map[EntryFields.ORIGIN] as Int
uri = Uri.parse(map["uri"] as String) uri = (map[EntryFields.URI] as String).toUri()
path = map["path"] as String? path = map[EntryFields.PATH] as String?
sourceMimeType = map["sourceMimeType"] as String sourceMimeType = map[EntryFields.SOURCE_MIME_TYPE] as String
width = map["width"] as Int? width = map[EntryFields.WIDTH] as Int?
height = map["height"] as Int? height = map[EntryFields.HEIGHT] as Int?
sourceRotationDegrees = map["sourceRotationDegrees"] as Int? sourceRotationDegrees = map[EntryFields.SOURCE_ROTATION_DEGREES] as Int?
sizeBytes = toLong(map["sizeBytes"]) sizeBytes = toLong(map[EntryFields.SIZE_BYTES])
title = map["title"] as String? title = map[EntryFields.TITLE] as String?
dateAddedSecs = toLong(map["dateAddedSecs"]) dateAddedSecs = toLong(map[EntryFields.DATE_ADDED_SECS])
dateModifiedSecs = toLong(map["dateModifiedSecs"]) dateModifiedMillis = toLong(map[EntryFields.DATE_MODIFIED_MILLIS])
sourceDateTakenMillis = toLong(map["sourceDateTakenMillis"]) sourceDateTakenMillis = toLong(map[EntryFields.SOURCE_DATE_TAKEN_MILLIS])
durationMillis = toLong(map["durationMillis"]) durationMillis = toLong(map[EntryFields.DURATION_MILLIS])
} }
fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedSecs: Long) { fun initFromFile(path: String, title: String, sizeBytes: Long, dateModifiedMillis: Long) {
this.path = path this.path = path
this.title = title this.title = title
this.sizeBytes = sizeBytes this.sizeBytes = sizeBytes
this.dateModifiedSecs = dateModifiedSecs this.dateModifiedMillis = dateModifiedMillis
} }
fun toMap(): FieldMap { fun toMap(): FieldMap {
return hashMapOf( return hashMapOf(
"origin" to origin, EntryFields.ORIGIN to origin,
"uri" to uri.toString(), EntryFields.URI to uri.toString(),
"path" to path, EntryFields.PATH to path,
"sourceMimeType" to sourceMimeType, EntryFields.SOURCE_MIME_TYPE to sourceMimeType,
"width" to width, EntryFields.WIDTH to width,
"height" to height, EntryFields.HEIGHT to height,
"sourceRotationDegrees" to (sourceRotationDegrees ?: 0), EntryFields.SOURCE_ROTATION_DEGREES to (sourceRotationDegrees ?: 0),
"sizeBytes" to sizeBytes, EntryFields.SIZE_BYTES to sizeBytes,
"title" to title, EntryFields.TITLE to title,
"dateAddedSecs" to dateAddedSecs, EntryFields.DATE_ADDED_SECS to dateAddedSecs,
"dateModifiedSecs" to dateModifiedSecs, EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
"sourceDateTakenMillis" to sourceDateTakenMillis, EntryFields.SOURCE_DATE_TAKEN_MILLIS to sourceDateTakenMillis,
"durationMillis" to durationMillis, EntryFields.DURATION_MILLIS to durationMillis,
// only for map export // only for map export
"contentId" to contentId, EntryFields.CONTENT_ID to contentId,
) )
} }
@ -163,7 +164,7 @@ class SourceEntry {
try { try {
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input ->
val metadata = Helper.safeRead(input) val metadata = Helper.safeRead(input, sizeBytes)
// do not switch on specific MIME types, as the reported MIME type could be wrong // do not switch on specific MIME types, as the reported MIME type could be wrong
// (e.g. PNG registered as JPG) // (e.g. PNG registered as JPG)

View file

@ -0,0 +1,18 @@
package deckers.thibault.aves.model.provider
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import java.util.Locale
class AvesEmbeddedMediaProvider : UnknownContentProvider() {
override val reliableProviderMimeType: Boolean
get() = true
companion object {
fun provides(context: Context, uri: Uri): Boolean {
if (uri.scheme?.lowercase(Locale.ROOT) != ContentResolver.SCHEME_CONTENT) return false
return uri.authority == "${context.applicationContext.packageName}.file_provider"
}
}
}

View file

@ -1,80 +0,0 @@
package deckers.thibault.aves.model.provider
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
internal class ContentImageProvider : ImageProvider() {
override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) {
// source MIME type may be incorrect, so we get a second opinion if possible
var extractorMimeType: String? = null
try {
val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri))
StorageUtils.openInputStream(context, safeUri)?.use { input ->
// `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives)
// cf https://github.com/drewnoakes/metadata-extractor/issues/296
Helper.readMimeType(input)?.takeIf { it != MimeTypes.TIFF }?.let {
extractorMimeType = it
if (extractorMimeType != sourceMimeType) {
Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri")
}
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
}
val mimeType = extractorMimeType ?: sourceMimeType
val fields: FieldMap = hashMapOf(
"origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT,
"uri" to uri.toString(),
"sourceMimeType" to mimeType,
)
try {
// some providers do not provide the mandatory `OpenableColumns`
// and the query fails when compiling a projection specifying them
// e.g. `content://mms/part/[id]` on Android KitKat
val cursor = context.contentResolver.query(uri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) }
cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields["sizeBytes"] = cursor.getLong(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATA).let { if (it != -1) fields["path"] = cursor.getString(it) }
// mime type fallback if it was not provided and not found via `metadata-extractor`
cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE).let { if (it != -1 && mimeType == null) fields["sourceMimeType"] = cursor.getString(it) }
cursor.close()
}
} catch (e: Exception) {
callback.onFailure(Exception("Failed to query content, error=${e.message}"))
return
}
if (fields["sourceMimeType"] == null) {
callback.onFailure(Exception("Failed to find MIME type for uri=$uri"))
return
}
val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
if (entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())
} else {
callback.onFailure(Exception("entry has no size"))
}
}
companion object {
private val LOG_TAG = LogUtils.createTag<ContentImageProvider>()
}
}

View file

@ -6,18 +6,27 @@ import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import java.io.File import java.io.File
internal class FileImageProvider : ImageProvider() { internal class FileImageProvider : ImageProvider() {
override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) {
var mimeType = sourceMimeType var mimeType = sourceMimeType
if (mimeType == null) { if (mimeType == null) {
val extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString()) var extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
if (extension != null) { if (extension.isEmpty()) {
uri.path?.let { path ->
val lastDotIndex = path.lastIndexOf('.')
if (lastDotIndex >= 0) {
extension = path.substring(lastDotIndex + 1)
}
}
}
if (extension.isNotEmpty()) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
} }
} }
@ -37,16 +46,17 @@ internal class FileImageProvider : ImageProvider() {
path = path, path = path,
title = file.name, title = file.name,
sizeBytes = file.length(), sizeBytes = file.length(),
dateModifiedSecs = file.lastModified() / 1000, dateModifiedMillis = file.lastModified(),
) )
} }
} catch (e: SecurityException) { } catch (e: SecurityException) {
callback.onFailure(e) callback.onFailure(e)
return
} }
} }
entry.fillPreCatalogMetadata(context) entry.fillPreCatalogMetadata(context)
if (entry.isSized || entry.isSvg || entry.isVideo) { if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap()) callback.onSuccess(entry.toMap())
} else { } else {
callback.onFailure(Exception("entry has no size")) callback.onFailure(Exception("entry has no size"))
@ -79,9 +89,9 @@ internal class FileImageProvider : ImageProvider() {
} }
return hashMapOf( return hashMapOf(
"uri" to Uri.fromFile(newFile).toString(), EntryFields.URI to Uri.fromFile(newFile).toString(),
"path" to newFile.path, EntryFields.PATH to newFile.path,
"dateModifiedSecs" to newFile.lastModified() / 1000, EntryFields.DATE_MODIFIED_MILLIS to newFile.lastModified(),
) )
} }
@ -89,8 +99,8 @@ internal class FileImageProvider : ImageProvider() {
try { try {
val file = File(path) val file = File(path)
if (file.exists()) { if (file.exists()) {
newFields["dateModifiedSecs"] = file.lastModified() / 1000 newFields[EntryFields.DATE_MODIFIED_MILLIS] = file.lastModified()
newFields["sizeBytes"] = file.length() newFields[EntryFields.SIZE_BYTES] = file.length()
} }
callback.onSuccess(newFields) callback.onSuccess(newFields)
} catch (e: SecurityException) { } catch (e: SecurityException) {

View file

@ -11,52 +11,63 @@ import android.net.Uri
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.FutureTarget import com.bumptech.glide.request.FutureTarget
import com.bumptech.glide.request.RequestOptions
import com.commonsware.cwac.document.DocumentFileCompat import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.decoder.MultiTrackImage import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.SvgImage import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF
import deckers.thibault.aves.metadata.Metadata.TYPE_IPTC import deckers.thibault.aves.metadata.Metadata.TYPE_IPTC
import deckers.thibault.aves.metadata.Metadata.TYPE_MP4 import deckers.thibault.aves.metadata.Metadata.TYPE_MP4
import deckers.thibault.aves.metadata.Metadata.TYPE_XMP import deckers.thibault.aves.metadata.Metadata.TYPE_XMP
import deckers.thibault.aves.metadata.Mp4ParserHelper
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateLocation import deckers.thibault.aves.metadata.Mp4ParserHelper.updateLocation
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateRotation import deckers.thibault.aves.metadata.Mp4ParserHelper.updateRotation
import deckers.thibault.aves.metadata.Mp4ParserHelper.updateXmp import deckers.thibault.aves.metadata.Mp4ParserHelper.updateXmp
import deckers.thibault.aves.metadata.MultiPage
import deckers.thibault.aves.metadata.PixyMetaHelper
import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString import deckers.thibault.aves.metadata.PixyMetaHelper.extendedXmpDocString
import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.* import deckers.thibault.aves.metadata.xmp.GoogleXMP
import deckers.thibault.aves.utils.* import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictResolution
import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BmpWriter
import deckers.thibault.aves.utils.FileUtils.transferFrom import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.FileUtils.transferTo import deckers.thibault.aves.utils.FileUtils.transferTo
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.canEditExif import deckers.thibault.aves.utils.MimeTypes.canEditExif
import deckers.thibault.aves.utils.MimeTypes.canEditIptc import deckers.thibault.aves.utils.MimeTypes.canEditIptc
import deckers.thibault.aves.utils.MimeTypes.canEditXmp import deckers.thibault.aves.utils.MimeTypes.canEditXmp
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta
import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata import deckers.thibault.aves.utils.MimeTypes.canRemoveMetadata
import deckers.thibault.aves.utils.MimeTypes.extensionFor import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import pixy.meta.meta.Metadata import java.io.ByteArrayInputStream
import pixy.meta.meta.MetadataType import java.io.File
import java.io.* import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.nio.channels.Channels import java.nio.channels.Channels
import java.util.* import java.util.Date
import java.util.TimeZone
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
abstract class ImageProvider { abstract class ImageProvider {
open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) {
callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider")) callback.onFailure(UnsupportedOperationException("`fetchSingle` is not supported by this image provider"))
} }
@ -64,10 +75,10 @@ abstract class ImageProvider {
return if (StorageUtils.isInVault(context, path)) { return if (StorageUtils.isInVault(context, path)) {
val uri = Uri.fromFile(File(path)) val uri = Uri.fromFile(File(path))
hashMapOf( hashMapOf(
"origin" to SourceEntry.ORIGIN_VAULT, EntryFields.ORIGIN to SourceEntry.ORIGIN_VAULT,
"uri" to uri.toString(), EntryFields.URI to uri.toString(),
"contentId" to null, EntryFields.CONTENT_ID to null,
"path" to path, EntryFields.PATH to path,
) )
} else { } else {
MediaStoreImageProvider().scanNewPathByMediaStore(context, path, mimeType) MediaStoreImageProvider().scanNewPathByMediaStore(context, path, mimeType)
@ -123,8 +134,7 @@ abstract class ImageProvider {
"success" to false, "success" to false,
) )
// prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store if (sourcePath != null) {
if (sourcePath != null && !desiredName.startsWith('.')) {
try { try {
var newFields: FieldMap = skippedFieldMap var newFields: FieldMap = skippedFieldMap
if (!isCancelledOp()) { if (!isCancelledOp()) {
@ -132,15 +142,18 @@ abstract class ImageProvider {
val oldFile = File(sourcePath) val oldFile = File(sourcePath)
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) { if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
val defaultExtension = oldFile.extension
oldFile.parent?.let { dir -> oldFile.parent?.let { dir ->
resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = dir, dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
defaultExtension = defaultExtension,
conflictStrategy = NameConflictStrategy.RENAME, conflictStrategy = NameConflictStrategy.RENAME,
)?.let { targetNameWithoutExtension -> )
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}"
val newFile = File(dir, targetFileName) val newFile = File(dir, targetFileName)
if (oldFile != newFile) { if (oldFile != newFile) {
newFields = renameSingle( newFields = renameSingle(
@ -180,7 +193,7 @@ abstract class ImageProvider {
} }
suspend fun convertMultiple( suspend fun convertMultiple(
activity: FragmentActivity, activity: Activity,
imageExportMimeType: String, imageExportMimeType: String,
targetDir: String, targetDir: String,
entries: List<AvesEntry>, entries: List<AvesEntry>,
@ -194,6 +207,7 @@ abstract class ImageProvider {
) { ) {
if (!supportedExportMimeTypes.contains(imageExportMimeType)) { if (!supportedExportMimeTypes.contains(imageExportMimeType)) {
callback.onFailure(Exception("unsupported export MIME type=$imageExportMimeType")) callback.onFailure(Exception("unsupported export MIME type=$imageExportMimeType"))
return
} }
val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) val targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
@ -239,7 +253,7 @@ abstract class ImageProvider {
} }
private suspend fun convertSingle( private suspend fun convertSingle(
activity: FragmentActivity, activity: Activity,
sourceEntry: AvesEntry, sourceEntry: AvesEntry,
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
@ -252,7 +266,7 @@ abstract class ImageProvider {
exportMimeType: String, exportMimeType: String,
): FieldMap { ): FieldMap {
val sourceMimeType = sourceEntry.mimeType val sourceMimeType = sourceEntry.mimeType
val sourceUri = sourceEntry.uri var sourceUri = sourceEntry.uri
val pageId = sourceEntry.pageId val pageId = sourceEntry.pageId
var desiredNameWithoutExtension = if (sourceEntry.path != null) { var desiredNameWithoutExtension = if (sourceEntry.path != null) {
@ -265,13 +279,23 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
} }
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension(
// there is no benefit providing input extension
// for known output MIME type
val defaultExtension = null
val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType, mimeType = exportMimeType,
defaultExtension = defaultExtension,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) ?: return skippedFieldMap )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
resolution.replacementFile?.let { file ->
sourceUri = Uri.fromFile(file)
}
val targetMimeType: String val targetMimeType: String
val write: (OutputStream) -> Unit val write: (OutputStream) -> Unit
@ -284,36 +308,28 @@ abstract class ImageProvider {
sourceDocFile.copyTo(output) sourceDocFile.copyTo(output)
} }
} else { } else {
var targetWidthPx: Int = if (sourceEntry.isRotated) height else width val targetWidthPx: Int
var targetHeightPx: Int = if (sourceEntry.isRotated) width else height val targetHeightPx: Int
if (lengthUnit == LENGTH_UNIT_PERCENT) { when (lengthUnit) {
targetWidthPx = sourceEntry.width * targetWidthPx / 100 LENGTH_UNIT_PERCENT -> {
targetHeightPx = sourceEntry.height * targetHeightPx / 100 targetWidthPx = sourceEntry.displayWidth * width / 100
targetHeightPx = sourceEntry.displayHeight * height / 100
}
else -> {
targetWidthPx = width
targetHeightPx = height
}
} }
val model: Any = if (MimeTypes.isHeic(sourceMimeType) && pageId != null) { target = Glide.with(activity.applicationContext)
MultiTrackImage(activity, sourceUri, pageId)
} else if (sourceMimeType == MimeTypes.TIFF) {
TiffImage(activity, sourceUri, pageId)
} else if (sourceMimeType == MimeTypes.SVG) {
SvgImage(activity, sourceUri)
} else {
StorageUtils.getGlideSafeUri(activity, sourceUri, sourceMimeType, sourceEntry.sizeBytes)
}
// request a fresh image with the highest quality format
val glideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
target = Glide.with(activity)
.asBitmap() .asBitmap()
.apply(glideOptions) .apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(model) .load(AvesAppGlideModule.getModel(activity, sourceUri, sourceMimeType, pageId, sourceEntry.sizeBytes))
.submit(targetWidthPx, targetHeightPx) .submit(targetWidthPx, targetHeightPx)
var bitmap = withContext(Dispatchers.IO) { target.get() } var bitmap = withContext(Dispatchers.IO) { target.get() }
if (MimeTypes.needRotationAfterGlide(sourceMimeType)) { if (MimeTypes.needRotationAfterGlide(sourceMimeType, pageId)) {
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped) bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
} }
bitmap ?: throw Exception("failed to get image for mimeType=$sourceMimeType uri=$sourceUri page=$pageId") bitmap ?: throw Exception("failed to get image for mimeType=$sourceMimeType uri=$sourceUri page=$pageId")
@ -350,11 +366,12 @@ abstract class ImageProvider {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
val newFields = scanNewPath(activity, targetPath, exportMimeType) val newFields = scanNewPath(activity, targetPath, exportMimeType)
val targetUri = Uri.parse(newFields["uri"] as String) val targetUri = (newFields[EntryFields.URI] as String).toUri()
if (writeMetadata) { if (writeMetadata) {
copyMetadata( copyMetadata(
context = activity, context = activity,
@ -369,7 +386,9 @@ abstract class ImageProvider {
return newFields return newFields
} finally { } finally {
// clearing Glide target should happen after effectively writing the bitmap // clearing Glide target should happen after effectively writing the bitmap
Glide.with(activity).clear(target) Glide.with(activity.applicationContext).clear(target)
resolution.replacementFile?.delete()
} }
} }
@ -388,39 +407,7 @@ abstract class ImageProvider {
} }
// copy IPTC / XMP via PixyMeta // copy IPTC / XMP via PixyMeta
PixyMetaHelper.copyIptcXmp(context, sourceMimeType, sourceUri, targetMimeType, targetUri, editableFile)
var pixyIptc: pixy.meta.meta.iptc.IPTC? = null
var pixyXmp: pixy.meta.meta.xmp.XMP? = null
if (canReadWithPixyMeta(sourceMimeType)) {
StorageUtils.openInputStream(context, sourceUri)?.use { input ->
val metadata = Metadata.readMetadata(input)
if (canEditIptc(targetMimeType)) {
pixyIptc = metadata[MetadataType.IPTC] as pixy.meta.meta.iptc.IPTC?
}
if (canEditXmp(targetMimeType)) {
pixyXmp = metadata[MetadataType.XMP] as pixy.meta.meta.xmp.XMP?
}
}
}
if (pixyIptc != null || pixyXmp != null) {
editableFile.outputStream().use { output ->
if (pixyIptc != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val iptcs = pixyIptc!!.dataSets.flatMap { it.value }
Metadata.insertIPTC(input, output, iptcs)
}
}
if (pixyXmp != null) {
// reopen input to read from start
StorageUtils.openInputStream(context, targetUri)?.use { input ->
val xmpString = pixyXmp!!.xmpDocString()
val extendedXmp = if (pixyXmp!!.hasExtendedXmp()) pixyXmp!!.extendedXmpDocString() else null
PixyMetaHelper.setXmp(input, output, xmpString, if (targetMimeType == MimeTypes.JPEG) extendedXmp else null)
}
}
}
}
// copy Exif via ExifInterface // copy Exif via ExifInterface
@ -481,12 +468,13 @@ abstract class ImageProvider {
} }
val captureMimeType = MimeTypes.JPEG val captureMimeType = MimeTypes.JPEG
val targetNameWithoutExtension = try { val resolution = try {
resolveTargetFileNameWithoutExtension( resolveTargetFileNameWithoutExtension(
contextWrapper = contextWrapper, contextWrapper = contextWrapper,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType, mimeType = captureMimeType,
defaultExtension = null,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -494,6 +482,7 @@ abstract class ImageProvider {
return return
} }
val targetNameWithoutExtension = resolution.nameWithoutExtension
if (targetNameWithoutExtension == null) { if (targetNameWithoutExtension == null) {
// skip it // skip it
callback.onSuccess(skippedFieldMap) callback.onSuccess(skippedFieldMap)
@ -572,42 +561,65 @@ abstract class ImageProvider {
} }
} }
fun createTimeStampFileName() = Date().time.toString()
private fun sanitizeDesiredFileName(desiredName: String): String {
var name = desiredName
// prevent creating hidden files
while (name.isNotEmpty() && name.startsWith(".")) {
name = name.substring(1)
}
if (name.isEmpty()) {
name = createTimeStampFileName()
}
return name
}
// returns available name to use, or `null` to skip it // returns available name to use, or `null` to skip it
suspend fun resolveTargetFileNameWithoutExtension( suspend fun resolveTargetFileNameWithoutExtension(
contextWrapper: ContextWrapper, contextWrapper: ContextWrapper,
dir: String, dir: String,
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
mimeType: String, mimeType: String,
defaultExtension: String?,
conflictStrategy: NameConflictStrategy, conflictStrategy: NameConflictStrategy,
): String? { ): NameConflictResolution {
val extension = extensionFor(mimeType) val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
val targetFile = File(dir, "$desiredNameWithoutExtension$extension") var resolvedName: String? = sanitizedNameWithoutExtension
return when (conflictStrategy) { var replacementFile: File? = null
val extension = extensionFor(mimeType, defaultExtension)
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) {
NameConflictStrategy.RENAME -> { NameConflictStrategy.RENAME -> {
var nameWithoutExtension = desiredNameWithoutExtension var nameWithoutExtension = sanitizedNameWithoutExtension
var i = 0 var i = 0
while (File(dir, "$nameWithoutExtension$extension").exists()) { while (File(dir, "$nameWithoutExtension$extension").exists()) {
i++ i++
nameWithoutExtension = "$desiredNameWithoutExtension ($i)" nameWithoutExtension = "$sanitizedNameWithoutExtension ($i)"
} }
nameWithoutExtension resolvedName = nameWithoutExtension
} }
NameConflictStrategy.REPLACE -> { NameConflictStrategy.REPLACE -> {
if (targetFile.exists()) { if (targetFile.exists()) {
// move replaced file to temp storage
// so that it can be used as a source for conversion or metadata copy
replacementFile = StorageUtils.createTempFile(contextWrapper).apply {
targetFile.transferTo(outputStream())
}
deletePath(contextWrapper, targetFile.path, mimeType) deletePath(contextWrapper, targetFile.path, mimeType)
} }
desiredNameWithoutExtension
} }
NameConflictStrategy.SKIP -> { NameConflictStrategy.SKIP -> {
if (targetFile.exists()) { if (targetFile.exists()) {
null resolvedName = null
} else {
desiredNameWithoutExtension
} }
} }
} }
return NameConflictResolution(resolvedName, replacementFile)
} }
// cf `MetadataFetchHandler.getCatalogMetadataByMetadataExtractor()` for a more thorough check // cf `MetadataFetchHandler.getCatalogMetadataByMetadataExtractor()` for a more thorough check
@ -645,19 +657,21 @@ abstract class ImageProvider {
} }
val originalFileSize = File(path).length() val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff } var trailerVideoBytes: ByteArray? = null
var videoBytes: ByteArray? = null
val editableFile = StorageUtils.createTempFile(context).apply { val editableFile = StorageUtils.createTempFile(context).apply {
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)?.let { it + trailerDiff }
val isTrailerVideoValid = trailerVideoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, trailerVideoSize) != null
try { try {
if (videoSize != null) { if (trailerVideoSize != null && isTrailerVideoValid) {
// handle motion photo and embedded video separately // handle motion photo and embedded video separately
val imageSize = (originalFileSize - videoSize).toInt() val imageSize = (originalFileSize - trailerVideoSize).toInt()
videoBytes = ByteArray(videoSize) val videoByteSize = trailerVideoSize.toInt()
trailerVideoBytes = ByteArray(videoByteSize)
StorageUtils.openInputStream(context, uri)?.let { input -> StorageUtils.openInputStream(context, uri)?.let { input ->
val imageBytes = ByteArray(imageSize) val imageBytes = ByteArray(imageSize)
input.read(imageBytes, 0, imageSize) input.read(imageBytes, 0, imageSize)
input.read(videoBytes, 0, videoSize) input.read(trailerVideoBytes, 0, videoByteSize)
// copy only the image to a temporary file for editing // copy only the image to a temporary file for editing
// video will be appended after metadata modification // video will be appended after metadata modification
@ -677,30 +691,31 @@ abstract class ImageProvider {
try { try {
edit(ExifInterface(editableFile)) 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) val editedMimeType = detectMimeType(context, Uri.fromFile(editableFile), mimeType)
if (editedMimeType != mimeType) { if (editedMimeType != mimeType) {
throw Exception("editing Exif changes mimeType=$mimeType -> $editedMimeType for uri=$uri path=$path") throw Exception("editing Exif changes mimeType=$mimeType -> $editedMimeType for uri=$uri path=$path")
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 1) as of androidx.exifinterface:exifinterface:1.3.6, editing some specific WEBP // editing may corrupt the file for various reasons,
// makes them undecodable by some decoders (including Android's and Chrome's)
// even though `BitmapFactory` successfully decodes their bounds,
// so we check whether decoding it throws an exception // 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)) ImageDecoder.decodeBitmap(ImageDecoder.createSource(editableFile))
} }
if (videoBytes != null) { if (trailerVideoBytes != null) {
// append trailer video, if any // append trailer video, if any
editableFile.appendBytes(videoBytes!!) editableFile.appendBytes(trailerVideoBytes!!)
} }
// copy the edited temporary file back to the original // copy the edited temporary file back to the original
editableFile.transferTo(outputStream(context, mimeType, uri, path)) editableFile.transferTo(outputStream(context, mimeType, uri, path))
if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, videoSize, editableFile, callback)) { if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, trailerVideoBytes?.size, editableFile, callback)) {
return false return false
} }
editableFile.delete() editableFile.delete()
@ -728,19 +743,21 @@ abstract class ImageProvider {
} }
val originalFileSize = File(path).length() val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff } var trailerVideoBytes: ByteArray? = null
var videoBytes: ByteArray? = null
val editableFile = StorageUtils.createTempFile(context).apply { val editableFile = StorageUtils.createTempFile(context).apply {
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)?.let { it + trailerDiff }
val isTrailerVideoValid = trailerVideoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, trailerVideoSize) != null
try { try {
if (videoSize != null) { if (trailerVideoSize != null && isTrailerVideoValid) {
// handle motion photo and embedded video separately // handle motion photo and embedded video separately
val imageSize = (originalFileSize - videoSize).toInt() val imageSize = (originalFileSize - trailerVideoSize).toInt()
videoBytes = ByteArray(videoSize) val videoByteSize = trailerVideoSize.toInt()
trailerVideoBytes = ByteArray(videoByteSize)
StorageUtils.openInputStream(context, uri)?.let { input -> StorageUtils.openInputStream(context, uri)?.let { input ->
val imageBytes = ByteArray(imageSize) val imageBytes = ByteArray(imageSize)
input.read(imageBytes, 0, imageSize) input.read(imageBytes, 0, imageSize)
input.read(videoBytes, 0, videoSize) input.read(trailerVideoBytes, 0, videoByteSize)
// copy only the image to a temporary file for editing // copy only the image to a temporary file for editing
// video will be appended after metadata modification // video will be appended after metadata modification
@ -776,15 +793,20 @@ abstract class ImageProvider {
} }
} }
if (videoBytes != null) { if (editableFile.length() == 0L) {
callback.onFailure(Exception("editing IPTC yielded an empty file"))
return false
}
if (trailerVideoBytes != null) {
// append trailer video, if any // append trailer video, if any
editableFile.appendBytes(videoBytes!!) editableFile.appendBytes(trailerVideoBytes!!)
} }
// copy the edited temporary file back to the original // copy the edited temporary file back to the original
editableFile.transferTo(outputStream(context, mimeType, uri, path)) editableFile.transferTo(outputStream(context, mimeType, uri, path))
if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, videoSize, editableFile, callback)) { if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, trailerVideoBytes?.size, editableFile, callback)) {
return false return false
} }
editableFile.delete() editableFile.delete()
@ -854,6 +876,7 @@ abstract class ImageProvider {
} }
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
callback.onFailure(e) callback.onFailure(e)
return false
} catch (e: Exception) { } catch (e: Exception) {
callback.onFailure(e) callback.onFailure(e)
return false return false
@ -893,7 +916,7 @@ abstract class ImageProvider {
} }
val originalFileSize = File(path).length() val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff } val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
val editableFile = StorageUtils.createTempFile(context).apply { val editableFile = StorageUtils.createTempFile(context).apply {
try { try {
editXmpWithPixy( editXmpWithPixy(
@ -911,11 +934,16 @@ abstract class ImageProvider {
} }
} }
if (editableFile.length() == 0L) {
callback.onFailure(Exception("editing XMP yielded an empty file"))
return false
}
try { try {
// copy the edited temporary file back to the original // copy the edited temporary file back to the original
editableFile.transferTo(outputStream(context, mimeType, uri, path)) editableFile.transferTo(outputStream(context, mimeType, uri, path))
if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, videoSize, editableFile, callback)) { if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, trailerVideoSize, editableFile, callback)) {
return false return false
} }
editableFile.delete() editableFile.delete()
@ -976,7 +1004,7 @@ abstract class ImageProvider {
path: String, path: String,
uri: Uri, uri: Uri,
mimeType: String, mimeType: String,
trailerOffset: Int?, trailerOffset: Number?,
editedFile: File, editedFile: File,
callback: ImageOpCallback, callback: ImageOpCallback,
): Boolean { ): Boolean {
@ -991,17 +1019,9 @@ abstract class ImageProvider {
LOG_TAG, "Edited file length=$expectedLength does not match final document file length=$actualLength. " + LOG_TAG, "Edited file length=$expectedLength does not match final document file length=$actualLength. " +
"We need to edit XMP to adjust trailer video offset by $diff bytes." "We need to edit XMP to adjust trailer video offset by $diff bytes."
) )
val newTrailerOffset = trailerOffset + diff val newTrailerOffset = trailerOffset.toLong() + diff
return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp -> return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp ->
xmp.replace( GoogleXMP.updateTrailingVideoOffset(xmp, trailerOffset, newTrailerOffset)
// GCamera motion photo
"${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$trailerOffset\"",
"${XMP.GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$newTrailerOffset\"",
).replace(
// Container motion photo
"${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$trailerOffset\"",
"${XMP.GCONTAINER_ITEM_LENGTH_PROP_NAME}=\"$newTrailerOffset\"",
)
}) })
} }
@ -1045,7 +1065,7 @@ abstract class ImageProvider {
uri: Uri, uri: Uri,
mimeType: String, mimeType: String,
dateMillis: Long?, dateMillis: Long?,
shiftMinutes: Long?, shiftSeconds: Long?,
fields: List<String>, fields: List<String>,
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
@ -1076,9 +1096,9 @@ abstract class ImageProvider {
} }
} }
shiftMinutes != null -> { shiftSeconds != null -> {
// shift // shift
val shiftMillis = shiftMinutes * 60000 val shiftMillis = shiftSeconds * 1000
listOf( listOf(
ExifInterface.TAG_DATETIME, ExifInterface.TAG_DATETIME,
ExifInterface.TAG_DATETIME_ORIGINAL, ExifInterface.TAG_DATETIME_ORIGINAL,
@ -1264,17 +1284,23 @@ abstract class ImageProvider {
callback: ImageOpCallback, callback: ImageOpCallback,
) { ) {
val originalFileSize = File(path).length() val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.toInt() val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)
if (videoSize == null) { if (trailerVideoSize == null) {
callback.onFailure(Exception("failed to get trailer video size")) callback.onFailure(Exception("failed to get trailer video size"))
return return
} }
val isTrailerVideoValid = MultiPage.getTrailerVideoInfo(context, uri, fileSize = originalFileSize, videoSize = trailerVideoSize) != null
if (!isTrailerVideoValid) {
callback.onFailure(Exception("failed to open trailer video with size=$trailerVideoSize"))
return
}
val editableFile = StorageUtils.createTempFile(context).apply { val editableFile = StorageUtils.createTempFile(context).apply {
try { try {
val inputStream = StorageUtils.openInputStream(context, uri) val inputStream = StorageUtils.openInputStream(context, uri)
// partial copy // partial copy
transferFrom(inputStream, originalFileSize - videoSize) transferFrom(inputStream, originalFileSize - trailerVideoSize)
} catch (e: Exception) { } catch (e: Exception) {
Log.d(LOG_TAG, "failed to remove trailer video", e) Log.d(LOG_TAG, "failed to remove trailer video", e)
callback.onFailure(e) callback.onFailure(e)
@ -1309,7 +1335,8 @@ abstract class ImageProvider {
} }
val originalFileSize = File(path).length() val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.toInt() val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)
val isTrailerVideoValid = trailerVideoSize != null && MultiPage.getTrailerVideoInfo(context, uri, originalFileSize, trailerVideoSize) != null
val editableFile = StorageUtils.createTempFile(context).apply { val editableFile = StorageUtils.createTempFile(context).apply {
try { try {
outputStream().use { output -> outputStream().use { output ->
@ -1325,11 +1352,16 @@ abstract class ImageProvider {
} }
} }
if (editableFile.length() == 0L) {
callback.onFailure(Exception("removing metadata yielded an empty file"))
return
}
try { try {
// copy the edited temporary file back to the original // copy the edited temporary file back to the original
editableFile.transferTo(outputStream(context, mimeType, uri, path)) editableFile.transferTo(outputStream(context, mimeType, uri, path))
if (!types.contains(TYPE_XMP) && !checkTrailerOffset(context, path, uri, mimeType, videoSize, editableFile, callback)) { if (!types.contains(TYPE_XMP) && isTrailerVideoValid && !checkTrailerOffset(context, path, uri, mimeType, trailerVideoSize, editableFile, callback)) {
return return
} }
editableFile.delete() editableFile.delete()

View file

@ -1,20 +1,24 @@
package deckers.thibault.aves.model.provider package deckers.thibault.aves.model.provider
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.net.Uri import android.net.Uri
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import java.util.* import java.util.Locale
object ImageProviderFactory { object ImageProviderFactory {
fun getProvider(uri: Uri): ImageProvider? { fun getProvider(context: Context, uri: Uri): ImageProvider? {
return when (uri.scheme?.lowercase(Locale.ROOT)) { return when (uri.scheme?.lowercase(Locale.ROOT)) {
ContentResolver.SCHEME_CONTENT -> { ContentResolver.SCHEME_CONTENT -> {
if (StorageUtils.isMediaStoreContentUri(uri)) { if (StorageUtils.isMediaStoreContentUri(uri)) {
MediaStoreImageProvider() MediaStoreImageProvider()
} else if (AvesEmbeddedMediaProvider.provides(context, uri)) {
AvesEmbeddedMediaProvider()
} else { } else {
ContentImageProvider() UnknownContentProvider()
} }
} }
ContentResolver.SCHEME_FILE -> FileImageProvider() ContentResolver.SCHEME_FILE -> FileImageProvider()
else -> null else -> null
} }

View file

@ -3,10 +3,16 @@ package deckers.thibault.aves.model.provider
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.RecoverableSecurityException import android.app.RecoverableSecurityException
import android.content.* import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.ContextWrapper
import android.graphics.BitmapFactory
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -14,6 +20,7 @@ import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.MainActivity import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
import deckers.thibault.aves.model.AvesEntry import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictStrategy import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.SourceEntry import deckers.thibault.aves.model.SourceEntry
@ -31,9 +38,10 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import java.io.SyncFailedException import java.io.SyncFailedException
import java.util.* import java.util.Locale
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -43,13 +51,14 @@ import kotlin.coroutines.suspendCoroutine
class MediaStoreImageProvider : ImageProvider() { class MediaStoreImageProvider : ImageProvider() {
fun fetchAll( fun fetchAll(
context: Context, context: Context,
knownEntries: Map<Int?, Int?>, knownEntries: Map<Long?, Long?>,
directory: String?, directory: String?,
handleNewEntry: NewEntryHandler, handleNewEntry: NewEntryHandler,
) { ) {
val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean { Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory")
val isModified = fun(contentId: Long, dateModifiedMillis: Long): Boolean {
val knownDate = knownEntries[contentId] val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs return knownDate == null || knownDate < dateModifiedMillis
} }
val handleNew: NewEntryHandler val handleNew: NewEntryHandler
var selection: String? = null var selection: String? = null
@ -68,7 +77,7 @@ class MediaStoreImageProvider : ImageProvider() {
val parentCheckDirectory = removeTrailingSeparator(directory) val parentCheckDirectory = removeTrailingSeparator(directory)
handleNew = { entry -> handleNew = { entry ->
// skip entries in subfolders // skip entries in subfolders
val path = entry["path"] as String? val path = entry[EntryFields.PATH] as String?
if (path != null && File(path).parent == parentCheckDirectory) { if (path != null && File(path).parent == parentCheckDirectory) {
handleNewEntry(entry) handleNewEntry(entry)
} }
@ -83,11 +92,11 @@ class MediaStoreImageProvider : ImageProvider() {
// the provided URI can point to the wrong media collection, // the provided URI can point to the wrong media collection,
// e.g. a GIF image with the URI `content://media/external/video/media/[ID]` // e.g. a GIF image with the URI `content://media/external/video/media/[ID]`
// so the effective entry URI may not match the provided URI // so the effective entry URI may not match the provided URI
override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) {
var found = false var found = false
val fetched = arrayListOf<FieldMap>() val fetched = arrayListOf<FieldMap>()
val id = uri.tryParseId() val id = uri.tryParseId()
val alwaysValid: NewEntryChecker = fun(_: Int, _: Int): Boolean = true val alwaysValid: NewEntryChecker = fun(_: Long, _: Long): Boolean = true
val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) } val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
if (id != null) { if (id != null) {
if (sourceMimeType == null || isImage(sourceMimeType)) { if (sourceMimeType == null || isImage(sourceMimeType)) {
@ -117,8 +126,8 @@ class MediaStoreImageProvider : ImageProvider() {
} }
} }
fun checkObsoleteContentIds(context: Context, knownContentIds: List<Int?>): List<Int> { fun checkObsoleteContentIds(context: Context, knownContentIds: List<Long?>): List<Long> {
val foundContentIds = HashSet<Int>() val foundContentIds = HashSet<Long>()
fun check(context: Context, contentUri: Uri) { fun check(context: Context, contentUri: Uri) {
val projection = arrayOf(MediaStore.MediaColumns._ID) val projection = arrayOf(MediaStore.MediaColumns._ID)
try { try {
@ -126,7 +135,7 @@ class MediaStoreImageProvider : ImageProvider() {
if (cursor != null) { if (cursor != null) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
foundContentIds.add(cursor.getInt(idColumn)) foundContentIds.add(cursor.getLong(idColumn))
} }
cursor.close() cursor.close()
} }
@ -139,8 +148,8 @@ class MediaStoreImageProvider : ImageProvider() {
return knownContentIds.subtract(foundContentIds).filterNotNull().toList() return knownContentIds.subtract(foundContentIds).filterNotNull().toList()
} }
fun checkObsoletePaths(context: Context, knownPathById: Map<Int?, String?>): List<Int> { fun checkObsoletePaths(context: Context, knownPathById: Map<Long?, String?>): List<Long> {
val obsoleteIds = ArrayList<Int>() val obsoleteIds = ArrayList<Long>()
fun check(context: Context, contentUri: Uri) { fun check(context: Context, contentUri: Uri) {
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA) val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
try { try {
@ -149,7 +158,7 @@ class MediaStoreImageProvider : ImageProvider() {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) val pathColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getInt(idColumn) val id = cursor.getLong(idColumn)
val path = cursor.getString(pathColumn) val path = cursor.getString(pathColumn)
if (knownPathById.containsKey(id) && knownPathById[id] != path) { if (knownPathById.containsKey(id) && knownPathById[id] != path) {
obsoleteIds.add(id) obsoleteIds.add(id)
@ -166,6 +175,31 @@ class MediaStoreImageProvider : ImageProvider() {
return obsoleteIds return obsoleteIds
} }
fun getChangedUris(context: Context, sinceGeneration: Int): List<String> {
val changedUris = ArrayList<String>()
fun check(context: Context, contentUri: Uri) {
val projection = arrayOf(MediaStore.MediaColumns._ID)
val selection = "${MediaStore.MediaColumns.GENERATION_MODIFIED} > ?"
val selectionArgs = arrayOf(sinceGeneration.toString())
try {
val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, null)
if (cursor != null) {
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
changedUris.add(ContentUris.withAppendedId(contentUri, id).toString())
}
cursor.close()
}
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to get content IDs for contentUri=$contentUri", e)
}
}
check(context, IMAGE_CONTENT_URI)
check(context, VIDEO_CONTENT_URI)
return changedUris
}
private fun fetchFrom( private fun fetchFrom(
context: Context, context: Context,
isValidEntry: NewEntryChecker, isValidEntry: NewEntryChecker,
@ -193,8 +227,8 @@ class MediaStoreImageProvider : ImageProvider() {
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED) val dateAddedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) val dateModifiedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN) val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN)
// image & video for API >=29, only for images for API <29 // image & video for API >=29, only for images for API <29
@ -205,39 +239,62 @@ class MediaStoreImageProvider : ImageProvider() {
val needDuration = projection.contentEquals(VIDEO_PROJECTION) val needDuration = projection.contentEquals(VIDEO_PROJECTION)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val contentId = cursor.getInt(idColumn) val id = cursor.getLong(idColumn)
val dateModifiedSecs = cursor.getInt(dateModifiedColumn) val dateModifiedMillis = cursor.getInt(dateModifiedSecsColumn) * 1000L
if (isValidEntry(contentId, dateModifiedSecs)) { if (isValidEntry(id, dateModifiedMillis)) {
// for multiple items, `contentUri` is the root without ID, // for multiple items, `contentUri` is the root without ID,
// but for single items, `contentUri` already contains the ID // but for single items, `contentUri` already contains the ID
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong()) val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, id)
// `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices) // `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices)
// in that case we try to use the MIME type provided along the URI // in that case we try to use the MIME type provided along the URI
val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType
val width = cursor.getInt(widthColumn) var width = cursor.getInt(widthColumn)
val height = cursor.getInt(heightColumn) var height = cursor.getInt(heightColumn)
val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L
if (mimeType == null) { if (mimeType == null) {
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type") Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else { } else {
var entryMap: FieldMap = hashMapOf( val path = cursor.getString(pathColumn)
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT, var entryFields: FieldMap = hashMapOf(
"uri" to itemUri.toString(), EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
"path" to cursor.getString(pathColumn), EntryFields.URI to itemUri.toString(),
"sourceMimeType" to mimeType, EntryFields.PATH to path,
"width" to width, EntryFields.SOURCE_MIME_TYPE to mimeType,
"height" to height, EntryFields.WIDTH to width,
"sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0, EntryFields.HEIGHT to height,
"sizeBytes" to cursor.getLong(sizeColumn), EntryFields.SOURCE_ROTATION_DEGREES to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
"dateAddedSecs" to cursor.getInt(dateAddedColumn), EntryFields.SIZE_BYTES to cursor.getLong(sizeColumn),
"dateModifiedSecs" to dateModifiedSecs, EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedSecsColumn),
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null, EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
"durationMillis" to durationMillis, EntryFields.SOURCE_DATE_TAKEN_MILLIS to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
EntryFields.DURATION_MILLIS to durationMillis,
// only for map export // only for map export
"contentId" to contentId, EntryFields.CONTENT_ID to id,
) )
if (MimeTypes.isHeic(mimeType)) {
// The reported size for some HEIC images is simply incorrect.
try {
StorageUtils.openInputStream(context, itemUri)?.use { input ->
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)
val outWidth = options.outWidth
val outHeight = options.outHeight
if (outWidth > 0 && outHeight > 0) {
width = outWidth
height = outHeight
entryFields[EntryFields.WIDTH] = width
entryFields[EntryFields.HEIGHT] = height
}
}
} catch (e: IOException) {
// ignore
}
}
if (MimeTypes.isRaw(mimeType) if (MimeTypes.isRaw(mimeType)
|| (width <= 0 || height <= 0) && needSize(mimeType) || (width <= 0 || height <= 0) && needSize(mimeType)
|| durationMillis == 0L && needDuration || durationMillis == 0L && needDuration
@ -246,11 +303,13 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation. // missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices // Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size). // and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context) val entry = SourceEntry(entryFields).fillPreCatalogMetadata(context)
entryMap = entry.toMap() entryFields = entry.toMap()
} }
handleNewEntry(entryMap) getFileModifiedDateMillis(path)?.let { entryFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
handleNewEntry(entryFields)
found = true found = true
} }
} }
@ -398,10 +457,8 @@ class MediaStoreImageProvider : ImageProvider() {
effectiveTargetDir = targetDir effectiveTargetDir = targetDir
targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir) targetDirDocFile = StorageUtils.createDirectoryDocIfAbsent(activity, targetDir)
if (!File(targetDir).exists()) { if (!File(targetDir).exists()) {
val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir)
val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath)
// download subdirectories can be created later by Media Store insertion // download subdirectories can be created later by Media Store insertion
if (!isDownloadSubdir) { if (!isDownloadSubdir(activity, targetDir)) {
callback.onFailure(Exception("failed to create directory at path=$targetDir")) callback.onFailure(Exception("failed to create directory at path=$targetDir"))
return return
} }
@ -425,64 +482,62 @@ class MediaStoreImageProvider : ImageProvider() {
"success" to false, "success" to false,
) )
if (sourcePath != null) { // on API 30 we cannot get access granted directly to a volume root from its document tree,
// on API 30 we cannot get access granted directly to a volume root from its document tree, // but it is still less constraining to use tree document files than to rely on the Media Store
// but it is still less constraining to use tree document files than to rely on the Media Store //
// // Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but:
// Relying on `DocumentFile`, we can create an item via `DocumentFile.createFile()`, but: // - we need to scan the file to get the Media Store content URI
// - we need to scan the file to get the Media Store content URI // - the underlying document provider controls the new file name
// - the underlying document provider controls the new file name //
// // Relying on the Media Store, we can create an item via `ContentResolver.insert()`
// Relying on the Media Store, we can create an item via `ContentResolver.insert()` // with a path, and retrieve its content URI, but:
// with a path, and retrieve its content URI, but: // - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`)
// - the Media Store isolates content by storage volume (e.g. `MediaStore.Images.Media.getContentUri(volumeName)`) // - the volume name should be lower case, not exactly as the `StorageVolume` UUID
// - the volume name should be lower case, not exactly as the `StorageVolume` UUID // cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()`
// cf new method in API 30 `StorageVolume.getMediaStoreVolumeName()` // - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?)
// - inserting on a removable volume works on API 29, but not on API 25 nor 26 (on which API/devices does it work?) // - there is no documentation regarding support for usage with removable storage
// - there is no documentation regarding support for usage with removable storage // - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage
// - the Media Store only allows inserting in specific primary directories ("DCIM", "Pictures") when using scoped storage try {
try { val appDir = when {
val appDir = when { toBin -> StorageUtils.trashDirFor(activity, sourcePath ?: StorageUtils.getPrimaryVolumePath(activity))
toBin -> StorageUtils.trashDirFor(activity, sourcePath) toVault -> File(targetDir)
toVault -> File(targetDir) else -> null
else -> null
}
if (appDir != null) {
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
if (toVault) {
appDir.mkdirs()
}
}
if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = File(sourcePath)
if (!sourceFile.exists() && toBin) {
delete(activity, sourceUri, sourcePath, mimeType = mimeType)
deletedFieldMap
} else {
moveSingle(
activity = activity,
sourceFile = sourceFile,
sourceUri = sourceUri,
targetDir = effectiveTargetDir,
targetDirDocFile = targetDirDocFile,
desiredName = desiredName ?: sourceFile.name,
nameConflictStrategy = nameConflictStrategy,
mimeType = mimeType,
copy = copy,
toBin = toBin,
)
}
}
result["newFields"] = newFields
result["success"] = true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e)
} }
if (appDir != null) {
effectiveTargetDir = ensureTrailingSeparator(appDir.path)
targetDirDocFile = DocumentFileCompat.fromFile(appDir)
if (toVault) {
appDir.mkdirs()
}
}
if (effectiveTargetDir != null) {
val newFields = if (isCancelledOp()) skippedFieldMap else {
val sourceFile = if (sourcePath != null) File(sourcePath) else null
if (sourceFile != null && !sourceFile.exists() && toBin) {
delete(activity, sourceUri, sourcePath, mimeType = mimeType)
deletedFieldMap
} else {
moveSingle(
activity = activity,
sourceFile = sourceFile,
sourceUri = sourceUri,
targetDir = effectiveTargetDir,
targetDirDocFile = targetDirDocFile,
desiredName = desiredName ?: sourceFile?.name ?: sourceUri.lastPathSegment ?: createTimeStampFileName(),
nameConflictStrategy = nameConflictStrategy,
mimeType = mimeType,
copy = copy,
toBin = toBin,
)
}
}
result["newFields"] = newFields
result["success"] = true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to move to targetDir=$targetDir entry with sourcePath=$sourcePath", e)
} }
callback.onSuccess(result) callback.onSuccess(result)
} }
@ -491,7 +546,7 @@ class MediaStoreImageProvider : ImageProvider() {
private suspend fun moveSingle( private suspend fun moveSingle(
activity: Activity, activity: Activity,
sourceFile: File, sourceFile: File?,
sourceUri: Uri, sourceUri: Uri,
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
@ -501,21 +556,24 @@ class MediaStoreImageProvider : ImageProvider() {
copy: Boolean, copy: Boolean,
toBin: Boolean, toBin: Boolean,
): FieldMap { ): FieldMap {
val sourcePath = sourceFile.path val sourcePath = sourceFile?.path
val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } val sourceExtension = sourceFile?.extension
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) { if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy // nothing to do unless it's a renamed copy
return skippedFieldMap return skippedFieldMap
} }
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") val desiredNameWithoutExtension = desiredName.substringBeforeLast(".")
val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
defaultExtension = sourceExtension,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) ?: return skippedFieldMap )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri) val sourceDocFile = DocumentFileCompat.fromSingleUri(activity, sourceUri)
val targetPath = createSingle( val targetPath = createSingle(
@ -524,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = sourceExtension,
) { output: OutputStream -> ) { output: OutputStream ->
try { try {
sourceDocFile.copyTo(output) sourceDocFile.copyTo(output)
@ -545,8 +604,8 @@ class MediaStoreImageProvider : ImageProvider() {
} }
return if (toBin) { return if (toBin) {
hashMapOf( hashMapOf(
"trashed" to true, EntryFields.TRASHED to true,
"trashPath" to targetPath, EntryFields.TRASH_PATH to targetPath,
) )
} else { } else {
scanNewPath(activity, targetPath, mimeType) scanNewPath(activity, targetPath, mimeType)
@ -559,24 +618,23 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String, targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit, write: (OutputStream) -> Unit,
): String { ): String {
if (StorageUtils.isInVault(activity, targetDir)) { if (StorageUtils.isInVault(activity, targetDir)) {
return insertByFile( return insertByFile(
targetDir = targetDir, targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write, write = write,
) )
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val downloadDirPath = StorageUtils.getDownloadDirPath(activity, targetDir) if (isDownloadSubdir(activity, targetDir)) {
val isDownloadSubdir = downloadDirPath != null && targetDir.startsWith(downloadDirPath)
if (isDownloadSubdir) {
return insertByMediaStore( return insertByMediaStore(
activity = activity, activity = activity,
targetDir = targetDir, targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write, write = write,
) )
} }
@ -588,10 +646,18 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
} }
private fun isDownloadSubdir(context: Context, dir: String): Boolean {
val volumePath = StorageUtils.getVolumePath(context, dir) ?: return false
val downloadDirPath = ensureTrailingSeparator(File(volumePath, Environment.DIRECTORY_DOWNLOADS).path)
// effective download path may have a different case
return dir.lowercase().startsWith(downloadDirPath.lowercase())
}
private fun insertByFile( private fun insertByFile(
targetDir: String, targetDir: String,
targetFileName: String, targetFileName: String,
@ -639,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String, targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit, write: (OutputStream) -> Unit,
): String { ): String {
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
@ -647,8 +714,22 @@ class MediaStoreImageProvider : ImageProvider() {
// but in order to open an output stream to it, we need to use a `SingleDocumentFile` // but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI // through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
// providing a display name and a MIME type does not guarantee
// that the created document will be backed by a file with a valid media extension,
// but having an extension is essential for media detection by Android,
// so we retry with a display name that includes the extension
if ((targetDocFile.extension == null || targetDocFile.extension.isEmpty() || targetDocFile.extension == "bin") && defaultExtension != null) {
if (targetDocFile.exists()) {
targetDocFile.delete()
}
val extension = if (defaultExtension.startsWith(".")) defaultExtension else ".$defaultExtension"
targetTreeFile = targetDirDocFile.createFile(mimeType, "$targetNameWithoutExtension$extension")
targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
}
try { try {
targetDocFile.openOutputStream().use(write) targetDocFile.openOutputStream().use(write)
@ -765,18 +846,32 @@ class MediaStoreImageProvider : ImageProvider() {
try { try {
val cursor = context.contentResolver.query(uri, projection, null, null, null) val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_MILLIS] = cursor.getInt(it) * 1000 }
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields["sizeBytes"] = cursor.getLong(it) } cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields[EntryFields.SIZE_BYTES] = cursor.getLong(it) }
cursor.close() cursor.close()
} }
} catch (e: Exception) { } catch (e: Exception) {
callback.onFailure(e) callback.onFailure(e)
return@scanFile return@scanFile
} }
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
callback.onSuccess(newFields) callback.onSuccess(newFields)
} }
} }
// try to fetch the modified date from the file,
// as it is more precise than the one from the Media Store
private fun getFileModifiedDateMillis(path: String?): Long? {
if (path != null) {
try {
return File(path).lastModified()
} catch (securityException: SecurityException) {
// ignore
}
}
return null
}
private fun scanObsoletePath(context: Context, uri: Uri, path: String, mimeType: String) { private fun scanObsoletePath(context: Context, uri: Uri, path: String, mimeType: String) {
val file = File(path) val file = File(path)
val delayMillis = 500L val delayMillis = 500L
@ -854,14 +949,15 @@ class MediaStoreImageProvider : ImageProvider() {
val cursor = context.contentResolver.query(uri, projection, null, null, null) val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
val newFields = hashMapOf<String, Any?>( val newFields = hashMapOf<String, Any?>(
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT, EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
"uri" to uri.toString(), EntryFields.URI to uri.toString(),
"contentId" to uri.tryParseId(), EntryFields.CONTENT_ID to uri.tryParseId(),
"path" to path, EntryFields.PATH to path,
) )
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields[EntryFields.DATE_ADDED_SECS] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields[EntryFields.DATE_MODIFIED_MILLIS] = cursor.getInt(it) * 1000 }
cursor.close() cursor.close()
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
return newFields return newFields
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -906,8 +1002,10 @@ class MediaStoreImageProvider : ImageProvider() {
try { try {
val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, null) val cursor = context.contentResolver.query(contentUri, projection, selection, selectionArgs, null)
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(MediaStore.MediaColumns._ID).let { val idColumn = cursor.getColumnIndex(MediaStore.MediaColumns._ID)
if (it != -1) mediaContentUri = ContentUris.withAppendedId(contentUri, cursor.getLong(it)) if (idColumn != -1) {
val id = cursor.getLong(idColumn)
mediaContentUri = ContentUris.withAppendedId(contentUri, id)
} }
cursor.close() cursor.close()
} }
@ -970,4 +1068,4 @@ object MediaColumns {
typealias NewEntryHandler = (entry: FieldMap) -> Unit typealias NewEntryHandler = (entry: FieldMap) -> Unit
private typealias NewEntryChecker = (contentId: Int, dateModifiedSecs: Int) -> Boolean private typealias NewEntryChecker = (contentId: Long, dateModifiedMillis: Long) -> Boolean

View file

@ -0,0 +1,85 @@
package deckers.thibault.aves.model.provider
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.util.Log
import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
open class UnknownContentProvider : ImageProvider() {
open val reliableProviderMimeType: Boolean
get() = false
override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) {
var mimeType = sourceMimeType
if (sourceMimeType == null || !reliableProviderMimeType) {
// source MIME type may be incorrect, so we get a second opinion if possible
try {
val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri))
StorageUtils.openInputStream(context, safeUri)?.use { input ->
// `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives)
// cf https://github.com/drewnoakes/metadata-extractor/issues/296
Helper.readMimeType(input)?.takeIf { it != MimeTypes.TIFF }?.let {
if (it != sourceMimeType) {
Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $it for uri=$uri")
mimeType = it
}
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} catch (e: NoClassDefFoundError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
}
}
val fields: FieldMap = hashMapOf(
EntryFields.ORIGIN to SourceEntry.ORIGIN_UNKNOWN_CONTENT,
EntryFields.URI to uri.toString(),
EntryFields.SOURCE_MIME_TYPE to mimeType,
)
try {
// some providers do not provide the mandatory `OpenableColumns`
// and the query fails when compiling a projection specifying them
// e.g. `content://mms/part/[id]` on Android KitKat
val cursor = context.contentResolver.query(uri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields[EntryFields.TITLE] = cursor.getString(it) }
cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields[EntryFields.SIZE_BYTES] = cursor.getLong(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATA).let { if (it != -1) fields[EntryFields.PATH] = cursor.getString(it) }
// mime type fallback if it was not provided and not found via `metadata-extractor`
cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE).let { if (it != -1 && mimeType == null) fields[EntryFields.SOURCE_MIME_TYPE] = cursor.getString(it) }
cursor.close()
}
} catch (e: Exception) {
callback.onFailure(Exception("Failed to query content, error=${e.message}"))
return
}
if (fields[EntryFields.SOURCE_MIME_TYPE] == null) {
callback.onFailure(Exception("Failed to find MIME type for uri=$uri"))
return
}
val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
if (allowUnsized || entry.isSized || entry.isSvg || entry.isVideo) {
callback.onSuccess(entry.toMap())
} else {
callback.onFailure(Exception("entry has no size"))
}
}
companion object {
private val LOG_TAG = LogUtils.createTag<UnknownContentProvider>()
}
}

View file

@ -2,25 +2,121 @@ package deckers.thibault.aves.utils
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.ColorSpace
import android.os.Build
import android.util.Half
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.TransformationUtils import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import deckers.thibault.aves.metadata.Metadata.getExifCode import deckers.thibault.aves.metadata.Metadata.getExifCode
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
object BitmapUtils { object BitmapUtils {
private val LOG_TAG = LogUtils.createTag<BitmapUtils>() private val LOG_TAG = LogUtils.createTag<BitmapUtils>()
private const val INITIAL_BUFFER_SIZE = 2 shl 17 // 256kB private const val INITIAL_BUFFER_SIZE = 2 shl 17 // 256kB
// arbitrary size to detect buffer that may yield an OOM
private const val BUFFER_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB
private val freeBaos = ArrayList<ByteArrayOutputStream>() private val freeBaos = ArrayList<ByteArrayOutputStream>()
private val mutex = Mutex() private val mutex = Mutex()
suspend fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? { private const val INT_BYTE_SIZE = 4
private const val MAX_2_BITS_FLOAT = 0x3.toFloat()
private const val MAX_8_BITS_FLOAT = 0xff.toFloat()
private const val MAX_10_BITS_FLOAT = 0x3ff.toFloat()
private const val RAW_BYTES_TRAILER_LENGTH = INT_BYTE_SIZE * 2
// bytes per pixel with different bitmap config
private const val BPP_ALPHA_8 = 1
private const val BPP_RGB_565 = 2
private const val BPP_ARGB_8888 = 4
private const val BPP_RGBA_1010102 = 4
private const val BPP_RGBA_F16 = 8
private fun getBytePerPixel(config: Bitmap.Config?): Int {
return when (config) {
Bitmap.Config.ALPHA_8 -> BPP_ALPHA_8
Bitmap.Config.RGB_565 -> BPP_RGB_565
Bitmap.Config.ARGB_8888 -> BPP_ARGB_8888
else -> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && config == Bitmap.Config.RGBA_F16) {
BPP_RGBA_F16
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && config == Bitmap.Config.RGBA_1010102) {
BPP_RGBA_1010102
} else {
// default
BPP_ARGB_8888
}
}
}
}
fun getExpectedImageSize(pixelCount: Long, config: Bitmap.Config?): Long {
return pixelCount * getBytePerPixel(config)
}
fun getRawBytes(bitmap: Bitmap?, recycle: Boolean): ByteArray? {
bitmap ?: return null
val byteCount = bitmap.byteCount
val width = bitmap.width
val height = bitmap.height
val config = bitmap.config
val colorSpace = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) bitmap.colorSpace else null
if (!MemoryUtils.canAllocate(byteCount)) {
throw Exception("bitmap buffer is $byteCount bytes, which cannot be allocated to a new byte array")
}
try {
// `ByteBuffer` initial order is always `BIG_ENDIAN`
var bytes = ByteBuffer.allocate(byteCount + RAW_BYTES_TRAILER_LENGTH).apply {
bitmap.copyPixelsToBuffer(this)
}.array()
// do not access bitmap after recycling
if (recycle) bitmap.recycle()
// convert pixel format and color space, if necessary
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
colorSpace?.let { srcColorSpace ->
val dstColorSpace = ColorSpace.get(ColorSpace.Named.SRGB)
val connector = ColorSpace.connect(srcColorSpace, dstColorSpace)
if (config == Bitmap.Config.ARGB_8888) {
if (srcColorSpace != dstColorSpace) {
argb8888ToArgb8888(bytes, connector, end = byteCount)
}
} else if (config == Bitmap.Config.RGBA_F16) {
rgbaf16ToArgb8888(bytes, connector, end = byteCount)
val newConfigByteCount = byteCount / (BPP_RGBA_F16 / BPP_ARGB_8888)
bytes = bytes.sliceArray(0..<newConfigByteCount + RAW_BYTES_TRAILER_LENGTH)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && config == Bitmap.Config.RGBA_1010102) {
rgba1010102ToArgb8888(bytes, connector, end = byteCount)
}
}
}
// append bitmap size for use by the caller to interpret the raw bytes
val trailerOffset = bytes.size - RAW_BYTES_TRAILER_LENGTH
bytes = ByteBuffer.wrap(bytes).apply {
position(trailerOffset)
putInt(width)
putInt(height)
}.array()
return bytes
} catch (e: Exception) {
Log.e(LOG_TAG, "failed to get bytes from bitmap", e)
}
return null
}
suspend fun getEncodedBytes(bitmap: Bitmap?, canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? {
bitmap ?: return null
val stream: ByteArrayOutputStream val stream: ByteArrayOutputStream
mutex.withLock { mutex.withLock {
// this method is called a lot, so we try and reuse output streams // this method is called a lot, so we try and reuse output streams
@ -32,19 +128,17 @@ object BitmapUtils {
} }
} }
try { try {
// the Bitmap raw bytes are not decodable by Flutter
// we need to format them (compress, or add a BMP header) before sending them
// `Bitmap.CompressFormat.PNG` is slower than `JPEG`, but it allows transparency // `Bitmap.CompressFormat.PNG` is slower than `JPEG`, but it allows transparency
// the BMP format allows an alpha channel, but Android decoding seems to ignore it // the BMP format allows an alpha channel, but Android decoding seems to ignore it
if (canHaveAlpha && hasAlpha()) { if (canHaveAlpha && bitmap.hasAlpha()) {
this.compress(Bitmap.CompressFormat.PNG, quality, stream) bitmap.compress(Bitmap.CompressFormat.PNG, quality, stream)
} else { } else {
this.compress(Bitmap.CompressFormat.JPEG, quality, stream) bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream)
} }
if (recycle) this.recycle() if (recycle) bitmap.recycle()
val bufferSize = stream.size() val bufferSize = stream.size()
if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD && !MemoryUtils.canAllocate(bufferSize)) { if (!MemoryUtils.canAllocate(bufferSize)) {
throw Exception("bitmap compressed to $bufferSize bytes, which cannot be allocated to a new byte array") throw Exception("bitmap compressed to $bufferSize bytes, which cannot be allocated to a new byte array")
} }
@ -60,6 +154,107 @@ object BitmapUtils {
return null return null
} }
// convert bytes, without reallocation:
// - from original color space to sRGB.
@RequiresApi(Build.VERSION_CODES.O)
private fun argb8888ToArgb8888(bytes: ByteArray, connector: ColorSpace.Connector, start: Int = 0, end: Int = bytes.size) {
// unpacking from ARGB_8888 and packing to ARGB_8888
// stored as [3,2,1,0] -> [AAAAAAAA BBBBBBBB GGGGGGGG RRRRRRRR]
for (i in start..<end step BPP_ARGB_8888) {
// mask with `0xff` to yield values in [0, 255], instead of [-128, 127]
val iB = bytes[i + 2].toInt() and 0xff
val iG = bytes[i + 1].toInt() and 0xff
val iR = bytes[i].toInt() and 0xff
// components as floats in sRGB
val srgbFloats = connector.transform(iR / MAX_8_BITS_FLOAT, iG / MAX_8_BITS_FLOAT, iB / MAX_8_BITS_FLOAT)
val srgbR = (srgbFloats[0] * 255.0f + 0.5f).toInt()
val srgbG = (srgbFloats[1] * 255.0f + 0.5f).toInt()
val srgbB = (srgbFloats[2] * 255.0f + 0.5f).toInt()
// keep alpha as it is, in `bytes[i + 3]`
bytes[i + 2] = srgbB.toByte()
bytes[i + 1] = srgbG.toByte()
bytes[i] = srgbR.toByte()
}
}
// convert bytes, without reallocation:
// - from config RGBA_F16 to ARGB_8888,
// - from original color space to sRGB.
@RequiresApi(Build.VERSION_CODES.O)
private fun rgbaf16ToArgb8888(bytes: ByteArray, connector: ColorSpace.Connector, start: Int = 0, end: Int = bytes.size) {
val indexDivider = BPP_RGBA_F16 / BPP_ARGB_8888
for (i in start..<end step BPP_RGBA_F16) {
// unpacking from RGBA_F16
// stored as [7,6,5,4,3,2,1,0] -> [AAAAAAAA AAAAAAAA BBBBBBBB BBBBBBBB GGGGGGGG GGGGGGGG RRRRRRRR RRRRRRRR]
val i7 = bytes[i + 7].toInt()
val i6 = bytes[i + 6].toInt()
val i5 = bytes[i + 5].toInt()
val i4 = bytes[i + 4].toInt()
val i3 = bytes[i + 3].toInt()
val i2 = bytes[i + 2].toInt()
val i1 = bytes[i + 1].toInt()
val i0 = bytes[i].toInt()
val hA = Half((((i7 and 0xff) shl 8) or (i6 and 0xff)).toShort())
val hB = Half((((i5 and 0xff) shl 8) or (i4 and 0xff)).toShort())
val hG = Half((((i3 and 0xff) shl 8) or (i2 and 0xff)).toShort())
val hR = Half((((i1 and 0xff) shl 8) or (i0 and 0xff)).toShort())
// components as floats in sRGB
val srgbFloats = connector.transform(hR.toFloat(), hG.toFloat(), hB.toFloat())
val srgbR = (srgbFloats[0] * 255.0f + 0.5f).toInt()
val srgbG = (srgbFloats[1] * 255.0f + 0.5f).toInt()
val srgbB = (srgbFloats[2] * 255.0f + 0.5f).toInt()
val alpha = (hA.toFloat() * 255.0f + 0.5f).toInt()
// packing to ARGB_8888
// stored as [3,2,1,0] -> [AAAAAAAA BBBBBBBB GGGGGGGG RRRRRRRR]
val dstI = i / indexDivider
bytes[dstI + 3] = alpha.toByte()
bytes[dstI + 2] = srgbB.toByte()
bytes[dstI + 1] = srgbG.toByte()
bytes[dstI] = srgbR.toByte()
}
}
// convert bytes, without reallocation:
// - from config RGBA_1010102 to ARGB_8888,
// - from original color space to sRGB.
@RequiresApi(Build.VERSION_CODES.O)
private fun rgba1010102ToArgb8888(bytes: ByteArray, connector: ColorSpace.Connector, start: Int = 0, end: Int = bytes.size) {
val alphaFactor = 255.0f / MAX_2_BITS_FLOAT
for (i in start..<end step BPP_RGBA_1010102) {
// unpacking from RGBA_1010102
// stored as [3,2,1,0] -> [AABBBBBB BBBBGGGG GGGGGGRR RRRRRRRR]
val i3 = bytes[i + 3].toInt()
val i2 = bytes[i + 2].toInt()
val i1 = bytes[i + 1].toInt()
val i0 = bytes[i].toInt()
val iA = ((i3 and 0xc0) shr 6)
val iB = ((i3 and 0x3f) shl 4) or ((i2 and 0xf0) shr 4)
val iG = ((i2 and 0x0f) shl 6) or ((i1 and 0xfc) shr 2)
val iR = ((i1 and 0x03) shl 8) or ((i0 and 0xff) shr 0)
// components as floats in sRGB
val srgbFloats = connector.transform(iR / MAX_10_BITS_FLOAT, iG / MAX_10_BITS_FLOAT, iB / MAX_10_BITS_FLOAT)
val srgbR = (srgbFloats[0] * 255.0f + 0.5f).toInt()
val srgbG = (srgbFloats[1] * 255.0f + 0.5f).toInt()
val srgbB = (srgbFloats[2] * 255.0f + 0.5f).toInt()
val alpha = (iA * alphaFactor + 0.5f).toInt()
// packing to ARGB_8888
// stored as [3,2,1,0] -> [AAAAAAAA BBBBBBBB GGGGGGGG RRRRRRRR]
bytes[i + 3] = alpha.toByte()
bytes[i + 2] = srgbB.toByte()
bytes[i + 1] = srgbG.toByte()
bytes[i] = srgbR.toByte()
}
}
fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? { fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? {
if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap
if (rotationDegrees == 0 && !isFlipped) return bitmap if (rotationDegrees == 0 && !isFlipped) return bitmap

View file

@ -90,12 +90,7 @@ object BmpWriter {
var column = 0 var column = 0
while (column < biWidth) { while (column < biWidth) {
/* // non-premultiplied ARGB values in the sRGB color space
alpha: (value shr 24 and 0xFF).toByte()
red: (value shr 16 and 0xFF).toByte()
green: (value shr 8 and 0xFF).toByte()
blue: (value and 0xFF).toByte()
*/
value = pixels[column] value = pixels[column]
// blue: [0], green: [1], red: [2] // blue: [0], green: [1], red: [2]
rgb[0] = (value and 0xFF).toByte() rgb[0] = (value and 0xFF).toByte()

View file

@ -8,6 +8,8 @@ fun ByteBuffer.toByteArray(): ByteArray {
return bytes return bytes
} }
fun Int.toHex(): String = "0x${byteArrayOf(shr(8).toByte(), toByte()).toHex()}"
fun ByteArray.toHex(): String = joinToString(separator = "") { it.toHex() } fun ByteArray.toHex(): String = joinToString(separator = "") { it.toHex() }
fun Byte.toHex(): String = "%02x".format(this) fun Byte.toHex(): String = "%02x".format(this)

View file

@ -20,6 +20,7 @@ fun <E> MutableList<E>.compatRemoveIf(filter: (t: E) -> Boolean): Boolean {
} }
// Boyer-Moore algorithm for pattern searching // Boyer-Moore algorithm for pattern searching
// Returns: an index of the first occurrence of the pattern or -1 if none is found.
fun ByteArray.indexOfBytes(pattern: ByteArray, start: Int = 0): Int { fun ByteArray.indexOfBytes(pattern: ByteArray, start: Int = 0): Int {
val n: Int = this.size val n: Int = this.size
val m: Int = pattern.size val m: Int = pattern.size

View file

@ -56,13 +56,7 @@ fun Geocoder.getFromLocationCompat(
onError: (errorCode: String, errorMessage: String?, errorDetails: Any?) -> Unit, onError: (errorCode: String, errorMessage: String?, errorDetails: Any?) -> Unit,
) { ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener { Compat33.geocoderGetFromLocation(this, latitude, longitude, maxResults, processAddresses, onError)
override fun onGeocode(addresses: List<Address?>) = processAddresses(addresses.filterNotNull())
override fun onError(errorMessage: String?) {
onError("getAddress-asyncerror", "failed to get address", errorMessage)
}
})
} else { } else {
try { try {
@Suppress("deprecation") @Suppress("deprecation")

View file

@ -0,0 +1,30 @@
package deckers.thibault.aves.utils
import android.location.Address
import android.location.Geocoder
import android.os.Build
import androidx.annotation.RequiresApi
/**
* Compatibility layer in a separate object to avoid class loading issues on older Android versions.
* e.g. `ClassNotFoundException` for `android.location.Geocoder$GeocodeListener`
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
object Compat33 {
fun geocoderGetFromLocation(
geocoder: Geocoder,
latitude: Double,
longitude: Double,
maxResults: Int,
processAddresses: (addresses: List<Address>) -> Unit,
onError: (errorCode: String, errorMessage: String?, errorDetails: Any?) -> Unit,
) {
geocoder.getFromLocation(latitude, longitude, maxResults, object : Geocoder.GeocodeListener {
override fun onGeocode(addresses: List<Address?>) = processAddresses(addresses.filterNotNull())
override fun onError(errorMessage: String?) {
onError("getAddress-asyncerror", "failed to get address", errorMessage)
}
})
}
}

View file

@ -0,0 +1,10 @@
package deckers.thibault.aves.utils
inline fun <reified T : Throwable> Exception.anyCauseIs(): Boolean {
var cause: Throwable? = this
while (cause != null) {
if (cause is T) return true
cause = cause.cause
}
return false
}

View file

@ -1,8 +1,6 @@
package deckers.thibault.aves.utils package deckers.thibault.aves.utils
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
@ -71,29 +69,4 @@ object FlutterUtils {
r.run() r.run()
} }
} }
fun Intent.enableSoftwareRendering() {
putExtra("enable-software-rendering", true)
Log.i(LOG_TAG, "Enable software rendering")
}
fun isSoftwareRenderingRequired() = Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT && isEmulator
private val isEmulator: Boolean
get() = (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
|| Build.FINGERPRINT.startsWith("generic")
|| Build.FINGERPRINT.startsWith("unknown")
|| Build.HARDWARE.contains("goldfish")
|| Build.HARDWARE.contains("ranchu")
|| Build.MODEL.contains("google_sdk")
|| Build.MODEL.contains("Emulator")
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| Build.PRODUCT.contains("sdk_google")
|| Build.PRODUCT.contains("google_sdk")
|| Build.PRODUCT.contains("sdk")
|| Build.PRODUCT.contains("sdk_x86")
|| Build.PRODUCT.contains("vbox86p")
|| Build.PRODUCT.contains("emulator")
|| Build.PRODUCT.contains("simulator"))
} }

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