Compare commits

..

42 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
133 changed files with 1117 additions and 703 deletions

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

View file

@ -28,6 +28,9 @@ jobs:
- name: Get Flutter packages - name: Get Flutter packages
run: ./flutterw pub get run: ./flutterw pub get
- name: Generate app localizations
run: ./flutterw gen-l10n
- name: Static analysis. - name: Static analysis.
run: ./flutterw analyze run: ./flutterw analyze
@ -69,7 +72,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@ -83,6 +86,6 @@ jobs:
./flutterw build apk --profile -t lib/main_play.dart --flavor play ./flutterw build apk --profile -t lib/main_play.dart --flavor play
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View file

@ -36,6 +36,9 @@ jobs:
- name: Get Flutter packages - name: Get Flutter packages
run: ./flutterw pub get run: ./flutterw pub get
- name: Generate app localizations
run: ./flutterw gen-l10n
- name: Update Flutter version file - name: Update Flutter version file
run: scripts/update_flutter_version.sh run: scripts/update_flutter_version.sh

View file

@ -41,7 +41,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@ -71,6 +71,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View file

@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file.
## <a id="unreleased"></a>[Unreleased] ## <a id="unreleased"></a>[Unreleased]
### Added
- Info: show matching dynamic albums
### Fixed
- crash when decoding some large thumbnails
## <a id="v1.13.2"></a>[v1.13.2] - 2025-06-02
### Changed
- downgraded Flutter to stable v3.27.4
- prevent display orientation flip when device rotation is locked
### Fixed
- moved file losing its extension and no longer being detected as media in some cases
- opening home when launching app as media picker
- removing groups with obsolete albums
- loading group custom covers
- crash when parsing some large media with trailing thumbnail
## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14 ## <a id="v1.13.1"></a>[v1.13.1] - 2025-05-14
### Fixed ### Fixed

View file

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

View file

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

1
android/.gitignore vendored
View file

@ -7,6 +7,7 @@ gradle-wrapper.jar
GeneratedPluginRegistrant.java GeneratedPluginRegistrant.java
.cxx/ .cxx/
.kotlin/ .kotlin/
/build/
# Remember to never publicly share your keystore. # Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore # See https://flutter.dev/to/reference-keystore

View file

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

View file

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

View file

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

View file

