Compare commits

..

278 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
820 changed files with 19386 additions and 5858 deletions

@ -1 +1 @@
Subproject commit 17025dd88227cd9532c33fa78f5250d548d87e9a
Subproject commit d8a9f9a52e5af486f80d932e838ee93861ffd863

View file

@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
@ -28,6 +28,9 @@ jobs:
- name: Get Flutter packages
run: ./flutterw pub get
- name: Generate app localizations
run: ./flutterw gen-l10n
- name: Static analysis.
run: ./flutterw analyze
@ -52,14 +55,14 @@ jobs:
build-mode: manual
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
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@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'temurin'
java-version: '21'
@ -69,7 +72,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@ -83,6 +86,6 @@ jobs:
./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with:
category: "/language:${{matrix.language}}"

View file

@ -18,14 +18,14 @@ jobs:
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
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@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
with:
distribution: 'temurin'
java-version: '21'
@ -36,6 +36,9 @@ jobs:
- name: Get Flutter packages
run: ./flutterw pub get
- name: Generate app localizations
run: ./flutterw gen-l10n
- name: Update Flutter version file
run: scripts/update_flutter_version.sh
@ -75,19 +78,19 @@ jobs:
AVES_GOOGLE_API_KEY: ${{ secrets.AVES_GOOGLE_API_KEY }}
- name: Generate artifact attestation
uses: actions/attest-build-provenance@7668571508540a607bdfd90a87a560489fe372eb # v2.1.0
uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0
with:
subject-path: 'outputs/*'
- name: Create GitHub release
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
uses: ncipollo/release-action@440c8c1cb0ed28b9f43e4d1d670870f059653174 # v1.16.0
with:
artifacts: "outputs/*"
body: "[Changelog](https://github.com/${{ github.repository }}/blob/develop/CHANGELOG.md#${{ github.ref_name }})"
token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload app bundle
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: appbundle
path: outputs/app-play-release.aab
@ -98,7 +101,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
@ -106,7 +109,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Get appbundle from artifacts
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: appbundle

View file

@ -31,7 +31,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
with:
egress-policy: audit
@ -41,7 +41,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with:
results_file: results.sarif
results_format: sarif
@ -63,7 +63,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: SARIF file
path: results.sarif
@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9
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
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
@ -27,7 +29,6 @@ migrate_working_dir/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
@ -46,3 +47,6 @@ app.*.map.json
# screenshot generation
/test_driver/assets/screenshots/
/screenshots/
# generated files
/lib/l10ngen/app_localizations*

29
.vscode/launch.json vendored Normal file
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,177 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased]
### Added
- Info: show matching dynamic albums
### Fixed
- crash when decoding some large thumbnails
## <a id="v1.13.2"></a>[v1.13.2] - 2025-06-02
### Changed
- downgraded Flutter to stable v3.27.4
- prevent display orientation flip when device rotation is locked
### Fixed
- moved file losing its extension and no longer being detected as media in some cases
- opening home when launching app as media picker
- removing groups with obsolete albums
- loading group custom covers
- crash when parsing some large media with trailing thumbnail
## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14
### Fixed
- albums: show groups to move/copy/export items
- albums: hide grouped albums containing hidden items only
## <a id="v1.13.0"></a>[v1.13.0] - 2025-05-12
### Added
- Albums: groups
- Collection: sort by storage path
- Search: week day filters
### Changed
- revert to Skia rendering engine
## <a id="v1.12.10"></a>[v1.12.10] - 2025-04-16
### Added
- Search: format filters
- Albums: sort by path
### Changed
- upgraded Flutter to stable v3.29.3
### Fixed
- region decoding failing to access decoder pool
## <a id="v1.12.9"></a>[v1.12.9] - 2025-04-06
### Added
- Kannada translation (thanks Chethan, Prasannakumar T Bhat)
### Changed
- enable Impeller rendering engine
### Fixed
- memory pressure during browsing
## <a id="v1.12.8"></a>[v1.12.8] - 2025-03-25
### Fixed
- swiping images for some combinations of screen size, device pixel ratio, and image size
## <a id="v1.12.7"></a>[v1.12.7] - 2025-03-16
### Added
- handle launch error to report and export DB
### Changed
- DB post-upgrade sanitization
- upgraded Flutter to stable v3.29.2
## <a id="v1.12.6"></a>[v1.12.6] - 2025-03-11
### Fixed
- data loss when editing metadata of items with incorrect mime types
- metadata inconsistency in the DB due to v1.12.4 upgrade
## <a id="v1.12.5"></a>[v1.12.5] - 2025-03-07
### Added
- 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

View file

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

View file

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

5
android/.gitignore vendored
View file

@ -5,9 +5,12 @@ gradle-wrapper.jar
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
.kotlin/
/build/
# 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
**/*.keystore
**/*.jks

View file

@ -33,15 +33,13 @@ kotlin {
}
android {
namespace 'deckers.thibault.aves'
compileSdk 35
// cf https://developer.android.com/studio/projects/install-ndk#default-ndk-per-agp
ndkVersion '27.0.12077973'
namespace = 'deckers.thibault.aves'
compileSdk = 36
defaultConfig {
applicationId packageName
minSdk flutter.minSdkVersion
targetSdk 35
targetSdk 36
versionCode flutter.versionCode
versionName flutter.versionName
manifestPlaceholders = [googleApiKey: keystoreProperties["googleApiKey"] ?: "<NONE>"]
@ -136,14 +134,14 @@ flutter {
repositories {
maven {
url 'https://jitpack.io'
url = 'https://jitpack.io'
content {
includeGroup "com.github.deckerst"
includeGroup "com.github.deckerst.mp4parser"
}
}
maven {
url 'https://s3.amazonaws.com/repo.commonsware.com'
url = 'https://s3.amazonaws.com/repo.commonsware.com'
content {
excludeGroupByRegex "com\\.github\\.deckerst.*"
}
@ -151,35 +149,36 @@ repositories {
}
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2'
implementation "androidx.appcompat:appcompat:1.7.0"
implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
implementation "androidx.appcompat:appcompat:1.7.1"
implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.lifecycle:lifecycle-process:2.9.1'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.security:security-crypto:1.1.0-alpha06'
implementation 'androidx.work:work-runtime-ktx:2.10.0'
implementation 'androidx.security:security-crypto:1.1.0-beta01'
implementation 'androidx.work:work-runtime-ktx:2.10.1'
implementation 'com.caverock:androidsvg-aar:1.4'
implementation 'com.commonsware.cwac:document:0.5.0'
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation 'com.google.android.material:material:1.12.0'
// SLF4J implementation for `mp4parser`
implementation 'org.slf4j:slf4j-simple:2.0.16'
implementation 'org.slf4j:slf4j-simple:2.0.17'
// forked, built by JitPack:
// - https://jitpack.io/p/deckerst/Android-TiffBitmapFactory
// - https://jitpack.io/p/deckerst/androidsvg
// - https://jitpack.io/p/deckerst/mp4parser
// - https://jitpack.io/p/deckerst/pixymeta-android
implementation 'com.github.deckerst:Android-TiffBitmapFactory:90c06eebf4'
implementation 'com.github.deckerst.mp4parser:isoparser:d5caf7a3dd'
implementation 'com.github.deckerst.mp4parser:muxer:d5caf7a3dd'
implementation 'com.github.deckerst:pixymeta-android:9ec7097f17'
implementation 'com.github.deckerst:Android-TiffBitmapFactory:d6b2b0aa4f'
implementation 'com.github.deckerst:androidsvg:67db933051'
implementation 'com.github.deckerst.mp4parser:isoparser:c2898f1832'
implementation 'com.github.deckerst.mp4parser:muxer:c2898f1832'
implementation 'com.github.deckerst:pixymeta-android:cb1cdc932e'
implementation project(':exifinterface')
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.11.3'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.13.1'
kapt 'androidx.annotation:annotation:1.9.1'
ksp "com.github.bumptech.glide:ksp:$glide_version"

View file

@ -329,10 +329,6 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!--
Screenshot driver scenario is not supported by Impeller:
"Compressed screenshots not supported for Impeller"
-->
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />

View file

@ -14,6 +14,7 @@ import androidx.work.ForegroundInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.channel.calls.DeviceHandler
import deckers.thibault.aves.channel.calls.GeocodingHandler
import deckers.thibault.aves.channel.calls.MediaFetchObjectHandler
@ -44,11 +45,12 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
private var backgroundChannel: MethodChannel? = null
override suspend fun doWork(): Result {
Log.i(LOG_TAG, "Start analysis worker $id")
defaultScope.launch {
// prevent ANR triggered by slow operations in main thread
createNotificationChannel()
setForeground(createForegroundInfo())
}
}.join()
suspendCoroutine { cont ->
workCont = cont
onStart()
@ -68,7 +70,6 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
}
private fun onStart() {
Log.i(LOG_TAG, "Start analysis worker $id")
runBlocking {
FlutterUtils.initFlutterEngine(applicationContext, SHARED_PREFERENCES_KEY, PREF_CALLBACK_HANDLE_KEY) {
flutterEngine = it
@ -132,12 +133,7 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
result.success(null)
}
"updateNotification" -> {
val title = call.argument<String>("title")
val message = call.argument<String>("message")
setForegroundAsync(createForegroundInfo(title, message))
result.success(null)
}
"updateNotification" -> defaultScope.launch { safeSuspend(call, result, ::updateNotification) }
"stop" -> {
workCont?.resume(null)
@ -180,17 +176,22 @@ class AnalysisWorker(context: Context, parameters: WorkerParameters) : Coroutine
.setContentIntent(openAppIntent)
.addAction(stopAction)
.build()
return if (Build.VERSION.SDK_INT == 34) {
// from Android 14 (API 34), foreground service type is mandatory for long-running workers:
// https://developer.android.com/guide/background/persistent/how-to/long-running
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else if (Build.VERSION.SDK_INT >= 35) {
ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
} else {
ForegroundInfo(NOTIFICATION_ID, notification)
// from Android 14 (API 34), foreground service type is mandatory for long-running workers:
// https://developer.android.com/guide/background/persistent/how-to/long-running
return when {
Build.VERSION.SDK_INT >= 35 -> ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
Build.VERSION.SDK_INT == 34 -> ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
else -> ForegroundInfo(NOTIFICATION_ID, notification)
}
}
private suspend fun updateNotification(call: MethodCall, result: MethodChannel.Result) {
val title = call.argument<String>("title")
val message = call.argument<String>("message")
setForeground(createForegroundInfo(title, message))
result.success(null)
}
companion object {
private val LOG_TAG = LogUtils.createTag<AnalysisWorker>()
private const val BACKGROUND_CHANNEL = "deckers.thibault/aves/analysis_service_background"

View file

@ -8,7 +8,6 @@ import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
@ -16,6 +15,8 @@ import android.os.Looper
import android.util.Log
import android.util.SizeF
import android.widget.RemoteViews
import androidx.core.graphics.createBitmap
import androidx.core.net.toUri
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.DeviceHandler
@ -83,7 +84,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
private fun getDevicePixelRatio(): Float = Resources.getSystem().displayMetrics.density
private fun getWidgetSizesDip(context: Context, widgetInfo: Bundle): List<FieldMap> {
private fun getWidgetSizesDip(context: Context, widgetInfo: Bundle): List<SizeF> {
var sizes: List<SizeF>? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
widgetInfo.getParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, SizeF::class.java)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -102,7 +103,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
sizes = listOf(SizeF(widthDip.toFloat(), heightDip.toFloat()))
}
return sizes.map { size -> hashMapOf("widthDip" to size.width, "heightDip" to size.height) }
return sizes
}
private suspend fun getProps(
@ -116,13 +117,14 @@ class HomeWidgetProvider : AppWidgetProvider() {
if (sizesDip.isEmpty()) return null
val sizeDip = sizesDip.first()
if (sizeDip["widthDip"] == 0 || sizeDip["heightDip"] == 0) return null
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 sizesDip,
"sizesDip" to sizesDipMap,
"devicePixelRatio" to getDevicePixelRatio(),
"drawEntryImage" to drawEntryImage,
"reuseEntry" to reuseEntry,
@ -217,7 +219,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
val heightPx = (sizeDip.height * devicePixelRatio).roundToInt()
try {
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888).also {
val bitmap = createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888).also {
bitmaps.add(it)
it.copyPixelsFromBuffer(ByteBuffer.wrap(bytes))
}
@ -259,7 +261,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
}
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))
return PendingIntent.getBroadcast(
@ -276,7 +278,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
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
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)
return PendingIntent.getActivity(

View file

@ -19,6 +19,7 @@ import androidx.annotation.RequiresApi
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.AvesByteSendingMethodCodec
import deckers.thibault.aves.channel.calls.AccessibilityHandler
@ -442,7 +443,7 @@ open class MainActivity : FlutterFragmentActivity() {
return
}
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this@MainActivity, Uri.parse(uriString)) }
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this@MainActivity, uriString.toUri()) }
val intent = Intent().apply {
val firstUri = toUri(pickedUris.first())
if (pickedUris.size == 1) {

View file

@ -5,7 +5,15 @@ import android.util.Log
import android.view.View
import app.loup.streams_channel.StreamsChannel
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.WindowHandler
import deckers.thibault.aves.channel.streams.ImageByteStreamHandler

View file

@ -16,8 +16,12 @@ import deckers.thibault.aves.utils.FlutterUtils
import deckers.thibault.aves.utils.LogUtils
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.util.*
import kotlinx.coroutines.CoroutineScope
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.resumeWithException
import kotlin.coroutines.suspendCoroutine

View file

@ -2,6 +2,7 @@ package deckers.thibault.aves
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import deckers.thibault.aves.channel.calls.AppAdapterHandler
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.getParcelableExtraCompat
@ -39,7 +40,7 @@ class WallpaperActivity : MainActivity() {
if (originalIntent != null) {
val pickedUris = call.argument<List<String>>("uris")
if (!pickedUris.isNullOrEmpty()) {
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, Uri.parse(uriString)) }
val toUri = { uriString: String -> AppAdapterHandler.getShareableUri(this, uriString.toUri()) }
onNewIntent(Intent().apply {
action = originalIntent
data = toUri(pickedUris.first())

View file

@ -21,27 +21,28 @@ class AvesByteSendingMethodCodec private constructor() : MethodCodec {
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 {
if (result is ByteArray) {
val size = result.size
return ByteBuffer.allocateDirect(4 + size).apply {
return ByteBuffer.allocateDirect(1 + result.size).apply {
// following `StandardMethodCodec`:
// First byte is zero in success case, and non-zero otherwise.
put(0)
put(result)
}
}
Log.e(LOG_TAG, "encodeSuccessEnvelope failed with result=$result")
return ByteBuffer.allocateDirect(0)
}
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)
return encodeErrorEnvelope("invalid-result-type", "Called success with a result which is not a `ByteArray`, type=${result?.javaClass}", null)
}
companion object {

View file

@ -6,6 +6,7 @@ import android.content.res.Configuration
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.view.ViewConfiguration
import android.view.accessibility.AccessibilityManager
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
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) {
when (call.method) {
"areAnimationsRemoved" -> safe(call, result, ::areAnimationsRemoved)
"getLongPressTimeout" -> safe(call, result, ::getLongPressTimeout)
"hasRecommendedTimeouts" -> safe(call, result, ::hasRecommendedTimeouts)
"getRecommendedTimeoutMillis" -> safe(call, result, ::getRecommendedTimeoutMillis)
"shouldUseBoldFont" -> safe(call, result, ::shouldUseBoldFont)
@ -34,6 +36,10 @@ class AccessibilityHandler(private val contextWrapper: ContextWrapper) : MethodC
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) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
}

View file

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

View file

@ -19,6 +19,7 @@ import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
@ -37,7 +38,6 @@ import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.anyCauseIs
import deckers.thibault.aves.utils.getApplicationInfoCompat
@ -153,7 +153,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// convert DIP to physical pixels here, instead of using `devicePixelRatio` in Flutter
val density = context.resources.displayMetrics.density
val size = (sizeDip * density).roundToInt()
var data: ByteArray? = null
var bytes: ByteArray? = null
try {
val iconResourceId = context.packageManager.getApplicationInfoCompat(packageName, 0).icon
if (iconResourceId != Resources.ID_NULL) {
@ -174,7 +174,9 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
try {
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) {
Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e)
}
@ -184,15 +186,15 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
Log.w(LOG_TAG, "failed to get app info for packageName=$packageName", e)
return
}
if (data != null) {
result.success(data)
if (bytes != null) {
result.success(bytes)
} else {
result.error("getAppIcon-null", "failed to get icon for packageName=$packageName", null)
}
}
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")
if (uri == null) {
result.error("copyToClipboard-args", "missing arguments", null)
@ -219,7 +221,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
private fun open(call: MethodCall, result: MethodChannel.Result) {
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 forceChooser = call.argument<Boolean>("forceChooser")
if (uri == null || forceChooser == null) {
@ -236,7 +238,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
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) {
result.error("openMap-args", "missing arguments", null)
return
@ -250,7 +252,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
private fun setAs(call: MethodCall, result: MethodChannel.Result) {
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")
if (uri == null) {
result.error("setAs-args", "missing arguments", null)
@ -273,7 +275,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
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()
// simplify share intent for a single item, as some apps can handle one item but not more
@ -366,8 +368,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
// route dependent arguments
val filters = call.argument<List<String>>("filters")
val explorerPath = call.argument<String>("path")
val viewUri = call.argument<String>("viewUri")?.let { Uri.parse(it) }
val geoUri = call.argument<String>("geoUri")?.let { Uri.parse(it) }
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)

View file

@ -12,7 +12,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import androidx.core.net.toUri
import com.drew.metadata.file.FileTypeDirectory
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifInterfaceHelper
@ -44,6 +44,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import org.mp4parser.IsoFile
import java.io.FileInputStream
import java.io.IOException
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
class DebugHandler(private val context: Context) : MethodCallHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -127,7 +128,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
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) {
result.error("getBitmapDecoderInfo-args", "missing arguments", null)
return
@ -156,7 +157,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getContentResolverMetadata(call: MethodCall, result: MethodChannel.Result) {
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) {
result.error("getContentResolverMetadata-args", "missing arguments", null)
return
@ -212,7 +213,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getExifInterfaceMetadata(call: MethodCall, result: MethodChannel.Result) {
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()
if (mimeType == null || uri == null) {
result.error("getExifInterfaceMetadata-args", "missing arguments", null)
@ -239,7 +240,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
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) {
result.error("getMediaMetadataRetrieverMetadata-args", "missing arguments", null)
return
@ -264,7 +265,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getMetadataExtractorSummary(call: MethodCall, result: MethodChannel.Result) {
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()
if (mimeType == null || uri == null) {
result.error("getMetadataExtractorSummary-args", "missing arguments", null)
@ -308,14 +309,14 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getMp4ParserDump(call: MethodCall, result: MethodChannel.Result) {
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) {
result.error("getMp4ParserDump-args", "missing arguments", null)
return
}
val sb = StringBuilder()
if (mimeType == MimeTypes.MP4) {
if (mimeType == MimeTypes.MP4 || MimeTypes.isHeic(mimeType)) {
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")
@ -338,7 +339,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
private fun getPixyMetadata(call: MethodCall, result: MethodChannel.Result) {
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) {
result.error("getPixyMetadata-args", "missing arguments", null)
return
@ -359,7 +360,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
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) {
result.error("getTiffStructure-args", "missing arguments", null)
return

View file

@ -1,17 +1,19 @@
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.Intent
import android.content.pm.PackageManager
import android.content.res.Resources
import android.location.Geocoder
import android.net.Uri
import android.os.Build
import android.os.LocaleList
import android.provider.MediaStore
import android.provider.Settings
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.model.FieldMap
@ -24,7 +26,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.Locale
import java.util.TimeZone
class DeviceHandler(private val context: Context) : MethodCallHandler {
private val defaultScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@ -62,10 +63,17 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"isDynamicColorAvailable" to DynamicColors.isDynamicColorAvailable(),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
"supportEdgeToEdgeUIMode" to (sdkInt >= Build.VERSION_CODES.Q),
"supportPictureInPicture" to supportPictureInPicture(),
)
)
}
private fun supportPictureInPicture(): Boolean {
// 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) {
fun toMap(locale: Locale): FieldMap = hashMapOf(
"language" to locale.language,
@ -95,6 +103,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
@SuppressLint("WrongConstant")
val lm = context.getSystemService(Context.LOCALE_SERVICE) as? LocaleManager
lm?.overrideLocaleConfig = LocaleConfig(LocaleList.forLanguageTags(locales.joinToString(",")))
}
@ -130,7 +139,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
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)
result.success(true)
}

View file

@ -1,16 +1,15 @@
package deckers.thibault.aves.channel.calls
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPUtils
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.metadata.xmp.XmpDirectory
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.MultiPage
import deckers.thibault.aves.metadata.metadataextractor.Helper
@ -18,11 +17,11 @@ 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.provider.ImageProvider
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.FileUtils.transferFrom
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
@ -46,7 +45,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getExifThumbnails" -> ioScope.launch { safeSuspend(call, result, ::getExifThumbnails) }
"getExifThumbnails" -> ioScope.launch { safe(call, result, ::getExifThumbnails) }
"extractGoogleDeviceItem" -> ioScope.launch { safe(call, result, ::extractGoogleDeviceItem) }
"extractJpegMpfItem" -> ioScope.launch { safe(call, result, ::extractJpegMpfItem) }
"extractMotionPhotoImage" -> ioScope.launch { safe(call, result, ::extractMotionPhotoImage) }
@ -57,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 uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) {
result.error("getExifThumbnails-args", "missing arguments", null)
@ -74,7 +73,9 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
exif.thumbnailBitmap?.let { bitmap ->
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) }
}
}
}
@ -88,7 +89,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractGoogleDeviceItem(call: MethodCall, result: MethodChannel.Result) {
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 displayName = call.argument<String>("displayName")
val dataUri = call.argument<String>("dataUri")
@ -143,7 +144,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractJpegMpfItem(call: MethodCall, result: MethodChannel.Result) {
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 displayName = call.argument<String>("displayName")
val id = call.argument<Int>("id")
@ -177,7 +178,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractMotionPhotoImage(call: MethodCall, result: MethodChannel.Result) {
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 displayName = call.argument<String>("displayName")
if (mimeType == null || uri == null || sizeBytes == null) {
@ -185,7 +186,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
return
}
MultiPage.getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
MultiPage.getTrailerVideoSize(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
val imageSizeBytes = sizeBytes - videoSizeBytes
StorageUtils.openInputStream(context, uri)?.let { input ->
copyEmbeddedBytes(result, mimeType, displayName, input, imageSizeBytes)
@ -198,7 +199,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractMotionPhotoVideo(call: MethodCall, result: MethodChannel.Result) {
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 displayName = call.argument<String>("displayName")
if (mimeType == null || uri == null || sizeBytes == null) {
@ -206,11 +207,10 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
return
}
MultiPage.getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
val videoStartOffset = sizeBytes - videoSizeBytes
MultiPage.getMotionPhotoVideoSizing(context, uri, mimeType, sizeBytes)?.let { (videoOffset, videoSize) ->
StorageUtils.openInputStream(context, uri)?.let { input ->
input.skip(videoStartOffset)
copyEmbeddedBytes(result, MimeTypes.MP4, displayName, input, videoSizeBytes)
input.skip(videoOffset)
copyEmbeddedBytes(result, MimeTypes.MP4, displayName, input, videoSize)
}
return
}
@ -219,7 +219,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
}
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")
if (uri == null) {
result.error("extractVideoEmbeddedPicture-args", "missing arguments", null)
@ -251,7 +251,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
private fun extractXmpDataProp(call: MethodCall, result: MethodChannel.Result) {
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 displayName = call.argument<String>("displayName")
val dataProp = call.argument<List<Any>>("propPath")
@ -311,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
embeddedByteStream: InputStream,
embeddedByteLength: Long,
) {
val extension = extensionFor(mimeType)
val extension = extensionFor(mimeType, defaultExtension = null)
val targetFile = StorageUtils.createTempFile(context, extension).apply {
transferFrom(embeddedByteStream, embeddedByteLength)
}
@ -319,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val authority = "${context.applicationContext.packageName}.file_provider"
val uri = if (displayName != null) {
// add extension to ease type identification when sharing this content
val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) {
val displayNameWithExtension = if (displayName.endsWith(extension, ignoreCase = true)) {
displayName
} else {
"$displayName$extension"
@ -329,8 +329,8 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
FileProvider.getUriForFile(context, authority, targetFile)
}
val resultFields: FieldMap = hashMapOf(
"uri" to uri.toString(),
"mimeType" to mimeType,
EntryFields.URI to uri.toString(),
EntryFields.MIME_TYPE to mimeType,
)
if (isImage(mimeType) || isVideo(mimeType)) {
val provider = getProvider(context, uri)

View file

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

View file

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

View file

@ -1,8 +1,8 @@
package deckers.thibault.aves.channel.calls
import android.content.ContextWrapper
import android.net.Uri
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.safeSuspend
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) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val uri = call.argument<String>("uri")?.toUri()
val desiredName = call.argument<String>("desiredName")
val exifFields = call.argument<FieldMap>("exif") ?: HashMap()
val bytes = call.argument<ByteArray>("bytes")

View file

@ -2,12 +2,13 @@ package deckers.thibault.aves.channel.calls
import android.content.Context
import android.graphics.Rect
import android.net.Uri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safeSuspend
import androidx.core.net.toUri
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.SvgRegionFetcher
import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.utils.MimeTypes
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -27,18 +28,18 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getThumbnail" -> ioScope.launch { safeSuspend(call, result, ::getThumbnail) }
"getRegion" -> ioScope.launch { safeSuspend(call, result, ::getRegion) }
"getThumbnail" -> ioScope.launch { safe(call, result, ::getThumbnail) }
"getRegion" -> ioScope.launch { safe(call, result, ::getRegion) }
else -> result.notImplemented()
}
}
private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")
val mimeType = call.argument<String>("mimeType")
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong()
val rotationDegrees = call.argument<Int>("rotationDegrees")
val isFlipped = call.argument<Boolean>("isFlipped")
private fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>(EntryFields.URI)
val mimeType = call.argument<String>(EntryFields.MIME_TYPE)
val dateModifiedMillis = call.argument<Number>(EntryFields.DATE_MODIFIED_MILLIS)?.toLong()
val rotationDegrees = call.argument<Int>(EntryFields.ROTATION_DEGREES)
val isFlipped = call.argument<Boolean>(EntryFields.IS_FLIPPED)
val widthDip = call.argument<Number>("widthDip")?.toDouble()
val heightDip = call.argument<Number>("heightDip")?.toDouble()
val pageId = call.argument<Int>("pageId")
@ -55,7 +56,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
context = context,
uri = uri,
mimeType = mimeType,
dateModifiedSecs = dateModifiedSecs ?: (Date().time / 1000),
dateModifiedMillis = dateModifiedMillis ?: (Date().time),
rotationDegrees = rotationDegrees,
isFlipped = isFlipped,
width = (widthDip * density).roundToInt(),
@ -67,8 +68,8 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
).fetch()
}
private suspend fun getRegion(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.toUri()
val mimeType = call.argument<String>("mimeType")
val pageId = call.argument<Int>("pageId")
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
@ -96,6 +97,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
imageHeight = imageHeight,
result = result,
)
MimeTypes.TIFF -> TiffRegionFetcher(context).fetch(
uri = uri,
page = pageId ?: 0,
@ -103,6 +105,7 @@ class MediaFetchBytesHandler(private val context: Context) : MethodCallHandler {
regionRect = regionRect,
result = result,
)
else -> regionFetcher.fetch(
uri = uri,
mimeType = mimeType,

View file

@ -1,7 +1,9 @@
package deckers.thibault.aves.channel.calls
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 deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap
@ -21,14 +23,15 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getEntry" -> ioScope.launch { safe(call, result, ::getEntry) }
"clearSizedThumbnailDiskCache" -> ioScope.launch { safe(call, result, ::clearSizedThumbnailDiskCache) }
"clearImageDiskCache" -> ioScope.launch { safe(call, result, ::clearImageDiskCache) }
"clearImageMemoryCache" -> ioScope.launch { safe(call, result, ::clearImageMemoryCache) }
else -> result.notImplemented()
}
}
private fun getEntry(call: MethodCall, result: MethodChannel.Result) {
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) {
result.error("getEntry-args", "missing arguments", null)
@ -47,11 +50,18 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
})
}
private fun clearSizedThumbnailDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
private fun clearImageDiskCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
Glide.get(context).clearDiskCache()
result.success(null)
}
private fun clearImageMemoryCache(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
Handler(Looper.getMainLooper()).post {
Glide.get(context).clearMemory()
}
result.success(null)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/media_fetch_object"
}

View file

@ -1,12 +1,16 @@
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.session.PlaybackState
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.net.toUri
import androidx.media.session.MediaButtonReceiver
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
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) {
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 durationMillis = call.argument<Number>("durationMillis")?.toLong()
val stateString = call.argument<String>("state")

View file

@ -1,7 +1,7 @@
package deckers.thibault.aves.channel.calls
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.metadata.Mp4TooLargeException
import deckers.thibault.aves.model.ExifOrientationOp
@ -54,7 +54,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
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 mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) {
@ -82,7 +82,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
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 mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) {
@ -109,7 +109,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
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 mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) {
@ -134,7 +134,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
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 mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) {
@ -160,7 +160,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
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 mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) {

View file

@ -6,6 +6,7 @@ import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.core.net.toUri
import com.adobe.internal.xmp.XMPException
import com.adobe.internal.xmp.XMPMeta
import com.adobe.internal.xmp.XMPMetaFactory
@ -22,6 +23,7 @@ import com.drew.metadata.exif.GpsDirectory
import com.drew.metadata.file.FileTypeDirectory
import com.drew.metadata.gif.GifAnimationDirectory
import com.drew.metadata.iptc.IptcDirectory
import com.drew.metadata.mov.metadata.QuickTimeMetadataDirectory
import com.drew.metadata.mp4.media.Mp4UuidBoxDirectory
import com.drew.metadata.png.PngDirectory
import com.drew.metadata.webp.WebpDirectory
@ -100,6 +102,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.json.JSONObject
import org.mp4parser.boxes.threegpp.ts26244.LocationInformationBox
import org.mp4parser.tools.Path
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import java.text.ParseException
@ -131,7 +135,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getAllMetadata(call: MethodCall, result: MethodChannel.Result) {
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()
if (mimeType == null || uri == null) {
result.error("getAllMetadata-args", "missing arguments", null)
@ -448,9 +452,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
if (isVideo(mimeType)) {
// `metadata-extractor` do not extract custom tags in user data box
val userDataDir = Mp4ParserHelper.getUserData(context, mimeType, uri)
if (userDataDir.isNotEmpty()) {
metadataMap[Metadata.DIR_MP4_USER_DATA] = userDataDir
Mp4ParserHelper.getUserDataBox(context, mimeType, uri)?.let { box ->
metadataMap[Metadata.DIR_MP4_USER_DATA] = Mp4ParserHelper.extractBoxFields(box)
}
// this is used as fallback when the video metadata cannot be found on the Dart side
@ -469,6 +472,12 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// Android's `MediaExtractor` and `MediaPlayer` cannot be used for details
// about embedded images as they do not list them as separate tracks
// 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()) {
@ -516,7 +525,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// - XMP / MicrosoftPhoto:Rating
private fun getCatalogMetadata(call: MethodCall, result: MethodChannel.Result) {
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 sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) {
@ -526,8 +535,33 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
val metadataMap = HashMap<String, Any>()
getCatalogMetadataByMetadataExtractor(mimeType, uri, path, sizeBytes, metadataMap)
if (isVideo(mimeType) || isHeic(mimeType)) {
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
@ -685,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) {
MimeTypes.PNG -> {
// date fallback to PNG time chunk
@ -829,7 +879,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
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)
if (locationString != null) {
val matcher = Metadata.VIDEO_LOCATION_PATTERN.matcher(locationString)
@ -869,7 +919,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getOverlayMetadata(call: MethodCall, result: MethodChannel.Result) {
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 fields = call.argument<List<String>>("fields")
if (mimeType == null || uri == null || fields == null) {
@ -957,7 +1007,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(metadataMap)
}
// return description from these fields (by precedence):
// returns description from these fields (by precedence):
// - XMP / dc:description
// - IPTC / caption-abstract
// - Exif / UserComment
@ -1000,7 +1050,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getGeoTiffInfo(call: MethodCall, result: MethodChannel.Result) {
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()
if (mimeType == null || uri == null) {
result.error("getGeoTiffInfo-args", "missing arguments", null)
@ -1041,7 +1091,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getMultiPageInfo(call: MethodCall, result: MethodChannel.Result) {
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 isMotionPhoto = call.argument<Boolean>("isMotionPhoto")
if (mimeType == null || uri == null || sizeBytes == null || isMotionPhoto == null) {
@ -1068,7 +1118,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getPanoramaInfo(call: MethodCall, result: MethodChannel.Result) {
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()
if (mimeType == null || uri == null) {
result.error("getPanoramaInfo-args", "missing arguments", null)
@ -1120,7 +1170,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getIptc(call: MethodCall, result: MethodChannel.Result) {
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) {
result.error("getIptc-args", "missing arguments", null)
return
@ -1142,11 +1192,11 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(null)
}
// return XMP components
// return an empty list if there is no XMP
// returns XMP components
// returns an empty list if there is no XMP
private fun getXmp(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val uri = call.argument<String>("uri")?.toUri()
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
if (mimeType == null || uri == null) {
result.error("getXmp-args", "missing arguments", null)
@ -1218,7 +1268,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getContentPropValue(call: MethodCall, result: MethodChannel.Result) {
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")
if (mimeType == null || uri == null || prop == null) {
result.error("getContentPropValue-args", "missing arguments", null)
@ -1235,7 +1285,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getDate(call: MethodCall, result: MethodChannel.Result) {
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 field = call.argument<String>("field")
if (mimeType == null || uri == null || field == null) {
@ -1304,7 +1354,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
private fun getFields(call: MethodCall, result: MethodChannel.Result) {
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 fields = call.argument<List<String>>("fields")
if (mimeType == null || uri == null || fields == null) {

View file

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

View file

@ -4,21 +4,24 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.ColorSpace
import android.graphics.Rect
import android.net.Uri
import android.os.Build
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.utils.BitmapRegionDecoderCompat
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
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.StorageUtils
import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.max
import kotlin.math.roundToInt
@ -28,16 +31,10 @@ import kotlin.math.roundToInt
class RegionFetcher internal constructor(
private val context: Context,
) {
private var lastDecoderRef: LastDecoderRef? = null
private val pageTempUris = HashMap<Pair<Uri, Int>, Uri>()
private val multiTrackGlideOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
suspend fun fetch(
// returns decoded bytes in ARGB_8888, with trailer bytes:
// - width (int32)
// - height (int32)
fun fetch(
uri: Uri,
mimeType: String,
pageId: Int?,
@ -45,41 +42,31 @@ class RegionFetcher internal constructor(
regionRect: Rect,
imageWidth: Int,
imageHeight: Int,
requestKey: Pair<Uri, Int?> = Pair(uri, pageId),
result: MethodChannel.Result,
) {
if (pageId != null && MultiPageImage.isSupported(mimeType)) {
val id = Pair(uri, pageId)
// use JPEG export for requested page
fetch(
uri = pageTempUris.getOrPut(id) { createJpegForPage(uri, mimeType, pageId) },
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
}
var currentDecoderRef = lastDecoderRef
if (currentDecoderRef != null && currentDecoderRef.uri != uri) {
currentDecoderRef = null
}
try {
if (currentDecoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
BitmapRegionDecoderCompat.newInstance(input)
}
if (newDecoder == null) {
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
return
}
currentDecoderRef = LastDecoderRef(uri, newDecoder)
val decoder = getOrCreateDecoder(context, uri, requestKey)
if (decoder == null) {
result.error("fetch-read-null", "failed to open file for mimeType=$mimeType uri=$uri regionRect=$regionRect", null)
return
}
val decoder = currentDecoderRef.decoder
lastDecoderRef = currentDecoderRef
// with raw images, the known image size may not match the decoded image size
// so we scale the requested region accordingly
@ -101,34 +88,71 @@ class RegionFetcher internal constructor(
}
}
// use `Long` as rect size could be unexpectedly large and go beyond `Int` max
val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * effectiveRect.width() * effectiveRect.height() / effectiveSampleSize
val options = BitmapFactory.Options().apply {
inSampleSize = effectiveSampleSize
// 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
}
val options = BitmapFactory.Options().apply {
inSampleSize = effectiveSampleSize
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 bitmap = decoder.decodeRegion(effectiveRect, options)
if (bitmap != null) {
result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true))
val bytes = BitmapUtils.getRawBytes(bitmap, recycle = true)
if (bytes != null) {
result.success(bytes)
} else {
result.error("fetch-null", "failed to decode region for uri=$uri regionRect=$regionRect", null)
}
} catch (e: Exception) {
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, mimeType: String, 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)
.asBitmap()
.apply(multiTrackGlideOptions)
.load(MultiPageImage(context, sourceUri, mimeType, pageId))
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId))
.submit()
try {
val bitmap = target.get()
val tempFile = StorageUtils.createTempFile(context).apply {
@ -142,8 +166,40 @@ class RegionFetcher internal constructor(
}
}
private data class LastDecoderRef(
val uri: Uri,
private data class DecoderRef(
val requestKey: Pair<Uri, Int?>,
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,25 @@ import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.RectF
import android.net.Uri
import androidx.core.graphics.createBitmap
import com.caverock.androidsvg.PreserveAspectRatio
import com.caverock.androidsvg.RenderOptions
import com.caverock.androidsvg.SVG
import com.caverock.androidsvg.SVGParseException
import deckers.thibault.aves.metadata.SVGParserBufferedInputStream
import deckers.thibault.aves.metadata.SvgHelper.normalizeSize
import deckers.thibault.aves.utils.BitmapUtils.ARGB_8888_BYTE_SIZE
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
import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.ceil
class SvgRegionFetcher internal constructor(
private val context: Context,
) {
private var lastSvgRef: LastSvgRef? = null
suspend fun fetch(
fun fetch(
uri: Uri,
sizeBytes: Long?,
scale: Int,
@ -39,32 +39,12 @@ class SvgRegionFetcher internal constructor(
return
}
var currentSvgRef = lastSvgRef
if (currentSvgRef != null && currentSvgRef.uri != uri) {
currentSvgRef = null
}
try {
if (currentSvgRef == null) {
val newSvg = StorageUtils.openInputStream(context, uri)?.use { input ->
try {
SVG.getFromInputStream(SVGParserBufferedInputStream(input))
} catch (ex: SVGParseException) {
result.error("fetch-parse", "failed to parse SVG for uri=$uri regionRect=$regionRect", null)
return
}
}
if (newSvg == null) {
result.error("fetch-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
return
}
newSvg.normalizeSize()
currentSvgRef = LastSvgRef(uri, newSvg)
val svg = getOrCreateDecoder(context, uri)
if (svg == null) {
result.error("fetch-read-null", "failed to open file for uri=$uri regionRect=$regionRect", null)
return
}
val svg = currentSvgRef.svg
lastSvgRef = currentSvgRef
// we scale the requested region accordingly to the viewbox size
val viewBox = svg.documentViewBox
@ -91,32 +71,65 @@ class SvgRegionFetcher internal constructor(
val targetBitmapWidth = regionRect.width()
val targetBitmapHeight = regionRect.height()
val canvasWidth = targetBitmapWidth + bleedX * 2
val canvasHeight = targetBitmapHeight + bleedY * 2
// use `Long` as rect size could be unexpectedly large and go beyond `Int` max
val targetBitmapSizeBytes: Long = ARGB_8888_BYTE_SIZE.toLong() * targetBitmapWidth * targetBitmapHeight
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 = Bitmap.createBitmap(
targetBitmapWidth + bleedX * 2,
targetBitmapHeight + bleedY * 2,
Bitmap.Config.ARGB_8888
)
var bitmap = createBitmap(canvasWidth, canvasHeight, config)
val canvas = Canvas(bitmap)
svg.renderToCanvas(canvas, renderOptions)
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) {
result.error("fetch-read-exception", "failed to initialize region decoder for uri=$uri regionRect=$regionRect", e.message)
}
}
private data class LastSvgRef(
private data class DecoderRef(
val uri: Uri,
val svg: SVG,
val decoder: SVG,
)
companion object {
private val PREFERRED_CONFIG = Bitmap.Config.ARGB_8888
private const val DECODER_POOL_SIZE = 3
private val decoderPool = ArrayList<DecoderRef>()
private val poolLock = ReentrantLock()
private fun getOrCreateDecoder(context: Context, uri: Uri): SVG? {
poolLock.withLock {
var decoderRef = decoderPool.firstOrNull { it.uri == uri }
if (decoderRef == null) {
val newDecoder = StorageUtils.openInputStream(context, uri)?.use { input ->
SVG.getFromInputStream(SVGParserBufferedInputStream(input))
}
if (newDecoder == null) {
return null
}
newDecoder.normalizeSize()
decoderRef = DecoderRef(uri, newDecoder)
} else {
decoderPool.remove(decoderRef)
}
decoderPool.add(0, decoderRef)
while (decoderPool.size > DECODER_POOL_SIZE) {
decoderPool.removeAt(decoderPool.size - 1)
}
return decoderRef.decoder
}
}
}
}

View file

@ -5,19 +5,21 @@ import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.core.graphics.scale
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.SvgImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.utils.BitmapUtils
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.SVG
import deckers.thibault.aves.utils.MimeTypes.isVideo
@ -26,12 +28,14 @@ import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodChannel
import kotlin.math.min
import kotlin.math.roundToInt
class ThumbnailFetcher internal constructor(
private val context: Context,
uri: String,
private val mimeType: String,
private val dateModifiedSecs: Long,
private val dateModifiedMillis: Long,
private val rotationDegrees: Int,
private val isFlipped: Boolean,
width: Int?,
@ -41,7 +45,7 @@ class ThumbnailFetcher internal constructor(
private val quality: Int,
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 height: Int = if (height?.takeIf { it > 0 } != null) height else defaultSize
private val svgFetch = mimeType == SVG
@ -49,7 +53,7 @@ class ThumbnailFetcher internal constructor(
private val multiPageFetch = pageId != null && MultiPageImage.isSupported(mimeType)
private val customFetch = svgFetch || tiffFetch || multiPageFetch
suspend fun fetch() {
fun fetch() {
var bitmap: Bitmap? = null
var exception: Exception? = null
@ -79,7 +83,33 @@ class ThumbnailFetcher internal constructor(
}
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 {
var errorDetails: String? = exception?.message
if (errorDetails?.isNotEmpty() == true) {
@ -120,30 +150,18 @@ class ThumbnailFetcher internal constructor(
// add signature to ignore cache for images which got modified but kept the same URI
var options = RequestOptions()
.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)
val target = if (isVideo(mimeType)) {
if (isVideo(mimeType)) {
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)
multiPageFetch -> MultiPageImage(context, uri, mimeType, 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 {
var bitmap = target.get()
if (needRotationAfterGlide(mimeType, pageId)) {
@ -154,4 +172,9 @@ class ThumbnailFetcher internal constructor(
Glide.with(context).clear(target)
}
}
companion object {
private val LOG_TAG = LogUtils.createTag<ThumbnailFetcher>()
private const val BITMAP_SIZE_DANGER_THRESHOLD = 20 * (1 shl 20) // MB
}
}

View file

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

View file

@ -77,19 +77,30 @@ class ActivityWindowHandler(private val activity: Activity) : WindowHandler(acti
)
}
override fun supportsHdr(call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.getDisplayCompat()?.isHdr ?: false)
override fun supportsWideGamut(call: MethodCall, result: MethodChannel.Result) {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && activity.resources.configuration.isScreenWideColorGamut)
}
override fun setHdrColorMode(call: MethodCall, result: MethodChannel.Result) {
val on = call.argument<Boolean>("on")
if (on == null) {
result.error("setHdrColorMode-args", "missing arguments", null)
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 (on) ActivityInfo.COLOR_MODE_HDR else ActivityInfo.COLOR_MODE_DEFAULT
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

@ -29,11 +29,15 @@ class ServiceWindowHandler(service: Service) : WindowHandler(service) {
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 setHdrColorMode(call: MethodCall, result: MethodChannel.Result) {
override fun setColorMode(call: MethodCall, result: MethodChannel.Result) {
result.success(null)
}
}

View file

@ -18,8 +18,9 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
"requestOrientation" -> Coresult.safe(call, result, ::requestOrientation)
"isCutoutAware" -> Coresult.safe(call, result, ::isCutoutAware)
"getCutoutInsets" -> Coresult.safe(call, result, ::getCutoutInsets)
"supportsWideGamut" -> Coresult.safe(call, result, ::supportsWideGamut)
"supportsHdr" -> Coresult.safe(call, result, ::supportsHdr)
"setHdrColorMode" -> Coresult.safe(call, result, ::setHdrColorMode)
"setColorMode" -> Coresult.safe(call, result, ::setColorMode)
else -> result.notImplemented()
}
}
@ -46,9 +47,11 @@ abstract class WindowHandler(private val contextWrapper: ContextWrapper) : Metho
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 setHdrColorMode(call: MethodCall, result: MethodChannel.Result)
abstract fun setColorMode(call: MethodCall, result: MethodChannel.Result)
companion object {
private val LOG_TAG = LogUtils.createTag<WindowHandler>()

View file

@ -7,6 +7,7 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.net.toUri
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.PendingStorageAccessResultHandler
import deckers.thibault.aves.channel.calls.AppAdapterHandler
@ -48,6 +49,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
"requestMediaFileAccess" -> ioScope.launch { requestMediaFileAccess() }
"createFile" -> ioScope.launch { createFile() }
"openFile" -> ioScope.launch { openFile() }
"copyFile" -> ioScope.launch { copyFile() }
"edit" -> edit()
"pickCollectionFilters" -> pickCollectionFilters()
else -> endOfStream()
@ -71,7 +73,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
}
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 }
if (uris.isNullOrEmpty() || mimeTypes == null || mimeTypes.size != uris.size) {
error("requestMediaFileAccess-args", "missing arguments", null)
@ -180,6 +182,49 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
safeStartActivityForStorageAccessResult(intent, MainActivity.OPEN_FILE_REQUEST, ::onGranted, ::onDenied)
}
private suspend fun copyFile() {
val name = args["name"] as String?
val mimeType = args["mimeType"] as String?
val sourceUri = (args["sourceUri"] as String?)?.toUri()
if (name == null || mimeType == null || sourceUri == null) {
error("copyFile-args", "missing arguments", null)
return
}
fun onGranted(uri: Uri) {
ioScope.launch {
try {
StorageUtils.openInputStream(activity, sourceUri)?.use { input ->
// truncate is necessary when overwriting a longer file
activity.contentResolver.openOutputStream(uri, "wt")?.use { output ->
val buffer = ByteArray(BUFFER_SIZE)
var len: Int
while (input.read(buffer).also { len = it } != -1) {
output.write(buffer, 0, len)
}
}
}
success(true)
} catch (e: Exception) {
error("copyFile-write", "failed to copy file from sourceUri=$sourceUri to uri=$uri", e.message)
}
endOfStream()
}
}
fun onDenied() {
success(null)
endOfStream()
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = mimeType
putExtra(Intent.EXTRA_TITLE, name)
}
safeStartActivityForStorageAccessResult(intent, MainActivity.CREATE_FILE_REQUEST, ::onGranted, ::onDenied)
}
private fun edit() {
val uri = args["uri"] as String?
val mimeType = args["mimeType"] as String? // optional
@ -190,7 +235,7 @@ class ActivityResultStreamHandler(private val activity: Activity, arguments: Any
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.parse(uri)), mimeType)
.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)

View file

@ -5,15 +5,11 @@ import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.net.toUri
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.VideoThumbnail
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.utils.BitmapUtils
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.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes
@ -28,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.ByteArrayInputStream
import java.io.InputStream
class ImageByteStreamHandler(private val context: Context, private val arguments: Any?) : EventChannel.StreamHandler {
@ -84,11 +81,13 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
return
}
val decoded = arguments["decoded"] as Boolean
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 rotationDegrees = arguments["rotationDegrees"] as Int
val isFlipped = arguments["isFlipped"] as Boolean
val isAnimated = arguments["isAnimated"] as Boolean
val pageId = arguments["pageId"] as Int?
if (mimeType == null || uri == null) {
@ -97,19 +96,31 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
return
}
if (isVideo(mimeType)) {
streamVideoByGlide(uri, mimeType, sizeBytes)
} else if (!canDecodeWithFlutter(mimeType, pageId, rotationDegrees, isFlipped)) {
// decode exotic format on platform side, then encode it in portable format for Flutter
streamImageByGlide(uri, pageId, mimeType, sizeBytes, rotationDegrees, isFlipped)
} else {
if (canDecodeWithFlutter(mimeType, isAnimated) && !decoded) {
// 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()
}
private fun streamImageAsIs(uri: Uri, mimeType: String, sizeBytes: Long?) {
private fun streamOriginalEncodedBytes(uri: Uri, mimeType: String, sizeBytes: Long?) {
if (!MemoryUtils.canAllocate(sizeBytes)) {
error("streamImage-image-read-large", "original image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
return
@ -129,19 +140,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
sizeBytes: Long?,
rotationDegrees: Int,
isFlipped: Boolean,
decoded: Boolean,
) {
val model: Any = if (pageId != null && MultiPageImage.isSupported(mimeType)) {
MultiPageImage(context, uri, mimeType, pageId)
} else if (mimeType == MimeTypes.TIFF) {
TiffImage(context, uri, pageId)
} else {
StorageUtils.getGlideSafeUri(context, uri, mimeType, sizeBytes)
}
val target = Glide.with(context)
.asBitmap()
.apply(glideOptions)
.load(model)
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, pageId, sizeBytes))
.submit()
try {
var bitmap = withContext(Dispatchers.IO) { target.get() }
@ -149,9 +153,16 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped)
}
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)) {
success(bytes)
streamBytes(ByteArrayInputStream(bytes))
} else {
error("streamImage-image-decode-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
}
@ -159,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)
}
} 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 {
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)
.asBitmap()
.apply(glideOptions)
.load(VideoThumbnail(context, uri))
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(context, uri, mimeType, null, sizeBytes))
.submit()
try {
val bitmap = withContext(Dispatchers.IO) { target.get() }
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)) {
success(bytes)
streamBytes(ByteArrayInputStream(bytes))
} else {
error("streamImage-video-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null)
}
@ -218,11 +236,5 @@ class ImageByteStreamHandler(private val context: Context, private val arguments
const val CHANNEL = "deckers.thibault/aves/media_byte_stream"
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
import android.app.Activity
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.net.toUri
import deckers.thibault.aves.channel.calls.MediaEditHandler.Companion.cancelledOps
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.FieldMap
@ -141,7 +141,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
// assume same provider for all entries
val firstEntry = entryMapList.first()
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(activity, it) }
val provider = (firstEntry["uri"] as String?)?.toUri()?.let { getProvider(activity, it) }
if (provider == null) {
error("convert-provider", "failed to find provider for entry=$firstEntry", null)
return

View file

@ -31,9 +31,15 @@ class MediaStoreChangeStreamHandler(private val context: Context) : EventChannel
init {
Log.i(LOG_TAG, "start listening to Media Store")
context.contentResolver.apply {
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
try {
context.contentResolver.apply {
registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true, contentObserver)
}
} catch (e: SecurityException) {
// Trying to register an observer may yield a security exception with this message:
// "Failed to find provider media for user 0; expected to find a valid ContentProvider for this authority"
Log.w(LOG_TAG, "failed to register content observer", e)
}
}

View file

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

View file

@ -7,6 +7,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.ViewConfiguration
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
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 var accelerometerRotation: Int = 0
private var transitionAnimationScale: Float = 1f
private var longPressTimeoutMillis: Int = 0
init {
update()
@ -36,6 +38,7 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
hashMapOf(
Settings.System.ACCELEROMETER_ROTATION to accelerometerRotation,
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
changed = true
}
val newLongPressTimeout = ViewConfiguration.getLongPressTimeout()
if (longPressTimeoutMillis != newLongPressTimeout) {
longPressTimeoutMillis = newLongPressTimeout
changed = true
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get settings with error=${e.message}", null)
}
@ -93,5 +101,8 @@ class SettingsChangeStreamHandler(private val context: Context) : EventChannel.S
companion object {
private val LOG_TAG = LogUtils.createTag<SettingsChangeStreamHandler>()
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
import android.content.Context
import android.net.Uri
import android.text.format.Formatter
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.ImageHeaderParser
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter
import com.bumptech.glide.load.engine.bitmap_recycle.LruArrayPool
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool
import com.bumptech.glide.load.engine.cache.DiskCache
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.engine.cache.LruResourceCache
import com.bumptech.glide.load.engine.cache.MemorySizeCalculator
import com.bumptech.glide.load.resource.bitmap.ExifInterfaceImageHeaderParser
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.compatRemoveIf
@GlideModule
@ -16,6 +32,30 @@ class AvesAppGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
// hide noisy warning (e.g. for images that can't be decoded)
builder.setLogLevel(Log.ERROR)
// sizing
val memorySizeCalculator = MemorySizeCalculator.Builder(context).build()
builder.setMemorySizeCalculator(memorySizeCalculator)
val size: Int = memorySizeCalculator.bitmapPoolSize
if (size > 0) {
builder.setBitmapPool(LruBitmapPool(size.toLong()))
} else {
builder.setBitmapPool(BitmapPoolAdapter())
}
builder.setArrayPool(LruArrayPool(memorySizeCalculator.arrayPoolSizeInBytes))
builder.setMemoryCache(LruResourceCache(memorySizeCalculator.memoryCacheSize.toLong()))
val diskCacheSize = DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE
val internalCacheDiskCacheFactory = InternalCacheDiskCacheFactory(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize.toLong())
builder.setDiskCache(internalCacheDiskCacheFactory)
fun toMb(bytes: Int) = Formatter.formatFileSize(context, bytes.toLong())
Log.d(
LOG_TAG, "Glide disk cache size=${toMb(diskCacheSize)}" +
", memory cache size=${toMb(memorySizeCalculator.memoryCacheSize)}" +
", bitmap pool size=${toMb(memorySizeCalculator.bitmapPoolSize)}" +
", array pool size=${toMb(memorySizeCalculator.arrayPoolSizeInBytes)}"
)
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
@ -25,4 +65,28 @@ class AvesAppGlideModule : AppGlideModule() {
}
override fun isManifestParsingEnabled(): Boolean = false
companion object {
private val LOG_TAG = LogUtils.createTag<AvesAppGlideModule>()
// request a fresh image with the highest quality format
val uncachedFullImageOptions = RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.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

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import androidx.core.graphics.createBitmap
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
import com.bumptech.glide.Registry
@ -68,7 +69,7 @@ internal class SvgFetcher(val model: SvgImage, val width: Int, val height: Int)
bitmapWidth = width
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)
svg.renderToCanvas(canvas)

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.decoder
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.graphics.scale
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
import com.bumptech.glide.Registry
@ -82,7 +83,9 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
inSampleSize = sampleSize
}
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) {
callback.onLoadFailed(Exception("Decoding full TIFF yielded null bitmap"))
} else if (customSize) {
@ -96,7 +99,7 @@ internal class TiffFetcher(val model: TiffImage, val width: Int, val height: Int
dstWidth = width
dstHeight = (width / aspectRatio).toInt()
}
callback.onDataReady(Bitmap.createScaledBitmap(bitmap, dstWidth, dstHeight, true))
callback.onDataReady(bitmap.scale(dstWidth, dstHeight))
} else {
callback.onDataReady(bitmap)
}

View file

@ -2,6 +2,7 @@ package deckers.thibault.aves.decoder
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
@ -20,7 +21,6 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.module.LibraryGlideModule
import com.bumptech.glide.signature.ObjectKey
import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
import kotlinx.coroutines.CoroutineScope
@ -28,45 +28,54 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.io.IOException
import kotlin.math.ceil
import kotlin.math.roundToInt
@GlideModule
class VideoThumbnailGlideModule : LibraryGlideModule() {
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)
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, Bitmap> {
override fun buildLoadData(model: VideoThumbnail, width: Int, height: Int, options: Options): ModelLoader.LoadData<Bitmap> {
return ModelLoader.LoadData(ObjectKey(model.uri), VideoThumbnailFetcher(model, width, height))
}
override fun handles(model: VideoThumbnail): Boolean = true
internal class Factory : ModelLoaderFactory<VideoThumbnail, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<VideoThumbnail, InputStream> = VideoThumbnailLoader()
internal class Factory : ModelLoaderFactory<VideoThumbnail, Bitmap> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<VideoThumbnail, Bitmap> = VideoThumbnailLoader()
override fun teardown() {}
}
}
internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val width: Int, val height: Int) : DataFetcher<InputStream> {
internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val width: Int, val height: Int) : DataFetcher<Bitmap> {
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 {
val retriever = openMetadataRetriever(model.context, model.uri)
if (retriever == null) {
callback.onLoadFailed(Exception("failed to initialize MediaMetadataRetriever for uri=${model.uri}"))
} else {
try {
var bytes = retriever.embeddedPicture
if (bytes == null) {
var bitmap: Bitmap? = 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
// the thumbnails returned by the content resolver / Media Store
// so we derive one in an arbitrary way
@ -111,8 +120,9 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
}
// the returned frame is already rotated according to the video metadata
val frame = if (dstWidth > 0 && dstHeight > 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
val targetBitmapSizeBytes: Long = FORMAT_BYTE_SIZE.toLong() * dstWidth * dstHeight
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")
}
@ -122,7 +132,8 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
retriever.getScaledFrameAtTime(timeMicros, option, dstWidth, dstHeight)
}
} else {
val targetBitmapSizeBytes: Long = (FORMAT_BYTE_SIZE.toLong() * videoWidth * videoHeight).toLong()
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")
}
@ -132,13 +143,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
retriever.getFrameAtTime(timeMicros, option)
}
}
bytes = frame?.getBytes(canHaveAlpha = false, recycle = false)
}
if (bytes != null) {
callback.onDataReady(ByteArrayInputStream(bytes))
if (bitmap == null) {
callback.onLoadFailed(Exception("failed to get embedded picture or any frame for uri=${model.uri}"))
} else {
callback.onLoadFailed(Exception("failed to get embedded picture or any frame"))
callback.onDataReady(bitmap)
}
} catch (e: Exception) {
callback.onLoadFailed(e)
@ -151,8 +161,14 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
}
@RequiresApi(Build.VERSION_CODES.P)
private fun getBitmapParams() = MediaMetadataRetriever.BitmapParams().apply {
preferredConfig = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
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
@ -167,12 +183,7 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail, val widt
// cannot 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
companion object {
// same for either `ARGB_8888` or `RGBA_1010102`
private const val FORMAT_BYTE_SIZE = BitmapUtils.ARGB_8888_BYTE_SIZE
}
}

View file

@ -1,7 +1,6 @@
package deckers.thibault.aves.metadata
import android.util.Log
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import com.drew.lang.Rational
import com.drew.metadata.Directory
import com.drew.metadata.exif.ExifDirectoryBase
@ -19,6 +18,7 @@ import java.util.Locale
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.roundToLong
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
object ExifInterfaceHelper {
private val LOG_TAG = LogUtils.createTag<ExifInterfaceHelper>()

View file

@ -111,20 +111,25 @@ object MediaMetadataRetrieverHelper {
// format
MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION,
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION -> "$value°"
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_BITRATE -> {
val bitrate = value.toLongOrNull() ?: 0
if (bitrate > 0) formatBitrate(bitrate) else null
}
MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE -> {
val framerate = value.toDoubleOrNull() ?: 0.0
if (framerate > 0.0) "$framerate" else null
}
MediaMetadataRetriever.METADATA_KEY_DURATION -> {
val dateMillis = value.toLongOrNull() ?: 0
if (dateMillis > 0) durationFormat.format(Date(dateMillis)) else null
}
MediaMetadataRetriever.METADATA_KEY_COLOR_RANGE -> {
when (value.toIntOrNull()) {
MediaFormat.COLOR_RANGE_FULL -> "Full"
@ -132,6 +137,7 @@ object MediaMetadataRetrieverHelper {
else -> value
}
}
MediaMetadataRetriever.METADATA_KEY_COLOR_STANDARD -> {
when (value.toIntOrNull()) {
MediaFormat.COLOR_STANDARD_BT709 -> "BT.709"
@ -141,6 +147,7 @@ object MediaMetadataRetrieverHelper {
else -> value
}
}
MediaMetadataRetriever.METADATA_KEY_COLOR_TRANSFER -> {
when (value.toIntOrNull()) {
MediaFormat.COLOR_TRANSFER_LINEAR -> "Linear"
@ -154,6 +161,7 @@ object MediaMetadataRetrieverHelper {
MediaMetadataRetriever.METADATA_KEY_COMPILATION,
MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER,
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_DATE -> {
val dateMillis = Metadata.parseVideoMetadataDate(value)
@ -168,4 +176,12 @@ object MediaMetadataRetrieverHelper {
}?.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

@ -9,7 +9,11 @@ import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.toByteArray
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.UserBox
import org.mp4parser.boxes.apple.AppleCoverBox
@ -17,8 +21,18 @@ import org.mp4parser.boxes.apple.AppleGPSCoordinatesBox
import org.mp4parser.boxes.apple.AppleItemListBox
import org.mp4parser.boxes.apple.AppleVariableSignedIntegerBox
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.LocationInformationBox
import org.mp4parser.support.AbstractBox
import org.mp4parser.support.Matrix
import org.mp4parser.tools.Path
@ -32,6 +46,15 @@ object Mp4ParserHelper {
// arbitrary size to detect boxes that may yield an OOM
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>> {
// 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")
@ -120,6 +143,35 @@ object Mp4ParserHelper {
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
fun IsoFile.updateLocation(locationIso6709: String?) {
@ -259,18 +311,18 @@ object Mp4ParserHelper {
)
setBoxSkipper { type, size ->
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
}
}
fun getUserData(
fun getUserDataBox(
context: Context,
mimeType: String,
uri: Uri,
): MutableMap<String, String> {
val fields = HashMap<String, String>()
if (mimeType != MimeTypes.MP4) return fields
): UserDataBox? {
if (mimeType != MimeTypes.MP4) return null
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")
@ -279,10 +331,7 @@ object Mp4ParserHelper {
stream.channel.use { channel ->
// creating `IsoFile` with a `File` or a `File.inputStream()` yields `No such device`
IsoFile(channel, metadataBoxParser()).use { isoFile ->
val userDataBox = Path.getPath<UserDataBox>(isoFile.movieBox, UserDataBox.TYPE)
if (userDataBox != null) {
fields.putAll(extractBoxFields(userDataBox))
}
return Path.getPath(isoFile.movieBox, UserDataBox.TYPE)
}
}
}
@ -292,10 +341,10 @@ object Mp4ParserHelper {
} catch (e: Exception) {
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>()
for (box in container.boxes) {
if (box is AbstractBox && !box.isParsed) {
@ -309,9 +358,20 @@ object Mp4ParserHelper {
is AppleGPSCoordinatesBox -> fields[key] = box.value
is AppleItemListBox -> fields.putAll(extractBoxFields(box))
is AppleVariableSignedIntegerBox -> fields[key] = box.value.toString()
is Utf8AppleDataBox -> fields[key] = box.value
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 -> {
val handlerBox = Path.getPath<HandlerBox>(box, HandlerBox.TYPE).apply { parseDetails() }
when (val handlerType = handlerBox?.handlerType ?: MetaBox.TYPE) {
@ -336,6 +396,8 @@ object Mp4ParserHelper {
}
}
is Utf8AppleDataBox -> fields[key] = box.value
else -> fields[key] = box.toString()
}
}
@ -348,6 +410,7 @@ object Mp4ParserHelper {
"catg" -> "Category"
"covr" -> "Cover Art"
"keyw" -> "Keyword"
"loci" -> "Location"
"mcvr" -> "Preview Image"
"pcst" -> "Podcast"
"SDLN" -> "Play Mode"

View file

@ -15,6 +15,8 @@ import com.drew.metadata.exif.ExifDirectoryBase
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.xmp.XmpDirectory
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeLong
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeInt
import deckers.thibault.aves.metadata.metadataextractor.mpf.MpEntry
@ -35,6 +37,8 @@ import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
object 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()
// page info
@ -47,14 +51,6 @@ object MultiPage {
private const val KEY_ROTATION_DEGREES = "rotationDegrees"
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 extractor = MediaExtractor()
extractor.setDataSource(context, uri, null)
@ -90,6 +86,26 @@ object MultiPage {
return tracks
}
fun isHeicSefdMotionPhoto(context: Context, uri: Uri): Boolean {
return getHeicSefdMotionPhotoVideoSizing(context, uri) != null
}
private fun getHeicSefdMotionPhotoVideoSizing(context: Context, uri: Uri): Pair<Long, Long>? {
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
}
private fun getJpegMpfPrimaryRotation(context: Context, uri: Uri, sizeBytes: Long): Int {
val mimeType = MimeTypes.JPEG
var rotationDegrees = 0
@ -250,70 +266,39 @@ object MultiPage {
}
fun getMotionPhotoPages(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): 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 pages = ArrayList<FieldMap>()
val extractor = MediaExtractor()
var pfd: ParcelFileDescriptor? = null
try {
getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
val videoStartOffset = sizeBytes - videoSizeBytes
pfd = context.contentResolver.openFileDescriptor(uri, "r")
pfd?.fileDescriptor?.let { fd ->
extractor.setDataSource(fd, videoStartOffset, videoSizeBytes)
// 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,
)
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,
)
// 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 page: FieldMap = hashMapOf(
KEY_PAGE to pageIndex++,
KEY_MIME_TYPE to MimeTypes.MP4,
KEY_IS_DEFAULT to false,
)
format.getSafeInt(MediaFormat.KEY_WIDTH) { page[KEY_WIDTH] = it }
format.getSafeInt(MediaFormat.KEY_HEIGHT) { page[KEY_HEIGHT] = it }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
format.getSafeInt(MediaFormat.KEY_ROTATION) { page[KEY_ROTATION_DEGREES] = it }
}
format.getSafeLong(MediaFormat.KEY_DURATION) { page[KEY_DURATION] = it / 1000 }
pages.add(page)
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get motion photo track information for uri=$uri, track num=$trackIndex", e)
}
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)
}
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to open motion photo for uri=$uri", e)
} finally {
extractor.release()
pfd?.close()
}
return pages
}
fun getMotionPhotoOffset(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
fun getTrailerVideoSize(context: Context, uri: Uri, mimeType: String, sizeBytes: Long): Long? {
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.
// This item does not contain the video itself, but only some kind of metadata (no doc, no spec),
@ -360,6 +345,62 @@ object MultiPage {
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 toMap(pageIndex: Int, options: TiffBitmapFactory.Options): FieldMap {
return hashMapOf(

View file

@ -26,7 +26,6 @@ import pixy.meta.string.XMLUtils
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.util.*
object PixyMetaHelper {
fun describe(input: InputStream): HashMap<String, String> {
@ -82,17 +81,18 @@ object PixyMetaHelper {
output: OutputStream,
iptcDataList: List<FieldMap>?,
) {
val iptc = iptcDataList?.flatMap {
val iptc: List<IPTCDataSet> = iptcDataList?.flatMap {
val record = it["record"] as Int
val tag = it["tag"] as Int
val values = it["values"] as List<*>
values.map { data -> IPTCDataSet(IPTCRecord.fromRecordNumber(record), tag, data as ByteArray) }
} ?: ArrayList<IPTCDataSet>()
} ?: ArrayList()
Metadata.insertIPTC(input, output, iptc)
}
fun getXmp(input: InputStream): XMP? = Metadata.readMetadata(input)[MetadataType.XMP] as XMP?
// PixyMeta may fail with just a log, and write nothing to the output
fun setXmp(
input: InputStream,
output: OutputStream,

View file

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

View file

@ -183,7 +183,7 @@ object GoogleXMP {
return offsetFromEnd
}
fun updateTrailingVideoOffset(xmp: String, oldOffset: Int, newOffset: Int): String {
fun updateTrailingVideoOffset(xmp: String, oldOffset: Number, newOffset: Number): String {
return xmp.replace(
// GCamera motion photo
"${GCAMERA_VIDEO_OFFSET_PROP_NAME}=\"$oldOffset\"",
@ -195,7 +195,6 @@ object GoogleXMP {
)
}
fun getDeviceContainer(meta: XMPMeta): GoogleDeviceContainer? {
return if (meta.doesPropPathExist(listOf(GDEVICE_CONTAINER_PROP_NAME, GDEVICE_CONTAINER_DIRECTORY_PROP_NAME))) {
GoogleDeviceContainer().apply { findItems(meta) }

View file

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

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

@ -5,6 +5,7 @@ import android.content.Context
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import androidx.core.net.toUri
import com.drew.metadata.avi.AviDirectory
import com.drew.metadata.exif.ExifIFD0Directory
import com.drew.metadata.jpeg.JpegDirectory
@ -41,7 +42,7 @@ class SourceEntry {
private var sourceRotationDegrees: Int? = null
private var sizeBytes: Long? = null
private var dateAddedSecs: Long? = null
private var dateModifiedSecs: Long? = null
private var dateModifiedMillis: Long? = null
private var sourceDateTakenMillis: Long? = null
private var durationMillis: Long? = null
@ -54,45 +55,45 @@ class SourceEntry {
}
constructor(map: FieldMap) {
origin = map["origin"] as Int
uri = Uri.parse(map["uri"] as String)
path = map["path"] as String?
sourceMimeType = map["sourceMimeType"] as String
width = map["width"] as Int?
height = map["height"] as Int?
sourceRotationDegrees = map["sourceRotationDegrees"] as Int?
sizeBytes = toLong(map["sizeBytes"])
title = map["title"] as String?
dateAddedSecs = toLong(map["dateAddedSecs"])
dateModifiedSecs = toLong(map["dateModifiedSecs"])
sourceDateTakenMillis = toLong(map["sourceDateTakenMillis"])
durationMillis = toLong(map["durationMillis"])
origin = map[EntryFields.ORIGIN] as Int
uri = (map[EntryFields.URI] as String).toUri()
path = map[EntryFields.PATH] as String?
sourceMimeType = map[EntryFields.SOURCE_MIME_TYPE] as String
width = map[EntryFields.WIDTH] as Int?
height = map[EntryFields.HEIGHT] as Int?
sourceRotationDegrees = map[EntryFields.SOURCE_ROTATION_DEGREES] as Int?
sizeBytes = toLong(map[EntryFields.SIZE_BYTES])
title = map[EntryFields.TITLE] as String?
dateAddedSecs = toLong(map[EntryFields.DATE_ADDED_SECS])
dateModifiedMillis = toLong(map[EntryFields.DATE_MODIFIED_MILLIS])
sourceDateTakenMillis = toLong(map[EntryFields.SOURCE_DATE_TAKEN_MILLIS])
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.title = title
this.sizeBytes = sizeBytes
this.dateModifiedSecs = dateModifiedSecs
this.dateModifiedMillis = dateModifiedMillis
}
fun toMap(): FieldMap {
return hashMapOf(
"origin" to origin,
"uri" to uri.toString(),
"path" to path,
"sourceMimeType" to sourceMimeType,
"width" to width,
"height" to height,
"sourceRotationDegrees" to (sourceRotationDegrees ?: 0),
"sizeBytes" to sizeBytes,
"title" to title,
"dateAddedSecs" to dateAddedSecs,
"dateModifiedSecs" to dateModifiedSecs,
"sourceDateTakenMillis" to sourceDateTakenMillis,
"durationMillis" to durationMillis,
EntryFields.ORIGIN to origin,
EntryFields.URI to uri.toString(),
EntryFields.PATH to path,
EntryFields.SOURCE_MIME_TYPE to sourceMimeType,
EntryFields.WIDTH to width,
EntryFields.HEIGHT to height,
EntryFields.SOURCE_ROTATION_DEGREES to (sourceRotationDegrees ?: 0),
EntryFields.SIZE_BYTES to sizeBytes,
EntryFields.TITLE to title,
EntryFields.DATE_ADDED_SECS to dateAddedSecs,
EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
EntryFields.SOURCE_DATE_TAKEN_MILLIS to sourceDateTakenMillis,
EntryFields.DURATION_MILLIS to durationMillis,
// only for map export
"contentId" to contentId,
EntryFields.CONTENT_ID to contentId,
)
}

View file

@ -6,6 +6,7 @@ import android.content.ContextWrapper
import android.net.Uri
import android.util.Log
import android.webkit.MimeTypeMap
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.SourceEntry
import deckers.thibault.aves.utils.LogUtils
@ -45,7 +46,7 @@ internal class FileImageProvider : ImageProvider() {
path = path,
title = file.name,
sizeBytes = file.length(),
dateModifiedSecs = file.lastModified() / 1000,
dateModifiedMillis = file.lastModified(),
)
}
} catch (e: SecurityException) {
@ -88,9 +89,9 @@ internal class FileImageProvider : ImageProvider() {
}
return hashMapOf(
"uri" to Uri.fromFile(newFile).toString(),
"path" to newFile.path,
"dateModifiedSecs" to newFile.lastModified() / 1000,
EntryFields.URI to Uri.fromFile(newFile).toString(),
EntryFields.PATH to newFile.path,
EntryFields.DATE_MODIFIED_MILLIS to newFile.lastModified(),
)
}
@ -98,8 +99,8 @@ internal class FileImageProvider : ImageProvider() {
try {
val file = File(path)
if (file.exists()) {
newFields["dateModifiedSecs"] = file.lastModified() / 1000
newFields["sizeBytes"] = file.length()
newFields[EntryFields.DATE_MODIFIED_MILLIS] = file.lastModified()
newFields[EntryFields.SIZE_BYTES] = file.length()
}
callback.onSuccess(newFields)
} catch (e: SecurityException) {

View file

@ -11,16 +11,11 @@ import android.net.Uri
import android.os.Binder
import android.os.Build
import android.util.Log
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import androidx.core.net.toUri
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.RequestOptions
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.decoder.MultiPageImage
import deckers.thibault.aves.decoder.SvgImage
import deckers.thibault.aves.decoder.TiffImage
import deckers.thibault.aves.decoder.AvesAppGlideModule
import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
import deckers.thibault.aves.metadata.Metadata.TYPE_EXIF
@ -38,6 +33,7 @@ import deckers.thibault.aves.metadata.PixyMetaHelper.xmpDocString
import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.metadata.xmp.GoogleXMP
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
@ -68,6 +64,7 @@ import java.nio.channels.Channels
import java.util.Date
import java.util.TimeZone
import kotlin.math.absoluteValue
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
abstract class ImageProvider {
open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, allowUnsized: Boolean, callback: ImageOpCallback) {
@ -78,10 +75,10 @@ abstract class ImageProvider {
return if (StorageUtils.isInVault(context, path)) {
val uri = Uri.fromFile(File(path))
hashMapOf(
"origin" to SourceEntry.ORIGIN_VAULT,
"uri" to uri.toString(),
"contentId" to null,
"path" to path,
EntryFields.ORIGIN to SourceEntry.ORIGIN_VAULT,
EntryFields.URI to uri.toString(),
EntryFields.CONTENT_ID to null,
EntryFields.PATH to path,
)
} else {
MediaStoreImageProvider().scanNewPathByMediaStore(context, path, mimeType)
@ -145,16 +142,18 @@ abstract class ImageProvider {
val oldFile = File(sourcePath)
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
val defaultExtension = oldFile.extension
oldFile.parent?.let { dir ->
val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity,
dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
defaultExtension = defaultExtension,
conflictStrategy = NameConflictStrategy.RENAME,
)
resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}"
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}"
val newFile = File(dir, targetFileName)
if (oldFile != newFile) {
newFields = renameSingle(
@ -280,11 +279,17 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
}
// there is no benefit providing input extension
// for known output MIME type
val defaultExtension = null
val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity,
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType,
defaultExtension = defaultExtension,
conflictStrategy = nameConflictStrategy,
)
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@ -317,27 +322,12 @@ abstract class ImageProvider {
}
}
val model: Any = if (pageId != null && MultiPageImage.isSupported(sourceMimeType)) {
MultiPageImage(activity, sourceUri, sourceMimeType, 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.applicationContext)
.asBitmap()
.apply(glideOptions)
.load(model)
.apply(AvesAppGlideModule.uncachedFullImageOptions)
.load(AvesAppGlideModule.getModel(activity, sourceUri, sourceMimeType, pageId, sourceEntry.sizeBytes))
.submit(targetWidthPx, targetHeightPx)
var bitmap = withContext(Dispatchers.IO) { target.get() }
if (MimeTypes.needRotationAfterGlide(sourceMimeType, pageId)) {
bitmap = BitmapUtils.applyExifOrientation(activity, bitmap, sourceEntry.rotationDegrees, sourceEntry.isFlipped)
@ -376,11 +366,12 @@ abstract class ImageProvider {
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write,
)
val newFields = scanNewPath(activity, targetPath, exportMimeType)
val targetUri = Uri.parse(newFields["uri"] as String)
val targetUri = (newFields[EntryFields.URI] as String).toUri()
if (writeMetadata) {
copyMetadata(
context = activity,
@ -483,6 +474,7 @@ abstract class ImageProvider {
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType,
defaultExtension = null,
conflictStrategy = nameConflictStrategy,
)
} catch (e: Exception) {
@ -589,13 +581,14 @@ abstract class ImageProvider {
dir: String,
desiredNameWithoutExtension: String,
mimeType: String,
defaultExtension: String?,
conflictStrategy: NameConflictStrategy,
): NameConflictResolution {
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
var resolvedName: String? = sanitizedNameWithoutExtension
var replacementFile: File? = null
val extension = extensionFor(mimeType)
val extension = extensionFor(mimeType, defaultExtension)
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) {
NameConflictStrategy.RENAME -> {
@ -664,19 +657,21 @@ abstract class ImageProvider {
}
val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
var videoBytes: ByteArray? = null
var trailerVideoBytes: ByteArray? = null
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 {
if (videoSize != null) {
if (trailerVideoSize != null && isTrailerVideoValid) {
// handle motion photo and embedded video separately
val imageSize = (originalFileSize - videoSize).toInt()
videoBytes = ByteArray(videoSize)
val imageSize = (originalFileSize - trailerVideoSize).toInt()
val videoByteSize = trailerVideoSize.toInt()
trailerVideoBytes = ByteArray(videoByteSize)
StorageUtils.openInputStream(context, uri)?.let { input ->
val imageBytes = ByteArray(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
// video will be appended after metadata modification
@ -696,30 +691,31 @@ abstract class ImageProvider {
try {
edit(ExifInterface(editableFile))
if (editableFile.length() == 0L) {
callback.onFailure(Exception("editing Exif yielded an empty file"))
return false
}
val editedMimeType = detectMimeType(context, Uri.fromFile(editableFile), mimeType)
if (editedMimeType != mimeType) {
throw Exception("editing Exif changes mimeType=$mimeType -> $editedMimeType for uri=$uri path=$path")
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// 1) as of androidx.exifinterface:exifinterface:1.3.6, editing some specific WEBP
// makes them undecodable by some decoders (including Android's and Chrome's)
// even though `BitmapFactory` successfully decodes their bounds,
// editing may corrupt the file for various reasons,
// so we check whether decoding it throws an exception
// 2) some users have reported corruption when editing JPEG as well,
// but conditions are unknown (specific image, custom ROM, low storage, race condition, etc.)
ImageDecoder.decodeBitmap(ImageDecoder.createSource(editableFile))
}
if (videoBytes != null) {
if (trailerVideoBytes != null) {
// append trailer video, if any
editableFile.appendBytes(videoBytes!!)
editableFile.appendBytes(trailerVideoBytes!!)
}
// copy the edited temporary file back to the original
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
}
editableFile.delete()
@ -747,19 +743,21 @@ abstract class ImageProvider {
}
val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.let { it.toInt() + trailerDiff }
var videoBytes: ByteArray? = null
var trailerVideoBytes: ByteArray? = null
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 {
if (videoSize != null) {
if (trailerVideoSize != null && isTrailerVideoValid) {
// handle motion photo and embedded video separately
val imageSize = (originalFileSize - videoSize).toInt()
videoBytes = ByteArray(videoSize)
val imageSize = (originalFileSize - trailerVideoSize).toInt()
val videoByteSize = trailerVideoSize.toInt()
trailerVideoBytes = ByteArray(videoByteSize)
StorageUtils.openInputStream(context, uri)?.let { input ->
val imageBytes = ByteArray(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
// video will be appended after metadata modification
@ -795,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
editableFile.appendBytes(videoBytes!!)
editableFile.appendBytes(trailerVideoBytes!!)
}
// copy the edited temporary file back to the original
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
}
editableFile.delete()
@ -913,7 +916,7 @@ abstract class ImageProvider {
}
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 {
try {
editXmpWithPixy(
@ -931,11 +934,16 @@ abstract class ImageProvider {
}
}
if (editableFile.length() == 0L) {
callback.onFailure(Exception("editing XMP yielded an empty file"))
return false
}
try {
// copy the edited temporary file back to the original
editableFile.transferTo(outputStream(context, mimeType, uri, path))
if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, videoSize, editableFile, callback)) {
if (autoCorrectTrailerOffset && !checkTrailerOffset(context, path, uri, mimeType, trailerVideoSize, editableFile, callback)) {
return false
}
editableFile.delete()
@ -996,7 +1004,7 @@ abstract class ImageProvider {
path: String,
uri: Uri,
mimeType: String,
trailerOffset: Int?,
trailerOffset: Number?,
editedFile: File,
callback: ImageOpCallback,
): Boolean {
@ -1011,7 +1019,7 @@ abstract class ImageProvider {
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."
)
val newTrailerOffset = trailerOffset + diff
val newTrailerOffset = trailerOffset.toLong() + diff
return editXmp(context, path, uri, mimeType, callback, trailerDiff = diff, editCoreXmp = { xmp ->
GoogleXMP.updateTrailingVideoOffset(xmp, trailerOffset, newTrailerOffset)
})
@ -1276,17 +1284,23 @@ abstract class ImageProvider {
callback: ImageOpCallback,
) {
val originalFileSize = File(path).length()
val videoSize = MultiPage.getMotionPhotoOffset(context, uri, mimeType, originalFileSize)?.toInt()
if (videoSize == null) {
val trailerVideoSize = MultiPage.getTrailerVideoSize(context, uri, mimeType, originalFileSize)
if (trailerVideoSize == null) {
callback.onFailure(Exception("failed to get trailer video size"))
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 {
try {
val inputStream = StorageUtils.openInputStream(context, uri)
// partial copy
transferFrom(inputStream, originalFileSize - videoSize)
transferFrom(inputStream, originalFileSize - trailerVideoSize)
} catch (e: Exception) {
Log.d(LOG_TAG, "failed to remove trailer video", e)
callback.onFailure(e)
@ -1321,7 +1335,8 @@ abstract class ImageProvider {
}
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 {
try {
outputStream().use { output ->
@ -1337,11 +1352,16 @@ abstract class ImageProvider {
}
}
if (editableFile.length() == 0L) {
callback.onFailure(Exception("removing metadata yielded an empty file"))
return
}
try {
// copy the edited temporary file back to the original
editableFile.transferTo(outputStream(context, mimeType, uri, path))
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
}
editableFile.delete()

View file

@ -20,6 +20,7 @@ import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.MainActivity
import deckers.thibault.aves.MainActivity.Companion.DELETE_SINGLE_PERMISSION_REQUEST
import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.EntryFields
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.NameConflictStrategy
import deckers.thibault.aves.model.SourceEntry
@ -40,7 +41,6 @@ import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.io.SyncFailedException
import java.util.Date
import java.util.Locale
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation
@ -51,14 +51,14 @@ import kotlin.coroutines.suspendCoroutine
class MediaStoreImageProvider : ImageProvider() {
fun fetchAll(
context: Context,
knownEntries: Map<Long?, Int?>,
knownEntries: Map<Long?, Long?>,
directory: String?,
handleNewEntry: NewEntryHandler,
) {
Log.d(LOG_TAG, "fetching all media store items for ${knownEntries.size} known entries, directory=$directory")
val isModified = fun(contentId: Long, dateModifiedSecs: Int): Boolean {
val isModified = fun(contentId: Long, dateModifiedMillis: Long): Boolean {
val knownDate = knownEntries[contentId]
return knownDate == null || knownDate < dateModifiedSecs
return knownDate == null || knownDate < dateModifiedMillis
}
val handleNew: NewEntryHandler
var selection: String? = null
@ -77,7 +77,7 @@ class MediaStoreImageProvider : ImageProvider() {
val parentCheckDirectory = removeTrailingSeparator(directory)
handleNew = { entry ->
// skip entries in subfolders
val path = entry["path"] as String?
val path = entry[EntryFields.PATH] as String?
if (path != null && File(path).parent == parentCheckDirectory) {
handleNewEntry(entry)
}
@ -96,7 +96,7 @@ class MediaStoreImageProvider : ImageProvider() {
var found = false
val fetched = arrayListOf<FieldMap>()
val id = uri.tryParseId()
val alwaysValid: NewEntryChecker = fun(_: Long, _: Int): Boolean = true
val alwaysValid: NewEntryChecker = fun(_: Long, _: Long): Boolean = true
val onSuccess: NewEntryHandler = fun(entry: FieldMap) { fetched.add(entry) }
if (id != null) {
if (sourceMimeType == null || isImage(sourceMimeType)) {
@ -227,8 +227,8 @@ class MediaStoreImageProvider : ImageProvider() {
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.SIZE)
val widthColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH)
val heightColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT)
val dateAddedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val dateAddedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_ADDED)
val dateModifiedSecsColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED)
val dateTakenColumn = cursor.getColumnIndex(MediaColumns.DATE_TAKEN)
// image & video for API >=29, only for images for API <29
@ -240,8 +240,8 @@ class MediaStoreImageProvider : ImageProvider() {
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val dateModifiedSecs = cursor.getInt(dateModifiedColumn)
if (isValidEntry(id, dateModifiedSecs)) {
val dateModifiedMillis = cursor.getInt(dateModifiedSecsColumn) * 1000L
if (isValidEntry(id, dateModifiedMillis)) {
// for multiple items, `contentUri` is the root without ID,
// but for single items, `contentUri` already contains the ID
val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, id)
@ -255,21 +255,22 @@ class MediaStoreImageProvider : ImageProvider() {
if (mimeType == null) {
Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type")
} else {
var entryMap: FieldMap = hashMapOf(
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
"uri" to itemUri.toString(),
"path" to cursor.getString(pathColumn),
"sourceMimeType" to mimeType,
"width" to width,
"height" to height,
"sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
"sizeBytes" to cursor.getLong(sizeColumn),
"dateAddedSecs" to cursor.getInt(dateAddedColumn),
"dateModifiedSecs" to dateModifiedSecs,
"sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
"durationMillis" to durationMillis,
val path = cursor.getString(pathColumn)
var entryFields: FieldMap = hashMapOf(
EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
EntryFields.URI to itemUri.toString(),
EntryFields.PATH to path,
EntryFields.SOURCE_MIME_TYPE to mimeType,
EntryFields.WIDTH to width,
EntryFields.HEIGHT to height,
EntryFields.SOURCE_ROTATION_DEGREES to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0,
EntryFields.SIZE_BYTES to cursor.getLong(sizeColumn),
EntryFields.DATE_ADDED_SECS to cursor.getInt(dateAddedSecsColumn),
EntryFields.DATE_MODIFIED_MILLIS to dateModifiedMillis,
EntryFields.SOURCE_DATE_TAKEN_MILLIS to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null,
EntryFields.DURATION_MILLIS to durationMillis,
// only for map export
"contentId" to id,
EntryFields.CONTENT_ID to id,
)
if (MimeTypes.isHeic(mimeType)) {
@ -285,8 +286,8 @@ class MediaStoreImageProvider : ImageProvider() {
if (outWidth > 0 && outHeight > 0) {
width = outWidth
height = outHeight
entryMap["width"] = width
entryMap["height"] = height
entryFields[EntryFields.WIDTH] = width
entryFields[EntryFields.HEIGHT] = height
}
}
} catch (e: IOException) {
@ -302,11 +303,13 @@ class MediaStoreImageProvider : ImageProvider() {
// missing some attributes such as width, height, orientation.
// Also, the reported size of raw images is inconsistent across devices
// and Android versions (sometimes the raw size, sometimes the decoded size).
val entry = SourceEntry(entryMap).fillPreCatalogMetadata(context)
entryMap = entry.toMap()
val entry = SourceEntry(entryFields).fillPreCatalogMetadata(context)
entryFields = entry.toMap()
}
handleNewEntry(entryMap)
getFileModifiedDateMillis(path)?.let { entryFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
handleNewEntry(entryFields)
found = true
}
}
@ -554,6 +557,7 @@ class MediaStoreImageProvider : ImageProvider() {
toBin: Boolean,
): FieldMap {
val sourcePath = sourceFile?.path
val sourceExtension = sourceFile?.extension
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy
@ -566,6 +570,7 @@ class MediaStoreImageProvider : ImageProvider() {
dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType,
defaultExtension = sourceExtension,
conflictStrategy = nameConflictStrategy,
)
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@ -577,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = sourceExtension,
) { output: OutputStream ->
try {
sourceDocFile.copyTo(output)
@ -598,8 +604,8 @@ class MediaStoreImageProvider : ImageProvider() {
}
return if (toBin) {
hashMapOf(
"trashed" to true,
"trashPath" to targetPath,
EntryFields.TRASHED to true,
EntryFields.TRASH_PATH to targetPath,
)
} else {
scanNewPath(activity, targetPath, mimeType)
@ -612,12 +618,13 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit,
): String {
if (StorageUtils.isInVault(activity, targetDir)) {
return insertByFile(
targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write,
)
}
@ -627,7 +634,7 @@ class MediaStoreImageProvider : ImageProvider() {
return insertByMediaStore(
activity = activity,
targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}",
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write,
)
}
@ -639,6 +646,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir,
targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write,
)
}
@ -697,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String,
targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit,
): String {
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
@ -705,8 +714,22 @@ class MediaStoreImageProvider : ImageProvider() {
// but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
// providing a display name and a MIME type does not guarantee
// that the created document will be backed by a file with a valid media extension,
// but having an extension is essential for media detection by Android,
// so we retry with a display name that includes the extension
if ((targetDocFile.extension == null || targetDocFile.extension.isEmpty() || targetDocFile.extension == "bin") && defaultExtension != null) {
if (targetDocFile.exists()) {
targetDocFile.delete()
}
val extension = if (defaultExtension.startsWith(".")) defaultExtension else ".$defaultExtension"
targetTreeFile = targetDirDocFile.createFile(mimeType, "$targetNameWithoutExtension$extension")
targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
}
try {
targetDocFile.openOutputStream().use(write)
@ -823,18 +846,32 @@ class MediaStoreImageProvider : ImageProvider() {
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields["sizeBytes"] = cursor.getLong(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[EntryFields.SIZE_BYTES] = cursor.getLong(it) }
cursor.close()
}
} catch (e: Exception) {
callback.onFailure(e)
return@scanFile
}
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
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) {
val file = File(path)
val delayMillis = 500L
@ -912,14 +949,15 @@ class MediaStoreImageProvider : ImageProvider() {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val newFields = hashMapOf<String, Any?>(
"origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
"uri" to uri.toString(),
"contentId" to uri.tryParseId(),
"path" to path,
EntryFields.ORIGIN to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
EntryFields.URI to uri.toString(),
EntryFields.CONTENT_ID to uri.tryParseId(),
EntryFields.PATH to path,
)
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = 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[EntryFields.DATE_MODIFIED_MILLIS] = cursor.getInt(it) * 1000 }
cursor.close()
getFileModifiedDateMillis(path)?.let { newFields[EntryFields.DATE_MODIFIED_MILLIS] = it }
return newFields
}
} catch (e: Exception) {
@ -1030,4 +1068,4 @@ object MediaColumns {
typealias NewEntryHandler = (entry: FieldMap) -> Unit
private typealias NewEntryChecker = (contentId: Long, dateModifiedSecs: Int) -> Boolean
private typealias NewEntryChecker = (contentId: Long, dateModifiedMillis: Long) -> Boolean

View file

@ -7,6 +7,7 @@ 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
@ -43,9 +44,9 @@ open class UnknownContentProvider : ImageProvider() {
}
val fields: FieldMap = hashMapOf(
"origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT,
"uri" to uri.toString(),
"sourceMimeType" to mimeType,
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`
@ -53,11 +54,11 @@ open class UnknownContentProvider : ImageProvider() {
// 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) }
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["sourceMimeType"] = cursor.getString(it) }
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) {
@ -65,7 +66,7 @@ open class UnknownContentProvider : ImageProvider() {
return
}
if (fields["sourceMimeType"] == null) {
if (fields[EntryFields.SOURCE_MIME_TYPE] == null) {
callback.onFailure(Exception("Failed to find MIME type for uri=$uri"))
return
}

View file

@ -2,27 +2,121 @@ package deckers.thibault.aves.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ColorSpace
import android.os.Build
import android.util.Half
import android.util.Log
import androidx.annotation.RequiresApi
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import deckers.thibault.aves.metadata.Metadata.getExifCode
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
object BitmapUtils {
private val LOG_TAG = LogUtils.createTag<BitmapUtils>()
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 mutex = Mutex()
const val ARGB_8888_BYTE_SIZE = 4
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
suspend fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? {
val stream: ByteArrayOutputStream
mutex.withLock {
// this method is called a lot, so we try and reuse output streams
@ -34,19 +128,17 @@ object BitmapUtils {
}
}
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
// the BMP format allows an alpha channel, but Android decoding seems to ignore it
if (canHaveAlpha && hasAlpha()) {
this.compress(Bitmap.CompressFormat.PNG, quality, stream)
if (canHaveAlpha && bitmap.hasAlpha()) {
bitmap.compress(Bitmap.CompressFormat.PNG, quality, stream)
} 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()
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")
}
@ -62,6 +154,107 @@ object BitmapUtils {
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? {
if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap
if (rotationDegrees == 0 && !isFlipped) return bitmap

View file

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

View file

@ -8,6 +8,8 @@ fun ByteBuffer.toByteArray(): ByteArray {
return bytes
}
fun Int.toHex(): String = "0x${byteArrayOf(shr(8).toByte(), toByte()).toHex()}"
fun ByteArray.toHex(): String = joinToString(separator = "") { it.toHex() }
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
// 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 {
val n: Int = this.size
val m: Int = pattern.size

View file

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

View file

@ -1,8 +1,8 @@
package deckers.thibault.aves.utils
import android.webkit.MimeTypeMap
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
import deckers.thibault.aves.decoder.MultiPageImage
import androidx.exifinterface.media.ExifInterfaceFork as ExifInterface
object MimeTypes {
const val ANY = "*/*"
@ -84,11 +84,11 @@ object MimeTypes {
else -> false
}
// as of Flutter v3.16.4, with additional custom handling for SVG
fun canDecodeWithFlutter(mimeType: String, pageId: Int?, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) {
// as of Flutter v3.16.4, with additional custom handling for SVG in Dart,
// while handling still PNG and JPEG on Android for color space and config conversion
fun canDecodeWithFlutter(mimeType: String, isAnimated: Boolean) = when (mimeType) {
GIF, WEBP, BMP, WBMP, ICO, SVG -> true
JPEG -> (pageId ?: 0) == 0
PNG -> (rotationDegrees ?: 0) == 0 && !(isFlipped ?: false)
JPEG, PNG -> isAnimated
else -> false
}
@ -163,12 +163,24 @@ object MimeTypes {
// among other refs:
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
fun extensionFor(mimeType: String): String? = when (mimeType) {
fun extensionFor(mimeType: String, defaultExtension: String?): String = when (mimeType) {
AVI, AVI_VND -> ".avi"
DNG, DNG_ADOBE -> ".dng"
HEIC, HEIF -> ".heif"
MP2T, MP2TS -> ".m2ts"
PSD_VND, PSD_X -> ".psd"
else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" }
else -> {
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: defaultExtension
if (ext != null) {
// fallback to provided extension when available,
// typically the original file extension when moving/renaming
if (ext.startsWith(".")) ext else ".$ext"
} else {
// fallback to generic extensions,
// as incorrect file extensions are better than none for media detection
if (isVideo(mimeType)) ".mp4" else ".jpg"
}
}
}
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)

View file

@ -9,13 +9,14 @@ import android.content.pm.PackageManager
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.text.TextUtils
import android.util.Log
import androidx.core.net.toUri
import androidx.core.text.isDigitsOnly
import com.commonsware.cwac.document.DocumentFileCompat
import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.utils.FileUtils.transferFrom
@ -81,7 +82,8 @@ object StorageUtils {
return null
}
val trashDir = File(externalFilesDir, "trash")
if (!trashDir.exists() && !trashDir.mkdirs()) {
trashDir.mkdirs()
if (!trashDir.exists()) {
Log.e(LOG_TAG, "failed to create directories at path=$trashDir")
return null
}
@ -228,7 +230,7 @@ object StorageUtils {
// Device has emulated storage; external storage paths should have userId burned into them.
// /storage/emulated/[0,1,2,...]/
val path = getPrimaryVolumePath(context)
val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { TextUtils.isDigitsOnly(it) } ?: ""
val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { it.isDigitsOnly() } ?: ""
if (rawUserId.isEmpty()) {
paths.add(rawEmulatedStorageTarget)
} else {
@ -499,7 +501,8 @@ object StorageUtils {
parentFile
} else {
val directory = File(cleanDirPath)
if (!directory.exists() && !directory.mkdirs()) {
directory.mkdirs()
if (!directory.exists()) {
Log.e(LOG_TAG, "failed to create directories at path=$cleanDirPath")
return null
}
@ -636,7 +639,7 @@ object StorageUtils {
// strip user info, if any
// e.g. `content://0@media/...`
private fun stripMediaUriUserInfo(uri: Uri) = Uri.parse(uri.toString().replaceFirst("${uri.userInfo}@", ""))
private fun stripMediaUriUserInfo(uri: Uri) = uri.toString().replaceFirst("${uri.userInfo}@", "").toUri()
fun openInputStream(context: Context, uri: Uri): InputStream? {
val effectiveUri = getOriginalUri(context, uri)
@ -712,7 +715,8 @@ object StorageUtils {
fun createTempFile(context: Context, extension: String? = null): File {
val directory = getTempDirectory(context)
if (!directory.exists() && !directory.mkdirs()) {
directory.mkdirs()
if (!directory.exists()) {
throw IOException("failed to create directories at path=$directory")
}
val tempFile = File.createTempFile("aves", extension, directory)

View file

@ -8,4 +8,5 @@
<string name="analysis_channel_name">Exploració de mitjans</string>
<string name="analysis_notification_default_title">Explorant mitjans</string>
<string name="analysis_notification_action_stop">Atura</string>
<string name="map_shortcut_short_label">Mapa</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_notification_default_title">Prohledávání médií</string>
<string name="analysis_notification_action_stop">Zastavit</string>
<string name="app_widget_label">Fotorámeček</string>
<string name="map_shortcut_short_label">Mapa</string>
</resources>

View file

@ -3,9 +3,10 @@
<string name="app_widget_label">Fotoramme</string>
<string name="wallpaper">Baggrund</string>
<string name="videos_shortcut_short_label">Videoer</string>
<string name="analysis_channel_name">Mediascanning</string>
<string name="analysis_channel_name">Mediescanning</string>
<string name="analysis_notification_default_title">Scanner medier</string>
<string name="analysis_notification_action_stop">Stop</string>
<string name="app_name">Aves</string>
<string name="search_shortcut_short_label">Søg</string>
<string name="map_shortcut_short_label">Kort</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_channel_name">Σάρωση πολυμέσων</string>
<string name="analysis_notification_default_title">Σάρωση στοιχείων</string>
<string name="analysis_notification_action_stop">Διακοπή</string>
<string name="map_shortcut_short_label">Χάρτης</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_notification_action_stop">توقف کردن</string>
<string name="app_widget_label">قاب عکس</string>
<string name="app_name">Aves</string>
<string name="map_shortcut_short_label">نقشه</string>
</resources>

View file

@ -5,7 +5,8 @@
<string name="wallpaper">Fondo da pantalla</string>
<string name="search_shortcut_short_label">Procura</string>
<string name="videos_shortcut_short_label">Vídeos</string>
<string name="analysis_channel_name">Escaneo multimedia</string>
<string name="analysis_channel_name">Escanear medios</string>
<string name="analysis_notification_default_title">Escaneando medios</string>
<string name="analysis_notification_action_stop">Pare</string>
<string name="map_shortcut_short_label">Mapa</string>
</resources>

View file

@ -6,7 +6,7 @@
<string name="videos_shortcut_short_label">Videók</string>
<string name="analysis_notification_action_stop">Állj</string>
<string name="app_widget_label">Fotó keret</string>
<string name="analysis_channel_name">Tartalom keresése</string>
<string name="analysis_channel_name">Médiafájlok keresése</string>
<string name="analysis_notification_default_title">Média beolvasása</string>
<string name="map_shortcut_short_label">Térkép</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="app_name">Aves</string>
<string name="analysis_channel_name">Skönnun myndefnis</string>
<string name="search_shortcut_short_label">Leita</string>
<string name="map_shortcut_short_label">Landakort</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">אייבז</string>
<string name="app_name">Aves</string>
<string name="app_widget_label">מסגרת תמונה</string>
<string name="wallpaper">טפט</string>
<string name="search_shortcut_short_label">חיפוש</string>

View file

@ -8,4 +8,5 @@
<string name="analysis_channel_name">メディアスキャン</string>
<string name="analysis_notification_default_title">メディアをスキャン中</string>
<string name="analysis_notification_action_stop">停止</string>
<string name="map_shortcut_short_label">マップ</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
<string name="app_name">ಎವೀಸ್</string>
<string name="app_widget_label">ಫೋಟೋ ಫ್ರೇಮ್</string>
<string name="wallpaper">ವಾಲ್ಪೇಪರ್</string>
<string name="videos_shortcut_short_label">ವೀಡಿಯೊಗಳು</string>
@ -8,4 +8,5 @@
<string name="analysis_notification_default_title">ಮೀಡಿಯಾ ಸ್ಕ್ಯಾನ್ ಮಾಡಲಾಗುತ್ತಿದೆ</string>
<string name="analysis_notification_action_stop">ನಿಲ್ಲಿಸಿ</string>
<string name="search_shortcut_short_label">ಹುಡುಕಿ</string>
<string name="map_shortcut_short_label">ನಕ್ಷೆ</string>
</resources>

View file

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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">एभस</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="analysis_notification_default_title">Scanarea suporturilor</string>
<string name="analysis_notification_action_stop">Stop</string>
<string name="search_shortcut_short_label">Căutare</string>
<string name="map_shortcut_short_label">Hartă</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Aves</string>
</resources>

View file

@ -8,4 +8,5 @@
<string name="search_shortcut_short_label">搜尋</string>
<string name="analysis_channel_name">媒體掃描</string>
<string name="analysis_notification_action_stop">停止</string>
<string name="map_shortcut_short_label">地圖</string>
</resources>

View file

@ -13,8 +13,8 @@ buildscript {
dependencies {
if (useCrashlytics) {
// GMS & Firebase Crashlytics (used by some flavors only)
classpath 'com.google.gms:google-services:4.4.1'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9'
classpath 'com.google.gms:google-services:4.4.2'
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.2'
}
}
}

View file

@ -22,10 +22,10 @@ import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongA
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.annotation.SuppressLint;
import android.content.res.AssetManager;
@ -54,6 +54,7 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileDescriptor;
@ -89,8 +90,9 @@ import java.util.regex.Pattern;
import java.util.zip.CRC32;
/*
* Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
* Forked from 'androidx.exifinterface:exifinterface:1.4.1'
* Named differently to let ExifInterface be loaded as subdependency.
* cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
*/
@ -136,6 +138,12 @@ public class ExifInterfaceFork {
// TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
// TLAD available heap size, to check allocations
private long getAvailableHeapSize() {
final Runtime runtime = Runtime.getRuntime();
return runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory());
}
private static final String TAG = "ExifInterface";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@ -190,6 +198,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #DATA_UNCOMPRESSED
* @see #DATA_JPEG
*/
@ -205,6 +214,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #PHOTOMETRIC_INTERPRETATION_RGB
* @see #PHOTOMETRIC_INTERPRETATION_YCBCR
*/
@ -219,6 +229,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #ORIENTATION_NORMAL}</li>
* </ul>
* <p>
*
* @see #ORIENTATION_UNDEFINED
* @see #ORIENTATION_NORMAL
* @see #ORIENTATION_FLIP_HORIZONTAL
@ -254,6 +265,7 @@ public class ExifInterfaceFork {
* <li>Count = 1</li>
* </ul>
* <p>
*
* @see #FORMAT_CHUNKY
* @see #FORMAT_PLANAR
*/
@ -294,6 +306,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #Y_CB_CR_POSITIONING_CENTERED}</li>
* </ul>
* <p>
*
* @see #Y_CB_CR_POSITIONING_CENTERED
* @see #Y_CB_CR_POSITIONING_CO_SITED
*/
@ -309,6 +322,7 @@ public class ExifInterfaceFork {
* <li>Default = 72</li>
* </ul>
* <p>
*
* @see #TAG_Y_RESOLUTION
* @see #TAG_RESOLUTION_UNIT
*/
@ -324,6 +338,7 @@ public class ExifInterfaceFork {
* <li>Default = 72</li>
* </ul>
* <p>
*
* @see #TAG_X_RESOLUTION
* @see #TAG_RESOLUTION_UNIT
*/
@ -340,6 +355,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
* </ul>
* <p>
*
* @see #RESOLUTION_UNIT_INCHES
* @see #RESOLUTION_UNIT_CENTIMETERS
* @see #TAG_X_RESOLUTION
@ -365,6 +381,7 @@ public class ExifInterfaceFork {
* <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
* / {@link #TAG_ROWS_PER_STRIP})</p>
* <p>
*
* @see #TAG_ROWS_PER_STRIP
* @see #TAG_STRIP_BYTE_COUNTS
*/
@ -381,6 +398,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #TAG_STRIP_OFFSETS
* @see #TAG_STRIP_BYTE_COUNTS
*/
@ -656,6 +674,7 @@ public class ExifInterfaceFork {
* <li>Count = 1</li>
* </ul>
* <p>
*
* @see #COLOR_SPACE_S_RGB
* @see #COLOR_SPACE_UNCALIBRATED
*/
@ -962,6 +981,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #EXPOSURE_PROGRAM_NOT_DEFINED}</li>
* </ul>
* <p>
*
* @see #EXPOSURE_PROGRAM_NOT_DEFINED
* @see #EXPOSURE_PROGRAM_MANUAL
* @see #EXPOSURE_PROGRAM_NORMAL
@ -1031,6 +1051,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #SENSITIVITY_TYPE_UNKNOWN
* @see #SENSITIVITY_TYPE_SOS
* @see #SENSITIVITY_TYPE_REI
@ -1197,6 +1218,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #METERING_MODE_UNKNOWN}</li>
* </ul>
* <p>
*
* @see #METERING_MODE_UNKNOWN
* @see #METERING_MODE_AVERAGE
* @see #METERING_MODE_CENTER_WEIGHT_AVERAGE
@ -1217,6 +1239,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #LIGHT_SOURCE_UNKNOWN}</li>
* </ul>
* <p>
*
* @see #LIGHT_SOURCE_UNKNOWN
* @see #LIGHT_SOURCE_DAYLIGHT
* @see #LIGHT_SOURCE_FLUORESCENT
@ -1253,6 +1276,7 @@ public class ExifInterfaceFork {
* <li>Count = 1</li>
* </ul>
* <p>
*
* @see #FLAG_FLASH_FIRED
* @see #FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED
* @see #FLAG_FLASH_RETURN_LIGHT_DETECTED
@ -1365,6 +1389,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
* </ul>
* <p>
*
* @see #TAG_RESOLUTION_UNIT
* @see #RESOLUTION_UNIT_INCHES
* @see #RESOLUTION_UNIT_CENTIMETERS
@ -1407,6 +1432,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #SENSOR_TYPE_NOT_DEFINED
* @see #SENSOR_TYPE_ONE_CHIP
* @see #SENSOR_TYPE_TWO_CHIP
@ -1427,6 +1453,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #FILE_SOURCE_DSC}</li>
* </ul>
* <p>
*
* @see #FILE_SOURCE_OTHER
* @see #FILE_SOURCE_TRANSPARENT_SCANNER
* @see #FILE_SOURCE_REFLEX_SCANNER
@ -1444,6 +1471,7 @@ public class ExifInterfaceFork {
* <li>Default = 1</li>
* </ul>
* <p>
*
* @see #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED
*/
public static final String TAG_SCENE_TYPE = "SceneType";
@ -1457,6 +1485,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #TAG_SENSING_METHOD
* @see #SENSOR_TYPE_ONE_CHIP
*/
@ -1473,6 +1502,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #RENDERED_PROCESS_NORMAL}</li>
* </ul>
* <p>
*
* @see #RENDERED_PROCESS_NORMAL
* @see #RENDERED_PROCESS_CUSTOM
*/
@ -1489,6 +1519,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #EXPOSURE_MODE_AUTO
* @see #EXPOSURE_MODE_MANUAL
* @see #EXPOSURE_MODE_AUTO_BRACKET
@ -1504,6 +1535,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #WHITEBALANCE_AUTO
* @see #WHITEBALANCE_MANUAL
*/
@ -1553,6 +1585,7 @@ public class ExifInterfaceFork {
* <li>Default = 0</li>
* </ul>
* <p>
*
* @see #SCENE_CAPTURE_TYPE_STANDARD
* @see #SCENE_CAPTURE_TYPE_LANDSCAPE
* @see #SCENE_CAPTURE_TYPE_PORTRAIT
@ -1569,6 +1602,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #GAIN_CONTROL_NONE
* @see #GAIN_CONTROL_LOW_GAIN_UP
* @see #GAIN_CONTROL_HIGH_GAIN_UP
@ -1587,6 +1621,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #CONTRAST_NORMAL}</li>
* </ul>
* <p>
*
* @see #CONTRAST_NORMAL
* @see #CONTRAST_SOFT
* @see #CONTRAST_HARD
@ -1603,6 +1638,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #SATURATION_NORMAL}</li>
* </ul>
* <p>
*
* @see #SATURATION_NORMAL
* @see #SATURATION_LOW
* @see #SATURATION_HIGH
@ -1619,6 +1655,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #SHARPNESS_NORMAL}</li>
* </ul>
* <p>
*
* @see #SHARPNESS_NORMAL
* @see #SHARPNESS_SOFT
* @see #SHARPNESS_HARD
@ -1646,6 +1683,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #SUBJECT_DISTANCE_RANGE_UNKNOWN
* @see #SUBJECT_DISTANCE_RANGE_MACRO
* @see #SUBJECT_DISTANCE_RANGE_CLOSE_VIEW
@ -1675,6 +1713,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @deprecated Use {@link #TAG_CAMERA_OWNER_NAME} instead.
*/
@Deprecated
@ -1780,6 +1819,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LATITUDE_NORTH
* @see #LATITUDE_SOUTH
*/
@ -1809,6 +1849,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LONGITUDE_EAST
* @see #LONGITUDE_WEST
*/
@ -1841,6 +1882,7 @@ public class ExifInterfaceFork {
* <li>Default = 0</li>
* </ul>
* <p>
*
* @see #ALTITUDE_ABOVE_SEA_LEVEL
* @see #ALTITUDE_BELOW_SEA_LEVEL
*/
@ -1899,6 +1941,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #GPS_MEASUREMENT_IN_PROGRESS
* @see #GPS_MEASUREMENT_INTERRUPTED
*/
@ -1915,6 +1958,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #GPS_MEASUREMENT_2D
* @see #GPS_MEASUREMENT_3D
*/
@ -1941,6 +1985,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_SPEED_KILOMETERS_PER_HOUR}</li>
* </ul>
* <p>
*
* @see #GPS_SPEED_KILOMETERS_PER_HOUR
* @see #GPS_SPEED_MILES_PER_HOUR
* @see #GPS_SPEED_KNOTS
@ -1968,6 +2013,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
* </ul>
* <p>
*
* @see #GPS_DIRECTION_TRUE
* @see #GPS_DIRECTION_MAGNETIC
*/
@ -1994,6 +2040,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
* </ul>
* <p>
*
* @see #GPS_DIRECTION_TRUE
* @see #GPS_DIRECTION_MAGNETIC
*/
@ -2032,6 +2079,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LATITUDE_NORTH
* @see #LATITUDE_SOUTH
*/
@ -2061,6 +2109,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #LONGITUDE_EAST
* @see #LONGITUDE_WEST
*/
@ -2090,6 +2139,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
* </ul>
* <p>
*
* @see #GPS_DIRECTION_TRUE
* @see #GPS_DIRECTION_MAGNETIC
*/
@ -2116,6 +2166,7 @@ public class ExifInterfaceFork {
* <li>Default = {@link #GPS_DISTANCE_KILOMETERS}</li>
* </ul>
* <p>
*
* @see #GPS_DISTANCE_KILOMETERS
* @see #GPS_DISTANCE_MILES
* @see #GPS_DISTANCE_NAUTICAL_MILES
@ -2177,6 +2228,7 @@ public class ExifInterfaceFork {
* <li>Default = None</li>
* </ul>
* <p>
*
* @see #GPS_MEASUREMENT_NO_DIFFERENTIAL
* @see #GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED
*/
@ -3132,11 +3184,18 @@ public class ExifInterfaceFork {
// See "Extensions to the PNG 1.2 Specification, Version 1.5.0",
// 3.7. eXIf Exchangeable Image File (Exif) Profile
private static final int PNG_CHUNK_TYPE_EXIF = 'e' << 24 | 'X' << 16 | 'I' << 8 | 'f';
// See "XMP Specification Part 3: Storage in Files" section 1.1.5
private static final int PNG_CHUNK_TYPE_ITXT = 'i' << 24 | 'T' << 16 | 'X' << 8 | 't';
private static final int PNG_CHUNK_TYPE_IHDR = 'I' << 24 | 'H' << 16 | 'D' << 8 | 'R';
private static final int PNG_CHUNK_TYPE_IEND = 'I' << 24 | 'E' << 16 | 'N' << 8 | 'D';
private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4;
private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
/**
* The keyword and 5 null bytes defined by XMP spec part 3 table 9 (section 1.1.5).
*/
@VisibleForTesting
static final byte[] PNG_ITXT_XMP_KEYWORD = "XML:com.adobe.xmp\0\0\0\0\0".getBytes(UTF_8);
// See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header"
private static final byte[] WEBP_SIGNATURE_1 = new byte[]{'R', 'I', 'F', 'F'};
private static final byte[] WEBP_SIGNATURE_2 = new byte[]{'W', 'E', 'B', 'P'};
@ -4069,20 +4128,33 @@ public class ExifInterfaceFork {
// Used to indicate offset from the start of the original input stream to EXIF data
private int mOffsetToExifData;
private int mOrfMakerNoteOffset;
/**
* The position of the thumbnail within the Exif data (from {@link #mOffsetToExifData}).
*/
private int mOrfThumbnailOffset;
private int mOrfThumbnailLength;
private boolean mModified;
/**
* XMP data can occur as either part of the TIFF/Exif data (tag number 700), or as a separate
* section of the file (e.g. a separate APP1 segment in JPEG). XMP read from within the
* TIFF/Exif data is stored in {@link #mAttributes}, while XMP read from a separate section is
* here. If both are present, the disambiguation rules vary per file format, see
* {@link #getXmpHandlingForImageType(int)}.
* section of the file (e.g. a separate APP1 segment in JPEG, or an iTXt chunk in PNG). XMP read
* from within the TIFF/Exif data is stored in {@link #mAttributes}, while XMP read from a
* separate section is here. If both are present, the disambiguation rules vary per file format,
* see {@link #getXmpHandlingForImageType(int)}.
*/
@Nullable
private ExifAttribute mXmpFromSeparateMarker;
/**
* True if the file on disk contains XMP in a separate section.
*
* <p>This means the file the instance was loaded with, or the file created by the last call to
* {@link #saveAttributes()}.
*/
private boolean mFileOnDiskContainsSeparateXmpMarker;
// Pattern to check non zero timestamp
private static final Pattern NON_ZERO_TIME_PATTERN = Pattern.compile(".*[1-9].*");
// Pattern to check gps timestamp
@ -4300,6 +4372,7 @@ public class ExifInterfaceFork {
return XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT;
case IMAGE_TYPE_AVIF:
case IMAGE_TYPE_HEIC:
case IMAGE_TYPE_PNG:
// RAF stores XMP/Exif in JPEG, but we have no documented backwards-compat obligations
// so we can implement the spec to store XMP in a separate APP1 segment.
case IMAGE_TYPE_RAF:
@ -4309,10 +4382,8 @@ public class ExifInterfaceFork {
case IMAGE_TYPE_PEF:
case IMAGE_TYPE_RW2:
case IMAGE_TYPE_UNKNOWN:
// PNG and WebP support a separate XMP chunk (so should be
// XMP_HANDLING_PREFER_SEPARATE), but ExifInterface doesn't currently read or write
// them.
case IMAGE_TYPE_PNG:
// WebP supports a separate XMP chunk (so should be XMP_HANDLING_PREFER_SEPARATE), but
// ExifInterface doesn't currently read or write it.
case IMAGE_TYPE_WEBP:
default:
return XMP_HANDLING_TIFF_700_ONLY;
@ -4487,7 +4558,7 @@ public class ExifInterfaceFork {
&& (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
|| (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
&& !containsTiff700Xmp)) {
mXmpFromSeparateMarker = ExifAttribute.createByte(value);
mXmpFromSeparateMarker = value != null ? ExifAttribute.createByte(value) : null;
return;
}
}
@ -5160,14 +5231,18 @@ public class ExifInterfaceFork {
}
/**
* Returns the offset and length of the requested tag inside the image file,
* or {@code null} if the tag is not contained.
* Returns the offset and length of the requested tag inside the image file, or {@code null} if
* the tag is not contained.
*
* @return two-element array, the offset in the first value, and length in
* the second, or {@code null} if no tag was found.
* @throws IllegalStateException if {@link #saveAttributes()} has been
* called since the underlying file was initially parsed, since
* that means offsets may have changed.
* <p>If the attribute has been modified with {@link #setAttribute(String, String)} but not yet
* written to disk with {@link #saveAttributes()}, the returned range will have the correct
* length for the modified value, but an offset of {@code -1} to indicate its position in the
* file isn't known.
*
* @return two-element array, the offset in the first value, and length in the second, or {@code
* null} if no tag was found.
* @throws IllegalStateException if {@link #saveAttributes()} has been called since the
* underlying file was initially parsed, since that means offsets may have changed.
*/
public long @Nullable [] getAttributeRange(@NonNull String tag) {
if (tag == null) {
@ -5841,6 +5916,7 @@ public class ExifInterfaceFork {
IDENTIFIER_XMP_APP1.length, bytes.length);
mXmpFromSeparateMarker =
new ExifAttribute(IFD_FORMAT_BYTE, value.length, offset, value);
mFileOnDiskContainsSeparateXmpMarker = true;
}
break;
}
@ -6165,6 +6241,7 @@ public class ExifInterfaceFork {
in.readFully(xmpBytes);
mXmpFromSeparateMarker =
new ExifAttribute(IFD_FORMAT_BYTE, xmpBytes.length, offset, xmpBytes);
mFileOnDiskContainsSeparateXmpMarker = true;
}
if (DEBUG) {
@ -6352,10 +6429,12 @@ public class ExifInterfaceFork {
// See PNG (Portable Network Graphics) Specification, Version 1.2,
// 3.2. Chunk layout
try {
while (true) {
boolean foundExif = false;
boolean foundXmpItxt = false;
while (!foundExif || !foundXmpItxt) {
int length = in.readInt();
int type = in.readInt();
int startOfNextChunk = in.position() + length + PNG_CHUNK_CRC_BYTE_LENGTH;
// The first chunk must be the IHDR chunk
if (in.position() - startPosition == 16 && type != PNG_CHUNK_TYPE_IHDR) {
@ -6367,7 +6446,7 @@ public class ExifInterfaceFork {
if (type == PNG_CHUNK_TYPE_IEND) {
// IEND marks the end of the image.
break;
} else if (type == PNG_CHUNK_TYPE_EXIF) {
} else if (type == PNG_CHUNK_TYPE_EXIF && !foundExif) {
// Save offset to EXIF data for handling thumbnail and attribute offsets.
mOffsetToExifData = in.position() - startPosition;
@ -6388,20 +6467,40 @@ public class ExifInterfaceFork {
updateCrcWithInt(crc, type);
crc.update(data);
if ((int) crc.getValue() != dataCrcValue) {
throw new IOException("Encountered invalid CRC value for PNG-EXIF chunk."
+ "\n recorded CRC value: " + dataCrcValue + ", calculated CRC "
+ "value: " + crc.getValue());
throw new IOException(
"Encountered invalid CRC value for PNG-EXIF chunk."
+ "\n recorded CRC value: "
+ dataCrcValue
+ ", calculated CRC "
+ "value: "
+ crc.getValue());
}
readExifSegment(data, IFD_TYPE_PRIMARY);
validateImages();
setThumbnailData(new ByteOrderedDataInputStream(data));
break;
} else {
// Skip to next chunk
in.skipFully(length + PNG_CHUNK_CRC_BYTE_LENGTH);
foundExif = true;
} else if (type == PNG_CHUNK_TYPE_ITXT
&& !foundXmpItxt
&& length >= PNG_ITXT_XMP_KEYWORD.length) {
// Read the 17 byte keyword and 5 expected null bytes.
byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length];
in.readFully(keyword);
if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) {
int xmpDataOffset = in.position() - startPosition;
int xmpLength = length - keyword.length;
byte[] xmpData = new byte[xmpLength];
in.readFully(xmpData);
mXmpFromSeparateMarker =
new ExifAttribute(
IFD_FORMAT_BYTE, xmpLength, xmpDataOffset, xmpData);
foundXmpItxt = true;
}
}
// Skip to next chunk
in.skipFully(startOfNextChunk - in.position());
}
mFileOnDiskContainsSeparateXmpMarker = foundXmpItxt;
} catch (EOFException e) {
// Should not reach here. Will only reach here if the file is corrupted or
// does not follow the PNG specifications
@ -6464,9 +6563,9 @@ public class ExifInterfaceFork {
// Exif data in WebP images (e.g.
// https://github.com/ImageMagick/ImageMagick/issues/3140)
if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
int adjustedChunkSize = chunkSize - IDENTIFIER_EXIF_APP1.length;
payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length,
adjustedChunkSize);
payload =
Arrays.copyOfRange(
payload, IDENTIFIER_EXIF_APP1.length, payload.length);
}
// Save offset to EXIF data for handling thumbnail and attribute offsets.
@ -6522,7 +6621,7 @@ public class ExifInterfaceFork {
// Write EXIF APP1 segment
dataOutputStream.writeByte(MARKER);
dataOutputStream.writeByte(MARKER_APP1);
writeExifSegment(dataOutputStream);
mOffsetToExifData = writeExifSegment(dataOutputStream);
if (mXmpFromSeparateMarker != null) {
// Write XMP APP1 segment. The XMP spec (part 3, section 1.1.3) recommends for this to
@ -6533,6 +6632,7 @@ public class ExifInterfaceFork {
dataOutputStream.writeUnsignedShort(length);
dataOutputStream.write(IDENTIFIER_XMP_APP1);
dataOutputStream.write(mXmpFromSeparateMarker.bytes);
mFileOnDiskContainsSeparateXmpMarker = true;
}
byte[] bytes = new byte[4096];
@ -6627,60 +6727,94 @@ public class ExifInterfaceFork {
// Copy PNG signature bytes
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
// EXIF chunk can appear anywhere between the first (IHDR) and last (IEND) chunks, except
// between IDAT chunks.
// Adhering to these rules,
// 1) if EXIF chunk did not exist in the original file, it will be stored right after the
// first chunk,
// 2) if EXIF chunk existed in the original file, it will be stored in the same location.
if (mOffsetToExifData == 0) {
// Copy IHDR chunk bytes
int ihdrChunkLength = dataInputStream.readInt();
dataOutputStream.writeInt(ihdrChunkLength);
copy(dataInputStream, dataOutputStream, PNG_CHUNK_TYPE_BYTE_LENGTH
+ ihdrChunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
} else {
// Copy up until the point where EXIF chunk length information is stored.
int copyLength = mOffsetToExifData - PNG_SIGNATURE.length
- 4 /* PNG EXIF chunk length bytes */
- PNG_CHUNK_TYPE_BYTE_LENGTH;
copy(dataInputStream, dataOutputStream, copyLength);
// Skip to the start of the chunk after the EXIF chunk
int exifChunkLength = dataInputStream.readInt();
dataInputStream.skipFully(PNG_CHUNK_TYPE_BYTE_LENGTH + exifChunkLength
+ PNG_CHUNK_CRC_BYTE_LENGTH);
}
// Write EXIF data
ByteArrayOutputStream exifByteArrayOutputStream = null;
try {
// A byte array is needed to calculate the CRC value of this chunk which requires
// the chunk type bytes and the chunk data bytes.
exifByteArrayOutputStream = new ByteArrayOutputStream();
ByteOrderedDataOutputStream exifDataOutputStream =
new ByteOrderedDataOutputStream(exifByteArrayOutputStream, BIG_ENDIAN);
// Store Exif data in separate byte array
writeExifSegment(exifDataOutputStream);
byte[] exifBytes =
((ByteArrayOutputStream) exifDataOutputStream.mOutputStream).toByteArray();
// Write EXIF chunk data
dataOutputStream.write(exifBytes);
// Write EXIF chunk CRC
CRC32 crc = new CRC32();
crc.update(exifBytes, 4 /* skip length bytes */, exifBytes.length - 4);
dataOutputStream.writeInt((int) crc.getValue());
} finally {
closeQuietly(exifByteArrayOutputStream);
boolean needToWriteExif = true;
// Either there's some XMP data to write, or it has been cleared locally but was present in
// the file when it was read (and so needs to be removed).
boolean needToHandleXmpChunk =
mXmpFromSeparateMarker != null || mFileOnDiskContainsSeparateXmpMarker;
while (needToWriteExif || needToHandleXmpChunk) {
int chunkLength = dataInputStream.readInt();
int chunkType = dataInputStream.readInt();
if (chunkType == PNG_CHUNK_TYPE_IHDR) {
dataOutputStream.writeInt(chunkLength);
dataOutputStream.writeInt(chunkType);
copy(dataInputStream, dataOutputStream, chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
if (mOffsetToExifData == 0) {
// There was no Exif segment in the original file, so we put it directly
// after the IHDR chunk.
writePngExifChunk(dataOutputStream);
needToWriteExif = false;
}
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
writePngXmpItxtChunk(dataOutputStream);
needToHandleXmpChunk = false;
}
continue;
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
writePngExifChunk(dataOutputStream);
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
needToWriteExif = false;
continue;
} else if (chunkType == PNG_CHUNK_TYPE_ITXT
&& chunkLength >= PNG_ITXT_XMP_KEYWORD.length) {
// Read the 17 byte keyword and 5 expected null bytes.
byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length];
dataInputStream.readFully(keyword);
int remainingChunkBytes = chunkLength - keyword.length + PNG_CHUNK_CRC_BYTE_LENGTH;
if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) {
if (mXmpFromSeparateMarker != null) {
writePngXmpItxtChunk(dataOutputStream);
}
dataInputStream.skipFully(remainingChunkBytes);
needToHandleXmpChunk = false;
} else {
// This is a non-XMP iTXt chunk, so just copy it to the output and continue.
dataOutputStream.writeInt(chunkLength);
dataOutputStream.writeInt(chunkType);
dataOutputStream.write(keyword);
copy(dataInputStream, dataOutputStream, remainingChunkBytes);
}
continue;
}
dataOutputStream.writeInt(chunkLength);
dataOutputStream.writeInt(chunkType);
copy(dataInputStream, dataOutputStream, chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
}
// Copy the rest of the file
copy(dataInputStream, dataOutputStream);
}
private void writePngExifChunk(ByteOrderedDataOutputStream dataOutputStream)
throws IOException {
// Write the eXIF chunk out to an intermediate byte array so we can calculate the CRC value.
ByteArrayOutputStream exifByteArrayOutputStream = new ByteArrayOutputStream();
// Write eXIF chunk data (including chunk type & length).
int exifOffset =
writeExifSegment(
new ByteOrderedDataOutputStream(exifByteArrayOutputStream, BIG_ENDIAN));
mOffsetToExifData = dataOutputStream.mOutputStream.size() + exifOffset;
byte[] exifBytes = exifByteArrayOutputStream.toByteArray();
dataOutputStream.write(exifBytes);
CRC32 crc = new CRC32();
crc.update(exifBytes, 4 /* skip length bytes */, exifBytes.length - 4);
dataOutputStream.writeInt((int) crc.getValue());
}
private void writePngXmpItxtChunk(ByteOrderedDataOutputStream dataOutputStream)
throws IOException {
dataOutputStream.writeInt(mXmpFromSeparateMarker.bytes.length + 22);
CRC32 crc = new CRC32();
dataOutputStream.writeInt(PNG_CHUNK_TYPE_ITXT);
updateCrcWithInt(crc, PNG_CHUNK_TYPE_ITXT);
dataOutputStream.write(PNG_ITXT_XMP_KEYWORD);
crc.update(PNG_ITXT_XMP_KEYWORD);
dataOutputStream.write(mXmpFromSeparateMarker.bytes);
crc.update(mXmpFromSeparateMarker.bytes);
dataOutputStream.writeInt((int) crc.getValue());
mFileOnDiskContainsSeparateXmpMarker = true;
}
// A WebP file has a header and a series of chunks.
// The header is composed of:
// "RIFF" + File Size + "WEBP"
@ -6726,11 +6860,12 @@ public class ExifInterfaceFork {
// WebP signature
copy(totalInputStream, totalOutputStream, WEBP_SIGNATURE_1.length);
// File length will be written after all the chunks have been written
totalInputStream.skipFully(WEBP_FILE_SIZE_BYTE_LENGTH + WEBP_SIGNATURE_2.length);
int riffLength = totalInputStream.readInt();
totalInputStream.skipFully(WEBP_SIGNATURE_2.length);
// Create a separate byte array to calculate file length
ByteArrayOutputStream nonHeaderByteArrayOutputStream = null;
int exifOffset = -1;
try {
nonHeaderByteArrayOutputStream = new ByteArrayOutputStream();
ByteOrderedDataOutputStream nonHeaderOutputStream =
@ -6756,7 +6891,7 @@ public class ExifInterfaceFork {
totalInputStream.skipFully(exifChunkLength);
// Write new EXIF chunk to output stream
writeExifSegment(nonHeaderOutputStream);
exifOffset = writeExifSegment(nonHeaderOutputStream);
} else {
// EXIF chunk does not exist in the original file
byte[] firstChunkType = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
@ -6801,7 +6936,7 @@ public class ExifInterfaceFork {
animationFinished = true;
}
if (animationFinished) {
writeExifSegment(nonHeaderOutputStream);
exifOffset = writeExifSegment(nonHeaderOutputStream);
break;
}
copyWebPChunk(totalInputStream, nonHeaderOutputStream, type);
@ -6810,7 +6945,7 @@ public class ExifInterfaceFork {
// Skip until we find the VP8 or VP8L chunk
copyChunksUpToGivenChunkType(totalInputStream, nonHeaderOutputStream,
WEBP_CHUNK_TYPE_VP8, WEBP_CHUNK_TYPE_VP8L);
writeExifSegment(nonHeaderOutputStream);
exifOffset = writeExifSegment(nonHeaderOutputStream);
}
} else if (Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8)
|| Arrays.equals(firstChunkType, WEBP_CHUNK_TYPE_VP8L)) {
@ -6897,18 +7032,24 @@ public class ExifInterfaceFork {
copy(totalInputStream, nonHeaderOutputStream, bytesToRead);
// Write EXIF chunk
writeExifSegment(nonHeaderOutputStream);
exifOffset = writeExifSegment(nonHeaderOutputStream);
}
}
// Copy the rest of the file
copy(totalInputStream, nonHeaderOutputStream);
// Copy the rest of the RIFF part of the file
int remainingRiffBytes = riffLength + 8 - totalInputStream.position();
copy(totalInputStream, nonHeaderOutputStream, remainingRiffBytes);
// Write file length + second signature
totalOutputStream.writeInt(nonHeaderByteArrayOutputStream.size()
+ WEBP_SIGNATURE_2.length);
totalOutputStream.write(WEBP_SIGNATURE_2);
if (exifOffset != -1) {
mOffsetToExifData = totalOutputStream.mOutputStream.size() + exifOffset;
}
nonHeaderByteArrayOutputStream.writeTo(totalOutputStream);
// Copy any non-RIFF trailing data
copy(totalInputStream, totalOutputStream);
} catch (Exception e) {
throw new IOException("Failed to save WebP file", e);
} finally {
@ -7419,6 +7560,13 @@ public class ExifInterfaceFork {
Log.d(TAG, "Invalid strip offset value");
return;
}
// TLAD start
if (bytesToSkip > getAvailableHeapSize()) {
throw new IOException("cannot allocate " + bytesToSkip + " bytes to skip to retrieve thumbnail");
}
// TLAD end
try {
in.skipFully(bytesToSkip);
} catch (EOFException e) {
@ -7624,7 +7772,12 @@ public class ExifInterfaceFork {
}
}
// Writes an Exif segment into the given output stream.
/**
* Writes an Exif segment into the given output stream.
*
* @return The offset of the start of the Exif data (the byte-order marker) written into {@code
* dataOutputStream}.
*/
private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream) throws IOException {
// The following variables are for calculating each IFD tag group size in bytes.
int[] ifdOffsets = new int[EXIF_TAGS.length];
@ -7772,6 +7925,8 @@ public class ExifInterfaceFork {
break;
}
int offsetToExifData = dataOutputStream.mOutputStream.size();
// Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
dataOutputStream.writeShort(mExifByteOrder == BIG_ENDIAN ? BYTE_ALIGN_MM : BYTE_ALIGN_II);
dataOutputStream.setByteOrder(mExifByteOrder);
@ -7844,7 +7999,7 @@ public class ExifInterfaceFork {
// Reset the byte order to big endian in order to write remaining parts of the JPEG file.
dataOutputStream.setByteOrder(BIG_ENDIAN);
return totalSize;
return offsetToExifData;
}
/**
@ -8240,12 +8395,12 @@ public class ExifInterfaceFork {
// An output stream to write EXIF data area, which can be written in either little or big endian
// order.
private static class ByteOrderedDataOutputStream extends FilterOutputStream {
final OutputStream mOutputStream;
final DataOutputStream mOutputStream;
private ByteOrder mByteOrder;
public ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
super(out);
mOutputStream = out;
mOutputStream = new DataOutputStream(out);
mByteOrder = byteOrder;
}

View file

@ -31,7 +31,7 @@ import java.io.InputStream;
import java.io.OutputStream;
/*
* Forked from 'androidx.exifinterface:exifinterface:1.4.0-alpha01' on 2024/11/17
* Forked from 'androidx.exifinterface:exifinterface:1.4.1'
* Named differently to let ExifInterface be loaded as subdependency.
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
*/

View file

@ -1,22 +1,10 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
# full mode is too aggressive and removes essential code
# of `metadata-extractor` even when adding `-keep class com.drew.**{ *; }`
android.enableR8.fullMode=false

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip

View file

@ -1,33 +0,0 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}
settings.ext.flutterSdkPath = flutterSdkPath()
settings.ext.kotlin_version = '1.9.24'
settings.ext.ksp_version = "$kotlin_version-1.0.20"
settings.ext.agp_version = '8.7.0'
includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version("1.0.0")
id("com.android.application") version("$agp_version") apply(false)
id("org.jetbrains.kotlin.android") version("$kotlin_version") apply(false)
id("com.google.devtools.ksp") version("$ksp_version") apply(false)
id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
}
include(":app")
include(":exifinterface")

View file

@ -0,0 +1,28 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.10.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.21" apply false
id("com.google.devtools.ksp") version "2.1.21-2.0.1" apply false
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
include(":app")
include(":exifinterface")

35
assets/terms.txt Normal file
View file

@ -0,0 +1,35 @@
Terms of Service
================
“Aves Gallery” is an open-source gallery and metadata explorer app allowing you to access and manage your local photos and videos.
The app is designed for legal, authorized and acceptable purposes.
Disclaimer
==========
The app is released “as-is”, without any warranty, responsibility or liability. Use of the app is at your own risk.
Privacy Policy
==============
The app does not collect any personal data. We never have access to your photos and videos. This also means that we cannot get them back for you if you delete them without backing them up.
Optionally, with your consent, the app accesses the inventory of installed apps to improve album display.
Optionally, with your consent, the app collects anonymous error and diagnostic data to improve the app quality. We use Firebase Crashlytics, and the anonymous data are stored on their servers. Please note that those are anonymous data, there is absolutely nothing personal about those data.
Contact
=======
Developer: Thibault Deckers
Email: gallery.aves@gmail.com
Website: https://github.com/deckerst/aves

View file

@ -1,3 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- provider: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 495 KiB

After

Width:  |  Height:  |  Size: 553 KiB

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