@ -311,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
embeddedByteStream: InputStream, embeddedByteStream: InputStream,
embeddedByteLength: Long, embeddedByteLength: Long,
) { ) {
val extension = extensionFor(mimeType) val extension = extensionFor(mimeType, defaultExtension = null)
val targetFile = StorageUtils.createTempFile(context, extension).apply { val targetFile = StorageUtils.createTempFile(context, extension).apply {
transferFrom(embeddedByteStream, embeddedByteLength) transferFrom(embeddedByteStream, embeddedByteLength)
} }
@ -319,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
val authority = "${context.applicationContext.packageName}.file_provider" val authority = "${context.applicationContext.packageName}.file_provider"
val uri = if (displayName != null) { val uri = if (displayName != null) {
// add extension to ease type identification when sharing this content // add extension to ease type identification when sharing this content
val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) { val displayNameWithExtension = if (displayName.endsWith(extension, ignoreCase = true)) {
displayName displayName
} else { } else {
"$displayName$extension" "$displayName$extension"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -142,16 +142,18 @@ abstract class ImageProvider {
val oldFile = File(sourcePath) val oldFile = File(sourcePath)
if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) { if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) {
val defaultExtension = oldFile.extension
oldFile.parent?.let { dir -> oldFile.parent?.let { dir ->
val resolution = resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = dir, dir = dir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
defaultExtension = defaultExtension,
conflictStrategy = NameConflictStrategy.RENAME, conflictStrategy = NameConflictStrategy.RENAME,
) )
resolution.nameWithoutExtension?.let { targetNameWithoutExtension -> resolution.nameWithoutExtension?.let { targetNameWithoutExtension ->
val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}"
val newFile = File(dir, targetFileName) val newFile = File(dir, targetFileName)
if (oldFile != newFile) { if (oldFile != newFile) {
newFields = renameSingle( newFields = renameSingle(
@ -277,11 +279,17 @@ abstract class ImageProvider {
val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId
desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}"
} }
// there is no benefit providing input extension
// for known output MIME type
val defaultExtension = null
val resolution = resolveTargetFileNameWithoutExtension( val resolution = resolveTargetFileNameWithoutExtension(
contextWrapper = activity, contextWrapper = activity,
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = exportMimeType, mimeType = exportMimeType,
defaultExtension = defaultExtension,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@ -358,6 +366,7 @@ abstract class ImageProvider {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
@ -465,6 +474,7 @@ abstract class ImageProvider {
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = captureMimeType, mimeType = captureMimeType,
defaultExtension = null,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) )
} catch (e: Exception) { } catch (e: Exception) {
@ -571,13 +581,14 @@ abstract class ImageProvider {
dir: String, dir: String,
desiredNameWithoutExtension: String, desiredNameWithoutExtension: String,
mimeType: String, mimeType: String,
defaultExtension: String?,
conflictStrategy: NameConflictStrategy, conflictStrategy: NameConflictStrategy,
): NameConflictResolution { ): NameConflictResolution {
val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension) val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension)
var resolvedName: String? = sanitizedNameWithoutExtension var resolvedName: String? = sanitizedNameWithoutExtension
var replacementFile: File? = null var replacementFile: File? = null
val extension = extensionFor(mimeType) val extension = extensionFor(mimeType, defaultExtension)
val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension") val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension")
when (conflictStrategy) { when (conflictStrategy) {
NameConflictStrategy.RENAME -> { NameConflictStrategy.RENAME -> {

View file

@ -557,6 +557,7 @@ class MediaStoreImageProvider : ImageProvider() {
toBin: Boolean, toBin: Boolean,
): FieldMap { ): FieldMap {
val sourcePath = sourceFile?.path val sourcePath = sourceFile?.path
val sourceExtension = sourceFile?.extension
val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) } val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) }
if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) { if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) {
// nothing to do unless it's a renamed copy // nothing to do unless it's a renamed copy
@ -569,6 +570,7 @@ class MediaStoreImageProvider : ImageProvider() {
dir = targetDir, dir = targetDir,
desiredNameWithoutExtension = desiredNameWithoutExtension, desiredNameWithoutExtension = desiredNameWithoutExtension,
mimeType = mimeType, mimeType = mimeType,
defaultExtension = sourceExtension,
conflictStrategy = nameConflictStrategy, conflictStrategy = nameConflictStrategy,
) )
val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap
@ -580,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = sourceExtension,
) { output: OutputStream -> ) { output: OutputStream ->
try { try {
sourceDocFile.copyTo(output) sourceDocFile.copyTo(output)
@ -615,12 +618,13 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String, targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit, write: (OutputStream) -> Unit,
): String { ): String {
if (StorageUtils.isInVault(activity, targetDir)) { if (StorageUtils.isInVault(activity, targetDir)) {
return insertByFile( return insertByFile(
targetDir = targetDir, targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write, write = write,
) )
} }
@ -630,7 +634,7 @@ class MediaStoreImageProvider : ImageProvider() {
return insertByMediaStore( return insertByMediaStore(
activity = activity, activity = activity,
targetDir = targetDir, targetDir = targetDir,
targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}",
write = write, write = write,
) )
} }
@ -642,6 +646,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
defaultExtension = defaultExtension,
write = write, write = write,
) )
} }
@ -700,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir: String, targetDir: String,
targetDirDocFile: DocumentFileCompat?, targetDirDocFile: DocumentFileCompat?,
targetNameWithoutExtension: String, targetNameWithoutExtension: String,
defaultExtension: String?,
write: (OutputStream) -> Unit, write: (OutputStream) -> Unit,
): String { ): String {
targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir")
@ -708,8 +714,22 @@ class MediaStoreImageProvider : ImageProvider() {
// but in order to open an output stream to it, we need to use a `SingleDocumentFile` // but in order to open an output stream to it, we need to use a `SingleDocumentFile`
// through a document URI, not a tree URI // through a document URI, not a tree URI
// note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first
val targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension)
val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
// providing a display name and a MIME type does not guarantee
// that the created document will be backed by a file with a valid media extension,
// but having an extension is essential for media detection by Android,
// so we retry with a display name that includes the extension
if ((targetDocFile.extension == null || targetDocFile.extension.isEmpty() || targetDocFile.extension == "bin") && defaultExtension != null) {
if (targetDocFile.exists()) {
targetDocFile.delete()
}
val extension = if (defaultExtension.startsWith(".")) defaultExtension else ".$defaultExtension"
targetTreeFile = targetDirDocFile.createFile(mimeType, "$targetNameWithoutExtension$extension")
targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri)
}
try { try {
targetDocFile.openOutputStream().use(write) targetDocFile.openOutputStream().use(write)

View file

@ -5,5 +5,5 @@ import kotlin.math.pow
object MathUtils { object MathUtils {
fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble()) fun highestPowerOf2(x: Int): Int = highestPowerOf2(x.toDouble())
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

@ -163,12 +163,24 @@ object MimeTypes {
// among other refs: // among other refs:
// - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types // - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types
fun extensionFor(mimeType: String): String? = when (mimeType) { fun extensionFor(mimeType: String, defaultExtension: String?): String = when (mimeType) {
AVI, AVI_VND -> ".avi" AVI, AVI_VND -> ".avi"
DNG, DNG_ADOBE -> ".dng"
HEIC, HEIF -> ".heif" HEIC, HEIF -> ".heif"
MP2T, MP2TS -> ".m2ts" MP2T, MP2TS -> ".m2ts"
PSD_VND, PSD_X -> ".psd" PSD_VND, PSD_X -> ".psd"
else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" } else -> {
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: defaultExtension
if (ext != null) {
// fallback to provided extension when available,
// typically the original file extension when moving/renaming
if (ext.startsWith(".")) ext else ".$ext"
} else {
// fallback to generic extensions,
// as incorrect file extensions are better than none for media detection
if (isVideo(mimeType)) ".mp4" else ".jpg"
}
}
} }
val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE) val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)

View file

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

View file

@ -22,7 +22,6 @@ import static androidx.exifinterface.media.ExifInterfaceUtilsFork.convertToLongA
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.copy;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.parseSubSeconds;
import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith; import static androidx.exifinterface.media.ExifInterfaceUtilsFork.startsWith;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN;
@ -91,7 +90,7 @@ import java.util.regex.Pattern;
import java.util.zip.CRC32; import java.util.zip.CRC32;
/* /*
* Forked from 'androidx.exifinterface:exifinterface:1.4.0' * Forked from 'androidx.exifinterface:exifinterface:1.4.1'
* Named differently to let ExifInterface be loaded as subdependency. * Named differently to let ExifInterface be loaded as subdependency.
* cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface * cf https://maven.google.com/web/index.html?q=exifinterface#androidx.exifinterface:exifinterface
* cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media * cf https://github.com/androidx/androidx/tree/androidx-main/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media
@ -139,6 +138,12 @@ public class ExifInterfaceFork {
// TLAD threshold for safer Exif attribute parsing // TLAD threshold for safer Exif attribute parsing
private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB private static final int ATTRIBUTE_SIZE_DANGER_THRESHOLD = 3 * (1 << 20); // MB
// TLAD available heap size, to check allocations
private long getAvailableHeapSize() {
final Runtime runtime = Runtime.getRuntime();
return runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory());
}
private static final String TAG = "ExifInterface"; private static final String TAG = "ExifInterface";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@ -4553,7 +4558,7 @@ public class ExifInterfaceFork {
&& (mXmpFromSeparateMarker != null || !containsTiff700Xmp)) && (mXmpFromSeparateMarker != null || !containsTiff700Xmp))
|| (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT || (xmpHandling == XMP_HANDLING_PREFER_TIFF_700_IF_PRESENT
&& !containsTiff700Xmp)) { && !containsTiff700Xmp)) {
mXmpFromSeparateMarker = ExifAttribute.createByte(value); mXmpFromSeparateMarker = value != null ? ExifAttribute.createByte(value) : null;
return; return;
} }
} }
@ -6558,8 +6563,9 @@ public class ExifInterfaceFork {
// Exif data in WebP images (e.g. // Exif data in WebP images (e.g.
// https://github.com/ImageMagick/ImageMagick/issues/3140) // https://github.com/ImageMagick/ImageMagick/issues/3140)
if (startsWith(payload, IDENTIFIER_EXIF_APP1)) { if (startsWith(payload, IDENTIFIER_EXIF_APP1)) {
payload = Arrays.copyOfRange(payload, IDENTIFIER_EXIF_APP1.length, payload =
payload.length); Arrays.copyOfRange(
payload, IDENTIFIER_EXIF_APP1.length, payload.length);
} }
// Save offset to EXIF data for handling thumbnail and attribute offsets. // Save offset to EXIF data for handling thumbnail and attribute offsets.
@ -6722,8 +6728,11 @@ public class ExifInterfaceFork {
copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length); copy(dataInputStream, dataOutputStream, PNG_SIGNATURE.length);
boolean needToWriteExif = true; boolean needToWriteExif = true;
boolean needToWriteXmp = mXmpFromSeparateMarker != null; // Either there's some XMP data to write, or it has been cleared locally but was present in
while (needToWriteExif || needToWriteXmp) { // the file when it was read (and so needs to be removed).
boolean needToHandleXmpChunk =
mXmpFromSeparateMarker != null || mFileOnDiskContainsSeparateXmpMarker;
while (needToWriteExif || needToHandleXmpChunk) {
int chunkLength = dataInputStream.readInt(); int chunkLength = dataInputStream.readInt();
int chunkType = dataInputStream.readInt(); int chunkType = dataInputStream.readInt();
if (chunkType == PNG_CHUNK_TYPE_IHDR) { if (chunkType == PNG_CHUNK_TYPE_IHDR) {
@ -6738,7 +6747,7 @@ public class ExifInterfaceFork {
} }
if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) { if (mXmpFromSeparateMarker != null && !mFileOnDiskContainsSeparateXmpMarker) {
writePngXmpItxtChunk(dataOutputStream); writePngXmpItxtChunk(dataOutputStream);
needToWriteXmp = false; needToHandleXmpChunk = false;
} }
continue; continue;
} else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) { } else if (chunkType == PNG_CHUNK_TYPE_EXIF && needToWriteExif) {
@ -6746,10 +6755,25 @@ public class ExifInterfaceFork {
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH);
needToWriteExif = false; needToWriteExif = false;
continue; continue;
} else if (chunkType == PNG_CHUNK_TYPE_ITXT && needToWriteXmp) { } else if (chunkType == PNG_CHUNK_TYPE_ITXT
writePngXmpItxtChunk(dataOutputStream); && chunkLength >= PNG_ITXT_XMP_KEYWORD.length) {
dataInputStream.skipFully(chunkLength + PNG_CHUNK_CRC_BYTE_LENGTH); // Read the 17 byte keyword and 5 expected null bytes.
needToWriteXmp = false; byte[] keyword = new byte[PNG_ITXT_XMP_KEYWORD.length];
dataInputStream.readFully(keyword);
int remainingChunkBytes = chunkLength - keyword.length + PNG_CHUNK_CRC_BYTE_LENGTH;
if (Arrays.equals(keyword, PNG_ITXT_XMP_KEYWORD)) {
if (mXmpFromSeparateMarker != null) {
writePngXmpItxtChunk(dataOutputStream);
}
dataInputStream.skipFully(remainingChunkBytes);
needToHandleXmpChunk = false;
} else {
// This is a non-XMP iTXt chunk, so just copy it to the output and continue.
dataOutputStream.writeInt(chunkLength);
dataOutputStream.writeInt(chunkType);
dataOutputStream.write(keyword);
copy(dataInputStream, dataOutputStream, remainingChunkBytes);
}
continue; continue;
} }
dataOutputStream.writeInt(chunkLength); dataOutputStream.writeInt(chunkLength);
@ -7536,6 +7560,13 @@ public class ExifInterfaceFork {
Log.d(TAG, "Invalid strip offset value"); Log.d(TAG, "Invalid strip offset value");
return; return;
} }
// TLAD start
if (bytesToSkip > getAvailableHeapSize()) {
throw new IOException("cannot allocate " + bytesToSkip + " bytes to skip to retrieve thumbnail");
}
// TLAD end
try { try {
in.skipFully(bytesToSkip); in.skipFully(bytesToSkip);
} catch (EOFException e) { } catch (EOFException e) {

View file

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

View file

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

View file

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

View file

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

View file

@ -599,7 +599,7 @@
"@settingsLanguagePageTitle": {}, "@settingsLanguagePageTitle": {},
"rootDirectoryDescription": "دليل الجذر", "rootDirectoryDescription": "دليل الجذر",
"@rootDirectoryDescription": {}, "@rootDirectoryDescription": {},
"viewDialogGroupSectionTitle": "مجموعة", "viewDialogGroupSectionTitle": "الأقسام",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"maxBrightnessAlways": "دائماً", "maxBrightnessAlways": "دائماً",
"@maxBrightnessAlways": {}, "@maxBrightnessAlways": {},
@ -1449,7 +1449,7 @@
"@binPageTitle": {}, "@binPageTitle": {},
"tagPlaceholderState": "الولاية", "tagPlaceholderState": "الولاية",
"@tagPlaceholderState": {}, "@tagPlaceholderState": {},
"sortByAlbumFileName": "حسب الألبوم واسم الملف", "sortByAlbumFileName": "حسب عنوان الألبوم والعنصر",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{هل تريد حذف هذه الألبومات والعنصر الموجود فيها؟} other{احذف هذه الألبومات و {count} العناصر فيها؟}}",
"@deleteMultiAlbumConfirmationDialogMessage": { "@deleteMultiAlbumConfirmationDialogMessage": {
@ -1599,8 +1599,28 @@
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "التنسيقات", "searchFormatSectionTitle": "التنسيقات",
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"chipActionGroup": "مجموعة", "chipActionGroup": "تغيير التجميع",
"@chipActionGroup": {}, "@chipActionGroup": {},
"createButtonLabel": "خلق", "createButtonLabel": "خلق",
"@createButtonLabel": {} "@createButtonLabel": {},
"sectionNone": "لا يوجد أقسام",
"@sectionNone": {},
"chipActionCreateGroup": "إنشاء مجموعة",
"@chipActionCreateGroup": {},
"albumTierGroups": "المجموعات",
"@albumTierGroups": {},
"newGroupDialogTitle": "مجموعة جديدة",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "اسم المجموعة",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "المجموعة موجودة بالفعل",
"@groupAlreadyExists": {},
"groupEmpty": "لا توجد مجموعات",
"@groupEmpty": {},
"ungrouped": "غير مجمعة",
"@ungrouped": {},
"groupPickerTitle": "اختر المجموعة",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "استخدم هذه المجموعة",
"@groupPickerUseThisGroupButton": {}
} }

View file

@ -142,5 +142,15 @@
"chipActionUnpin": "Sabitləməyin", "chipActionUnpin": "Sabitləməyin",
"@chipActionUnpin": {}, "@chipActionUnpin": {},
"chipActionRename": "Bir də adlandır", "chipActionRename": "Bir də adlandır",
"@chipActionRename": {} "@chipActionRename": {},
"chipActionDecompose": "Böl",
"@chipActionDecompose": {},
"chipActionCreateAlbum": "Albom yarat",
"@chipActionCreateAlbum": {},
"createButtonLabel": "YARAT",
"@createButtonLabel": {},
"chipActionGroup": "Qruplandırmanı dəyişdir",
"@chipActionGroup": {},
"chipActionCreateGroup": "Qrup yarat",
"@chipActionCreateGroup": {}
} }

View file

@ -1624,5 +1624,19 @@
"sortByPath": "Според пътя", "sortByPath": "Според пътя",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Формати", "searchFormatSectionTitle": "Формати",
"@searchFormatSectionTitle": {} "@searchFormatSectionTitle": {},
"chipActionCreateGroup": "Създайте група",
"@chipActionCreateGroup": {},
"chipActionGroup": "Групиране",
"@chipActionGroup": {},
"newGroupDialogTitle": "Нова Група",
"@newGroupDialogTitle": {},
"groupAlreadyExists": "Групата вече съществува",
"@groupAlreadyExists": {},
"albumTierGroups": "Групи",
"@albumTierGroups": {},
"groupPickerUseThisGroupButton": "Използвайте тази група",
"@groupPickerUseThisGroupButton": {},
"newGroupDialogNameLabel": "Име на групата",
"@newGroupDialogNameLabel": {}
} }

View file

@ -850,7 +850,7 @@
"@drawerCollectionRaws": {}, "@drawerCollectionRaws": {},
"sortByRating": "Efter bedømmelse", "sortByRating": "Efter bedømmelse",
"@sortByRating": {}, "@sortByRating": {},
"sortByAlbumFileName": "Efter album og filnavn", "sortByAlbumFileName": "Efter album og elementtitel",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"albumGroupVolume": "Efter lagervolume", "albumGroupVolume": "Efter lagervolume",
"@albumGroupVolume": {}, "@albumGroupVolume": {},
@ -1627,7 +1627,7 @@
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"createButtonLabel": "OPRET", "createButtonLabel": "OPRET",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Gruppér", "chipActionGroup": "Ændr gruppering",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Opret gruppe", "chipActionCreateGroup": "Opret gruppe",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -105,7 +105,7 @@
"chipActionLock": "Lock", "chipActionLock": "Lock",
"chipActionPin": "Pin to top", "chipActionPin": "Pin to top",
"chipActionUnpin": "Unpin from top", "chipActionUnpin": "Unpin from top",
"chipActionGroup": "Group", "chipActionGroup": "Change grouping",
"chipActionRename": "Rename", "chipActionRename": "Rename",
"chipActionSetCover": "Set cover", "chipActionSetCover": "Set cover",
"chipActionShowCountryStates": "Show states", "chipActionShowCountryStates": "Show states",
@ -767,7 +767,7 @@
"sortByName": "By name", "sortByName": "By name",
"sortByItemCount": "By item count", "sortByItemCount": "By item count",
"sortBySize": "By size", "sortBySize": "By size",
"sortByAlbumFileName": "By album & file name", "sortByAlbumFileName": "By album & item title",
"sortByRating": "By rating", "sortByRating": "By rating",
"sortByDuration": "By duration", "sortByDuration": "By duration",
"sortByPath": "By path", "sortByPath": "By path",

View file

@ -790,7 +790,7 @@
"@aboutLicensesDartPackagesSectionTitle": {}, "@aboutLicensesDartPackagesSectionTitle": {},
"aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse", "aboutLicensesShowAllButtonLabel": "Näita kõiki litsentse",
"@aboutLicensesShowAllButtonLabel": {}, "@aboutLicensesShowAllButtonLabel": {},
"policyPageTitle": "Privaatsuspoliitika", "policyPageTitle": "Andmekaitsepõhimõtted",
"@policyPageTitle": {}, "@policyPageTitle": {},
"collectionPageTitle": "Meediakogu", "collectionPageTitle": "Meediakogu",
"@collectionPageTitle": {}, "@collectionPageTitle": {},
@ -1036,7 +1036,7 @@
"@sortBySize": {}, "@sortBySize": {},
"sortByName": "Nime alusel", "sortByName": "Nime alusel",
"@sortByName": {}, "@sortByName": {},
"sortByAlbumFileName": "Albumi ja failinime alusel", "sortByAlbumFileName": "Albumi ja objekti nime alusel",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Hinnangu alusel", "sortByRating": "Hinnangu alusel",
"@sortByRating": {}, "@sortByRating": {},
@ -1645,7 +1645,7 @@
"@groupPickerUseThisGroupButton": {}, "@groupPickerUseThisGroupButton": {},
"createButtonLabel": "LOO", "createButtonLabel": "LOO",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Rühmita", "chipActionGroup": "Muuda grupeerimist",
"@chipActionGroup": {}, "@chipActionGroup": {},
"sectionNone": "Rubriike pole", "sectionNone": "Rubriike pole",
"@sectionNone": {} "@sectionNone": {}

View file

@ -637,7 +637,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "par taille", "sortBySize": "par taille",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "alphabétique", "sortByAlbumFileName": "par titre dalbum et élément",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "par notation", "sortByRating": "par notation",
"@sortByRating": {}, "@sortByRating": {},
@ -1407,7 +1407,7 @@
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Formats", "searchFormatSectionTitle": "Formats",
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"chipActionGroup": "Grouper", "chipActionGroup": "Modifier groupement",
"@chipActionGroup": {}, "@chipActionGroup": {},
"createButtonLabel": "CRÉER", "createButtonLabel": "CRÉER",
"@createButtonLabel": {}, "@createButtonLabel": {},

View file

@ -158,5 +158,49 @@
"chipActionCreateGroup": "צור קבוצה", "chipActionCreateGroup": "צור קבוצה",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},
"chipActionCreateVault": "צור כספת", "chipActionCreateVault": "צור כספת",
"@chipActionCreateVault": {} "@chipActionCreateVault": {},
"newGroupDialogTitle": "קבוצה חדשה",
"@newGroupDialogTitle": {},
"groupAlreadyExists": "הקבוצה כבר קיימת",
"@groupAlreadyExists": {},
"entryActionDelete": "מחיקה",
"@entryActionDelete": {},
"entryActionConvert": "המרה",
"@entryActionConvert": {},
"entryActionRotateCCW": "סובב נגד כיוון השעון",
"@entryActionRotateCCW": {},
"entryActionShare": "שיתוף",
"@entryActionShare": {},
"entryActionShareVideoOnly": "שיתוף וידיאו בלבד‍",
"@entryActionShareVideoOnly": {},
"videoActionSelectStreams": "בחר מסלולים",
"@videoActionSelectStreams": {},
"videoActionShowPreviousFrame": "הצג פריים קודם",
"@videoActionShowPreviousFrame": {},
"videoActionShowNextFrame": "הצג פריים הבא",
"@videoActionShowNextFrame": {},
"chipActionConfigureVault": "הגדרת כספת",
"@chipActionConfigureVault": {},
"entryActionCopyToClipboard": "הועתק ללוח",
"@entryActionCopyToClipboard": {},
"entryActionShareImageOnly": "שיתוף תמונה בלבד",
"@entryActionShareImageOnly": {},
"entryActionRotateCW": "סובב עם כיוון השעון",
"@entryActionRotateCW": {},
"entryActionFlip": "הפוך אופקית",
"@entryActionFlip": {},
"entryActionPrint": "הדפסה",
"@entryActionPrint": {},
"entryActionViewSource": "מקור וידאו",
"@entryActionViewSource": {},
"entryActionShowGeoTiffOnMap": "הצג כשכבת מפה",
"@entryActionShowGeoTiffOnMap": {},
"entryActionInfo": "מידע",
"@entryActionInfo": {},
"entryActionExport": "ייצוא",
"@entryActionExport": {},
"entryActionRename": "שינוי שם",
"@entryActionRename": {},
"entryActionRestore": "שחזור",
"@entryActionRestore": {}
} }

View file

@ -291,7 +291,7 @@
"@tileLayoutMosaic": {}, "@tileLayoutMosaic": {},
"tileLayoutGrid": "Rács", "tileLayoutGrid": "Rács",
"@tileLayoutGrid": {}, "@tileLayoutGrid": {},
"viewDialogGroupSectionTitle": "Csoport", "viewDialogGroupSectionTitle": "Szekciók",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"menuActionStats": "Statisztikák", "menuActionStats": "Statisztikák",
"@menuActionStats": {}, "@menuActionStats": {},
@ -1594,5 +1594,33 @@
"editEntryLocationDialogTimeShift": "Időeltolódás", "editEntryLocationDialogTimeShift": "Időeltolódás",
"@editEntryLocationDialogTimeShift": {}, "@editEntryLocationDialogTimeShift": {},
"removeEntryMetadataDialogAll": "Összes", "removeEntryMetadataDialogAll": "Összes",
"@removeEntryMetadataDialogAll": {} "@removeEntryMetadataDialogAll": {},
"sortByPath": "Útvonal szerint",
"@sortByPath": {},
"chipActionCreateGroup": "Csoport létrehozása",
"@chipActionCreateGroup": {},
"albumTierGroups": "Csoportok",
"@albumTierGroups": {},
"chipActionGroup": "Csoportosítás",
"@chipActionGroup": {},
"createButtonLabel": "LÉTREHOZÁS",
"@createButtonLabel": {},
"newGroupDialogTitle": "Új csoport",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "Csoport neve",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "Csoport már létezik",
"@groupAlreadyExists": {},
"groupEmpty": "Nincsenek csoportok",
"@groupEmpty": {},
"ungrouped": "Csoportosítatlan",
"@ungrouped": {},
"groupPickerTitle": "Válassza ki a csoportot",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Használja ezt a csoportot",
"@groupPickerUseThisGroupButton": {},
"searchFormatSectionTitle": "Formátumok",
"@searchFormatSectionTitle": {},
"sectionNone": "Semmi szerint",
"@sectionNone": {}
} }

View file

@ -453,7 +453,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Sortir", "viewDialogSortSectionTitle": "Sortir",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grup", "viewDialogGroupSectionTitle": "Bagian",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Tata letak", "viewDialogLayoutSectionTitle": "Tata letak",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -1406,5 +1406,29 @@
"sortByPath": "Melalui lokasi", "sortByPath": "Melalui lokasi",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Format", "searchFormatSectionTitle": "Format",
"@searchFormatSectionTitle": {} "@searchFormatSectionTitle": {},
"sectionNone": "Tidak ada bagian",
"@sectionNone": {},
"albumTierGroups": "Kelompok",
"@albumTierGroups": {},
"createButtonLabel": "BUAT",
"@createButtonLabel": {},
"chipActionGroup": "Kelompok",
"@chipActionGroup": {},
"chipActionCreateGroup": "Buat kelompok",
"@chipActionCreateGroup": {},
"ungrouped": "Tidak dikelompokkan",
"@ungrouped": {},
"newGroupDialogTitle": "Kelompok Baru",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "Nama kelompok",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "Kelompok sudah ada",
"@groupAlreadyExists": {},
"groupEmpty": "Tidak ada kelompok",
"@groupEmpty": {},
"groupPickerTitle": "Pilih Kelompok",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Gunakan kelompok ini",
"@groupPickerUseThisGroupButton": {}
} }

View file

@ -1346,7 +1346,7 @@
"@binPageTitle": {}, "@binPageTitle": {},
"tagPlaceholderState": "Hérað", "tagPlaceholderState": "Hérað",
"@tagPlaceholderState": {}, "@tagPlaceholderState": {},
"sortByAlbumFileName": "Eftir heiti albúma og skráa", "sortByAlbumFileName": "Eftir heiti albúma og atriða",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Eyða þessum albúmum og atriðinu í þeim?} other{Eyða þessum albúmum og {count} atriðum í þeim??}}", "deleteMultiAlbumConfirmationDialogMessage": "{count, plural, =1{Eyða þessum albúmum og atriðinu í þeim?} other{Eyða þessum albúmum og {count} atriðum í þeim??}}",
"@deleteMultiAlbumConfirmationDialogMessage": { "@deleteMultiAlbumConfirmationDialogMessage": {

View file

@ -317,7 +317,7 @@
"@binEntriesConfirmationDialogMessage": {}, "@binEntriesConfirmationDialogMessage": {},
"deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}", "deleteEntriesConfirmationDialogMessage": "{count, plural, =1{このアイテムを削除しますか?} other{{count} 件のアイテムを削除しますか?}}",
"@deleteEntriesConfirmationDialogMessage": {}, "@deleteEntriesConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogMessage": "いくつかのアイテムはメタデータ上に日付がありません。メタデータ上の日付が設定されない場合、この操作によりこれらの現在の日付はリセットされます", "moveUndatedConfirmationDialogMessage": "続行する前にアイテムの日付を保存しますか?",
"@moveUndatedConfirmationDialogMessage": {}, "@moveUndatedConfirmationDialogMessage": {},
"moveUndatedConfirmationDialogSetDate": "日付を設定", "moveUndatedConfirmationDialogSetDate": "日付を設定",
"@moveUndatedConfirmationDialogSetDate": {}, "@moveUndatedConfirmationDialogSetDate": {},

View file

@ -637,7 +637,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "크기", "sortBySize": "크기",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "이름", "sortByAlbumFileName": "앨범 및 항목 제목",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "별점", "sortByRating": "별점",
"@sortByRating": {}, "@sortByRating": {},
@ -1409,7 +1409,7 @@
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"chipActionCreateGroup": "그룹 만들기", "chipActionCreateGroup": "그룹 만들기",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},
"chipActionGroup": "그룹으로 이동", "chipActionGroup": "그룹 변경",
"@chipActionGroup": {}, "@chipActionGroup": {},
"albumTierGroups": "그룹", "albumTierGroups": "그룹",
"@albumTierGroups": {}, "@albumTierGroups": {},

View file

@ -1312,5 +1312,19 @@
"settingsVideoPlaybackTile": "ဖွင့်ကြည့်ခြင်း", "settingsVideoPlaybackTile": "ဖွင့်ကြည့်ခြင်း",
"@settingsVideoPlaybackTile": {}, "@settingsVideoPlaybackTile": {},
"chipActionShowCollection": "စုစည်းမှုထဲမှာ ပြရန်", "chipActionShowCollection": "စုစည်းမှုထဲမှာ ပြရန်",
"@chipActionShowCollection": {} "@chipActionShowCollection": {},
"chipActionDecompose": "ဖြတ်ထုတ်ရန်",
"@chipActionDecompose": {},
"chipActionGroup": "အုပ်စုဖွဲ့မည်",
"@chipActionGroup": {},
"stopTooltip": "ရပ်ရန်",
"@stopTooltip": {},
"createButtonLabel": "အသစ်ထည့်ရန်",
"@createButtonLabel": {},
"chipActionRemove": "ဖယ်ရှားမည်",
"@chipActionRemove": {},
"chipActionGoToExplorerPage": "Explorer ထဲတွင်ပြမည်",
"@chipActionGoToExplorerPage": {},
"chipActionCreateGroup": "အုပ်စုအသစ်ပြုလုပ်မည်",
"@chipActionCreateGroup": {}
} }

View file

@ -627,7 +627,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "Op grootte", "sortBySize": "Op grootte",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "Op album- en bestandsnaam", "sortByAlbumFileName": "Op album- en itemnaam",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Op waardering", "sortByRating": "Op waardering",
"@sortByRating": {}, "@sortByRating": {},
@ -1416,7 +1416,7 @@
"@groupPickerUseThisGroupButton": {}, "@groupPickerUseThisGroupButton": {},
"newGroupDialogTitle": "Nieuwe groep", "newGroupDialogTitle": "Nieuwe groep",
"@newGroupDialogTitle": {}, "@newGroupDialogTitle": {},
"chipActionGroup": "Groeperen", "chipActionGroup": "Groepering wijzigen",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Groep aanmaken", "chipActionCreateGroup": "Groep aanmaken",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -769,7 +769,7 @@
"@drawerCollectionPanoramas": {}, "@drawerCollectionPanoramas": {},
"drawerCollectionRaws": "Nieprzetworzone zdjęcia", "drawerCollectionRaws": "Nieprzetworzone zdjęcia",
"@drawerCollectionRaws": {}, "@drawerCollectionRaws": {},
"sortByAlbumFileName": "Według albumu i nazwy pliku", "sortByAlbumFileName": "Według albumu i nazwy elementu",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"albumMimeTypeMixed": "Mieszane", "albumMimeTypeMixed": "Mieszane",
"@albumMimeTypeMixed": {}, "@albumMimeTypeMixed": {},
@ -1603,7 +1603,7 @@
"@sectionNone": {}, "@sectionNone": {},
"createButtonLabel": "UTWÓRZ", "createButtonLabel": "UTWÓRZ",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Grupuj", "chipActionGroup": "Zmień grupowanie",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Utwórz grupę", "chipActionCreateGroup": "Utwórz grupę",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -463,7 +463,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Organizar", "viewDialogSortSectionTitle": "Organizar",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grupo", "viewDialogGroupSectionTitle": "Seções",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Layout", "viewDialogLayoutSectionTitle": "Layout",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -633,7 +633,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "Por tamanho", "sortBySize": "Por tamanho",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "Por álbum e nome de arquivo", "sortByAlbumFileName": "Por álbum e título do item",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Por classificação", "sortByRating": "Por classificação",
"@sortByRating": {}, "@sortByRating": {},
@ -1406,5 +1406,29 @@
"sortByPath": "Pelo caminho", "sortByPath": "Pelo caminho",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Formatos", "searchFormatSectionTitle": "Formatos",
"@searchFormatSectionTitle": {} "@searchFormatSectionTitle": {},
"createButtonLabel": "CRIAR",
"@createButtonLabel": {},
"chipActionGroup": "Alterar agrupamento",
"@chipActionGroup": {},
"chipActionCreateGroup": "Criar grupo",
"@chipActionCreateGroup": {},
"albumTierGroups": "Grupos",
"@albumTierGroups": {},
"newGroupDialogTitle": "Novo Grupo",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "Nome do grupo",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "O grupo já existe",
"@groupAlreadyExists": {},
"groupEmpty": "Nenhum grupo",
"@groupEmpty": {},
"ungrouped": "Desagrupado",
"@ungrouped": {},
"groupPickerTitle": "Selecionar Grupo",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Usar este grupo",
"@groupPickerUseThisGroupButton": {},
"sectionNone": "Nenhuma seção",
"@sectionNone": {}
} }

View file

@ -526,7 +526,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Sortează", "viewDialogSortSectionTitle": "Sortează",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grup", "viewDialogGroupSectionTitle": "Secțiuni",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Aspect", "viewDialogLayoutSectionTitle": "Aspect",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -887,7 +887,7 @@
"@drawerCollectionSphericalVideos": {}, "@drawerCollectionSphericalVideos": {},
"drawerAlbumPage": "Albume", "drawerAlbumPage": "Albume",
"@drawerAlbumPage": {}, "@drawerAlbumPage": {},
"sortByAlbumFileName": "După album și numele fișierului", "sortByAlbumFileName": "După album și numele elementului",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortOrderZtoA": "De la Z la A", "sortOrderZtoA": "De la Z la A",
"@sortOrderZtoA": {}, "@sortOrderZtoA": {},
@ -1598,5 +1598,29 @@
"sortByPath": "După cale", "sortByPath": "După cale",
"@sortByPath": {}, "@sortByPath": {},
"searchFormatSectionTitle": "Formate", "searchFormatSectionTitle": "Formate",
"@searchFormatSectionTitle": {} "@searchFormatSectionTitle": {},
"createButtonLabel": "CREARE",
"@createButtonLabel": {},
"chipActionCreateGroup": "Creați un grup",
"@chipActionCreateGroup": {},
"newGroupDialogTitle": "Grup nou",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "Nume grup",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "Grupul deja există",
"@groupAlreadyExists": {},
"chipActionGroup": "Grupe",
"@chipActionGroup": {},
"albumTierGroups": "Grupe",
"@albumTierGroups": {},
"groupPickerTitle": "Alege un grup",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Folosește acest grup",
"@groupPickerUseThisGroupButton": {},
"sectionNone": "Nicio secțiune",
"@sectionNone": {},
"ungrouped": "Fără grup",
"@ungrouped": {},
"groupEmpty": "Niciun grup",
"@groupEmpty": {}
} }

View file

@ -463,7 +463,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Сортировка", "viewDialogSortSectionTitle": "Сортировка",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Группировка", "viewDialogGroupSectionTitle": "Разделы",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Макет", "viewDialogLayoutSectionTitle": "Макет",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -633,7 +633,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "По размеру", "sortBySize": "По размеру",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "По имени альбома и файла", "sortByAlbumFileName": "По названию альбома и пункта",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "По рейтингу", "sortByRating": "По рейтингу",
"@sortByRating": {}, "@sortByRating": {},
@ -1406,5 +1406,29 @@
"searchFormatSectionTitle": "Форматы", "searchFormatSectionTitle": "Форматы",
"@searchFormatSectionTitle": {}, "@searchFormatSectionTitle": {},
"sortByPath": "По пути", "sortByPath": "По пути",
"@sortByPath": {} "@sortByPath": {},
"chipActionGroup": "Изменить группировку",
"@chipActionGroup": {},
"createButtonLabel": "СОЗДАТЬ",
"@createButtonLabel": {},
"chipActionCreateGroup": "Создать группу",
"@chipActionCreateGroup": {},
"albumTierGroups": "Группы",
"@albumTierGroups": {},
"newGroupDialogTitle": "Новая группа",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "Название группы",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "Группа уже существует",
"@groupAlreadyExists": {},
"groupEmpty": "Групп нету",
"@groupEmpty": {},
"ungrouped": "Без группировки",
"@ungrouped": {},
"groupPickerTitle": "Выбор группы",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Использовать эту группу",
"@groupPickerUseThisGroupButton": {},
"sectionNone": "Без разделов",
"@sectionNone": {}
} }

View file

@ -417,7 +417,7 @@
"@menuActionStats": {}, "@menuActionStats": {},
"viewDialogSortSectionTitle": "Sırala", "viewDialogSortSectionTitle": "Sırala",
"@viewDialogSortSectionTitle": {}, "@viewDialogSortSectionTitle": {},
"viewDialogGroupSectionTitle": "Grup", "viewDialogGroupSectionTitle": "Bölümler",
"@viewDialogGroupSectionTitle": {}, "@viewDialogGroupSectionTitle": {},
"viewDialogLayoutSectionTitle": "Düzen", "viewDialogLayoutSectionTitle": "Düzen",
"@viewDialogLayoutSectionTitle": {}, "@viewDialogLayoutSectionTitle": {},
@ -489,7 +489,7 @@
"@collectionActionHideTitleSearch": {}, "@collectionActionHideTitleSearch": {},
"collectionActionAddShortcut": "Kısayol ekle", "collectionActionAddShortcut": "Kısayol ekle",
"@collectionActionAddShortcut": {}, "@collectionActionAddShortcut": {},
"collectionActionEmptyBin": "Boş çöp kutusu", "collectionActionEmptyBin": "Çöp kutusu boş",
"@collectionActionEmptyBin": {}, "@collectionActionEmptyBin": {},
"collectionActionCopy": "Albüme kopyala", "collectionActionCopy": "Albüme kopyala",
"@collectionActionCopy": {}, "@collectionActionCopy": {},
@ -583,7 +583,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "Boyuta göre", "sortBySize": "Boyuta göre",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "Albüm ve dosya adına göre", "sortByAlbumFileName": "Albüm ve başlığı göre",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "Derecelendirmeye göre", "sortByRating": "Derecelendirmeye göre",
"@sortByRating": {}, "@sortByRating": {},
@ -1400,5 +1400,35 @@
"collectionActionAddDynamicAlbum": "Dinamik albüm ekle", "collectionActionAddDynamicAlbum": "Dinamik albüm ekle",
"@collectionActionAddDynamicAlbum": {}, "@collectionActionAddDynamicAlbum": {},
"searchFormatSectionTitle": "Biçimler", "searchFormatSectionTitle": "Biçimler",
"@searchFormatSectionTitle": {} "@searchFormatSectionTitle": {},
"createButtonLabel": "YARAT",
"@createButtonLabel": {},
"chipActionGroup": "Gruplandırmayı değiştir",
"@chipActionGroup": {},
"chipActionCreateGroup": "Grup oluştur",
"@chipActionCreateGroup": {},
"albumTierGroups": "Gruplar",
"@albumTierGroups": {},
"coordinateFormatDdm": "DDS",
"@coordinateFormatDdm": {},
"newGroupDialogTitle": "Yeni grup",
"@newGroupDialogTitle": {},
"newGroupDialogNameLabel": "Grup adı",
"@newGroupDialogNameLabel": {},
"groupAlreadyExists": "Grup zaten var",
"@groupAlreadyExists": {},
"groupEmpty": "Grup yok",
"@groupEmpty": {},
"ungrouped": "Gruplandırılmamış",
"@ungrouped": {},
"groupPickerTitle": "Grubu seç",
"@groupPickerTitle": {},
"groupPickerUseThisGroupButton": "Bu grubu kullan",
"@groupPickerUseThisGroupButton": {},
"sectionNone": "Bölüm yok",
"@sectionNone": {},
"sortByPath": "Yolu",
"@sortByPath": {},
"editEntryLocationDialogTimeShift": "Zaman farkı",
"@editEntryLocationDialogTimeShift": {}
} }

View file

@ -607,7 +607,7 @@
"@drawerCountryPage": {}, "@drawerCountryPage": {},
"sortByName": "За назвою", "sortByName": "За назвою",
"@sortByName": {}, "@sortByName": {},
"sortByAlbumFileName": "За назвою альбому та файлу", "sortByAlbumFileName": "За назвою альбому та елемента",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByItemCount": "За кількістю елементів", "sortByItemCount": "За кількістю елементів",
"@sortByItemCount": {}, "@sortByItemCount": {},
@ -1601,7 +1601,7 @@
"@sortByPath": {}, "@sortByPath": {},
"createButtonLabel": "СТВОРИТИ", "createButtonLabel": "СТВОРИТИ",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "Згрупувати", "chipActionGroup": "Змінити групування",
"@chipActionGroup": {}, "@chipActionGroup": {},
"chipActionCreateGroup": "Створити групу", "chipActionCreateGroup": "Створити групу",
"@chipActionCreateGroup": {}, "@chipActionCreateGroup": {},

View file

@ -625,7 +625,7 @@
"@sortByItemCount": {}, "@sortByItemCount": {},
"sortBySize": "按大小", "sortBySize": "按大小",
"@sortBySize": {}, "@sortBySize": {},
"sortByAlbumFileName": "按相册和文件名", "sortByAlbumFileName": "按相册和项目标题",
"@sortByAlbumFileName": {}, "@sortByAlbumFileName": {},
"sortByRating": "按评分", "sortByRating": "按评分",
"@sortByRating": {}, "@sortByRating": {},
@ -1419,7 +1419,7 @@
"@newGroupDialogTitle": {}, "@newGroupDialogTitle": {},
"createButtonLabel": "创建", "createButtonLabel": "创建",
"@createButtonLabel": {}, "@createButtonLabel": {},
"chipActionGroup": "分组", "chipActionGroup": "更改分组",
"@chipActionGroup": {}, "@chipActionGroup": {},
"groupAlreadyExists": "组已存在", "groupAlreadyExists": "组已存在",
"@groupAlreadyExists": {}, "@groupAlreadyExists": {},

View file

@ -140,10 +140,14 @@ class Contributors {
Contributor('Miquel Martí', 'miquelmarti111@gmail.com'), Contributor('Miquel Martí', 'miquelmarti111@gmail.com'),
Contributor('Yurt Page', 'yurtpage@gmail.com'), Contributor('Yurt Page', 'yurtpage@gmail.com'),
Contributor('Murcielago', 'weblate.j9bmx@slmail.me'), Contributor('Murcielago', 'weblate.j9bmx@slmail.me'),
Contributor('vm', 'varga.m007@gmail.com'),
Contributor('WMatheist', 'wmatheist@protonmail.com'),
// Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani // Contributor('Femini', 'nizamismidov4@gmail.com'), // Azerbaijani
// Contributor('Jamil Farajov', 'jamilfarajov@gmail.com'), // Azerbaijani
// Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali // Contributor('Alvi Khan', 'aveenalvi@gmail.com'), // Bengali
// Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese // Contributor('Htet Oo Hlaing', 'htetoh2006@outlook.com'), // Burmese
// Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese // Contributor('Khant', 'khant@users.noreply.hosted.weblate.org'), // Burmese
// Contributor('Thit Lwin', 'thitlwincoder@gmail.com'), // Burmese
// Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish // Contributor('Åzze', 'laitinen.jere222@gmail.com'), // Finnish
// Contributor('Olli', 'ollinen@ollit.dev'), // Finnish // Contributor('Olli', 'ollinen@ollit.dev'), // Finnish
// Contributor('Ricky Tigg', 'ricky.tigg@gmail.com'), // Finnish // Contributor('Ricky Tigg', 'ricky.tigg@gmail.com'), // Finnish

View file

@ -212,9 +212,9 @@ class Dependencies {
sourceUrl: 'https://github.com/fleaflet/flutter_map', sourceUrl: 'https://github.com/fleaflet/flutter_map',
), ),
Dependency( Dependency(
name: 'Flutter Markdown', name: 'Flutter Markdown Plus',
license: bsd3, license: bsd3,
sourceUrl: 'https://github.com/flutter/packages/tree/main/packages/flutter_markdown', sourceUrl: 'https://github.com/foresightmobile/flutter_markdown_plus',
), ),
Dependency( Dependency(
name: 'Flutter Staggered Animations', name: 'Flutter Staggered Animations',

View file

@ -25,7 +25,7 @@ final Covers covers = Covers._private();
typedef CoverProps = (int? entryId, String? packageName, Color? color); typedef CoverProps = (int? entryId, String? packageName, Color? color);
class Covers { class Covers {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final _lock = Lock(); final _lock = Lock();
final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast(); final StreamController<Set<CollectionFilter>?> _entryChangeStreamController = StreamController.broadcast();
@ -40,6 +40,8 @@ class Covers {
Set<CoverRow> _rows = {}; Set<CoverRow> _rows = {};
// do not subscribe to events from other modules in constructor
// so that modules can subscribe to each other
Covers._private(); Covers._private();
Future<void> init() async { Future<void> init() async {

View file

@ -15,19 +15,21 @@ import 'package:synchronized/synchronized.dart';
final DynamicAlbums dynamicAlbums = DynamicAlbums._private(); final DynamicAlbums dynamicAlbums = DynamicAlbums._private();
class DynamicAlbums with ChangeNotifier { class DynamicAlbums with ChangeNotifier {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final _lock = Lock(); final _lock = Lock();
Set<DynamicAlbumFilter> _rows = {}; Set<DynamicAlbumFilter> _rows = {};
final EventBus eventBus = EventBus(); final EventBus eventBus = EventBus();
// do not subscribe to events from other modules in constructor
// so that modules can subscribe to each other
DynamicAlbums._private() { DynamicAlbums._private() {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this); if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
_subscriptions.add(albumGrouping.eventBus.on<GroupUriChangedEvent>().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
} }
Future<void> init() async { Future<void> init() async {
_rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet(); _rows = (await localMediaDb.loadAllDynamicAlbums()).map((v) => DynamicAlbumFilter(v.name, v.filter)).toSet();
_subscriptions.add(albumGrouping.eventBus.on<GroupUriChangedEvent>().listen((e) => _onGroupUriChanged(e.oldGroupUri, e.newGroupUri)));
} }
int get count => _rows.length; int get count => _rows.length;
@ -57,6 +59,7 @@ class DynamicAlbums with ChangeNotifier {
await _lock.synchronized(() async { await _lock.synchronized(() async {
await _doRemove(filters.map((filter) => filter.name).toSet()); await _doRemove(filters.map((filter) => filter.name).toSet());
notifyListeners(); notifyListeners();
eventBus.fire(DynamicAlbumChangedEvent(Map.fromEntries(filters.map((v) => MapEntry(v, null)))));
}); });
} }
@ -81,13 +84,7 @@ class DynamicAlbums with ChangeNotifier {
}); });
} }
Future<void> clear() async { Future<void> clear() => remove(all);
await _lock.synchronized(() async {
await localMediaDb.clearDynamicAlbums();
_rows.clear();
notifyListeners();
});
}
DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name); DynamicAlbumFilter? get(String name) => _rows.firstWhereOrNull((row) => row.name == name);

View file

@ -4,8 +4,8 @@ import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/keys.dart'; import 'package:aves/model/entry/extensions/keys.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/media/geotiff.dart'; import 'package:aves/model/media/geotiff.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/media/video/metadata.dart'; import 'package:aves/model/media/video/metadata.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart';

View file

@ -1,7 +1,7 @@
import 'package:aves/model/filters/container/container.dart'; import 'package:aves/model/filters/container/container.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart'; import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -38,8 +38,6 @@ class SetAndFilter extends CollectionFilter with ContainerFilter {
static SetAndFilter? fromMap(Map<String, dynamic> json) { static SetAndFilter? fromMap(Map<String, dynamic> json) {
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet(); final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
if (filters.isEmpty) return null;
return SetAndFilter( return SetAndFilter(
filters, filters,
reversed: json['reversed'] ?? false, reversed: json['reversed'] ?? false,

View file

@ -38,8 +38,6 @@ class SetOrFilter extends CollectionFilter with ContainerFilter {
static SetOrFilter? fromMap(Map<String, dynamic> json) { static SetOrFilter? fromMap(Map<String, dynamic> json) {
final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet(); final filters = (json['filters'] as List).cast<String>().map(CollectionFilter.fromJson).nonNulls.toSet();
if (filters.isEmpty) return null;
return SetOrFilter( return SetOrFilter(
filters, filters,
reversed: json['reversed'] ?? false, reversed: json['reversed'] ?? false,

View file

@ -14,4 +14,3 @@ mixin CoveredFilter on CollectionFilter {
return super.color(context); return super.color(context);
} }
} }

View file

@ -2,9 +2,11 @@ import 'dart:convert';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/aspect_ratio.dart'; import 'package:aves/model/filters/aspect_ratio.dart';
import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/container/album_group.dart'; import 'package:aves/model/filters/container/album_group.dart';
import 'package:aves/model/filters/container/dynamic_album.dart'; import 'package:aves/model/filters/container/dynamic_album.dart';
import 'package:aves/model/filters/container/set_and.dart';
import 'package:aves/model/filters/container/set_or.dart';
import 'package:aves/model/filters/coordinate.dart';
import 'package:aves/model/filters/covered/location.dart'; import 'package:aves/model/filters/covered/location.dart';
import 'package:aves/model/filters/covered/stored_album.dart'; import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/covered/tag.dart'; import 'package:aves/model/filters/covered/tag.dart';
@ -17,8 +19,6 @@ import 'package:aves/model/filters/placeholder.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/rating.dart';
import 'package:aves/model/filters/recent.dart'; import 'package:aves/model/filters/recent.dart';
import 'package:aves/model/filters/container/set_and.dart';
import 'package:aves/model/filters/container/set_or.dart';
import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/trash.dart';
import 'package:aves/model/filters/type.dart'; import 'package:aves/model/filters/type.dart';
import 'package:aves/model/filters/weekday.dart'; import 'package:aves/model/filters/weekday.dart';

View file

@ -1,10 +1,17 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:aves/model/dynamic_albums.dart';
import 'package:aves/model/filters/container/album_group.dart'; import 'package:aves/model/filters/container/album_group.dart';
import 'package:aves/model/filters/container/dynamic_album.dart';
import 'package:aves/model/filters/container/group_base.dart'; import 'package:aves/model/filters/container/group_base.dart';
import 'package:aves/model/filters/container/set_or.dart'; import 'package:aves/model/filters/container/set_or.dart';
import 'package:aves/model/filters/covered/stored_album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/grouping/convert.dart'; import 'package:aves/model/grouping/convert.dart';
import 'package:aves/model/source/album.dart';
import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/events.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
import 'package:aves/utils/collection_utils.dart'; import 'package:aves/utils/collection_utils.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -28,18 +35,53 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
final String _host; final String _host;
final T Function(Uri uri, SetOrFilter filter) _createGroupFilter; final T Function(Uri uri, SetOrFilter filter) _createGroupFilter;
final Map<Uri, Set<Uri>> _groups = {}; final Map<Uri, Set<Uri>> _groups = {};
final Set<StreamSubscription> _subscriptions = {};
final Map<CollectionSource, Set<StreamSubscription>> _sourceSubscriptions = {};
CollectionSource? _source;
Map<Uri, Set<Uri>> get allGroups => Map.unmodifiable(_groups); Map<Uri, Set<Uri>> get allGroups => Map.unmodifiable(_groups);
// do not subscribe to events from other modules in constructor
// so that modules can subscribe to each other
FilterGrouping._private(this._host, this._createGroupFilter) { FilterGrouping._private(this._host, this._createGroupFilter) {
if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this); if (kFlutterMemoryAllocationsEnabled) ChangeNotifier.maybeDispatchObjectCreation(this);
} }
void init(Map<Uri, Set<Uri>> groups) { void init() {
_subscriptions.add(dynamicAlbums.eventBus.on<DynamicAlbumChangedEvent>().listen((e) => _clearObsoleteFilters()));
}
void setGroups(Map<Uri, Set<Uri>> groups) {
_groups.clear(); _groups.clear();
_groups.addAll(groups); _groups.addAll(groups);
} }
@override
void dispose() {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
_sourceSubscriptions.keys.toSet().forEach(unregisterSource);
super.dispose();
}
void registerSource(CollectionSource source) {
unregisterSource(_source);
final sourceEvents = source.eventBus;
_sourceSubscriptions[source] = {
sourceEvents.on<EntryMovedEvent>().listen((e) => _clearObsoleteFilters()),
sourceEvents.on<EntryRemovedEvent>().listen((e) => _clearObsoleteFilters()),
sourceEvents.on<AlbumsChangedEvent>().listen((e) => _clearObsoleteFilters()),
};
_source = source;
}
void unregisterSource(CollectionSource? source) {
_sourceSubscriptions.remove(source)
?..forEach((sub) => sub.cancel())
..clear();
}
void addToGroup(Set<Uri> childrenUris, Uri? destinationGroup) { void addToGroup(Set<Uri> childrenUris, Uri? destinationGroup) {
_removeFromGroups(childrenUris); _removeFromGroups(childrenUris);
if (destinationGroup != null) { if (destinationGroup != null) {
@ -73,9 +115,9 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
int countLeaves(Uri? groupUri) { int countLeaves(Uri? groupUri) {
int count = 0; int count = 0;
if (groupUri != null) { if (groupUri != null) {
final childrenUri = _groups[groupUri]; final childrenUris = _groups[groupUri];
if (childrenUri != null) { if (childrenUris != null) {
childrenUri.map(uriToFilter).nonNulls.forEach((filter) { childrenUris.map(uriToFilter).nonNulls.forEach((filter) {
if (filter is GroupBaseFilter) { if (filter is GroupBaseFilter) {
count += countLeaves(filter.uri); count += countLeaves(filter.uri);
} else { } else {
@ -93,15 +135,15 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
if (currentGroupUri == null) { if (currentGroupUri == null) {
return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) { return _groups.entries.where((kv) => getParentGroup(kv.key) == currentGroupUri).map((kv) {
final groupUri = kv.key; final groupUri = kv.key;
final childrenUri = kv.value; final childrenUris = kv.value;
final childrenFilters = childrenUri.map(uriToFilter).nonNulls.toSet(); final childrenFilters = childrenUris.map(uriToFilter).nonNulls.toSet();
return _createGroupFilter(groupUri, SetOrFilter(childrenFilters)); return _createGroupFilter(groupUri, SetOrFilter(childrenFilters));
}).toSet(); }).toSet();
} }
final childrenUri = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value; final childrenUris = _groups.entries.firstWhereOrNull((kv) => kv.key == currentGroupUri)?.value;
if (childrenUri != null) { if (childrenUris != null) {
return childrenUri.map(uriToFilter).nonNulls.toSet(); return childrenUris.map(uriToFilter).nonNulls.toSet();
} }
return {}; return {};
@ -172,6 +214,46 @@ class FilterGrouping<T extends GroupBaseFilter> with ChangeNotifier {
} }
} }
void _clearObsoleteFilters() {
final source = _source;
if (source == null || source.targetScope != CollectionSource.fullScope || !source.isReady) return;
_groups.entries.forEach((kv) {
final groupUri = kv.key;
final childrenUris = kv.value;
final rawAlbums = source.rawAlbums;
final allEntries = source.allEntries;
childrenUris.toSet().forEach((childUri) {
final filter = uriToFilter(childUri);
var valid = false;
if (filter != null) {
switch (filter) {
case GroupBaseFilter _:
valid = true;
case StoredAlbumFilter _:
// check album itself
final isVisibleAlbum = rawAlbums.contains(filter.album);
if (isVisibleAlbum) {
valid = true;
} else {
// check non-visible content (hidden, trash, etc.)
valid = allEntries.any(filter.test);
}
case DynamicAlbumFilter _:
valid = dynamicAlbums.contains(filter.name);
}
}
if (!valid) {
childrenUris.remove(childUri);
debugPrint('Removed obsolete childUri=$childUri from group=$groupUri');
}
});
});
_cleanEmptyGroups();
}
// group uri / filter conversion // group uri / filter conversion
static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey]; static String? getGroupPath(Uri? uri) => uri?.queryParameters[_groupPathParamKey];

View file

@ -46,7 +46,7 @@ import 'package:latlong2/latlong.dart';
final Settings settings = Settings._private(); final Settings settings = Settings._private();
class Settings with ChangeNotifier, SettingsAccess, SearchSettings, AppSettings, CollectionSettings, DebugSettings, DisplaySettings, FilterGridsSettings, InfoSettings, NavigationSettings, PrivacySettings, ScreenSaverSettings, SlideshowSettings, SubtitlesSettings, VideoSettings, ViewerSettings, WidgetSettings { class Settings with ChangeNotifier, SettingsAccess, SearchSettings, AppSettings, CollectionSettings, DebugSettings, DisplaySettings, FilterGridsSettings, InfoSettings, NavigationSettings, PrivacySettings, ScreenSaverSettings, SlideshowSettings, SubtitlesSettings, VideoSettings, ViewerSettings, WidgetSettings {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change'); final EventChannel _platformSettingsChangeChannel = const OptionalEventChannel('deckers.thibault/aves/settings_change');
final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast(); final StreamController<SettingsChangedEvent> _updateStreamController = StreamController.broadcast();
final StreamController<SettingsChangedEvent> _updateTileExtentStreamController = StreamController.broadcast(); final StreamController<SettingsChangedEvent> _updateTileExtentStreamController = StreamController.broadcast();

View file

@ -34,7 +34,7 @@ class CollectionLens with ChangeNotifier {
EntrySortFactor sortFactor; EntrySortFactor sortFactor;
bool sortReverse; bool sortReverse;
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier(); final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
int? id; int? id;
bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort; bool listenToSource, stackBursts, stackDevelopedRaws, fixedSort;
List<AvesEntry>? fixedSelection; List<AvesEntry>? fixedSelection;

View file

@ -60,7 +60,9 @@ class MediaStoreSource extends CollectionSource {
await localMediaDb.init(); await localMediaDb.init();
await vaults.init(); await vaults.init();
await favourites.init(); await favourites.init();
albumGrouping.init(settings.albumGroups); albumGrouping.init();
albumGrouping.setGroups(settings.albumGroups);
albumGrouping.registerSource(this);
await covers.init(); await covers.init();
await dynamicAlbums.init(); await dynamicAlbums.init();

View file

@ -15,7 +15,7 @@ import 'package:provider/provider.dart';
final Vaults vaults = Vaults._private(); final Vaults vaults = Vaults._private();
class Vaults extends ChangeNotifier { class Vaults extends ChangeNotifier {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
Set<VaultDetails> _rows = {}; Set<VaultDetails> _rows = {};
final Set<String> _unlockedDirPaths = {}; final Set<String> _unlockedDirPaths = {};

View file

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

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui;
import 'package:aves/model/app/support.dart'; import 'package:aves/model/app/support.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
@ -13,7 +14,6 @@ import 'package:equatable/equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:streams_channel/streams_channel.dart'; import 'package:streams_channel/streams_channel.dart';
import 'dart:ui' as ui;
abstract class MediaFetchService { abstract class MediaFetchService {
Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false}); Future<AvesEntry?> getEntry(String uri, String? mimeType, {bool allowUnsized = false});
@ -247,7 +247,7 @@ class PlatformMediaFetchService implements MediaFetchService {
return InteropDecoding.bytesToCodec(bytes); return InteropDecoding.bytesToCodec(bytes);
} }
} on PlatformException catch (e, stack) { } on PlatformException catch (e, stack) {
if (_isUnknownVisual(mimeType)) { if (_isUnknownVisual(mimeType) || e.code == 'getThumbnail-large') {
await reportService.recordError(e, stack); await reportService.recordError(e, stack);
} }
} }

View file

@ -25,7 +25,7 @@ abstract class MediaSessionService {
class PlatformMediaSessionService implements MediaSessionService, Disposable { class PlatformMediaSessionService implements MediaSessionService, Disposable {
static const _platformObject = MethodChannel('deckers.thibault/aves/media_session'); static const _platformObject = MethodChannel('deckers.thibault/aves/media_session');
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command'); final EventChannel _mediaCommandChannel = const OptionalEventChannel('deckers.thibault/aves/media_command');
final StreamController _streamController = StreamController.broadcast(); final StreamController _streamController = StreamController.broadcast();

View file

@ -74,20 +74,24 @@ class PlatformWindowService implements WindowService {
return false; return false;
} }
// cf https://developer.android.com/guide/topics/manifest/activity-element#screen
// cf Android `ActivityInfo.ScreenOrientation`
static const screenOrientationUnspecified = -1; // SCREEN_ORIENTATION_UNSPECIFIED
// use the `USER` variants rather than the `SENSOR` ones,
// so that it does not flip even if it is reversed by sensor
static const screenOrientationUserLandscape = 11; // SCREEN_ORIENTATION_USER_LANDSCAPE
static const screenOrientationUserPortrait = 12; // SCREEN_ORIENTATION_USER_PORTRAIT
@override @override
Future<void> requestOrientation([Orientation? orientation]) async { Future<void> requestOrientation([Orientation? orientation]) async {
// cf Android `ActivityInfo.ScreenOrientation`
late final int orientationCode; late final int orientationCode;
switch (orientation) { switch (orientation) {
case Orientation.landscape: case Orientation.landscape:
// SCREEN_ORIENTATION_SENSOR_LANDSCAPE orientationCode = screenOrientationUserLandscape;
orientationCode = 6;
case Orientation.portrait: case Orientation.portrait:
// SCREEN_ORIENTATION_SENSOR_PORTRAIT orientationCode = screenOrientationUserPortrait;
orientationCode = 7;
default: default:
// SCREEN_ORIENTATION_UNSPECIFIED orientationCode = screenOrientationUnspecified;
orientationCode = -1;
} }
try { try {
await _platform.invokeMethod('requestOrientation', <String, dynamic>{ await _platform.invokeMethod('requestOrientation', <String, dynamic>{

View file

@ -1,4 +1,3 @@
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
class AStyles { class AStyles {

View file

@ -160,7 +160,7 @@ class AvesApp extends StatefulWidget {
} }
class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver { class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late final Future<void> _appSetup; late final Future<void> _appSetup;
late final Future<bool> _shouldUseBoldFontLoader; late final Future<bool> _shouldUseBoldFontLoader;
final TvRailController _tvRailController = TvRailController(); final TvRailController _tvRailController = TvRailController();

View file

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

View file

@ -52,7 +52,7 @@ class CollectionPage extends StatefulWidget {
} }
class _CollectionPageState extends State<CollectionPage> { class _CollectionPageState extends State<CollectionPage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late CollectionLens _collection; late CollectionLens _collection;
final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast(); final StreamController<DraggableScrollbarEvent> _draggableScrollBarEventStreamController = StreamController.broadcast();

View file

@ -10,8 +10,8 @@ import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/container/dynamic_album.dart'; import 'package:aves/model/filters/container/dynamic_album.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/container/set_and.dart'; import 'package:aves/model/filters/container/set_and.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/grouping/common.dart'; import 'package:aves/model/grouping/common.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
import 'package:aves/model/metadata/date_modifier.dart'; import 'package:aves/model/metadata/date_modifier.dart';

View file

@ -9,8 +9,8 @@ import 'package:provider/provider.dart';
class FilterBar extends StatefulWidget { class FilterBar extends StatefulWidget {
static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4); static const EdgeInsets chipPadding = EdgeInsets.symmetric(horizontal: 4);
static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4); static const EdgeInsets rowPadding = EdgeInsets.symmetric(horizontal: 4);
static const double verticalPadding = 16; static const EdgeInsets padding = EdgeInsets.only(top: 4, bottom: 8);
static const double preferredHeight = AvesFilterChip.minChipHeight + verticalPadding; static final double preferredHeight = AvesFilterChip.minChipHeight + padding.vertical;
final List<CollectionFilter> filters; final List<CollectionFilter> filters;
final bool interactive; final bool interactive;
@ -84,6 +84,7 @@ class _FilterBarState extends State<FilterBar> {
return Container( return Container(
// specify transparent as a workaround to prevent // specify transparent as a workaround to prevent
// chip border clipping when the floating app bar is fading // chip border clipping when the floating app bar is fading
padding: FilterBar.padding,
color: Colors.transparent, color: Colors.transparent,
height: FilterBar.preferredHeight, height: FilterBar.preferredHeight,
child: AnimatedList( child: AnimatedList(

View file

@ -45,7 +45,7 @@ class MenuQuickChooser<T> extends StatefulWidget {
} }
class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> { class _MenuQuickChooserState<T> extends State<MenuQuickChooser<T>> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero); final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
int _scrollDirection = 0; int _scrollDirection = 0;

View file

@ -23,7 +23,7 @@ class RateQuickChooser extends StatefulWidget {
} }
class _RateQuickChooserState extends State<RateQuickChooser> { class _RateQuickChooserState extends State<RateQuickChooser> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
ValueNotifier<int?> get valueNotifier => widget.valueNotifier; ValueNotifier<int?> get valueNotifier => widget.valueNotifier;

View file

@ -28,7 +28,7 @@ class PlayToggler extends StatefulWidget {
} }
class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStateMixin { class _PlayTogglerState extends State<PlayToggler> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late AnimationController _playPauseAnimation; late AnimationController _playPauseAnimation;
AvesVideoController? get controller => widget.controller; AvesVideoController? get controller => widget.controller;

View file

@ -1,4 +1,3 @@
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/model/source/events.dart'; import 'package:aves/model/source/events.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';

View file

@ -8,6 +8,8 @@ class CrumbLine<T> extends StatefulWidget {
final T Function(BuildContext context, int index) combine; final T Function(BuildContext context, int index) combine;
final void Function(T combined) onTap; final void Function(T combined) onTap;
static const EdgeInsets padding = EdgeInsets.only(top: 6, bottom: 20);
const CrumbLine({ const CrumbLine({
super.key, super.key,
required this.split, required this.split,
@ -18,7 +20,7 @@ class CrumbLine<T> extends StatefulWidget {
@override @override
State<CrumbLine<T>> createState() => _CrumbLineState<T>(); State<CrumbLine<T>> createState() => _CrumbLineState<T>();
static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(kToolbarHeight); static double getPreferredHeight(TextScaler textScaler) => textScaler.scale(22) + padding.vertical;
} }
class _CrumbLineState<T> extends State<CrumbLine<T>> { class _CrumbLineState<T> extends State<CrumbLine<T>> {

View file

@ -522,7 +522,7 @@ class _InkResponseStateWidget extends StatefulWidget {
if (onSecondaryTap != null) 'secondary tap', if (onSecondaryTap != null) 'secondary tap',
if (onSecondaryTapUp != null) 'secondary tap up', if (onSecondaryTapUp != null) 'secondary tap up',
if (onSecondaryTapDown != null) 'secondary tap down', if (onSecondaryTapDown != null) 'secondary tap down',
if (onSecondaryTapCancel != null) 'secondary tap cancel' if (onSecondaryTapCancel != null) 'secondary tap cancel',
]; ];
properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>')); properties.add(IterableProperty<String>('gestures', gestures, ifEmpty: '<none>'));
properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor)); properties.add(DiagnosticsProperty<MouseCursor>('mouseCursor', mouseCursor));
@ -544,10 +544,7 @@ enum _HighlightType {
focus, focus,
} }
class _InkResponseState extends State<_InkResponseStateWidget> class _InkResponseState extends State<_InkResponseStateWidget> with AutomaticKeepAliveClientMixin<_InkResponseStateWidget> implements _ParentInkResponseState {
with AutomaticKeepAliveClientMixin<_InkResponseStateWidget>
implements _ParentInkResponseState
{
Set<InteractiveInkFeature>? _splashes; Set<InteractiveInkFeature>? _splashes;
InteractiveInkFeature? _currentSplash; InteractiveInkFeature? _currentSplash;
bool _hovering = false; bool _hovering = false;
@ -578,6 +575,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed); widget.parentState?.markChildInkResponsePressed(this, nowAnyPressed);
} }
} }
bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty; bool get _anyChildInkResponsePressed => _activeChildren.isNotEmpty;
void activateOnIntent(Intent? intent) { void activateOnIntent(Intent? intent) {
@ -611,7 +609,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
void handleStatesControllerChange() { void handleStatesControllerChange() {
// Force a rebuild to resolve widget.overlayColor, widget.mouseCursor // Force a rebuild to resolve widget.overlayColor, widget.mouseCursor
setState(() { }); setState(() {});
} }
WidgetStatesController get statesController => widget.statesController ?? internalStatesController!; WidgetStatesController get statesController => widget.statesController ?? internalStatesController!;
@ -642,9 +640,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
initStatesController(); initStatesController();
} }
if (widget.radius != oldWidget.radius || if (widget.radius != oldWidget.radius || widget.highlightShape != oldWidget.highlightShape || widget.borderRadius != oldWidget.borderRadius) {
widget.highlightShape != oldWidget.highlightShape ||
widget.borderRadius != oldWidget.borderRadius) {
final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover]; final InkHighlight? hoverHighlight = _highlights[_HighlightType.hover];
if (hoverHighlight != null) { if (hoverHighlight != null) {
hoverHighlight.dispose(); hoverHighlight.dispose();
@ -701,7 +697,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
} }
void updateHighlight(_HighlightType type, { required bool value, bool callOnHover = true }) { void updateHighlight(_HighlightType type, {required bool value, bool callOnHover = true}) {
final InkHighlight? highlight = _highlights[type]; final InkHighlight? highlight = _highlights[type];
void handleInkRemoval() { void handleInkRemoval() {
assert(_highlights[type] != null); assert(_highlights[type] != null);
@ -717,7 +713,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
statesController.update(WidgetState.hovered, value); statesController.update(WidgetState.hovered, value);
} }
case _HighlightType.focus: case _HighlightType.focus:
// see handleFocusUpdate() // see handleFocusUpdate()
break; break;
} }
@ -730,9 +726,9 @@ class _InkResponseState extends State<_InkResponseStateWidget>
if (value) { if (value) {
if (highlight == null) { if (highlight == null) {
final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value) final Color resolvedOverlayColor = widget.overlayColor?.resolve(statesController.value) ??
?? switch (type) { switch (type) {
// Use the backwards compatible defaults // Use the backwards compatible defaults
_HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor, _HighlightType.pressed => widget.highlightColor ?? Theme.of(context).highlightColor,
_HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor, _HighlightType.focus => widget.focusColor ?? Theme.of(context).focusColor,
_HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor, _HighlightType.hover => widget.hoverColor ?? Theme.of(context).hoverColor,
@ -789,7 +785,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
final MaterialInkController inkController = Material.of(context); final MaterialInkController inkController = Material.of(context);
final RenderBox referenceBox = context.findRenderObject()! as RenderBox; final RenderBox referenceBox = context.findRenderObject()! as RenderBox;
final Offset position = referenceBox.globalToLocal(globalPosition); final Offset position = referenceBox.globalToLocal(globalPosition);
final Color color = widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor; final Color color = widget.overlayColor?.resolve(statesController.value) ?? widget.splashColor ?? Theme.of(context).splashColor;
final RectCallback? rectCallback = widget.containedInkWell ? widget.getRectCallback!(referenceBox) : null; final RectCallback? rectCallback = widget.containedInkWell ? widget.getRectCallback!(referenceBox) : null;
final BorderRadius? borderRadius = widget.borderRadius; final BorderRadius? borderRadius = widget.borderRadius;
final ShapeBorder? customBorder = widget.customBorder; final ShapeBorder? customBorder = widget.customBorder;
@ -846,6 +842,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
bool _hasFocus = false; bool _hasFocus = false;
void handleFocusUpdate(bool hasFocus) { void handleFocusUpdate(bool hasFocus) {
_hasFocus = hasFocus; _hasFocus = hasFocus;
// Set here rather than updateHighlight because this widget's // Set here rather than updateHighlight because this widget's
@ -978,21 +975,17 @@ class _InkResponseState extends State<_InkResponseStateWidget>
} }
bool _primaryButtonEnabled(_InkResponseStateWidget widget) { bool _primaryButtonEnabled(_InkResponseStateWidget widget) {
return widget.onTap != null return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null || widget.onTapUp != null || widget.onTapDown != null;
|| widget.onDoubleTap != null
|| widget.onLongPress != null
|| widget.onTapUp != null
|| widget.onTapDown != null;
} }
bool _secondaryButtonEnabled(_InkResponseStateWidget widget) { bool _secondaryButtonEnabled(_InkResponseStateWidget widget) {
return widget.onSecondaryTap != null return widget.onSecondaryTap != null || widget.onSecondaryTapUp != null || widget.onSecondaryTapDown != null;
|| widget.onSecondaryTapUp != null
|| widget.onSecondaryTapDown != null;
} }
bool get enabled => isWidgetEnabled(widget); bool get enabled => isWidgetEnabled(widget);
bool get _primaryEnabled => _primaryButtonEnabled(widget); bool get _primaryEnabled => _primaryButtonEnabled(widget);
bool get _secondaryEnabled => _secondaryButtonEnabled(widget); bool get _secondaryEnabled => _secondaryButtonEnabled(widget);
void handleMouseEnter(PointerEnterEvent event) { void handleMouseEnter(PointerEnterEvent event) {
@ -1032,14 +1025,15 @@ class _InkResponseState extends State<_InkResponseStateWidget>
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
return switch (type) { return switch (type) {
// The pressed state triggers a ripple (ink splash), per the current // The pressed state triggers a ripple (ink splash), per the current
// Material Design spec. A separate highlight is no longer used. // Material Design spec. A separate highlight is no longer used.
// See https://material.io/design/interaction/states.html#pressed // See https://material.io/design/interaction/states.html#pressed
_HighlightType.pressed => widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor, _HighlightType.pressed => widget.overlayColor?.resolve(pressed) ?? widget.highlightColor ?? theme.highlightColor,
_HighlightType.focus => widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor, _HighlightType.focus => widget.overlayColor?.resolve(focused) ?? widget.focusColor ?? theme.focusColor,
_HighlightType.hover => widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor, _HighlightType.hover => widget.overlayColor?.resolve(hovered) ?? widget.hoverColor ?? theme.hoverColor,
}; };
} }
for (final _HighlightType type in _highlights.keys) { for (final _HighlightType type in _highlights.keys) {
_highlights[type]?.color = getHighlightColorForType(type); _highlights[type]?.color = getHighlightColorForType(type);
} }
@ -1077,7 +1071,7 @@ class _InkResponseState extends State<_InkResponseStateWidget>
onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null, onDoubleTap: widget.onDoubleTap != null ? handleDoubleTap : null,
onLongPress: widget.onLongPress != null ? handleLongPress : null, onLongPress: widget.onLongPress != null ? handleLongPress : null,
onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null, onSecondaryTapDown: _secondaryEnabled ? handleSecondaryTapDown : null,
onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp: null, onSecondaryTapUp: _secondaryEnabled ? handleSecondaryTapUp : null,
onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null, onSecondaryTap: _secondaryEnabled ? handleSecondaryTap : null,
onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null, onSecondaryTapCancel: _secondaryEnabled ? handleSecondaryTapCancel : null,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,

View file

@ -3,7 +3,7 @@ import 'package:aves/theme/themes.dart';
import 'package:aves/widgets/aves_app.dart'; import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/borders.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
class MarkdownContainer extends StatelessWidget { class MarkdownContainer extends StatelessWidget {
final String data; final String data;

View file

@ -20,7 +20,8 @@ class BlurredRect extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRect( return ClipRect(
child: BackdropFilter.grouped( // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
child: BackdropFilter(
// do not modify tree when disabling filter // do not modify tree when disabling filter
filter: enabled ? _filter : _identity, filter: enabled ? _filter : _identity,
child: child, child: child,
@ -59,7 +60,8 @@ class BlurredRRect extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: borderRadius ?? BorderRadius.zero, borderRadius: borderRadius ?? BorderRadius.zero,
child: BackdropFilter.grouped( // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
child: BackdropFilter(
// do not modify tree when disabling filter // do not modify tree when disabling filter
filter: enabled ? _filter : _identity, filter: enabled ? _filter : _identity,
child: child, child: child,
@ -81,7 +83,8 @@ class BlurredOval extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipOval( return ClipOval(
child: BackdropFilter.grouped( // TODO TLAD [flutter vNext] use `BackdropFilter.grouped`
child: BackdropFilter(
// do not modify tree when disabling filter // do not modify tree when disabling filter
filter: enabled ? _filter : _identity, filter: enabled ? _filter : _identity,
child: child, child: child,

View file

@ -44,7 +44,7 @@ class _GridItemTrackerState<T> extends State<GridItemTracker<T>> with WidgetsBin
return (scrollableContext.findRenderObject() as RenderBox).size; return (scrollableContext.findRenderObject() as RenderBox).size;
} }
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
// grid section metrics before the app is laid out with the new orientation // grid section metrics before the app is laid out with the new orientation
late SectionedListLayout<T> _lastSectionedListLayout; late SectionedListLayout<T> _lastSectionedListLayout;

View file

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

View file

@ -150,7 +150,7 @@ class AvesFilterChip extends StatefulWidget {
} }
class _AvesFilterChipState extends State<AvesFilterChip> { class _AvesFilterChipState extends State<AvesFilterChip> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late Future<Color> _colorFuture; late Future<Color> _colorFuture;
late Color _outlineColor; late Color _outlineColor;
late bool _tapped; late bool _tapped;

View file

@ -5,7 +5,7 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class Attribution extends StatelessWidget { class Attribution extends StatelessWidget {

View file

@ -83,7 +83,7 @@ class GeoMap extends StatefulWidget {
} }
class _GeoMapState extends State<GeoMap> { class _GeoMapState extends State<GeoMap> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
// as of google_maps_flutter v2.0.6, Google map initialization is blocking // as of google_maps_flutter v2.0.6, Google map initialization is blocking
// cf https://github.com/flutter/flutter/issues/28493 // cf https://github.com/flutter/flutter/issues/28493
@ -249,14 +249,13 @@ class _GeoMapState extends State<GeoMap> {
child = Column( child = Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
BackdropGroup( // TODO TLAD [flutter vNext] wrap into `BackdropGroup`
child: mapHeight != null mapHeight != null
? SizedBox( ? SizedBox(
height: mapHeight, height: mapHeight,
child: child, child: child,
) )
: Expanded(child: child), : Expanded(child: child),
),
SafeArea( SafeArea(
top: false, top: false,
bottom: false, bottom: false,

View file

@ -66,7 +66,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin { class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin {
final MapController _leafletMapController = MapController(); final MapController _leafletMapController = MapController();
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {}; Map<MarkerKey<T>, GeoEntry<T>> _geoEntryByMarkerKey = {};
final Debouncer _debouncer = Debouncer(delay: ADurations.mapIdleDebounceDelay); final Debouncer _debouncer = Debouncer(delay: ADurations.mapIdleDebounceDelay);

View file

@ -43,10 +43,12 @@ abstract class AvesSearchDelegate extends SearchDelegate {
final animate = context.read<Settings>().animate; final animate = context.read<Settings>().animate;
return canPop return canPop
? IconButton( ? IconButton(
icon: animate ? AnimatedIcon( icon: animate
icon: AnimatedIcons.menu_arrow, ? AnimatedIcon(
progress: transitionAnimation, icon: AnimatedIcons.menu_arrow,
): const Icon(Icons.arrow_back), progress: transitionAnimation,
)
: const Icon(Icons.arrow_back),
onPressed: () => goBack(context), onPressed: () => goBack(context),
tooltip: MaterialLocalizations.of(context).backButtonTooltip, tooltip: MaterialLocalizations.of(context).backButtonTooltip,
) )

View file

@ -15,7 +15,7 @@ class TileExtentController {
late double userPreferredExtent; late double userPreferredExtent;
Size _viewportSize = Size.zero; Size _viewportSize = Size.zero;
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
Size get viewportSize => _viewportSize; Size get viewportSize => _viewportSize;

View file

@ -56,7 +56,7 @@ class EditEntryLocationDialog extends StatefulWidget {
} }
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> with FeedbackMixin { class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> with FeedbackMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
LocationEditAction _action = LocationEditAction.chooseOnMap; LocationEditAction _action = LocationEditAction.chooseOnMap;
LatLng? _mapCoordinates; LatLng? _mapCoordinates;
late final AvesEntry mainEntry; late final AvesEntry mainEntry;

View file

@ -63,7 +63,7 @@ class _Content extends StatefulWidget {
} }
class _ContentState extends State<_Content> with SingleTickerProviderStateMixin { class _ContentState extends State<_Content> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final AvesMapController _mapController = AvesMapController(); final AvesMapController _mapController = AvesMapController();
late final ValueNotifier<bool> _isPageAnimatingNotifier; late final ValueNotifier<bool> _isPageAnimatingNotifier;
final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null), _infoLocationNotifier = ValueNotifier(null); final ValueNotifier<LatLng?> _dotLocationNotifier = ValueNotifier(null), _infoLocationNotifier = ValueNotifier(null);

View file

@ -28,7 +28,7 @@ class ImageEditorPage extends StatefulWidget {
} }
class _ImageEditorPageState extends State<ImageEditorPage> { class _ImageEditorPageState extends State<ImageEditorPage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null); final ValueNotifier<EditorAction?> _actionNotifier = ValueNotifier(null);
final ValueNotifier<EdgeInsets> _marginNotifier = ValueNotifier(EdgeInsets.zero); final ValueNotifier<EdgeInsets> _marginNotifier = ValueNotifier(EdgeInsets.zero);
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero); final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier<ViewState>(ViewState.zero);
@ -118,7 +118,7 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
} }
void _onActionChanged() { void _onActionChanged() {
switch(_actionNotifier.value) { switch (_actionNotifier.value) {
case EditorAction.transform: case EditorAction.transform:
_transformController.reset(); _transformController.reset();
_marginNotifier.value = Cropper.imageMargin; _marginNotifier.value = Cropper.imageMargin;

View file

@ -36,7 +36,7 @@ class EditorImage extends StatefulWidget {
} }
class _EditorImageState extends State<EditorImage> { class _EditorImageState extends State<EditorImage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<double> _scrimOpacityNotifier = ValueNotifier(0); final ValueNotifier<double> _scrimOpacityNotifier = ValueNotifier(0);
AvesEntry get entry => widget.entry; AvesEntry get entry => widget.entry;

View file

@ -38,7 +38,7 @@ class Cropper extends StatefulWidget {
} }
class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin { class _CropperState extends State<Cropper> with SingleTickerProviderStateMixin {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero); final ValueNotifier<Rect> _outlineNotifier = ValueNotifier(Rect.zero);
final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0); final ValueNotifier<int> _gridDivisionNotifier = ValueNotifier(0);
late AnimationController _gridAnimationController; late AnimationController _gridAnimationController;

View file

@ -65,7 +65,8 @@ class _ExplorerAppBarState extends State<ExplorerAppBar> with WidgetsBindingObse
actions: _buildActions, actions: _buildActions,
bottom: LayoutBuilder( bottom: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SizedBox( return Container(
padding: CrumbLine.padding,
width: constraints.maxWidth, width: constraints.maxWidth,
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)), height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
child: ValueListenableBuilder<VolumeRelativeDirectory?>( child: ValueListenableBuilder<VolumeRelativeDirectory?>(

View file

@ -40,7 +40,7 @@ class ExplorerPage extends StatefulWidget {
} }
class _ExplorerPageState extends State<ExplorerPage> { class _ExplorerPageState extends State<ExplorerPage> {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
final ValueNotifier<VolumeRelativeDirectory?> _directory = ValueNotifier(null); final ValueNotifier<VolumeRelativeDirectory?> _directory = ValueNotifier(null);
final ValueNotifier<VolumeRelativeDirectory?> _contentsDirectory = ValueNotifier(null); final ValueNotifier<VolumeRelativeDirectory?> _contentsDirectory = ValueNotifier(null);
final ValueNotifier<List<Directory>> _contents = ValueNotifier([]); final ValueNotifier<List<Directory>> _contents = ValueNotifier([]);

View file

@ -44,12 +44,12 @@ class AlbumListPage extends StatelessWidget {
child: Builder( child: Builder(
// to access filter group provider from subtree context // to access filter group provider from subtree context
builder: (context) { builder: (context) {
return Selector<Settings, (AlbumChipSectionFactor, ChipSortFactor, bool, Set<CollectionFilter>)>( return Selector<Settings, (AlbumChipSectionFactor, ChipSortFactor, bool, Set<CollectionFilter>, Set<CollectionFilter>)>(
selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.pinnedFilters), selector: (context, s) => (s.albumSectionFactor, s.albumSortFactor, s.albumSortReverse, s.hiddenFilters, s.pinnedFilters),
shouldRebuild: (t1, t2) { shouldRebuild: (t1, t2) {
// `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within records // `Selector` by default uses `DeepCollectionEquality`, which does not go deep in collections within records
const eq = DeepCollectionEquality(); const eq = DeepCollectionEquality();
return !(eq.equals(t1.$1, t2.$1) && eq.equals(t1.$2, t2.$2) && eq.equals(t1.$3, t2.$3) && eq.equals(t1.$4, t2.$4)); return !(eq.equals(t1.$1, t2.$1) && eq.equals(t1.$2, t2.$2) && eq.equals(t1.$3, t2.$3) && eq.equals(t1.$4, t2.$4) && eq.equals(t1.$5, t2.$5));
}, },
builder: (context, s, child) { builder: (context, s, child) {
return ValueListenableBuilder<bool>( return ValueListenableBuilder<bool>(
@ -123,7 +123,7 @@ class AlbumListPage extends StatelessWidget {
final listedDynamicAlbums = <DynamicAlbumFilter>{}; final listedDynamicAlbums = <DynamicAlbumFilter>{};
if (albumChipTypes.contains(AlbumChipType.dynamic)) { if (albumChipTypes.contains(AlbumChipType.dynamic)) {
final allDynamicAlbums = dynamicAlbums.all; final allDynamicAlbums = dynamicAlbums.all.whereNot(settings.hiddenFilters.contains).toSet();
if (groupUri == null) { if (groupUri == null) {
final withinGroups = whereTypeRecursively<DynamicAlbumFilter>(groupContent).toSet(); final withinGroups = whereTypeRecursively<DynamicAlbumFilter>(groupContent).toSet();
listedDynamicAlbums.addAll(allDynamicAlbums.whereNot(withinGroups.contains)); listedDynamicAlbums.addAll(allDynamicAlbums.whereNot(withinGroups.contains));
@ -134,7 +134,7 @@ class AlbumListPage extends StatelessWidget {
} }
// always show groups, which are needed to navigate to other types // always show groups, which are needed to navigate to other types
final albumGroupFilters = groupContent.whereType<AlbumGroupFilter>().toSet(); final albumGroupFilters = groupContent.whereType<AlbumGroupFilter>().whereNot(settings.hiddenFilters.contains).toSet();
final filters = <AlbumBaseFilter>{ final filters = <AlbumBaseFilter>{
...albumGroupFilters, ...albumGroupFilters,

View file

@ -11,6 +11,7 @@ import 'package:aves/model/source/collection_source.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/themes.dart'; import 'package:aves/theme/themes.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/action_controls/togglers/title_search.dart'; import 'package:aves/widgets/common/action_controls/togglers/title_search.dart';
import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar/app_bar_subtitle.dart';
import 'package:aves/widgets/common/app_bar/app_bar_title.dart'; import 'package:aves/widgets/common/app_bar/app_bar_title.dart';
@ -78,8 +79,8 @@ class FilterGridAppBar<T extends CollectionFilter, CSAD extends ChipSetActionDel
} }
} }
class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with SingleTickerProviderStateMixin, WidgetsBindingObserver { class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetActionDelegate<T>> extends State<FilterGridAppBar<T, CSAD>> with RouteAware, SingleTickerProviderStateMixin, WidgetsBindingObserver {
final List<StreamSubscription> _subscriptions = []; final Set<StreamSubscription> _subscriptions = {};
late AnimationController _browseToSelectAnimation; late AnimationController _browseToSelectAnimation;
final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false); final ValueNotifier<bool> _isSelectingNotifier = ValueNotifier(false);
final FocusNode _queryBarFocusNode = FocusNode(); final FocusNode _queryBarFocusNode = FocusNode();
@ -112,6 +113,15 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
WidgetsBinding.instance.addPostFrameCallback((_) => _updateAppBarHeight()); WidgetsBinding.instance.addPostFrameCallback((_) => _updateAppBarHeight());
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute) {
AvesApp.pageRouteObserver.subscribe(this, route);
}
}
@override @override
void dispose() { void dispose() {
_queryBarFocusNode.dispose(); _queryBarFocusNode.dispose();
@ -122,9 +132,17 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
..forEach((sub) => sub.cancel()) ..forEach((sub) => sub.cancel())
..clear(); ..clear();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
AvesApp.pageRouteObserver.unsubscribe(this);
super.dispose(); super.dispose();
} }
@override
void didPushNext() {
// unfocus when navigating away, so that when navigating back,
// the query bar does not get back focus and bring the keyboard
_queryBarFocusNode.unfocus();
}
@override @override
void didChangeMetrics() { void didChangeMetrics() {
// when text scale factor changes // when text scale factor changes
@ -170,7 +188,8 @@ class _FilterGridAppBarState<T extends CollectionFilter, CSAD extends ChipSetAct
if (_showGroupCrumbLine(context)) if (_showGroupCrumbLine(context))
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return SizedBox( return Container(
padding: CrumbLine.padding,
width: constraints.maxWidth, width: constraints.maxWidth,
height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)), height: CrumbLine.getPreferredHeight(MediaQuery.textScalerOf(context)),
child: Selector<FilterGroupNotifier, Uri?>( child: Selector<FilterGroupNotifier, Uri?>(

View file

@ -61,9 +61,13 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4)); static Radius radius(double extent) => Radius.circular(min<double>(AvesFilterChip.defaultRadius, extent / 4));
static double detailIconSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 8); static double detailIconSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 7);
static double detailFontSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 6); static double detailFontSize(double extent) => min<double>(AvesFilterChip.fontSize, extent / 7);
static double detailIconPadding(double extent) => min<double>(8.0, extent / 16);
static double detailIconTextPadding(double extent) => detailIconPadding(extent) / 2;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -201,30 +205,33 @@ class CoveredFilterChip<T extends CollectionFilter> extends StatelessWidget {
if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked), if (filter is StoredAlbumFilter && vaults.isVault(filter.album)) _buildDetailIcon(context, AIcons.locked),
if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum), if (filter is DynamicAlbumFilter) _buildDetailIcon(context, AIcons.dynamicAlbum),
if (filter is AlbumGroupFilter) ...[ if (filter is AlbumGroupFilter) ...[
_buildDetailIcon(context, AIcons.album), _buildDetailIcon(context, AIcons.album, padding: detailIconTextPadding(extent)),
Text( Text(
'${NumberFormat.decimalPattern(context.locale).format(albumGrouping.countLeaves(filter.uri))}${AText.separator}', '${NumberFormat.decimalPattern(context.locale).format(albumGrouping.countLeaves(filter.uri))}${AText.separator}',
style: textStyle, style: textStyle,
), ),
], ],
Text( Flexible(
locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)), child: Text(
style: textStyle, locked ? AText.valueNotAvailable : NumberFormat.decimalPattern(context.locale).format(source.count(filter)),
style: textStyle,
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
),
), ),
], ],
); );
} }
Widget _buildDetailIcon(BuildContext context, IconData icon) { Widget _buildDetailIcon(BuildContext context, IconData icon, {double? padding}) {
final padding = min<double>(8.0, extent / 16);
final iconSize = detailIconSize(extent);
return AnimatedPadding( return AnimatedPadding(
padding: EdgeInsetsDirectional.only(end: padding), padding: EdgeInsetsDirectional.only(end: padding ?? detailIconPadding(extent)),
duration: ADurations.chipDecorationAnimation, duration: ADurations.chipDecorationAnimation,
child: Icon( child: Icon(
icon, icon,
color: _detailColor(context), color: _detailColor(context),
size: iconSize, size: detailIconSize(extent),
), ),
); );
} }

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