Compare commits

...

158 Commits

Author SHA1 Message Date
Sylvia van Os
47bdd5209d Adjust text sizing 2025-12-20 12:45:11 +01:00
Sylvia van Os
1f6db32155 Fix build issues 2025-12-17 21:30:24 +01:00
Sylvia van Os
705b80a32c Fix Gradle setup 2025-12-17 21:19:54 +01:00
LooKeR
91bb808d19 test: Fix configuration of compose tests 2025-12-17 21:13:56 +01:00
LooKeR
497669b3f8 test: Add more comprehensive tests for about screen 2025-12-17 21:13:31 +01:00
LooKeR
3fb77bd607 style: Format AboutActivity.kt 2025-12-17 21:13:31 +01:00
LooKeR
cc2fe5b52b refactor: Best practise apply theme as high as possible for most cases 2025-12-17 21:13:31 +01:00
LooKeR
abb01eed66 refactor: Best practise to make previews private to reduce pollution 2025-12-17 21:13:31 +01:00
LooKeR
a1469aa550 refactor: Make showRateOnGooglePlay default to app/build.gradle.kts/defaultConfig value 2025-12-17 21:13:31 +01:00
LooKeR
f8deeee32c refactor: Move compose tests to unit tests 2025-12-17 21:13:31 +01:00
LooKeR
021fd13ea6 refactor: Add defaults for AboutScreenContent 2025-12-17 21:12:35 +01:00
LooKeR
3f753e8f51 test: Add basic test for compose about screen 2025-12-17 21:12:35 +01:00
LooKeR
2c1210e8cd test: Add test tags for compose components 2025-12-17 21:12:35 +01:00
Sylvia van Os
10193ffd85 WIP 2025-12-17 21:11:40 +01:00
Sylvia van Os
17d3e9b3d0 Merge pull request #2876 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-12-17 20:34:32 +01:00
امیرضا
de47b9e774 Translated using Weblate (Persian)
Currently translated at 83.9% (278 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fa/
2025-12-17 20:01:10 +01:00
امیرضا
acfa8d9fe2 Translated using Weblate (Persian)
Currently translated at 27.2% (42 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fa/
2025-12-17 20:01:08 +01:00
Sylvia van Os
930246e6c5 Merge pull request #2874 from CatimaLoyalty/create-pull-request/patch-1765747610
Update Fastlane changelogs
2025-12-14 22:28:01 +01:00
TheLastProject
c6b8272448 Update Fastlane changelogs 2025-12-14 21:26:50 +00:00
Sylvia van Os
24b832a217 Update CHANGELOG 2025-12-14 22:26:37 +01:00
Charalampos Kardaris
3acf002f95 [Fix] Issue #2812: Show duplicate action in long press menu (#2873)
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2025-12-14 22:25:23 +01:00
Sylvia van Os
16f9b3f6b1 Merge pull request #2872 from CatimaLoyalty/create-pull-request/patch-1765686940
Update contributors
2025-12-14 10:51:09 +01:00
TheLastProject
3c38c7cc25 Update contributors 2025-12-14 04:35:39 +00:00
Methum Menthusa
efbc930125 Merge pull request #2868 from methum-m/dependency-cooldown
Add 7 day dependency cooldown
2025-12-12 15:26:02 +01:00
Sylvia van Os
90b326e6b9 Merge pull request #2870 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-12-12 11:34:18 +01:00
Francisco Serrador
d3e7fe212d Translated using Weblate (Spanish)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2025-12-12 11:00:29 +01:00
Sylvia van Os
803d83f8e1 Merge pull request #2869 from CatimaLoyalty/dependabot/gradle/com.android.application-8.13.2
Bump com.android.application from 8.13.1 to 8.13.2
2025-12-12 08:25:25 +01:00
dependabot[bot]
59b060fbc0 Bump com.android.application from 8.13.1 to 8.13.2
Bumps com.android.application from 8.13.1 to 8.13.2.

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-version: 8.13.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-12 02:02:59 +00:00
Sylvia van Os
6d64bd4cdf Fix release staps 2025-12-08 18:45:13 +01:00
Sylvia van Os
ada4850f65 Merge branch 'main' of github.com:CatimaLoyalty/Android 2025-12-08 18:40:48 +01:00
Sylvia van Os
479fce68d5 Release Catima 2.40.0 2025-12-08 18:32:40 +01:00
Sylvia van Os
2c0b49d7f8 Merge pull request #2865 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-12-08 18:31:45 +01:00
Hosted Weblate
e534eebc4d Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/
2025-12-08 18:03:39 +01:00
Yasin Tanış
db16676cc4 Translated using Weblate (Turkish)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/tr/
2025-12-08 18:03:38 +01:00
Gideon
0f1e5b858b Translated using Weblate (Dutch)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2025-12-08 18:03:38 +01:00
Yasin Tanış
a39d2e46e1 Translated using Weblate (Turkish)
Currently translated at 66.8% (103 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/tr/
2025-12-08 18:03:37 +01:00
Sylvia van Os
4370cd2383 Merge pull request #2864 from CatimaLoyalty/dependabot/github_actions/peter-evans/create-pull-request-7.0.11
Bump peter-evans/create-pull-request from 7.0.9 to 7.0.11
2025-12-08 10:15:25 +01:00
dependabot[bot]
0c4ef730e0 Bump peter-evans/create-pull-request from 7.0.9 to 7.0.11
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.9 to 7.0.11.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v7.0.9...v7.0.11)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 7.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 02:04:42 +00:00
Sylvia van Os
ee909e0047 Merge pull request #2863 from CatimaLoyalty/create-pull-request/patch-1765081875
Update contributors
2025-12-07 11:11:45 +01:00
TheLastProject
6eee4a25f3 Update contributors 2025-12-07 04:31:15 +00:00
Sylvia van Os
ffa99231c6 Merge pull request #2862 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-12-06 20:58:41 +01:00
Hosted Weblate
cbcd74f735 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/
2025-12-06 19:03:25 +00:00
Richard Varga
4f46a3c8ab Translated using Weblate (Slovak)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sk/
2025-12-06 19:03:24 +00:00
Ati
5cec75c4c7 Translated using Weblate (Slovak)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sk/
2025-12-06 19:03:24 +00:00
Patrik
38d3731027 Translated using Weblate (Slovak)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sk/
2025-12-06 19:03:23 +00:00
Richard Varga
2a9f911a39 Translated using Weblate (Slovak)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sk/
2025-12-06 19:03:22 +00:00
Sylvia van Os
c762fcf6cc Merge pull request #2861 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-12-05 22:17:50 +01:00
VKing9
59db6642c3 Translated using Weblate (Hindi)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/hi/
2025-12-05 16:01:04 +01:00
Diego Menezes
eb5168ef83 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/pt_BR/
2025-12-05 16:01:03 +01:00
VKing9
7221ea64c8 Translated using Weblate (Hindi)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/hi/
2025-12-05 16:01:03 +01:00
Marko Zakrajsek
af17bde7c5 Translated using Weblate (Slovenian)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sl/
2025-12-05 16:01:02 +01:00
Sylvia van Os
67e2abde8b Merge pull request #2860 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-12-04 15:26:38 +01:00
Liner Seven
f28a9e7ba3 Translated using Weblate (Japanese)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ja/
2025-12-04 15:04:26 +01:00
Joel A
96f01b6a2c Translated using Weblate (Swedish)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sv/
2025-12-04 15:04:26 +01:00
B o d o
56bc429c4b Translated using Weblate (German)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2025-12-04 15:04:25 +01:00
Fjuro
163da4b021 Translated using Weblate (Czech)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/cs/
2025-12-04 15:04:24 +01:00
Sylvain Pichon
6d9c168125 Translated using Weblate (French)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2025-12-04 15:04:24 +01:00
solokot
ccaef1adc5 Translated using Weblate (Russian)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ru/
2025-12-04 15:04:23 +01:00
4ipset
f73222597c Translated using Weblate (Russian)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ru/
2025-12-04 15:04:22 +01:00
大王叫我来巡山
1714606744 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/zh_Hans/
2025-12-04 15:04:22 +01:00
Максим Горпиніч
ac3ef7fb36 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/uk/
2025-12-04 15:04:21 +01:00
Fjuro
9417814268 Translated using Weblate (Czech)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/cs/
2025-12-04 15:04:20 +01:00
Vasilis K.
72737074fb Translated using Weblate (Greek)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/el/
2025-12-04 15:04:20 +01:00
Максим Горпиніч
e05386620b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/uk/
2025-12-04 15:04:19 +01:00
jack son
7cfe1ad833 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/zh_Hant/
2025-12-04 15:04:18 +01:00
Edgars Andersons
00b975a140 Translated using Weblate (Latvian)
Currently translated at 12.3% (19 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/lv/
2025-12-04 15:04:18 +01:00
Liner Seven
424e57e41a Translated using Weblate (Japanese)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ja/
2025-12-04 15:04:17 +01:00
B o d o
cdb169c4e0 Translated using Weblate (German)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/
2025-12-04 15:04:16 +01:00
109247019824
17aa18397e Translated using Weblate (Bulgarian)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2025-12-04 15:04:15 +01:00
4ipset
bb72eefc7f Translated using Weblate (Russian)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2025-12-04 15:04:15 +01:00
Joel A
a007ed6a8f Translated using Weblate (Swedish)
Currently translated at 6.4% (10 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sv/
2025-12-04 15:04:14 +01:00
jack son
2219e86576 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/zh_Hant/
2025-12-04 15:04:13 +01:00
大王叫我来巡山
7f90e06ac5 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/zh_Hans/
2025-12-04 15:04:13 +01:00
Edgars Andersons
5dd089c976 Translated using Weblate (Latvian)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/lv/
2025-12-04 15:04:12 +01:00
Sylvain Pichon
ef9aacd609 Translated using Weblate (French)
Currently translated at 100.0% (154 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fr/
2025-12-04 15:04:11 +01:00
Priit Jõerüüt
fa19960c5e Translated using Weblate (Estonian)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/et/
2025-12-04 15:04:11 +01:00
josé m.
a5428b80ff Translated using Weblate (Galician)
Currently translated at 100.0% (331 of 331 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/gl/
2025-12-04 15:04:10 +01:00
Sylvia van Os
56512101c2 Merge pull request #2859 from CatimaLoyalty/dependabot/gradle/androidx.exifinterface-exifinterface-1.4.2
Bump androidx.exifinterface:exifinterface from 1.4.1 to 1.4.2
2025-12-04 07:22:02 +01:00
dependabot[bot]
2bc8312511 Bump androidx.exifinterface:exifinterface from 1.4.1 to 1.4.2
Bumps androidx.exifinterface:exifinterface from 1.4.1 to 1.4.2.

---
updated-dependencies:
- dependency-name: androidx.exifinterface:exifinterface
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-04 02:03:23 +00:00
ProgramminCat
43ccf9b48e Convert MainActivity to Kotlin (#2830)
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2025-12-02 20:54:33 +01:00
Sylvia van Os
e1a4ed6634 Merge pull request #2858 from CatimaLoyalty/dependabot/github_actions/actions/setup-python-6.1.0
Bump actions/setup-python from 6.0.0 to 6.1.0
2025-12-02 20:15:08 +01:00
Sylvia van Os
272e249d5e Merge pull request #2843 from CatimaLoyalty/gradlew-update-9.2.1
Update Gradle Wrapper from 9.2.0 to 9.2.1
2025-12-02 20:14:21 +01:00
dependabot[bot]
5636653c16 Bump actions/setup-python from 6.0.0 to 6.1.0
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6.0.0...v6.1.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-01 02:16:46 +00:00
Sylvia van Os
06b1b32ce7 Merge pull request #2857 from CatimaLoyalty/create-pull-request/patch-1764477253
Update contributors
2025-11-30 11:09:05 +01:00
TheLastProject
202936b21f Update contributors 2025-11-30 04:34:13 +00:00
Sylvia van Os
122f7f64b7 Merge pull request #2856 from CatimaLoyalty/create-pull-request/patch-1764415067
Update Fastlane changelogs
2025-11-29 12:21:44 +01:00
TheLastProject
fbb913862a Update Fastlane changelogs 2025-11-29 11:17:46 +00:00
Sylvia van Os
61892f9f22 Update CHANGELOG 2025-11-29 12:17:34 +01:00
Sylvia van Os
166056fedd Merge pull request #2855 from CatimaLoyalty/fix/2847
Swap currency and balance fields to reduce chance of accidental conversions
2025-11-29 12:16:56 +01:00
Sylvia van Os
e03c883a9c Swap currency and balance fields to reduce chance of accidental conversions
This swaps the currency and balance fields to reduce the risk of users
entering a decimal value (1,23) first and having to changed to 1 due to
the default currency (Points) having no decimals.

The changes in the LoyaltyCardEditActivity are purely cosmetic, just a
swap of function order to more closely stick to the order in the XML
layout file
2025-11-29 11:59:31 +01:00
Sylvia van Os
1b4559fa3c Merge pull request #2854 from CatimaLoyalty/create-pull-request/patch-1764269098
Update Fastlane changelogs
2025-11-27 19:46:16 +01:00
TheLastProject
ae8487f8d9 Update Fastlane changelogs 2025-11-27 18:44:57 +00:00
Sylvia van Os
922b517d37 Update CHANGELOG 2025-11-27 19:44:46 +01:00
Aditya Varma
5a0d99fc80 Copy card ID to clipboard from menu or long press (#2789)
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2025-11-27 19:43:26 +01:00
Sylvia van Os
c89a759c8b Merge pull request #2852 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-27 08:27:49 +01:00
Liner Seven
4c81fdcefb Translated using Weblate (Japanese)
Currently translated at 100.0% (328 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ja/
2025-11-27 05:05:54 +00:00
Marbino Timatim Jr.
7a35b7f598 Translated using Weblate (Filipino)
Currently translated at 19.5% (64 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fil/
2025-11-27 05:05:53 +00:00
Sylvia van Os
e1108c08ac Merge pull request #2851 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-25 16:36:52 +01:00
Hosted Weblate
6ab943f776 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/
2025-11-25 15:51:14 +01:00
Sylvia van Os
aac3570431 Translated using Weblate (Hebrew (Israel))
Currently translated at 24.0% (79 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/he_IL/
2025-11-25 15:51:13 +01:00
דוד משה המבורגר
3abf287c67 Translated using Weblate (Hebrew (Israel))
Currently translated at 24.3% (80 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/he_IL/
2025-11-25 13:51:27 +00:00
Edgars Andersons
cb26d23b02 Translated using Weblate (Latvian)
Currently translated at 100.0% (328 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/lv/
2025-11-25 13:51:26 +00:00
Maria Vacari
9afcb0d7c6 Translated using Weblate (Romanian)
Currently translated at 85.0% (279 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ro/
2025-11-25 13:51:25 +00:00
Sylvia van Os
acc50c4fda Merge pull request #2849 from CatimaLoyalty/dependabot/github_actions/peter-evans/create-pull-request-7.0.9
Bump peter-evans/create-pull-request from 7.0.8 to 7.0.9
2025-11-24 07:15:20 +01:00
Sylvia van Os
91fab12da6 Merge pull request #2848 from CatimaLoyalty/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 5 to 6
2025-11-24 07:14:47 +01:00
dependabot[bot]
e094b969ee Bump peter-evans/create-pull-request from 7.0.8 to 7.0.9
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.8 to 7.0.9.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v7.0.8...v7.0.9)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: 7.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:05:16 +00:00
dependabot[bot]
3103d3a9cf Bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-24 02:05:11 +00:00
Sylvia van Os
c82b255eaa Merge pull request #2845 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-18 10:13:36 +01:00
Nam Nguyen Thanh
9ee4f0da9b Translated using Weblate (Vietnamese)
Currently translated at 27.4% (42 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/vi/
2025-11-18 09:52:06 +01:00
Sylvia van Os
3b5e6ac450 Merge pull request #2844 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-18 09:26:57 +01:00
Nam Nguyen Thanh
7ab270f323 Translated using Weblate (Vietnamese)
Currently translated at 16.9% (26 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/vi/
2025-11-18 07:52:27 +00:00
Nam Nguyen Thanh
6f9b4739c8 Translated using Weblate (Vietnamese)
Currently translated at 16.3% (25 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/vi/
2025-11-18 07:52:26 +00:00
gradle-update-robot
e654a657a0 Update Gradle Wrapper from 9.2.0 to 9.2.1
Signed-off-by: gradle-update-robot <gradle-update-robot@regolo.cc>
2025-11-18 00:59:39 +00:00
Sylvia van Os
ae6567a784 Merge pull request #2841 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-16 17:19:24 +01:00
Dao Duy Tin
be12707ab1 Translated using Weblate (Vietnamese)
Currently translated at 84.4% (277 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/vi/
2025-11-16 15:51:51 +00:00
Sylvia van Os
e8d40ec679 Merge pull request #2839 from CatimaLoyalty/create-pull-request/patch-1763267079
Update contributors
2025-11-16 10:14:34 +01:00
TheLastProject
e9c40c88a9 Update contributors 2025-11-16 04:24:38 +00:00
Sylvia van Os
82bb9b2817 Merge pull request #2838 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-15 00:12:13 +01:00
Francisco Serrador
976dd6f80b Translated using Weblate (Spanish)
Currently translated at 66.0% (101 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/es/
2025-11-14 23:52:36 +01:00
Sylvia van Os
2ac36fa415 Merge pull request #2836 from CatimaLoyalty/create-pull-request/patch-1762978593
Update feature graphic
2025-11-12 23:07:39 +01:00
TheLastProject
19399dd17f Update feature graphic 2025-11-12 20:16:33 +00:00
Sylvia van Os
acae8c1a0d Merge pull request #2835 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-12 21:14:35 +01:00
Mohammad Alhasan
64073e210e Translated using Weblate (Arabic)
Currently translated at 96.0% (315 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ar/
2025-11-12 19:51:23 +00:00
Francisco Serrador
3adbf61be7 Translated using Weblate (Spanish)
Currently translated at 63.3% (97 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/es/
2025-11-12 17:52:18 +00:00
Francisco Serrador
f0df4622eb Translated using Weblate (Spanish)
Currently translated at 45.7% (70 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/es/
2025-11-12 17:52:01 +01:00
Francisco Serrador
81c0f284f6 Translated using Weblate (Spanish)
Currently translated at 100.0% (328 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2025-11-12 17:51:59 +01:00
Adrián Gelmotto Ruiz
fce60ca712 Translated using Weblate (Spanish)
Currently translated at 100.0% (328 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2025-11-12 17:51:58 +01:00
Sylvia van Os
2be4d1cd2b Merge pull request #2834 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-12 09:11:26 +01:00
Alì Mortacci
72b19a8272 Translated using Weblate (Italian)
Currently translated at 83.6% (128 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/it/
2025-11-12 08:52:04 +01:00
Sylvia van Os
9f9d404632 Merge pull request #2833 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-12 08:25:25 +01:00
Sylvia van Os
157d1ecc49 Merge pull request #2832 from CatimaLoyalty/dependabot/gradle/com.google.zxing-core-3.5.4
Bump com.google.zxing:core from 3.5.3 to 3.5.4
2025-11-12 08:20:51 +01:00
Alì Mortacci
54b21167ec Translated using Weblate (Italian)
Currently translated at 85.9% (282 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2025-11-12 07:52:57 +01:00
Alì Mortacci
34a125008f Translated using Weblate (Italian)
Currently translated at 81.6% (125 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/it/
2025-11-12 07:52:56 +01:00
Alì Mortacci
1776f8fd90 Translated using Weblate (Italian)
Currently translated at 85.3% (280 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2025-11-12 07:52:55 +01:00
dependabot[bot]
55269f748f Bump com.google.zxing:core from 3.5.3 to 3.5.4
Bumps [com.google.zxing:core](https://github.com/zxing/zxing) from 3.5.3 to 3.5.4.
- [Release notes](https://github.com/zxing/zxing/releases)
- [Changelog](https://github.com/zxing/zxing/blob/master/CHANGES)
- [Commits](https://github.com/zxing/zxing/compare/zxing-3.5.3...zxing-3.5.4)

---
updated-dependencies:
- dependency-name: com.google.zxing:core
  dependency-version: 3.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-12 02:03:17 +00:00
Sylvia van Os
6e42d0dcd9 Merge pull request #2831 from CatimaLoyalty/dependabot/gradle/com.android.application-8.13.1
Bump com.android.application from 8.13.0 to 8.13.1
2025-11-11 08:12:29 +01:00
dependabot[bot]
48113eba18 Bump com.android.application from 8.13.0 to 8.13.1
Bumps com.android.application from 8.13.0 to 8.13.1.

---
updated-dependencies:
- dependency-name: com.android.application
  dependency-version: 8.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-11 02:03:16 +00:00
Sylvia van Os
41c8bb1815 Merge pull request #2829 from CatimaLoyalty/create-pull-request/patch-1762662107
Update contributors
2025-11-09 09:17:16 +01:00
Sylvia van Os
eab4f9e123 Merge pull request #2828 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-09 09:16:31 +01:00
TheLastProject
05f4cfc07b Update contributors 2025-11-09 04:21:46 +00:00
asdasd gfsdfdfg
b07b09e703 Translated using Weblate (Polish)
Currently translated at 94.7% (145 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pl/
2025-11-08 21:51:56 +01:00
Sylvia van Os
42e69916f0 Merge pull request #2827 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-08 17:13:41 +01:00
Aliaksandr Truš
254c9fee14 Translated using Weblate (Belarusian)
Currently translated at 86.8% (285 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/be/
2025-11-08 11:51:37 +01:00
Sylvia van Os
66e568aa06 Merge pull request #2825 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-07 07:56:05 +01:00
Joel A
1f38110e94 Translated using Weblate (Swedish)
Currently translated at 100.0% (328 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sv/
2025-11-07 03:51:21 +01:00
Joel A
e92a98a956 Translated using Weblate (Swedish)
Currently translated at 99.3% (326 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sv/
2025-11-07 01:51:38 +00:00
Rishab Mamgai
875e90e940 Basic (incomplete) app_name consistency check script
Co-authored-by: ProgramminCat <72707293+ProgramminCat@users.noreply.github.com>
Co-authored-by: Sylvia van Os <sylvia@hackerchick.me>
2025-11-06 20:02:03 +01:00
Sylvia van Os
5ebd23a88a Merge pull request #2824 from CatimaLoyalty/dependabot/gradle/androidx.core-core-splashscreen-1.2.0
Bump androidx.core:core-splashscreen from 1.0.1 to 1.2.0
2025-11-06 06:53:52 +01:00
Sylvia van Os
6559857ba2 Merge pull request #2823 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-06 06:52:25 +01:00
dependabot[bot]
684b3e9836 Bump androidx.core:core-splashscreen from 1.0.1 to 1.2.0
Bumps androidx.core:core-splashscreen from 1.0.1 to 1.2.0.

---
updated-dependencies:
- dependency-name: androidx.core:core-splashscreen
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-06 02:03:18 +00:00
Joel A
d4b4599496 Translated using Weblate (Swedish)
Currently translated at 5.2% (8 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/sv/
2025-11-05 23:51:21 +00:00
Joel A
decd0a104d Translated using Weblate (Swedish)
Currently translated at 97.5% (320 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/sv/
2025-11-05 23:51:41 +01:00
Sylvia van Os
177f8e43e2 Merge pull request #2822 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-05 22:27:47 +01:00
ssantos
75234a6cd4 Translated using Weblate (Portuguese)
Currently translated at 100.0% (153 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pt/
2025-11-05 18:52:34 +00:00
ssantos
86b5c2998e Translated using Weblate (Portuguese (Portugal))
Currently translated at 100.0% (153 of 153 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pt_PT/
2025-11-05 18:52:33 +00:00
Sylvia van Os
0fee650cee Merge pull request #2820 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2025-11-05 17:18:13 +01:00
Francisco Serrador
61ee8b5910 Translated using Weblate (Spanish)
Currently translated at 99.6% (327 of 328 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2025-11-05 11:51:45 +01:00
Sylvia van Os
dae66a63f1 Remove Stocard import reference from README
Stocard importer was removed a while ago
2025-11-04 22:43:36 +01:00
206 changed files with 2188 additions and 1937 deletions

View File

@@ -9,10 +9,15 @@ updates:
- mavenCentral
schedule:
interval: "daily"
cooldown:
default-days: 7
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
# Workaround for https://github.com/dependabot/dependabot-core/issues/6888
registries:

View File

@@ -32,9 +32,7 @@ jobs:
matrix:
flavor: [Foss, Gplay]
steps:
- uses: actions/checkout@v5
- name: Fail on bad translations
run: if grep -ri "&lt;xliff" app/src/main/res/values*/strings.xml; then echo "Invalidly escaped translations found"; exit 1; fi
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v5
- name: set up OpenJDK 21
run: |

View File

@@ -19,15 +19,15 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6.0.0
uses: actions/setup-python@v6.1.0
with:
python-version: '3.x'
- name: Run converter script
run: python .scripts/changelog_to_fastlane.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v7.0.11
with:
title: "Update Fastlane changelogs"
commit-message: "Update Fastlane changelogs"

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout repo
id: checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Update contributors
id: update_contributors
uses: TheLastProject/contributors-to-file-action@v3.2.0
@@ -25,7 +25,7 @@ jobs:
file_in_repo: app/src/main/res/raw/contributors.txt
min_commit_count: 5
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v7.0.11
with:
title: "Update contributors"
commit-message: "Update contributors"

View File

@@ -17,7 +17,7 @@ jobs:
generate-feature-graphic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install requirements
run: |
sudo apt-get update
@@ -31,7 +31,7 @@ jobs:
- name: Generate featureGraphic.png for each language
run: .scripts/generate_feature_graphic/generate_feature_graphic.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v7.0.11
with:
title: "Update feature graphic"
commit-message: "Update feature graphic"

34
.github/workflows/i18n-check.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: i18n check
on:
workflow_dispatch:
push:
branches:
- main
- staging
- trying
pull_request:
branches:
- main
permissions:
actions: none
checks: none
contents: read
deployments: none
discussions: none
id-token: none
issues: none
packages: none
pages: none
pull-requests: none
repository-projects: none
security-events: none
statuses: none
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Fail on bad translations
run: if grep -ri "&lt;xliff" app/src/main/res/values*/strings.xml; then echo "Invalidly escaped translations found"; exit 1; fi
- name: Check app_name consistency
run: bash .scripts/check_app_name.sh

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Update Gradle Wrapper
uses: gradle-update/update-gradle-wrapper-action@v2

View File

@@ -17,13 +17,13 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Add new locales
run: .scripts/new-locales.py
- name: Update locales
run: .scripts/locales.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7.0.8
uses: peter-evans/create-pull-request@v7.0.11
with:
title: "Update locales"
commit-message: "Update locales"

View File

@@ -0,0 +1,71 @@
#!/bin/bash
set -e
shopt -s lastpipe # Run last command in a pipeline in the current shell.
# Colors
LIGHTCYAN='\033[1;36m'
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Vars
SUCCESS=1
CANONICAL_TITLE="Catima"
ALLOWLIST=("ar" "bn" "fa" "fa-IR" "he-IL" "hi" "hi-IN" "kn" "kn-IN" "ml" "mr" "ta" "ta-IN" "zh-rTW" "zh-TW") # TODO: Link values and fastlane with different codes together
function get_lang() {
LANG_DIRNAME=$(dirname "$FILE" | xargs basename)
LANG=${LANG_DIRNAME#values-} # Fetch lang name
LANG=${LANG#values} # Handle "app/src/main/res/values"
LANG=${LANG:-en} # Default to en
}
# FIXME: This function should use its own variables and return a success/fail status, instead of working on global variables
function check() {
# FIXME: This allows inconsistency between values and fastlane if the app name is not Catima
# When the app name is not Catima, it should still check if title.txt and strings.xml use the same app name (or start)
if echo "${ALLOWLIST[*]}" | grep -w -q "${LANG}" || [[ -z ${APP_NAME} ]]; then
return 0
fi
if [[ ${FILE} == *"title.txt" ]]; then
if [[ ! ${APP_NAME} =~ ^${CANONICAL_TITLE} ]]; then
echo -e "${RED}Error: ${LIGHTCYAN}title in $FILE ($LANG) is ${RED}'$APP_NAME'${LIGHTCYAN}, expected to start with ${GREEN}'$CANONICAL_TITLE'. ${NC}"
SUCCESS=0
fi
else
if [[ ${APP_NAME} != "${CANONICAL_TITLE}" ]]; then
echo -e "${RED}Error: ${LIGHTCYAN}app_name in $FILE ($LANG) is ${RED}'$APP_NAME'${LIGHTCYAN}, expected ${GREEN}'$CANONICAL_TITLE'. ${NC}"
SUCCESS=0
fi
fi
}
# FIXME: This checks all title.txt and strings.xml files separately, but it needs to check if the title.txt and strings.xml match for a language as well
echo -e "${LIGHTCYAN}Checking title.txt's. ${NC}"
find fastlane/metadata/android/* -maxdepth 1 -type f -name "title.txt" | while read -r FILE; do
APP_NAME=$(head -n 1 "$FILE")
get_lang
check
done
echo -e "${LIGHTCYAN}Checking string.xml's. ${NC}"
find app/src/main/res/values* -maxdepth 1 -type f -name "strings.xml" | while read -r FILE; do
# FIXME: This only checks app_name, but there are more strings with Catima inside it
# It should check the original English text for all strings that contain Catima and ensure they use the correct app_name for consistency
APP_NAME=$(grep -oP '<string name="app_name">\K[^<]+' "$FILE" | head -n1)
get_lang
check
done
if [[ $SUCCESS -eq 1 ]]; then
echo -e "\n${GREEN}Success!! All app_name values match the canonical title. ${NC}"
else
echo -e "\n${RED}Unsuccessful!! Some app_name values did not match the canonical titles. ${NC}"
exit 1
fi

View File

@@ -1,5 +1,14 @@
# Changelog
## Unreleased - 157
- Add duplicate option to main screen and reorder options slightly
## v2.40.0 - 156 (2025-12-08)
- Copy card ID to clipboard from view dialog or long press
- Swap balance and currency fields to hopefully reduce unintended rounding
## v2.39.2 - 155 (2025-11-04)
- Preparations for future improvements (rewrote many classes to Kotlin)

View File

@@ -3,6 +3,7 @@ import com.android.build.gradle.internal.tasks.factory.dependsOn
plugins {
alias(libs.plugins.com.android.application)
alias(libs.plugins.org.jetbrains.kotlin.android)
alias(libs.plugins.org.jetbrains.kotlin.plugin.compose)
}
kotlin {
@@ -17,8 +18,8 @@ android {
applicationId = "me.hackerchick.catima"
minSdk = 21
targetSdk = 36
versionCode = 155
versionName = "2.39.2"
versionCode = 156
versionName = "2.40.0"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
@@ -47,6 +48,7 @@ android {
buildFeatures {
buildConfig = true
compose = true
viewBinding = true
}
@@ -103,11 +105,7 @@ android {
lintConfig = file("lint.xml")
}
kotlinOptions {
jvmTarget = "21"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
jvmTarget = "17"
}
}
@@ -124,6 +122,19 @@ dependencies {
implementation(libs.com.google.android.material.material)
coreLibraryDesugaring(libs.com.android.tools.desugar.jdk.libs)
// Compose
implementation(libs.androidx.activity.activity.compose)
val composeBom = platform(libs.androidx.compose.compose.bom)
implementation(composeBom)
implementation(libs.androidx.compose.foundation.foundation)
implementation(libs.androidx.compose.material3.material3)
implementation(libs.androidx.compose.material.material.icons.extended)
implementation(libs.androidx.compose.ui.ui.tooling.preview.android)
debugImplementation(libs.androidx.compose.ui.ui.test.manifest)
androidTestImplementation(composeBom)
androidTestImplementation(libs.androidx.compose.ui.ui.test.junit4)
// Third-party
implementation(libs.com.journeyapps.zxing.android.embedded)
implementation(libs.com.github.yalantis.ucrop)
@@ -143,6 +154,8 @@ dependencies {
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.junit.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.test.rules)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.uiautomator.uiautomator)
androidTestImplementation(libs.androidx.test.espresso.espresso.core)
}

View File

@@ -0,0 +1,89 @@
package protect.card_locker
import android.app.Instrumentation
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.runComposeUiTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import protect.card_locker.compose.theme.CatimaTheme
@OptIn(ExperimentalTestApi::class)
@RunWith(AndroidJUnit4::class)
class AboutActivityTest {
@get:Rule
private val rule: ComposeContentTestRule = createComposeRule()
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
private val content: AboutContent = AboutContent(instrumentation.targetContext)
@Test
fun testInitialState(): Unit = runComposeUiTest {
setContent {
AboutScreenContent(content = content)
}
onNodeWithTag("topbar_catima").assertIsDisplayed()
onNodeWithTag("card_version_history").assertIsDisplayed()
onNodeWithText(content.versionHistory).assertIsDisplayed()
onNodeWithTag("card_credits").assertIsDisplayed()
onNodeWithText(content.copyrightShort).assertIsDisplayed()
onNodeWithTag("card_translate").assertIsDisplayed()
onNodeWithTag("card_license").assertIsDisplayed()
// We might be off the screen so start scrolling
onNodeWithTag("card_source_github").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
// Dont scroll to this, since its not displayed
onNodeWithTag("card_rate_google").assertIsNotDisplayed()
onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
}
@Test
fun testDonateAndGoogleCardVisible(): Unit = runComposeUiTest {
setContent {
CatimaTheme {
AboutScreenContent(
content = content,
showDonate = true,
showRateOnGooglePlay = true,
)
}
}
onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_rate_google").performScrollTo().assertIsDisplayed()
}
@Test
fun testDonateAndGoogleCardHidden(): Unit = runComposeUiTest {
setContent {
CatimaTheme {
AboutScreenContent(
content = content,
showDonate = false,
showRateOnGooglePlay = false,
)
}
}
onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
onNodeWithTag("card_donate").assertIsNotDisplayed()
onNodeWithTag("card_rate_google").assertIsNotDisplayed()
onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
}
}

View File

@@ -1,149 +1,167 @@
package protect.card_locker
import android.os.Bundle
import android.text.Spanned
import android.view.MenuItem
import android.view.View
import android.widget.ScrollView
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import protect.card_locker.compose.CatimaAboutSection
import protect.card_locker.compose.CatimaTopAppBar
import protect.card_locker.compose.theme.CatimaTheme
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import protect.card_locker.databinding.AboutActivityBinding
class AboutActivity : CatimaAppCompatActivity() {
private companion object {
private const val TAG = "Catima"
}
private lateinit var binding: AboutActivityBinding
class AboutActivity : ComponentActivity() {
private lateinit var content: AboutContent
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = AboutActivityBinding.inflate(layoutInflater)
content = AboutContent(this)
title = content.pageTitle
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
enableToolbarBackButton()
binding.apply {
creditsSub.text = content.copyrightShort
versionHistorySub.text = content.versionHistory
versionHistory.tag = "https://catima.app/changelog/"
translate.tag = "https://hosted.weblate.org/engage/catima/"
license.tag = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE"
repo.tag = "https://github.com/CatimaLoyalty/Android/"
privacy.tag = "https://catima.app/privacy-policy/"
reportError.tag = "https://github.com/CatimaLoyalty/Android/issues"
rate.tag = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
donate.tag = "https://catima.app/donate"
// Hide Google Play rate button if not on Google Play
rate.isVisible = BuildConfig.showRateOnGooglePlay
// Hide donate button on Google Play (Google Play doesn't allow donation links)
donate.isVisible = BuildConfig.showDonate
}
bindClickListeners()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
setContent {
CatimaTheme {
AboutScreenContent(
content = content,
showDonate = BuildConfig.showDonate,
showRateOnGooglePlay = BuildConfig.showRateOnGooglePlay,
onBackPressedDispatcher = onBackPressedDispatcher
)
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
content.destroy()
clearClickListeners()
}
private fun bindClickListeners() {
binding.apply {
versionHistory.setOnClickListener { showHistory(it) }
translate.setOnClickListener { openExternalBrowser(it) }
license.setOnClickListener { showLicense(it) }
repo.setOnClickListener { openExternalBrowser(it) }
privacy.setOnClickListener { showPrivacy(it) }
reportError.setOnClickListener { openExternalBrowser(it) }
rate.setOnClickListener { openExternalBrowser(it) }
donate.setOnClickListener { openExternalBrowser(it) }
credits.setOnClickListener { showCredits() }
}
}
private fun clearClickListeners() {
binding.apply {
versionHistory.setOnClickListener(null)
translate.setOnClickListener(null)
license.setOnClickListener(null)
repo.setOnClickListener(null)
privacy.setOnClickListener(null)
reportError.setOnClickListener(null)
rate.setOnClickListener(null)
donate.setOnClickListener(null)
credits.setOnClickListener(null)
}
}
private fun showCredits() {
showHTML(R.string.credits, content.contributorInfo, null)
}
private fun showHistory(view: View) {
showHTML(R.string.version_history, content.historyInfo, view)
}
private fun showLicense(view: View) {
showHTML(R.string.license, content.licenseInfo, view)
}
private fun showPrivacy(view: View) {
showHTML(R.string.privacy_policy, content.privacyInfo, view)
}
private fun showHTML(@StringRes title: Int, text: Spanned, view: View?) {
val dialogContentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
val textView = TextView(this).apply {
setText(text)
Utils.makeTextViewLinksClickable(this, text)
}
val scrollView = ScrollView(this).apply {
addView(textView)
setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0)
}
MaterialAlertDialogBuilder(this).apply {
setTitle(title)
setView(scrollView)
setPositiveButton(R.string.ok, null)
// Add View online button if an URL is linked to this view
view?.tag?.let {
setNeutralButton(R.string.view_online) { _, _ -> openExternalBrowser(view) }
}
show()
}
}
private fun openExternalBrowser(view: View) {
val tag = view.tag
if (tag is String && tag.startsWith("https://")) {
OpenWebLinkHandler().openBrowser(this, tag)
}
}
}
@Composable
fun AboutScreenContent(
content: AboutContent,
showDonate: Boolean = true,
showRateOnGooglePlay: Boolean = false,
onBackPressedDispatcher: OnBackPressedDispatcher? = null,
) {
Scaffold(
topBar = { CatimaTopAppBar(content.pageTitle.toString(), onBackPressedDispatcher) }
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) {
CatimaAboutSection(
stringResource(R.string.version_history),
content.versionHistory,
modifier = Modifier.testTag("card_version_history"),
onClickUrl = "https://catima.app/changelog/",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.historyHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
CatimaAboutSection(
stringResource(R.string.credits),
content.copyrightShort,
modifier = Modifier.testTag("card_credits"),
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.contributorInfoHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
CatimaAboutSection(
stringResource(R.string.help_translate_this_app),
stringResource(R.string.translate_platform),
modifier = Modifier.testTag("card_translate"),
onClickUrl = "https://hosted.weblate.org/engage/catima/"
)
CatimaAboutSection(
stringResource(R.string.license),
stringResource(R.string.app_license),
modifier = Modifier.testTag("card_license"),
onClickUrl = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.licenseHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
CatimaAboutSection(
stringResource(R.string.source_repository),
stringResource(R.string.on_github),
modifier = Modifier.testTag("card_source_github"),
onClickUrl = "https://github.com/CatimaLoyalty/Android/"
)
CatimaAboutSection(
stringResource(R.string.privacy_policy),
stringResource(R.string.and_data_usage),
modifier = Modifier.testTag("card_privacy_policy"),
onClickUrl = "https://catima.app/privacy-policy/",
onClickDialogText = AnnotatedString.fromHtml(
htmlString = content.privacyHtml,
linkStyles = TextLinkStyles(
style = SpanStyle(
textDecoration = TextDecoration.Underline,
color = MaterialTheme.colorScheme.primary
)
)
)
)
if (showDonate) {
CatimaAboutSection(
stringResource(R.string.donate),
"",
modifier = Modifier.testTag("card_donate"),
onClickUrl = "https://catima.app/donate"
)
}
if (showRateOnGooglePlay) {
CatimaAboutSection(
stringResource(R.string.rate_this_app),
stringResource(R.string.on_google_play),
modifier = Modifier.testTag("card_rate_google"),
onClickUrl = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
)
}
CatimaAboutSection(
stringResource(R.string.report_error),
stringResource(R.string.on_github),
modifier = Modifier.testTag("card_report_error"),
onClickUrl = "https://github.com/CatimaLoyalty/Android/issues"
)
}
}
}
@Preview
@Composable
private fun AboutActivityPreview() {
AboutScreenContent(AboutContent(LocalContext.current))
}

View File

@@ -3,11 +3,8 @@ package protect.card_locker;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.Spanned;
import android.util.Log;
import androidx.core.text.HtmlCompat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
@@ -55,7 +52,7 @@ public class AboutContent {
return context.getString(R.string.app_copyright_short);
}
public String getContributors() {
public String getContributorsHtml() {
String contributors;
try {
contributors = "<br/>" + Utils.readTextFile(context, R.raw.contributors);
@@ -65,7 +62,7 @@ public class AboutContent {
return contributors.replace("\n", "<br />");
}
public String getHistory() {
public String getHistoryHtml() {
String versionHistory;
try {
versionHistory = Utils.readTextFile(context, R.raw.changelog)
@@ -77,7 +74,7 @@ public class AboutContent {
.replace("\n", "<br />");
}
public String getLicense() {
public String getLicenseHtml() {
try {
return Utils.readTextFile(context, R.raw.license);
} catch (IOException ignored) {
@@ -85,7 +82,7 @@ public class AboutContent {
}
}
public String getPrivacy() {
public String getPrivacyHtml() {
String privacyPolicy;
try {
privacyPolicy = Utils.readTextFile(context, R.raw.privacy)
@@ -97,7 +94,7 @@ public class AboutContent {
.replace("\n", "<br />");
}
public String getThirdPartyLibraries() {
public String getThirdPartyLibrariesHtml() {
final List<ThirdPartyInfo> usedLibraries = new ArrayList<>();
usedLibraries.add(new ThirdPartyInfo("ACRA", "https://github.com/ACRA/acra", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
@@ -116,7 +113,7 @@ public class AboutContent {
return result.toString();
}
public String getUsedThirdPartyAssets() {
public String getUsedThirdPartyAssetsHtml() {
final List<ThirdPartyInfo> usedAssets = new ArrayList<>();
usedAssets.add(new ThirdPartyInfo("Android icons", "https://fonts.google.com/icons?selected=Material+Icons", "Apache 2.0"));
@@ -129,31 +126,19 @@ public class AboutContent {
return result.toString();
}
public Spanned getContributorInfo() {
public String getContributorInfoHtml() {
StringBuilder contributorInfo = new StringBuilder();
contributorInfo.append(getCopyright());
contributorInfo.append("<br/><br/>");
contributorInfo.append(context.getString(R.string.app_copyright_old));
contributorInfo.append("<br/><br/>");
contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributors()));
contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributorsHtml()));
contributorInfo.append("<br/><br/>");
contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibraries()));
contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibrariesHtml()));
contributorInfo.append("<br/><br/>");
contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssets()));
contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssetsHtml()));
return HtmlCompat.fromHtml(contributorInfo.toString(), HtmlCompat.FROM_HTML_MODE_COMPACT);
}
public Spanned getHistoryInfo() {
return HtmlCompat.fromHtml(getHistory(), HtmlCompat.FROM_HTML_MODE_COMPACT);
}
public Spanned getLicenseInfo() {
return HtmlCompat.fromHtml(getLicense(), HtmlCompat.FROM_HTML_MODE_LEGACY);
}
public Spanned getPrivacyInfo() {
return HtmlCompat.fromHtml(getPrivacy(), HtmlCompat.FROM_HTML_MODE_COMPACT);
return contributorInfo.toString();
}
public String getVersionHistory() {

View File

@@ -123,8 +123,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
ChipGroup groupsChips;
AutoCompleteTextView validFromField;
AutoCompleteTextView expiryField;
EditText balanceField;
AutoCompleteTextView balanceCurrencyField;
EditText balanceField;
TextView cardIdFieldView;
AutoCompleteTextView barcodeIdField;
AutoCompleteTextView barcodeTypeField;
@@ -148,9 +148,9 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
boolean onRestoring = false;
AlertDialog confirmExitDialog = null;
boolean validBalance = true;
HashMap<String, Currency> currencies = new HashMap<>();
HashMap<String, String> currencySymbols = new HashMap<>();
boolean validBalance = true;
ActivityResultLauncher<Uri> mPhotoTakerLauncher;
ActivityResultLauncher<Intent> mPhotoPickerLauncher;
@@ -193,14 +193,14 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
viewModel.setHasChanged(true);
}
protected void setLoyaltyCardBalance(@NonNull BigDecimal balance) {
viewModel.getLoyaltyCard().setBalance(balance);
protected void setLoyaltyCardBalanceType(@Nullable Currency balanceType) {
viewModel.getLoyaltyCard().setBalanceType(balanceType);
viewModel.setHasChanged(true);
}
protected void setLoyaltyCardBalanceType(@Nullable Currency balanceType) {
viewModel.getLoyaltyCard().setBalanceType(balanceType);
protected void setLoyaltyCardBalance(@NonNull BigDecimal balance) {
viewModel.getLoyaltyCard().setBalance(balance);
viewModel.setHasChanged(true);
}
@@ -329,8 +329,8 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
groupsChips = binding.groupChips;
validFromField = binding.validFromField;
expiryField = binding.expiryField;
balanceField = binding.balanceField;
balanceCurrencyField = binding.balanceCurrencyField;
balanceField = binding.balanceField;
cardIdFieldView = binding.cardIdView;
barcodeIdField = binding.barcodeIdField;
barcodeTypeField = binding.barcodeTypeField;
@@ -373,33 +373,6 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
setMaterialDatePickerResultListener();
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus && !onResuming && !onRestoring) {
if (balanceField.getText().toString().isEmpty()) {
setLoyaltyCardBalance(BigDecimal.valueOf(0));
}
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType));
}
});
balanceField.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (onResuming || onRestoring) return;
try {
BigDecimal balance = Utils.parseBalance(s.toString(), viewModel.getLoyaltyCard().balanceType);
setLoyaltyCardBalance(balance);
balanceField.setError(null);
validBalance = true;
} catch (ParseException e) {
e.printStackTrace();
balanceField.setError(getString(R.string.balanceParsingFailed));
validBalance = false;
}
}
});
balanceCurrencyField.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
@@ -452,6 +425,33 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
}
});
balanceField.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus && !onResuming && !onRestoring) {
if (balanceField.getText().toString().isEmpty()) {
setLoyaltyCardBalance(BigDecimal.valueOf(0));
}
balanceField.setText(Utils.formatBalanceWithoutCurrencySymbol(viewModel.getLoyaltyCard().balance, viewModel.getLoyaltyCard().balanceType));
}
});
balanceField.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (onResuming || onRestoring) return;
try {
BigDecimal balance = Utils.parseBalance(s.toString(), viewModel.getLoyaltyCard().balanceType);
setLoyaltyCardBalance(balance);
balanceField.setError(null);
validBalance = true;
} catch (ParseException e) {
e.printStackTrace();
balanceField.setError(getString(R.string.balanceParsingFailed));
validBalance = false;
}
}
});
cardIdFieldView.addTextChangedListener(new SimpleTextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

View File

@@ -1,8 +1,9 @@
package protect.card_locker;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.database.sqlite.SQLiteDatabase;
@@ -704,10 +705,22 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(LoyaltyCardViewActivity.this);
builder.setTitle(R.string.cardId);
builder.setView(cardIdView);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss());
builder.setNeutralButton(R.string.copy_value, (dialog, which) -> {
copyCardIdToClipboard();
});
AlertDialog dialog = builder.create();
dialog.show();
});
binding.mainImageDescription.setOnLongClickListener(view -> {
if (mainImageIndex != 0) {
// Don't copy to clipboard, we're showing something else
return false;
}
copyCardIdToClipboard();
return true;
});
int backgroundHeaderColor = Utils.getHeaderColor(this, loyaltyCard);
@@ -1247,4 +1260,20 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
);
}
}
private void copyCardIdToClipboard() {
// Take the value thats already displayed to the user
String value = loyaltyCard.cardId;
if (value == null || value.isEmpty()) {
Toast.makeText(this, R.string.nothing_to_copy, Toast.LENGTH_SHORT).show();
return;
}
ClipboardManager cm = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.cardId), value);
cm.setPrimaryClip(clip);
Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}

View File

@@ -1,882 +0,0 @@
package protect.card_locker;
import android.app.Activity;
import android.app.SearchManager;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.CursorIndexOutOfBoundsException;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.core.splashscreen.SplashScreen;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import protect.card_locker.databinding.ContentMainBinding;
import protect.card_locker.databinding.MainActivityBinding;
import protect.card_locker.databinding.SortingOptionBinding;
import protect.card_locker.preferences.Settings;
import protect.card_locker.preferences.SettingsActivity;
public class MainActivity extends CatimaAppCompatActivity implements LoyaltyCardCursorAdapter.CardAdapterListener {
private MainActivityBinding binding;
private ContentMainBinding contentMainBinding;
private static final String TAG = "Catima";
public static final String RESTART_ACTIVITY_INTENT = "restart_activity_intent";
private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
static final String STATE_SEARCH_QUERY = "SEARCH_QUERY";
private SQLiteDatabase mDatabase;
private LoyaltyCardCursorAdapter mAdapter;
private ActionMode mCurrentActionMode;
private SearchView mSearchView;
private int mLoyaltyCardCount = 0;
protected String mFilter = "";
private String currentQuery = "";
private String finalQuery = "";
protected Object mGroup = null;
protected DBHelper.LoyaltyCardOrder mOrder = DBHelper.LoyaltyCardOrder.Alpha;
protected DBHelper.LoyaltyCardOrderDirection mOrderDirection = DBHelper.LoyaltyCardOrderDirection.Ascending;
protected int selectedTab = 0;
private RecyclerView mCardList;
private View mHelpSection;
private View mNoMatchingCardsText;
private View mNoGroupCardsText;
private TabLayout groupsTabLayout;
private Runnable mUpdateLoyaltyCardListRunnable;
private ActivityResultLauncher<Intent> mBarcodeScannerLauncher;
private ActivityResultLauncher<Intent> mSettingsLauncher;
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode inputMode, Menu inputMenu) {
inputMode.getMenuInflater().inflate(R.menu.card_longclick_menu, inputMenu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode inputMode, Menu inputMenu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode inputMode, MenuItem inputItem) {
if (inputItem.getItemId() == R.id.action_share) {
final ImportURIHelper importURIHelper = new ImportURIHelper(MainActivity.this);
try {
importURIHelper.startShareIntent(mAdapter.getSelectedItems());
} catch (UnsupportedEncodingException e) {
Toast.makeText(MainActivity.this, R.string.failedGeneratingShareURL, Toast.LENGTH_LONG).show();
e.printStackTrace();
}
inputMode.finish();
return true;
} else if (inputItem.getItemId() == R.id.action_edit) {
if (mAdapter.getSelectedItemCount() != 1) {
throw new IllegalArgumentException("Cannot edit more than 1 card at a time");
}
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = new Bundle();
bundle.putInt(LoyaltyCardEditActivity.BUNDLE_ID, mAdapter.getSelectedItems().get(0).id);
bundle.putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true);
intent.putExtras(bundle);
startActivity(intent);
inputMode.finish();
return true;
} else if (inputItem.getItemId() == R.id.action_delete) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(MainActivity.this);
// The following may seem weird, but it is necessary to give translators enough flexibility.
// For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11".
// So while in English the extra non-plural form seems unnecessary duplication, it is necessary to give translators enough flexibility.
// In here, we use the plain string when meaning exactly 1, and otherwise use the plural forms
if (mAdapter.getSelectedItemCount() == 1) {
builder.setTitle(R.string.deleteTitle);
builder.setMessage(R.string.deleteConfirmation);
} else {
builder.setTitle(getResources().getQuantityString(R.plurals.deleteCardsTitle, mAdapter.getSelectedItemCount(), mAdapter.getSelectedItemCount()));
builder.setMessage(getResources().getQuantityString(R.plurals.deleteCardsConfirmation, mAdapter.getSelectedItemCount(), mAdapter.getSelectedItemCount()));
}
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Deleting card: " + loyaltyCard.id);
DBHelper.deleteLoyaltyCard(mDatabase, MainActivity.this, loyaltyCard.id);
ShortcutHelper.removeShortcut(MainActivity.this, loyaltyCard.id);
}
TabLayout.Tab tab = groupsTabLayout.getTabAt(selectedTab);
mGroup = tab != null ? tab.getTag() : null;
updateLoyaltyCardList(true);
dialog.dismiss();
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
return true;
} else if (inputItem.getItemId() == R.id.action_archive) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Archiving card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1);
ShortcutHelper.removeShortcut(MainActivity.this, loyaltyCard.id);
updateLoyaltyCardList(false);
inputMode.finish();
invalidateOptionsMenu();
}
return true;
} else if (inputItem.getItemId() == R.id.action_unarchive) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Unarchiving card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 0);
updateLoyaltyCardList(false);
inputMode.finish();
invalidateOptionsMenu();
}
return true;
} else if (inputItem.getItemId() == R.id.action_star) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Starring card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 1);
updateLoyaltyCardList(false);
inputMode.finish();
}
return true;
} else if (inputItem.getItemId() == R.id.action_unstar) {
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.d(TAG, "Unstarring card: " + loyaltyCard.id);
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 0);
updateLoyaltyCardList(false);
inputMode.finish();
}
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode inputMode) {
mAdapter.clearSelections();
mCurrentActionMode = null;
}
};
@Override
protected void onCreate(Bundle inputSavedInstanceState) {
SplashScreen.installSplashScreen(this);
super.onCreate(inputSavedInstanceState);
// Delete old cache files
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
new Thread(() -> {
long twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24);
File[] tempFiles = getCacheDir().listFiles();
if (tempFiles == null) {
Log.e(TAG, "getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup...");
return;
}
for (File file : tempFiles) {
if (file.lastModified() < twentyFourHoursAgo) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete cache file " + file.getPath());
}
};
}
}).start();
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
extractIntentFields(getIntent());
binding = MainActivityBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
Utils.applyWindowInsets(binding.getRoot());
setSupportActionBar(binding.toolbar);
groupsTabLayout = binding.groups;
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot());
mDatabase = new DBHelper(this).getWritableDatabase();
mUpdateLoyaltyCardListRunnable = () -> {
updateLoyaltyCardList(false);
};
groupsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
selectedTab = tab.getPosition();
Log.d("onTabSelected", "Tab Position " + tab.getPosition());
mGroup = tab.getTag();
updateLoyaltyCardList(false);
// Store active tab in Shared Preference to restore next app launch
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
Context.MODE_PRIVATE);
SharedPreferences.Editor activeTabPrefEditor = activeTabPref.edit();
activeTabPrefEditor.putInt(getString(R.string.sharedpreference_active_tab), tab.getPosition());
activeTabPrefEditor.apply();
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
});
mHelpSection = contentMainBinding.helpSection;
mNoMatchingCardsText = contentMainBinding.noMatchingCardsText;
mNoGroupCardsText = contentMainBinding.noGroupCardsText;
mCardList = contentMainBinding.list;
mAdapter = new LoyaltyCardCursorAdapter(this, null, this, mUpdateLoyaltyCardListRunnable);
mCardList.setAdapter(mAdapter);
registerForContextMenu(mCardList);
mBarcodeScannerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
// Exit early if the user cancelled the scan (pressed back/home)
if (result.getResultCode() != RESULT_OK) {
return;
}
Intent editIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
editIntent.putExtras(result.getData().getExtras());
startActivity(editIntent);
});
mSettingsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent intent = result.getData();
if (intent != null && intent.getBooleanExtra(RESTART_ACTIVITY_INTENT, false)) {
recreate();
}
}
});
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (mSearchView != null && !mSearchView.isIconified()) {
mSearchView.setIconified(true);
} else {
finish();
}
}
});
}
@Override
protected void onResume() {
super.onResume();
if (mCurrentActionMode != null) {
mAdapter.clearSelections();
mCurrentActionMode.finish();
}
if (mSearchView != null && !mSearchView.isIconified()) {
mFilter = mSearchView.getQuery().toString();
}
// Start of active tab logic
updateTabGroups(groupsTabLayout);
// Restore selected tab from Shared Preference
SharedPreferences activeTabPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
Context.MODE_PRIVATE);
selectedTab = activeTabPref.getInt(getString(R.string.sharedpreference_active_tab), 0);
// Restore sort preferences from Shared Preferences
mOrder = Utils.getLoyaltyCardOrder(this);
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this);
mGroup = null;
if (groupsTabLayout.getTabCount() != 0) {
TabLayout.Tab tab = groupsTabLayout.getTabAt(selectedTab);
if (tab == null) {
tab = groupsTabLayout.getTabAt(0);
}
groupsTabLayout.selectTab(tab);
assert tab != null;
mGroup = tab.getTag();
} else {
scaleScreen();
}
updateLoyaltyCardList(true);
// End of active tab logic
FloatingActionButton addButton = binding.fabAdd;
addButton.setOnClickListener(v -> {
Intent intent = new Intent(getApplicationContext(), ScanActivity.class);
Bundle bundle = new Bundle();
if (selectedTab != 0) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, groupsTabLayout.getTabAt(selectedTab).getText().toString());
}
intent.putExtras(bundle);
mBarcodeScannerLauncher.launch(intent);
});
addButton.bringToFront();
var layoutManager = (GridLayoutManager) mCardList.getLayoutManager();
if (layoutManager != null) {
var settings = new Settings(this);
layoutManager.setSpanCount(settings.getPreferredColumnCount());
}
}
private void displayCardSetupOptions(Menu menu, boolean shouldShow) {
for (int id : new int[]{R.id.action_search, R.id.action_display_options, R.id.action_sort}) {
menu.findItem(id).setVisible(shouldShow);
}
}
private void updateLoyaltyCardCount() {
mLoyaltyCardCount = DBHelper.getLoyaltyCardCount(mDatabase);
}
private void updateLoyaltyCardList(boolean updateCount) {
Group group = null;
if (mGroup != null) {
group = (Group) mGroup;
}
mAdapter.swapCursor(DBHelper.getLoyaltyCardCursor(mDatabase, mFilter, group, mOrder, mOrderDirection, mAdapter.showingArchivedCards() ? DBHelper.LoyaltyCardArchiveFilter.All : DBHelper.LoyaltyCardArchiveFilter.Unarchived));
if (updateCount) {
updateLoyaltyCardCount();
// Update menu icons if necessary
invalidateOptionsMenu();
}
if (mLoyaltyCardCount > 0) {
// We want the cardList to be visible regardless of the filtered match count
// to ensure that the noMatchingCardsText doesn't end up being shown below
// the keyboard
mHelpSection.setVisibility(View.GONE);
mNoGroupCardsText.setVisibility(View.GONE);
if (mAdapter.getItemCount() > 0) {
mCardList.setVisibility(View.VISIBLE);
mNoMatchingCardsText.setVisibility(View.GONE);
} else {
mCardList.setVisibility(View.GONE);
if (!mFilter.isEmpty()) {
// Actual Empty Search Result
mNoMatchingCardsText.setVisibility(View.VISIBLE);
mNoGroupCardsText.setVisibility(View.GONE);
} else {
// Group Tab with no Group Cards
mNoMatchingCardsText.setVisibility(View.GONE);
mNoGroupCardsText.setVisibility(View.VISIBLE);
}
}
} else {
mCardList.setVisibility(View.GONE);
mHelpSection.setVisibility(View.VISIBLE);
mNoMatchingCardsText.setVisibility(View.GONE);
mNoGroupCardsText.setVisibility(View.GONE);
}
if (mCurrentActionMode != null) {
mCurrentActionMode.finish();
}
new ListWidget().updateAll(mAdapter.mContext);
}
private void processParseResultList(List<ParseResult> parseResultList, String group, boolean closeAppOnNoBarcode) {
if (parseResultList.isEmpty()) {
throw new IllegalArgumentException("parseResultList may not be empty");
}
Utils.makeUserChooseParseResultFromList(MainActivity.this, parseResultList, new ParseResultListDisambiguatorCallback() {
@Override
public void onUserChoseParseResult(ParseResult parseResult) {
Intent intent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle bundle = parseResult.toLoyaltyCardBundle(MainActivity.this);
if (group != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group);
}
intent.putExtras(bundle);
startActivity(intent);
}
@Override
public void onUserDismissedSelector() {
if (closeAppOnNoBarcode) {
finish();
}
}
});
}
private void onSharedIntent(Intent intent) {
String receivedAction = intent.getAction();
String receivedType = intent.getType();
if (receivedAction == null || receivedType == null) {
return;
}
List<ParseResult> parseResultList;
// Check for shared text
if (receivedAction.equals(Intent.ACTION_SEND) && receivedType.equals("text/plain")) {
LoyaltyCard loyaltyCard = new LoyaltyCard();
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT));
parseResultList = Collections.singletonList(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
} else {
// Parse whatever file was sent, regardless of opening or sharing
Uri data;
if (receivedAction.equals(Intent.ACTION_VIEW)) {
data = intent.getData();
} else if (receivedAction.equals(Intent.ACTION_SEND)) {
data = intent.getParcelableExtra(Intent.EXTRA_STREAM);
} else {
Log.e(TAG, "Wrong action type to parse intent");
return;
}
if (receivedType.startsWith("image/")) {
parseResultList = Utils.retrieveBarcodesFromImage(this, data);
} else if (receivedType.equals("application/pdf")) {
parseResultList = Utils.retrieveBarcodesFromPdf(this, data);
} else if (Arrays.asList("application/vnd.apple.pkpass", "application/vnd-com.apple.pkpass").contains(receivedType)) {
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
} else if (receivedType.equals("application/vnd.espass-espass")) {
// FIXME: espass is not pkpass
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data);
} else if (receivedType.equals("application/vnd.apple.pkpasses")) {
parseResultList = Utils.retrieveBarcodesFromPkPasses(this, data);
} else {
Log.e(TAG, "Wrong mime-type");
return;
}
}
// Give up if we should parse but there is nothing to parse
if (parseResultList == null || parseResultList.isEmpty()) {
finish();
return;
}
processParseResultList(parseResultList, null, true);
}
private void extractIntentFields(Intent intent) {
onSharedIntent(intent);
}
public void updateTabGroups(TabLayout groupsTabLayout) {
List<Group> newGroups = DBHelper.getGroups(mDatabase);
if (newGroups.size() == 0) {
groupsTabLayout.removeAllTabs();
groupsTabLayout.setVisibility(View.GONE);
return;
}
groupsTabLayout.removeAllTabs();
TabLayout.Tab allTab = groupsTabLayout.newTab();
allTab.setText(R.string.all);
allTab.setTag(null);
groupsTabLayout.addTab(allTab, false);
for (Group group : newGroups) {
TabLayout.Tab tab = groupsTabLayout.newTab();
tab.setText(group._id);
tab.setTag(group);
groupsTabLayout.addTab(tab, false);
}
groupsTabLayout.setVisibility(View.VISIBLE);
}
@Override
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
finalQuery = currentQuery;
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
if (mSearchView != null) {
outState.putString(STATE_SEARCH_QUERY, finalQuery);
}
}
@Override
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "");
}
@Override
public boolean onCreateOptionsMenu(Menu inputMenu) {
getMenuInflater().inflate(R.menu.main_menu, inputMenu);
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
if (searchManager != null) {
MenuItem searchMenuItem = inputMenu.findItem(R.id.action_search);
mSearchView = (SearchView) searchMenuItem.getActionView();
mSearchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
mSearchView.setSubmitButtonEnabled(false);
mSearchView.setOnCloseListener(() -> {
invalidateOptionsMenu();
return false;
});
/*
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
* and collapses the search view at the same time.
* This brings back the old behavior on Android 12 and lower: pressing Back once
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
@Override
public boolean onMenuItemActionExpand(@NonNull MenuItem item) {
return true;
}
@Override
public boolean onMenuItemActionCollapse(@NonNull MenuItem item) {
if (mSearchView.hasFocus()) {
mSearchView.clearFocus();
return false;
}
currentQuery = "";
mFilter = "";
updateLoyaltyCardList(false);
return true;
}
});
}
mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
mFilter = newText;
// New logic to ensure search history after coming back from picked card - user will see the last search query
if (newText.isEmpty()) {
if(!finalQuery.isEmpty()){
// Setting the query text for user after coming back from picked card from finalQuery
mSearchView.setQuery(finalQuery, false);
}
else if(!currentQuery.isEmpty()){
// Else if is needed in case user deletes search - expected behaviour is to show all cards
currentQuery = "";
mSearchView.setQuery(currentQuery, false);
}
} else {
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
currentQuery = newText;
}
TabLayout.Tab currentTab = groupsTabLayout.getTabAt(groupsTabLayout.getSelectedTabPosition());
mGroup = currentTab != null ? currentTab.getTag() : null;
updateLoyaltyCardList(false);
return true;
}
});
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
if(!finalQuery.isEmpty()){
// Expand the search view to show the query
searchMenuItem.expandActionView();
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
finalQuery = "";
mSearchView.setQuery(currentQuery, false);
}
}
return super.onCreateOptionsMenu(inputMenu);
}
@Override
public boolean onOptionsItemSelected(MenuItem inputItem) {
int id = inputItem.getItemId();
if (id == android.R.id.home) {
getOnBackPressedDispatcher().onBackPressed();
}
if (id == R.id.action_display_options) {
mAdapter.showDisplayOptionsDialog();
invalidateOptionsMenu();
return true;
}
if (id == R.id.action_sort) {
AtomicInteger currentIndex = new AtomicInteger();
List<DBHelper.LoyaltyCardOrder> loyaltyCardOrders = Arrays.asList(DBHelper.LoyaltyCardOrder.values());
for (int i = 0; i < loyaltyCardOrders.size(); i++) {
if (mOrder == loyaltyCardOrders.get(i)) {
currentIndex.set(i);
break;
}
}
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(MainActivity.this);
builder.setTitle(R.string.sort_by);
SortingOptionBinding sortingOptionBinding = SortingOptionBinding
.inflate(LayoutInflater.from(MainActivity.this), null, false);
final View customLayout = sortingOptionBinding.getRoot();
builder.setView(customLayout);
CheckBox showReversed = sortingOptionBinding.checkBoxReverse;
showReversed.setChecked(mOrderDirection == DBHelper.LoyaltyCardOrderDirection.Descending);
builder.setSingleChoiceItems(R.array.sort_types_array, currentIndex.get(), (dialog, which) -> currentIndex.set(which));
builder.setPositiveButton(R.string.sort, (dialog, which) -> {
setSort(
loyaltyCardOrders.get(currentIndex.get()),
showReversed.isChecked() ? DBHelper.LoyaltyCardOrderDirection.Descending : DBHelper.LoyaltyCardOrderDirection.Ascending
);
new ListWidget().updateAll(this);
dialog.dismiss();
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
return true;
}
if (id == R.id.action_manage_groups) {
Intent i = new Intent(getApplicationContext(), ManageGroupsActivity.class);
startActivity(i);
return true;
}
if (id == R.id.action_import_export) {
Intent i = new Intent(getApplicationContext(), ImportExportActivity.class);
startActivity(i);
return true;
}
if (id == R.id.action_settings) {
Intent i = new Intent(getApplicationContext(), SettingsActivity.class);
mSettingsLauncher.launch(i);
return true;
}
if (id == R.id.action_about) {
Intent i = new Intent(getApplicationContext(), AboutActivity.class);
startActivity(i);
return true;
}
return super.onOptionsItemSelected(inputItem);
}
private void setSort(DBHelper.LoyaltyCardOrder order, DBHelper.LoyaltyCardOrderDirection direction) {
// Update values
mOrder = order;
mOrderDirection = direction;
// Store in Shared Preference to restore next app launch
SharedPreferences sortPref = getApplicationContext().getSharedPreferences(
getString(R.string.sharedpreference_sort),
Context.MODE_PRIVATE);
SharedPreferences.Editor sortPrefEditor = sortPref.edit();
sortPrefEditor.putString(getString(R.string.sharedpreference_sort_order), order.name());
sortPrefEditor.putString(getString(R.string.sharedpreference_sort_direction), direction.name());
sortPrefEditor.apply();
// Update card list
updateLoyaltyCardList(false);
}
@Override
public void onRowLongClicked(int inputPosition) {
enableActionMode(inputPosition);
}
private void enableActionMode(int inputPosition) {
if (mCurrentActionMode == null) {
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback);
}
toggleSelection(inputPosition);
}
private void scaleScreen() {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int screenHeight = displayMetrics.heightPixels;
float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
boolean shouldScaleSmaller = screenHeight < mediumSizePx;
binding.include.welcomeIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
}
private void toggleSelection(int inputPosition) {
mAdapter.toggleSelection(inputPosition);
int count = mAdapter.getSelectedItemCount();
if (count == 0) {
mCurrentActionMode.finish();
} else {
mCurrentActionMode.setTitle(getResources().getQuantityString(R.plurals.selectedCardCount, count, count));
MenuItem editItem = mCurrentActionMode.getMenu().findItem(R.id.action_edit);
MenuItem archiveItem = mCurrentActionMode.getMenu().findItem(R.id.action_archive);
MenuItem unarchiveItem = mCurrentActionMode.getMenu().findItem(R.id.action_unarchive);
MenuItem starItem = mCurrentActionMode.getMenu().findItem(R.id.action_star);
MenuItem unstarItem = mCurrentActionMode.getMenu().findItem(R.id.action_unstar);
boolean hasStarred = false;
boolean hasUnstarred = false;
boolean hasArchived = false;
boolean hasUnarchived = false;
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
if (loyaltyCard.starStatus == 1) {
hasStarred = true;
} else {
hasUnstarred = true;
}
if (loyaltyCard.archiveStatus == 1) {
hasArchived = true;
} else {
hasUnarchived = true;
}
// We have all types, no need to keep checking
if (hasStarred && hasUnstarred && hasArchived && hasUnarchived) {
break;
}
}
unarchiveItem.setVisible(hasArchived);
archiveItem.setVisible(hasUnarchived);
if (count == 1) {
starItem.setVisible(!hasStarred);
unstarItem.setVisible(!hasUnstarred);
editItem.setVisible(true);
editItem.setEnabled(true);
} else {
starItem.setVisible(hasUnstarred);
unstarItem.setVisible(hasStarred);
editItem.setVisible(false);
editItem.setEnabled(false);
}
mCurrentActionMode.invalidate();
}
}
@Override
public void onRowClicked(int inputPosition) {
if (mAdapter.getSelectedItemCount() > 0) {
enableActionMode(inputPosition);
} else {
// FIXME
//
// There is a really nasty edge case that can happen when someone taps a card but right
// after it swipes (very small window, hard to reproduce). The cursor gets replaced and
// may not have a card at the ID number that is returned from onRowClicked.
//
// The proper fix, obviously, would involve makes sure an onFling can't happen while a
// click is being processed. Sadly, I have not yet found a way to make that possible.
LoyaltyCard loyaltyCard;
try {
loyaltyCard = mAdapter.getCard(inputPosition);
} catch (CursorIndexOutOfBoundsException e) {
Log.w(TAG, "Prevented crash from tap + swipe on ID " + inputPosition + ": " + e);
return;
}
Intent intent = new Intent(this, LoyaltyCardViewActivity.class);
intent.setAction("");
final Bundle b = new Bundle();
b.putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id);
ArrayList<Integer> cardList = new ArrayList<>();
for (int i = 0; i < mAdapter.getItemCount(); i++) {
cardList.add(mAdapter.getCard(i).id);
}
b.putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList);
intent.putExtras(b);
startActivity(intent);
}
}
}

View File

@@ -0,0 +1,945 @@
package protect.card_locker
import android.app.SearchManager
import android.content.DialogInterface
import android.content.Intent
import android.database.CursorIndexOutOfBoundsException
import android.database.sqlite.SQLiteDatabase
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import protect.card_locker.DBHelper.LoyaltyCardOrder
import protect.card_locker.DBHelper.LoyaltyCardOrderDirection
import protect.card_locker.LoyaltyCardCursorAdapter.CardAdapterListener
import protect.card_locker.databinding.ContentMainBinding
import protect.card_locker.databinding.MainActivityBinding
import protect.card_locker.databinding.SortingOptionBinding
import protect.card_locker.preferences.Settings
import protect.card_locker.preferences.SettingsActivity
import java.io.UnsupportedEncodingException
import java.util.concurrent.atomic.AtomicInteger
import androidx.core.content.edit
class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
private lateinit var binding: MainActivityBinding
private lateinit var contentMainBinding: ContentMainBinding
private lateinit var mDatabase: SQLiteDatabase
private lateinit var mAdapter: LoyaltyCardCursorAdapter
private var mCurrentActionMode: ActionMode? = null
private var mSearchView: SearchView? = null
private var mLoyaltyCardCount = 0
@JvmField
var mFilter: String = ""
private var currentQuery = ""
private var finalQuery = ""
private var mGroup: Any? = null
private var mOrder: LoyaltyCardOrder = LoyaltyCardOrder.Alpha
private var mOrderDirection: LoyaltyCardOrderDirection = LoyaltyCardOrderDirection.Ascending
private var selectedTab: Int = 0
private lateinit var groupsTabLayout: TabLayout
private lateinit var mUpdateLoyaltyCardListRunnable: Runnable
private lateinit var mBarcodeScannerLauncher: ActivityResultLauncher<Intent>
private lateinit var mSettingsLauncher: ActivityResultLauncher<Intent>
private val mCurrentActionModeCallback: ActionMode.Callback = object : ActionMode.Callback {
override fun onCreateActionMode(inputMode: ActionMode, inputMenu: Menu?): Boolean {
inputMode.menuInflater.inflate(R.menu.card_longclick_menu, inputMenu)
return true
}
override fun onPrepareActionMode(inputMode: ActionMode?, inputMenu: Menu?): Boolean {
return false
}
override fun onActionItemClicked(inputMode: ActionMode, inputItem: MenuItem): Boolean {
when (inputItem.itemId) {
R.id.action_share -> {
try {
ImportURIHelper(this@MainActivity).startShareIntent(mAdapter.getSelectedItems())
} catch (e: UnsupportedEncodingException) {
Toast.makeText(
this@MainActivity,
R.string.failedGeneratingShareURL,
Toast.LENGTH_LONG
).show()
e.printStackTrace()
}
inputMode.finish()
return true
}
R.id.action_edit -> {
require(mAdapter.selectedItemCount == 1) { "Cannot edit more than 1 card at a time" }
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(Bundle().apply {
putInt(
LoyaltyCardEditActivity.BUNDLE_ID,
mAdapter.getSelectedItems()[0].id
)
putBoolean(LoyaltyCardEditActivity.BUNDLE_UPDATE, true)
})
}
)
inputMode.finish()
return true
}
R.id.action_duplicate -> {
require(mAdapter.selectedItemCount == 1) { "Cannot duplicate more than 1 card at a time" }
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(Bundle().apply {
putInt(
LoyaltyCardEditActivity.BUNDLE_ID,
mAdapter.getSelectedItems()[0].id
)
putBoolean(LoyaltyCardEditActivity.BUNDLE_DUPLICATE_ID, true)
})
}
)
inputMode.finish()
return true
}
R.id.action_delete -> {
MaterialAlertDialogBuilder(this@MainActivity).apply {
// The following may seem weird, but it is necessary to give translators enough flexibility.
// For example, in Russian, Android's plural quantity "one" actually refers to "any number ending on 1 but not ending in 11".
// So while in English the extra non-plural form seems unnecessary duplication, it is necessary to give translators enough flexibility.
// In here, we use the plain string when meaning exactly 1, and otherwise use the plural forms
if (mAdapter.selectedItemCount == 1) {
setTitle(R.string.deleteTitle)
setMessage(R.string.deleteConfirmation)
} else {
setTitle(
getResources().getQuantityString(
R.plurals.deleteCardsTitle,
mAdapter.selectedItemCount,
mAdapter.selectedItemCount
)
)
setMessage(
getResources().getQuantityString(
R.plurals.deleteCardsConfirmation,
mAdapter.selectedItemCount,
mAdapter.selectedItemCount
)
)
}
setPositiveButton(
R.string.confirm
) { dialog, _ ->
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Deleting card: " + loyaltyCard.id)
DBHelper.deleteLoyaltyCard(mDatabase, this@MainActivity, loyaltyCard.id)
ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id)
}
val tab = groupsTabLayout.getTabAt(selectedTab)
mGroup = tab?.tag
updateLoyaltyCardList(true)
dialog.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}.create().show()
return true
}
R.id.action_archive -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Archiving card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1)
ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id)
updateLoyaltyCardList(false)
inputMode.finish()
invalidateOptionsMenu()
}
return true
}
R.id.action_unarchive -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Unarchiving card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 0)
updateLoyaltyCardList(false)
inputMode.finish()
invalidateOptionsMenu()
}
return true
}
R.id.action_star -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Starring card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 1)
updateLoyaltyCardList(false)
inputMode.finish()
}
return true
}
R.id.action_unstar -> {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Unstarring card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardStarStatus(mDatabase, loyaltyCard.id, 0)
updateLoyaltyCardList(false)
inputMode.finish()
}
return true
}
}
return false
}
override fun onDestroyActionMode(inputMode: ActionMode?) {
mAdapter.clearSelections()
mCurrentActionMode = null
}
}
override fun onCreate(inputSavedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(inputSavedInstanceState)
// Delete old cache files
// These could be temporary images for the cropper, temporary images in LoyaltyCard toBundle/writeParcel/ etc.
Thread {
val twentyFourHoursAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24)
val tempFiles = cacheDir.listFiles()
if (tempFiles == null) {
Log.e(
TAG,
"getCacheDir().listFiles() somehow returned null, this should never happen... Skipping cache cleanup..."
)
return@Thread
}
for (file in tempFiles) {
if (file.lastModified() < twentyFourHoursAgo) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete cache file " + file.path)
}
}
}
}.start()
// We should extract the share intent after we called the super.onCreate as it may need to spawn a dialog window and the app needs to be initialized to not crash
extractIntentFields(intent)
binding = MainActivityBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
Utils.applyWindowInsets(binding.getRoot())
setSupportActionBar(binding.toolbar)
groupsTabLayout = binding.groups
contentMainBinding = ContentMainBinding.bind(binding.include.getRoot())
mDatabase = DBHelper(this).writableDatabase
mUpdateLoyaltyCardListRunnable = Runnable {
updateLoyaltyCardList(false)
}
groupsTabLayout.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
selectedTab = tab.position
Log.d("onTabSelected", "Tab Position " + tab.position)
mGroup = tab.tag
updateLoyaltyCardList(false)
// Store active tab in Shared Preference to restore next app launch
applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
).edit {
putInt(
getString(R.string.sharedpreference_active_tab),
tab.position
)
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
mAdapter = LoyaltyCardCursorAdapter(this, null, this, mUpdateLoyaltyCardListRunnable)
contentMainBinding.list.setAdapter(mAdapter)
registerForContextMenu(contentMainBinding.list)
mBarcodeScannerLauncher = registerForActivityResult(
StartActivityForResult(),
ActivityResultCallback registerForActivityResult@{ result: ActivityResult? ->
// Exit early if the user cancelled the scan (pressed back/home)
if (result == null || result.resultCode != RESULT_OK) {
return@registerForActivityResult
}
startActivity(
Intent(applicationContext, LoyaltyCardEditActivity::class.java).apply {
putExtras(result.data!!.extras!!)
}
)
})
mSettingsLauncher = registerForActivityResult(
StartActivityForResult()
) { result: ActivityResult? ->
if (result?.resultCode == RESULT_OK) {
val intent = result.data
if (intent != null && intent.getBooleanExtra(RESTART_ACTIVITY_INTENT, false)) {
recreate()
}
}
}
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (mSearchView != null && !mSearchView!!.isIconified) {
mSearchView!!.isIconified = true
} else {
finish()
}
}
})
}
override fun onResume() {
super.onResume()
if (mCurrentActionMode != null) {
mAdapter.clearSelections()
mCurrentActionMode!!.finish()
}
if (mSearchView != null && !mSearchView!!.isIconified) {
mFilter = mSearchView!!.query.toString()
}
// Start of active tab logic
updateTabGroups(groupsTabLayout)
// Restore selected tab from Shared Preference
selectedTab = applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_active_tab),
MODE_PRIVATE
).getInt(getString(R.string.sharedpreference_active_tab), 0)
// Restore sort preferences from Shared Preferences
mOrder = Utils.getLoyaltyCardOrder(this)
mOrderDirection = Utils.getLoyaltyCardOrderDirection(this)
mGroup = null
if (groupsTabLayout.tabCount != 0) {
var tab = groupsTabLayout.getTabAt(selectedTab)
if (tab == null) {
tab = groupsTabLayout.getTabAt(0)
}
groupsTabLayout.selectTab(tab)
checkNotNull(tab)
mGroup = tab.tag
} else {
scaleScreen()
}
updateLoyaltyCardList(true)
// End of active tab logic
binding.fabAdd.setOnClickListener {
mBarcodeScannerLauncher.launch(
Intent(applicationContext, ScanActivity::class.java).apply {
putExtras(Bundle().apply {
if (selectedTab != 0) {
putString(
LoyaltyCardEditActivity.BUNDLE_ADDGROUP,
groupsTabLayout.getTabAt(selectedTab)!!.text.toString()
)
}
})
}
)
}
binding.fabAdd.bringToFront()
val layoutManager = contentMainBinding.list.layoutManager as GridLayoutManager?
if (layoutManager != null) {
val settings = Settings(this)
layoutManager.setSpanCount(settings.getPreferredColumnCount())
}
}
private fun displayCardSetupOptions(menu: Menu, shouldShow: Boolean) {
for (id in intArrayOf(R.id.action_search, R.id.action_display_options, R.id.action_sort)) {
menu.findItem(id).isVisible = shouldShow
}
}
private fun updateLoyaltyCardCount() {
mLoyaltyCardCount = DBHelper.getLoyaltyCardCount(mDatabase)
}
private fun updateLoyaltyCardList(updateCount: Boolean) {
var group: Group? = null
if (mGroup != null) {
group = mGroup as Group
}
mAdapter.swapCursor(
DBHelper.getLoyaltyCardCursor(
mDatabase,
mFilter,
group,
mOrder,
mOrderDirection,
if (mAdapter.showingArchivedCards()) DBHelper.LoyaltyCardArchiveFilter.All else DBHelper.LoyaltyCardArchiveFilter.Unarchived
)
)
if (updateCount) {
updateLoyaltyCardCount()
// Update menu icons if necessary
invalidateOptionsMenu()
}
if (mLoyaltyCardCount > 0) {
// We want the cardList to be visible regardless of the filtered match count
// to ensure that the noMatchingCardsText doesn't end up being shown below
// the keyboard
contentMainBinding.helpSection.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.GONE
if (mAdapter.itemCount > 0) {
contentMainBinding.list.visibility = View.VISIBLE
contentMainBinding.noMatchingCardsText.visibility = View.GONE
} else {
contentMainBinding.list.visibility = View.GONE
if (!mFilter.isEmpty()) {
// Actual Empty Search Result
contentMainBinding.noMatchingCardsText.visibility = View.VISIBLE
contentMainBinding.noGroupCardsText.visibility = View.GONE
} else {
// Group Tab with no Group Cards
contentMainBinding.noMatchingCardsText.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.VISIBLE
}
}
} else {
contentMainBinding.list.visibility = View.GONE
contentMainBinding.helpSection.visibility = View.VISIBLE
contentMainBinding.noMatchingCardsText.visibility = View.GONE
contentMainBinding.noGroupCardsText.visibility = View.GONE
}
if (mCurrentActionMode != null) {
mCurrentActionMode!!.finish()
}
ListWidget().updateAll(mAdapter.mContext)
}
private fun processParseResultList(
parseResultList: MutableList<ParseResult?>,
group: String?,
closeAppOnNoBarcode: Boolean
) {
require(!parseResultList.isEmpty()) { "parseResultList may not be empty" }
Utils.makeUserChooseParseResultFromList(
this@MainActivity,
parseResultList,
object : ParseResultListDisambiguatorCallback {
override fun onUserChoseParseResult(parseResult: ParseResult) {
val intent =
Intent(applicationContext, LoyaltyCardEditActivity::class.java)
val bundle = parseResult.toLoyaltyCardBundle(this@MainActivity)
if (group != null) {
bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, group)
}
intent.putExtras(bundle)
startActivity(intent)
}
override fun onUserDismissedSelector() {
if (closeAppOnNoBarcode) {
finish()
}
}
})
}
private fun onSharedIntent(intent: Intent) {
val receivedAction = intent.action
val receivedType = intent.type
if (receivedAction == null || receivedType == null) {
return
}
val parseResultList: MutableList<ParseResult?>?
// Check for shared text
if (receivedAction == Intent.ACTION_SEND && receivedType == "text/plain") {
val loyaltyCard = LoyaltyCard()
loyaltyCard.setCardId(intent.getStringExtra(Intent.EXTRA_TEXT)!!)
parseResultList = mutableListOf(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
} else {
// Parse whatever file was sent, regardless of opening or sharing
val data: Uri? = when (receivedAction) {
Intent.ACTION_VIEW -> {
intent.data
}
Intent.ACTION_SEND -> {
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
else -> {
Log.e(TAG, "Wrong action type to parse intent")
return
}
}
if (receivedType.startsWith("image/")) {
parseResultList = Utils.retrieveBarcodesFromImage(this, data)
} else if (receivedType == "application/pdf") {
parseResultList = Utils.retrieveBarcodesFromPdf(this, data)
} else if (mutableListOf<String?>(
"application/vnd.apple.pkpass",
"application/vnd-com.apple.pkpass"
).contains(receivedType)
) {
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
} else if (receivedType == "application/vnd.espass-espass") {
// FIXME: espass is not pkpass
// However, several users stated in https://github.com/CatimaLoyalty/Android/issues/2197 that the formats are extremely similar to the point they could rename an .espass file to .pkpass and have it imported
// So it makes sense to "unofficially" treat it as a PKPASS for now, even though not completely correct
parseResultList = Utils.retrieveBarcodesFromPkPass(this, data)
} else if (receivedType == "application/vnd.apple.pkpasses") {
parseResultList = Utils.retrieveBarcodesFromPkPasses(this, data)
} else {
Log.e(TAG, "Wrong mime-type")
return
}
}
// Give up if we should parse but there is nothing to parse
if (parseResultList == null || parseResultList.isEmpty()) {
finish()
return
}
processParseResultList(parseResultList, null, true)
}
private fun extractIntentFields(intent: Intent) {
onSharedIntent(intent)
}
fun updateTabGroups(groupsTabLayout: TabLayout) {
val newGroups = DBHelper.getGroups(mDatabase)
if (newGroups.isEmpty()) {
groupsTabLayout.removeAllTabs()
groupsTabLayout.visibility = View.GONE
return
}
groupsTabLayout.removeAllTabs()
groupsTabLayout.addTab(
groupsTabLayout.newTab().apply {
setText(R.string.all)
tag = null
},
false
)
for (group in newGroups) {
groupsTabLayout.addTab(
groupsTabLayout.newTab().apply {
text = group._id
tag = group
},
false
)
}
groupsTabLayout.visibility = View.VISIBLE
}
// Saving currentQuery to finalQuery for user, this will be used to restore search history, happens when user clicks a card from list
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
finalQuery = currentQuery
// Putting the query also into outState for later use in onRestoreInstanceState when rotating screen
if (mSearchView != null) {
outState.putString(STATE_SEARCH_QUERY, finalQuery)
}
}
// Restoring instance state when rotation of screen happens with the goal to restore search query for user
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
finalQuery = savedInstanceState.getString(STATE_SEARCH_QUERY, "")
}
override fun onCreateOptionsMenu(inputMenu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, inputMenu)
displayCardSetupOptions(inputMenu, mLoyaltyCardCount > 0)
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager?
if (searchManager != null) {
val searchMenuItem = inputMenu.findItem(R.id.action_search)
mSearchView = searchMenuItem.actionView as SearchView?
mSearchView!!.setSearchableInfo(searchManager.getSearchableInfo(componentName))
mSearchView!!.setSubmitButtonEnabled(false)
mSearchView!!.setOnCloseListener {
invalidateOptionsMenu()
false
}
/*
* On Android 13 and later, pressing Back while the search view is open hides the keyboard
* and collapses the search view at the same time.
* This brings back the old behavior on Android 12 and lower: pressing Back once
* hides the keyboard, press again while keyboard is hidden to collapse the search view.
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
if (mSearchView!!.hasFocus()) {
mSearchView!!.clearFocus()
return false
}
currentQuery = ""
mFilter = ""
updateLoyaltyCardList(false)
return true
}
})
}
mSearchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String): Boolean {
mFilter = newText
// New logic to ensure search history after coming back from picked card - user will see the last search query
if (newText.isEmpty()) {
if (!finalQuery.isEmpty()) {
// Setting the query text for user after coming back from picked card from finalQuery
mSearchView!!.setQuery(finalQuery, false)
} else if (!currentQuery.isEmpty()) {
// Else if is needed in case user deletes search - expected behaviour is to show all cards
currentQuery = ""
mSearchView!!.setQuery(currentQuery, false)
}
} else {
// Setting search query each time user changes the text in search to temporary variable to be used later in finalQuery String which will be used to restore search history
currentQuery = newText
}
val currentTab =
groupsTabLayout.getTabAt(groupsTabLayout.selectedTabPosition)
mGroup = currentTab?.tag
updateLoyaltyCardList(false)
return true
}
})
// Check if we came from a picked card back to search, in that case we want to show the search view with previous search query
if (!finalQuery.isEmpty()) {
// Expand the search view to show the query
searchMenuItem.expandActionView()
// Setting the query text to empty String due to behaviour of onQueryTextChange after coming back from picked card - onQueryTextChange is called automatically without users interaction
finalQuery = ""
mSearchView!!.setQuery(currentQuery, false)
}
}
return super.onCreateOptionsMenu(inputMenu)
}
override fun onOptionsItemSelected(inputItem: MenuItem): Boolean {
when (inputItem.itemId) {
android.R.id.home -> {
onBackPressedDispatcher.onBackPressed()
}
R.id.action_display_options -> {
mAdapter.showDisplayOptionsDialog()
invalidateOptionsMenu()
return true
}
R.id.action_sort -> {
val currentIndex = AtomicInteger()
val loyaltyCardOrders = listOf<LoyaltyCardOrder?>(*LoyaltyCardOrder.entries.toTypedArray())
for (i in loyaltyCardOrders.indices) {
if (mOrder == loyaltyCardOrders[i]) {
currentIndex.set(i)
break
}
}
MaterialAlertDialogBuilder(this@MainActivity).apply {
setTitle(R.string.sort_by)
val sortingOptionBinding = SortingOptionBinding.inflate(LayoutInflater.from(this@MainActivity), null, false)
val customLayout: View = sortingOptionBinding.getRoot()
setView(customLayout)
val showReversed = sortingOptionBinding.checkBoxReverse
showReversed.isChecked = mOrderDirection == LoyaltyCardOrderDirection.Descending
setSingleChoiceItems(
R.array.sort_types_array,
currentIndex.get()
) { _: DialogInterface?, which: Int ->
currentIndex.set(which)
}
setPositiveButton(
R.string.sort
) { dialog, _ ->
setSort(
loyaltyCardOrders[currentIndex.get()]!!,
if (showReversed.isChecked) LoyaltyCardOrderDirection.Descending else LoyaltyCardOrderDirection.Ascending
)
ListWidget().updateAll(this@MainActivity)
dialog?.dismiss()
}
setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
}.create().show()
return true
}
R.id.action_manage_groups -> {
startActivity(
Intent(applicationContext, ManageGroupsActivity::class.java)
)
return true
}
R.id.action_import_export -> {
startActivity(
Intent(applicationContext, ImportExportActivity::class.java)
)
return true
}
R.id.action_settings -> {
mSettingsLauncher.launch(
Intent(applicationContext, SettingsActivity::class.java)
)
return true
}
R.id.action_about -> {
startActivity(
Intent(applicationContext, AboutActivity::class.java)
)
return true
}
}
return super.onOptionsItemSelected(inputItem)
}
private fun setSort(order: LoyaltyCardOrder, direction: LoyaltyCardOrderDirection) {
// Update values
mOrder = order
mOrderDirection = direction
// Store in Shared Preference to restore next app launch
applicationContext.getSharedPreferences(
getString(R.string.sharedpreference_sort),
MODE_PRIVATE
).edit {
putString(
getString(R.string.sharedpreference_sort_order),
order.name
)
putString(
getString(R.string.sharedpreference_sort_direction),
direction.name
)
}
// Update card list
updateLoyaltyCardList(false)
}
override fun onRowLongClicked(inputPosition: Int) {
enableActionMode(inputPosition)
}
private fun enableActionMode(inputPosition: Int) {
if (mCurrentActionMode == null) {
mCurrentActionMode = startSupportActionMode(mCurrentActionModeCallback)
}
toggleSelection(inputPosition)
}
private fun scaleScreen() {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
val screenHeight = displayMetrics.heightPixels
val mediumSizePx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
MEDIUM_SCALE_FACTOR_DIP.toFloat(),
getResources().displayMetrics
)
val shouldScaleSmaller = screenHeight < mediumSizePx
binding.include.welcomeIcon.visibility = if (shouldScaleSmaller) View.GONE else View.VISIBLE
}
private fun toggleSelection(inputPosition: Int) {
mAdapter.toggleSelection(inputPosition)
val count = mAdapter.selectedItemCount
if (count == 0) {
mCurrentActionMode!!.finish()
} else {
mCurrentActionMode!!.title = getResources().getQuantityString(
R.plurals.selectedCardCount,
count,
count
)
val editItem = mCurrentActionMode!!.menu.findItem(R.id.action_edit)
val duplicateItem = mCurrentActionMode!!.menu.findItem(R.id.action_duplicate)
val archiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_archive)
val unarchiveItem = mCurrentActionMode!!.menu.findItem(R.id.action_unarchive)
val starItem = mCurrentActionMode!!.menu.findItem(R.id.action_star)
val unstarItem = mCurrentActionMode!!.menu.findItem(R.id.action_unstar)
var hasStarred = false
var hasUnstarred = false
var hasArchived = false
var hasUnarchived = false
for (loyaltyCard in mAdapter.getSelectedItems()) {
if (loyaltyCard.starStatus == 1) {
hasStarred = true
} else {
hasUnstarred = true
}
if (loyaltyCard.archiveStatus == 1) {
hasArchived = true
} else {
hasUnarchived = true
}
// We have all types, no need to keep checking
if (hasStarred && hasUnstarred && hasArchived && hasUnarchived) {
break
}
}
unarchiveItem.isVisible = hasArchived
archiveItem.isVisible = hasUnarchived
if (count == 1) {
starItem.isVisible = !hasStarred
unstarItem.isVisible = !hasUnstarred
editItem.isVisible = true
editItem.isEnabled = true
duplicateItem.isVisible = true
duplicateItem.isEnabled = true
} else {
starItem.isVisible = hasUnstarred
unstarItem.isVisible = hasStarred
editItem.isVisible = false
editItem.isEnabled = false
duplicateItem.isVisible = false
duplicateItem.isEnabled = false
}
mCurrentActionMode!!.invalidate()
}
}
override fun onRowClicked(inputPosition: Int) {
if (mAdapter.selectedItemCount > 0) {
enableActionMode(inputPosition)
} else {
// FIXME
//
// There is a really nasty edge case that can happen when someone taps a card but right
// after it swipes (very small window, hard to reproduce). The cursor gets replaced and
// may not have a card at the ID number that is returned from onRowClicked.
//
// The proper fix, obviously, would involve makes sure an onFling can't happen while a
// click is being processed. Sadly, I have not yet found a way to make that possible.
val loyaltyCard: LoyaltyCard
try {
loyaltyCard = mAdapter.getCard(inputPosition)
} catch (e: CursorIndexOutOfBoundsException) {
Log.w(TAG, "Prevented crash from tap + swipe on ID $inputPosition: $e")
return
}
startActivity(
Intent(this, LoyaltyCardViewActivity::class.java).apply {
action = ""
putExtras(Bundle().apply {
putInt(LoyaltyCardViewActivity.BUNDLE_ID, loyaltyCard.id)
val cardList = ArrayList<Int?>()
for (i in 0..<mAdapter.itemCount) {
cardList.add(mAdapter.getCard(i).id)
}
putIntegerArrayList(LoyaltyCardViewActivity.BUNDLE_CARDLIST, cardList)
})
}
)
}
}
companion object {
private const val TAG = "Catima"
const val RESTART_ACTIVITY_INTENT: String = "restart_activity_intent"
private const val MEDIUM_SCALE_FACTOR_DIP = 460
const val STATE_SEARCH_QUERY: String = "SEARCH_QUERY"
}
}

View File

@@ -1,18 +1,17 @@
package protect.card_locker;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class OpenWebLinkHandler {
private static final String TAG = "Catima";
public void openBrowser(AppCompatActivity activity, String url) {
public void openBrowser(Activity activity, String url) {
if (url == null) {
return;
}

View File

@@ -538,7 +538,7 @@ class ScanActivity : CatimaAppCompatActivity() {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
@@ -548,7 +548,7 @@ class ScanActivity : CatimaAppCompatActivity() {
override fun onMockedRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
permissions: Array<String>,
grantResults: IntArray
) {
val granted =

View File

@@ -0,0 +1,97 @@
package protect.card_locker.compose
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import protect.card_locker.OpenWebLinkHandler
import protect.card_locker.R
@Composable
fun CatimaAboutSection(
title: String,
message: String,
modifier: Modifier = Modifier,
onClickUrl: String? = null,
onClickDialogText: AnnotatedString? = null,
) {
val activity = LocalActivity.current
val openDialog = remember { mutableStateOf(false) }
Row(
modifier = modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.clickable {
if (onClickDialogText != null) {
openDialog.value = true
} else if (onClickUrl != null) {
OpenWebLinkHandler().openBrowser(activity, onClickUrl)
}
}
) {
Column(modifier = Modifier.weight(1F)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
Text(text = message)
}
Text(modifier = Modifier.align(Alignment.CenterVertically),
text = ">",
style = MaterialTheme.typography.bodyMedium
)
}
if (openDialog.value && onClickDialogText != null) {
AlertDialog(
icon = {},
title = {
Text(text = title)
},
text = {
Text(
text = onClickDialogText,
modifier = Modifier.verticalScroll(rememberScrollState())
)
},
onDismissRequest = {
openDialog.value = false
},
confirmButton = {
TextButton(
onClick = {
openDialog.value = false
}
) {
Text(stringResource(R.string.ok))
}
},
dismissButton = {
if (onClickUrl != null) {
TextButton(
onClick = {
OpenWebLinkHandler().openBrowser(activity, onClickUrl)
}
) {
Text(stringResource(R.string.view_online))
}
}
}
)
}
}

View File

@@ -0,0 +1,34 @@
package protect.card_locker.compose
import androidx.activity.OnBackPressedDispatcher
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import protect.card_locker.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) {
TopAppBar(
modifier = Modifier.testTag("topbar_catima"),
title = { Text(text = title) },
navigationIcon = {
if (onBackPressedDispatcher != null) {
IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
}
}
)
}

View File

@@ -0,0 +1,46 @@
package protect.card_locker.compose.theme
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import protect.card_locker.R
import protect.card_locker.preferences.Settings
@Composable
fun CatimaTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
val settings = Settings(context)
val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val lightTheme = if (isDynamicColorSupported) {
dynamicLightColorScheme(context)
} else {
lightColorScheme(primary = colorResource(id = R.color.md_theme_light_primary))
}
val darkTheme = if (isDynamicColorSupported) {
dynamicDarkColorScheme(context)
} else {
darkColorScheme(primary = colorResource(id = R.color.md_theme_dark_primary))
}
val colorScheme = when (settings.theme) {
AppCompatDelegate.MODE_NIGHT_NO -> lightTheme
AppCompatDelegate.MODE_NIGHT_YES -> darkTheme
else -> if (isSystemInDarkTheme()) darkTheme else lightTheme
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}

View File

@@ -1,421 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="protect.card_locker.MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
style="?attr/toolbarStyle" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
android:paddingVertical="8dp"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/version_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/version_history_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/version_history"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/version_history_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/version_history_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/credits"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/credits_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/credits"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/credits_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/credits_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/translate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/translate_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/help_translate_this_app"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/translate_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/translate_platform"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/translate_main"/>
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/license"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/license_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/license"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/license_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/app_license"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/license_main"/>
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/repo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/repo_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/source_repository"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/repo_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/on_github"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/repo_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/privacy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/privacy_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/privacy_policy"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/privacy_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/and_data_usage"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/privacy_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/donate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/donate_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/donate"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/rate_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/rate_this_app"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/rate_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/on_google_play"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rate_main" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/report_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?android:selectableItemBackground">
<TextView
android:id="@+id/report_error_main"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:text="@string/report_error"
android:textSize="18sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/report_error_sub"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/report_error_main"
app:layout_constraintStart_toStartOf="parent"
android:paddingStart="2dp"
android:paddingEnd="30dp"
android:textSize="16sp"
android:text="@string/on_github" />
<TextView
android:importantForAccessibility="no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:text="@string/arrow"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -276,6 +276,24 @@
android:paddingTop="@dimen/inputPadding"
android:orientation="horizontal">
<!-- Currency -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/balanceCurrencyView"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:hint="@string/currency"
android:labelFor="@+id/balanceCurrencyField">
<AutoCompleteTextView
android:id="@+id/balanceCurrencyField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<!-- Balance -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/balanceView"
@@ -294,24 +312,6 @@
android:digits="0123456789,." />
</com.google.android.material.textfield.TextInputLayout>
<!-- Currency -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/balanceCurrencyView"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1.0"
android:hint="@string/currency"
android:labelFor="@+id/balanceCurrencyField">
<AutoCompleteTextView
android:id="@+id/balanceCurrencyField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- Valid from -->

View File

@@ -40,10 +40,16 @@
android:titleCondensed="@string/unarchive"
app:showAsAction="never"/>
<item
android:id="@+id/action_duplicate"
android:title="@string/duplicateCard"
android:titleCondensed="@string/duplicateCard"
app:showAsAction="never" />
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_white_24dp"
android:title="@string/delete"
android:titleCondensed="@string/delete"
app:showAsAction="never"/>
</menu>
</menu>

View File

@@ -21,25 +21,25 @@
app:showAsAction="always">
<menu>
<item
android:id="@+id/action_archive"
android:title="@string/archive"
app:showAsAction="never"/>
<item
android:id="@+id/action_unarchive"
android:title="@string/unarchive"
app:showAsAction="never"/>
<item
android:id="@+id/action_duplicate"
android:title="@string/duplicateCard"
app:showAsAction="never" />
<item
android:id="@+id/action_archive"
android:title="@string/archive"
app:showAsAction="never"/>
<item
android:id="@+id/action_unarchive"
android:title="@string/unarchive"
app:showAsAction="never"/>
<item
android:id="@+id/action_delete"
android:title="@string/delete"
app:showAsAction="never"/>
</menu>
</item>

View File

@@ -7,8 +7,8 @@ Heimen Stoffels
Oğuz Ersen
FC (Fay) Stegerman
StoyanDimitrov
SlavekB
大王叫我来巡山
SlavekB
Katharine Chui
B o d o
mondstern
@@ -16,17 +16,17 @@ IllusiveMan196
Silvério Santos
Altonss
Edgars Andersons
Michael Moroni
Joel A
Eric
Priit Jõerüüt
Максим Горпиніч
Michael Moroni
Liner Seven
GM
Petr Novák
laralem
Priit Jõerüüt
Eric
Максим Горпиніч
GitSpoon
GM
Fjuro
laralem
Petr Novák
Taco
nadiafekihahmed
pfaffenrodt
@@ -38,44 +38,47 @@ Nyatsuki
Giovanni Donisi
Milo Ivir
HudobniVolk
Jiri Grönroos
Vasilis
Warder
Kachelkaiser
Samantaz Fox
Горпиніч Максим Олександрович
Vasilis
Kachelkaiser
Jiri Grönroos
Warder
Samantaz Fox
Balázs Meskó
Feike Donia
Arno-github
Ankit Tiwari
Cliff Heraldo
Sergio Paredes
Jose Delvani
Ankit Tiwari
109247019824
mdvhimself
Feike Donia
Arno-github
Jose Delvani
Milan Šalka
Robin
mdvhimself
தமிழ்நேரம்
huuhaa
Skrripy
Govindgopalyadav
damjang
Projjal Moitra
Quentin PAGÈS
StellarSand
aradxxx
ngocanhtve
Marnick L'Eau
Govindgopalyadav
Skrripy
huuhaa
waffshappen
Marnick L'Eau
ngocanhtve
aradxxx
StellarSand
Quentin PAGÈS
Projjal Moitra
e-michalak
JungHee Lee
hajertabbane
inavleb
Ziad OUALHADJ
Robin Liu
Aliaksandr Trush
Denis Shilin
Traductor
Gideon
Renko
Ricky Tigg
Robin Liu
Ziad OUALHADJ
delvani
しいたけ
Alexander Ivanov
Miha Frangež
@@ -84,15 +87,13 @@ mrestivill
ehrt74
Virginie
Tim Trek
Peter Dave Hello
Aliaksandr Trush
MisterCosta96
arshbeerSingh
Augustin LAVILLE
Freddo espresso
Gideon
n3m0-blip
vasudev-cell
Kim Seohyun
rudy3
Michael Gangolf
PRATHAMESH BHAGAT
Peter Dave Hello

View File

@@ -71,7 +71,7 @@
<string name="privacy_policy">سياسة الخصوصية</string>
<string name="accept">قبول</string>
<string name="importCatima">الاستيراد من Catima</string>
<string name="importCatimaMessage">حدّد ملفك <i>catima.zip</i> تصدير من Catima للاستيراد. \nإنشئها من قائمة الاستيراد / التصدير لتطبيق Catima آخر بالضغط على تصدير هناك أولاً.</string>
<string name="importCatimaMessage">حدّد ملفك تصدير من Catima للاستيراد.\nإنشئها من قائمة الاستيراد / التصدير لتطبيق Catima آخر بالضغط على تصدير .</string>
<string name="importFidme">الاستيراد من FidMe</string>
<string name="importFidmeMessage">حدّد ملفك <i>fidme-export-request-xxxxxx.zip</i> تصدير من FidMe للاستيراد، ثم حدد أنواع الباركود يدويًا بعد ذلك. \nإنشئها من ملف تعريف FidMe الخاص بك عن طريق اختيار حماية البيانات ثم الضغط على استخراج بياناتي أولاً.</string>
<string name="importVoucherVault">الاستيراد من Voucher Vault</string>

View File

@@ -306,4 +306,14 @@
<string name="generic_error_please_retry">На жаль, нешта пайшло не так, паспрабуйце яшчэ раз...</string>
<string name="setBarcodeWidth">Задаць шырыню штрыхкода</string>
<string name="app_license">Свабоднае копілефт праграмнае забеспячэнне, ліцэнзаванае паводле GPLv3+</string>
<string name="cardWithNumber">Карта <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Карта <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Калі ласка, не паварочвайце прыладу, бо гэта адменіць дзеянне</string>
<string name="acra_explain_crash">Калі магчыма, дадайце больш падрабязную інфармацыю пра тое, што вы тут рабілі:</string>
<string name="acra_crash_email_subject">Справаздача аб збоі <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Запытваць дазвол на адпраўку справаздач аб збоях</string>
<string name="pref_enable_acra_summary">Калі гэта ўключана, вам будзе прапанавана паведаміць пра збой, калі ён адбудзецца. Справаздачы аб збоях ніколі не адпраўляюцца аўтаматычна.</string>
<string name="card_list_widget_name">Спіс карт</string>
<string name="card_list_widget_empty">Пасля таго, як вы дадасце некалькі картак лаяльнасці ў Catima, яны з\'явяцца тут. Калі ў вас ёсць карты, пераканайцеся, што яны не ўсе заархіваваны.</string>
<string name="acra_catima_has_crashed">Прабачце, але ў праграме <xliff:g id="app_name">%s</xliff:g> адбыўся збой. Калі ласка, дапамажыце нам выправіць гэту праблему, даслаўшы нам справаздачу аб памылцы.</string>
</resources>

View File

@@ -305,4 +305,7 @@
<string name="pref_enable_acra">Питане преди изпращане на доклад за срив</string>
<string name="pref_enable_acra_summary">Когато е отметнато, при срив ще ви бъде предложено да докладвате за него. Докладите никога не се изпращат автоматично.</string>
<string name="acra_explain_crash">Ако е възможно добавете подробности за вашите действия:</string>
<string name="copy_value">Копиране на стойността</string>
<string name="copied_to_clipboard">Копирано</string>
<string name="nothing_to_copy">Няма стойност</string>
</resources>

View File

@@ -311,4 +311,7 @@
<string name="acra_crash_email_subject">Hlášení o pádu <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Ptát se na odesílání hlášení o pádech</string>
<string name="pref_enable_acra_summary">Pokud je povoleno, budete při pádu aplikace dotázáni na jeho nahlášení. Hlášení nejsou nikdy odesílána automaticky.</string>
<string name="copy_value">Kopírovat hodnotu</string>
<string name="copied_to_clipboard">Zkopírováno do schránky</string>
<string name="nothing_to_copy">Nenalezena žádná hodnota</string>
</resources>

View File

@@ -305,4 +305,7 @@
<string name="acra_explain_crash">Wenn möglich, bitte übermittle mehr Details zu dem, was du hier getan hast:</string>
<string name="acra_catima_has_crashed">Es tut uns leid, aber <xliff:g id="app_name">%s</xliff:g> ist abgestürzt. Bitte hilf uns diesen Fehler zu beheben und übermittle uns einen Absturzbericht.</string>
<string name="pleaseDoNotRotateTheDevice">Bitte drehe nicht das Gerät, weil sonst die Aktion abbricht</string>
<string name="copy_value">Kopiere Betrag</string>
<string name="copied_to_clipboard">In die Zwischenablage kopiert</string>
<string name="nothing_to_copy">Keinen Betrag gefunden</string>
</resources>

View File

@@ -305,4 +305,7 @@
<string name="acra_crash_email_subject">Αναφορά σφάλματος <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Ερώτηση για αποστολή αναφορών σφαλμάτων</string>
<string name="pref_enable_acra_summary">Όταν είναι ενεργοποιημένη, θα σας ζητηθεί να αναφέρετε ένα σφάλμα όταν συμβεί. Οι αναφορές σφάλματος δεν αποστέλλονται ποτέ αυτόματα.</string>
<string name="copy_value">Αντιγραφή τιμής</string>
<string name="copied_to_clipboard">Αντιγράφηκε στο πρόχειρο</string>
<string name="nothing_to_copy">Δεν βρέθηκε τιμή</string>
</resources>

View File

@@ -232,7 +232,7 @@
<string name="height">Alto</string>
<string name="switchToFrontImage">Cambiar a imagen frontal</string>
<string name="openFrontImageInGalleryApp">Abrir imagen frontal en la aplicación de la galería</string>
<string name="openBackImageInGalleryApp">Abrir imagen trasera en la aplicación de la galería</string>
<string name="openBackImageInGalleryApp">Abrir imagen trasera en la aplicación de visor de imagen</string>
<string name="setBarcodeHeight">Ajustar la altura del código de barras</string>
<string name="donate">Donar</string>
<string name="switchToBarcode">Cambiar a código de barras</string>
@@ -268,7 +268,7 @@
<string name="app_name">Catima</string>
<string name="continue_">Continuar</string>
<string name="add_manually_warning_title">Se recomienda escanear</string>
<string name="add_manually_warning_message">En algunas tiendas, el valor del código de barras difiere del número escrito en la tarjeta. Por este motivo, es posible que la introducción manual del código de barras no siempre funcione. Se recomienda encarecidamente escanear el código de barras con la cámara. ¿Aún desea continuar?</string>
<string name="add_manually_warning_message">En algunas tarjetas, el valor del código de barras difiere del número escrito en la tarjeta. Por este motivo, introducir manualmente puede que no siempre funcione. Se recomienda analizar el código de barras con su cámara en su lugar. ¿Aún desea continuar?</string>
<string name="spend">Gastar</string>
<string name="receive">Recibió</string>
<string name="amountParsingFailed">Importe incorrecto</string>
@@ -311,4 +311,7 @@
<string name="acra_crash_email_subject">Reporte del fallo <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Solicitar envío de reportes de fallos</string>
<string name="pref_enable_acra_summary">Cuando está activado, se le pedirá que informe sobre un fallo cuando ocurra. Los informes de fallo nunca se envían automáticamente.</string>
<string name="copy_value">Copia valor</string>
<string name="copied_to_clipboard">Copiado al portapapeles</string>
<string name="nothing_to_copy">Ningún valor encontrado</string>
</resources>

View File

@@ -305,4 +305,7 @@
<string name="acra_crash_email_subject">Kokkujooksmise aruanne: <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Küsi luba kokkujooksmiste aruannete saatmiseks</string>
<string name="pref_enable_acra_summary">Kui eelistus on kasutusel, siis rakendus küsib sinult luba veateate saatmiseks. Seda ei tehta iialgi automaatselt.</string>
<string name="copy_value">Kopeeri väärtus</string>
<string name="copied_to_clipboard">Kopeeritud lõikelauale</string>
<string name="nothing_to_copy">Ühtegi väärtust ei leidu</string>
</resources>

View File

@@ -36,7 +36,7 @@
<string name="scanCardBarcode">اسکن بارکد</string>
<string name="cardShortcut">میان‌بر کارت</string>
<string name="noCardsMessage">ابتدا یک کارت بیافزایید</string>
<string name="noCardExistsError">کارت پیدا نشد</string>
<string name="noCardExistsError">آن کارت پیدا نشد</string>
<string name="importFailedTitle">ایمپورت ناموفق بود</string>
<string name="importFailed">نمیتوان ایمپورت کرد</string>
<string name="exportSuccessfulTitle">خروجی گرفته شده</string>
@@ -68,7 +68,7 @@
<string name="permissionReadCardsDescription">کارت های کاتیما و تمام جزئیاتشان از جمله یادداشت‌ها و عکس‌ها را بخوانید</string>
<string name="cameraPermissionDeniedTitle">نمیتوان به دوربین دسترسی پیدا کرد</string>
<string name="noCameraPermissionDirectToSystemSetting">برای اسکن بارکد ها، کاتیما نیاز دارد به دوربین شما دسترسی داشته باشد. اینجا بزنید تا تنظیمات دسترسی خود را تغییر دهید.</string>
<string name="importExport">ایمپورت/خروجی گرفتن</string>
<string name="importExport">واردات/صادرات</string>
<string name="settings_category_title_privacy">حریم شخصی</string>
<string name="settings_category_title_general">عمومی</string>
<string name="settings_category_title_cards">نمایش کارت</string>
@@ -117,8 +117,8 @@
<string name="importCatimaMessage">فایل <i>catima.zip</i> خروجی خود را از Catima برای وارد کردن انتخاب کنید.\nآن را از منوی وارد/صادر کردن در یک اپلیکیشن دیگر Catima با فشردن دکمه صادرکردن ابتدا ایجاد کنید.</string>
<string name="unsupportedBarcodeType">این نوع بارکد هنوز نمی‌تواند نمایش داده شود. ممکن است در نسخه آینده برنامه پشتیبانی شود.</string>
<plurals name="balancePoints">
<item quantity="one"><xliff:g>%s</xliff:g> امتیاز</item>
<item quantity="other"><xliff:g>%s</xliff:g> امتیاز</item>
<item quantity="one"><xliff:g>%s</xliff:g> نقطه</item>
<item quantity="other"><xliff:g>%s</xliff:g> نقطه</item>
</plurals>
<string name="importFidmeMessage">فایل خروجی <i>fidme-export-request-xxxxxx.zip</i> خود را از FidMe برای وارد کردن انتخاب کنید، و سپس نوع بارکدها را به صورت دستی مشخص کنید.\nآن را از پروفایل FidMe خود با انتخاب گزینه حفاظت از داده و سپس فشار دادن گزینه استخراج داده من ابتدا ایجاد کنید.</string>
<string name="leaveWithoutSaveTitle">خروج</string>
@@ -294,4 +294,5 @@
<string name="spend">خرج کردن</string>
<string name="addFromPkpass">یک فایل دفترچه حساب (.pkpass) انتخاب کنید</string>
<string name="noCameraFoundGuideText">به نظر نمی‌رسد دستگاه شما دوربین داشته باشد. اگر دارد، دستگاه را مجدداً راه‌اندازی کنید. در غیر این صورت، از دکمه گزینه‌های بیشتر در زیر برای افزودن بارکد به روش دیگری استفاده کنید.</string>
<string name="card_list_widget_empty">بعد از اینکه چند کارت وفاداری در کاتیما اضافه کردید، آنها اینجا ظاهر می‌شوند. اگر کارت دارید، مطمئن شوید که همه آنها بایگانی نشده‌اند.</string>
</resources>

View File

@@ -15,4 +15,16 @@
<item quantity="one"><xliff:g>%d</xliff:g> napili</item>
<item quantity="other"><xliff:g>%d</xliff:g> ang napili</item>
</plurals>
<string name="star">Sa card viewing, ang text ay naka-display lamang tuwing naka-long press ang star icon</string>
<string name="cancel">I-kansela</string>
<string name="save">I-save</string>
<string name="edit">I-edit</string>
<string name="delete">I-delete</string>
<string name="confirm">I-confirm</string>
<string name="share">I-share</string>
<string name="sendLabel">I-send…</string>
<string name="editCardTitle">I-edit ang card</string>
<string name="noCardsMessage">Mag-add ng card muna</string>
<string name="noCardExistsError">Hindi mahanap ang card</string>
<string name="exportName">I-export</string>
</resources>

View File

@@ -311,4 +311,7 @@
<string name="acra_crash_email_subject">Rapport de plantage de <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Demander pour envoyer des rapports de plantage</string>
<string name="pref_enable_acra_summary">Quand activé, il vous sera demandé d\'envoyer un rapport de plantage en cas de plantage. Les rapports de plantage ne sont jamais envoyés automatiquement.</string>
<string name="copy_value">Copier la valeur</string>
<string name="copied_to_clipboard">Copié dans le presse-papier</string>
<string name="nothing_to_copy">Aucune valeur trouvée</string>
</resources>

View File

@@ -304,4 +304,7 @@
<string name="acra_crash_email_subject">Informe do fallo de <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Solicitar informar sobre os fallos</string>
<string name="pref_enable_acra_summary">Se está activo, váiseche pedir informar sobre os fallos cando acontezan. Os informes nunca se envían automaticamente.</string>
<string name="copy_value">Copiar valor</string>
<string name="copied_to_clipboard">Copiado ao portapapeis</string>
<string name="nothing_to_copy">Non hai ningún valor</string>
</resources>

View File

@@ -298,4 +298,13 @@
<string name="card_list_widget_empty">कैटिमा में कुछ लॉयल्टी कार्ड जोड़ने के बाद, वे यहाँ दिखाई देंगे। अगर आपके पास कार्ड हैं, तो सुनिश्चित करें कि वे सभी संग्रहित न हों।</string>
<string name="cardWithNumber">कार्ड <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">कार्ड <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">कृपया डिवाइस को घुमाएँ नहीं, क्योंकि इससे कार्रवाई रद्द हो जाएगी</string>
<string name="acra_catima_has_crashed">हमें खेद है, लेकिन <xliff:g id="app_name">%s</xliff:g> क्रैश हो गया है। कृपया हमें एक त्रुटि रिपोर्ट भेजकर इस समस्या को ठीक करने में हमारी सहायता करें।</string>
<string name="acra_explain_crash">यदि संभव हो तो कृपया यहां आप क्या कर रहे थे, इसके बारे में अधिक विवरण जोड़ें:</string>
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> क्रैश रिपोर्ट</string>
<string name="pref_enable_acra">दुर्घटना रिपोर्ट भेजने के लिए कहें</string>
<string name="pref_enable_acra_summary">सक्षम होने पर, क्रैश होने पर आपको रिपोर्ट करने के लिए कहा जाएगा। क्रैश रिपोर्ट कभी भी स्वचालित रूप से नहीं भेजी जाती हैं।</string>
<string name="copy_value">मान कॉपी करें</string>
<string name="copied_to_clipboard">क्लिपबोर्ड पर कॉपी किया गया</string>
<string name="nothing_to_copy">कोई मूल्य नहीं मिला</string>
</resources>

View File

@@ -17,14 +17,14 @@
<string name="sendLabel">Invia…</string>
<string name="editCardTitle">Modifica carta</string>
<string name="addCardTitle">Aggiungi carta</string>
<string name="scanCardBarcode">Scansiona il codice</string>
<string name="scanCardBarcode">Scansiona codice a barre</string>
<string name="cardShortcut">Scorciatoia per la carta</string>
<string name="noCardsMessage">Aggiungi prima una carta</string>
<string name="noCardExistsError">Impossibile trovare quella carta</string>
<string name="failedParsingImportUriError">Impossibile analizzare l\'URI di importazione</string>
<string name="importExport">Importa/Esporta</string>
<string name="importExport">Importa/esporta</string>
<string name="exportName">Esporta</string>
<string name="importExportHelp">Il backup dei dati permette di spostarli su un altro dispositivo.</string>
<string name="importExportHelp">Il backup dei dati permette di spostarli su un altro dispositivo</string>
<string name="importSuccessfulTitle">Importato</string>
<string name="importFailedTitle">Importazione fallita</string>
<string name="importFailed">Impossibile eseguire l\'importazione</string>

View File

@@ -45,7 +45,7 @@
<string name="enter_group_name">グループ名を入力</string>
<string name="exportSuccessful">データがエクスポートされました</string>
<string name="importSuccessful">データがインポートされました</string>
<string name="intent_import_card_from_url_share_text">カード共有しましょう</string>
<string name="intent_import_card_from_url_share_text">カード共有しましょう</string>
<string name="settings_disable_lockscreen_while_viewing_card">バーコード表示中に画面をロックしない</string>
<string name="settings_keep_screen_on">バーコード表示中に画面を点けたままにする</string>
<string name="settings_display_barcode_max_brightness">画面を明るくする</string>
@@ -109,8 +109,8 @@
<string name="action_add">追加</string>
<string name="action_search">検索</string>
<string name="intent_import_card_from_url_share_multiple_text">カードを共有しましょう</string>
<string name="turn_flashlight_off">ライトをオフにする</string>
<string name="turn_flashlight_on">ライトをオンにする</string>
<string name="turn_flashlight_off">ライトを消灯する</string>
<string name="turn_flashlight_on">ライトを点灯する</string>
<string name="failedGeneratingShareURL">共有可能なURLを作成できませんでした</string>
<string name="passwordRequired">パスワードを入力</string>
<string name="no">いいえ</string>
@@ -119,11 +119,11 @@
<string name="updateBarcodeQuestionTitle">バーコードの番号を変更しますか?</string>
<string name="takePhoto">写真を撮影する</string>
<string name="removeImage">画像を削除</string>
<string name="setBackImage">面の画像を設定</string>
<string name="setFrontImage">面の画像を設定</string>
<string name="setBackImage">面の画像を設定</string>
<string name="setFrontImage">面の画像を設定</string>
<string name="photos">画像</string>
<string name="backImageDescription"></string>
<string name="frontImageDescription"></string>
<string name="backImageDescription">背面</string>
<string name="frontImageDescription">前面</string>
<plurals name="selectedCardCount">
<item quantity="other">選択済み: <xliff:g>%d</xliff:g></item>
</plurals>
@@ -298,4 +298,7 @@
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> クラッシュレポート</string>
<string name="pref_enable_acra">クラッシュレポートを送信する</string>
<string name="pref_enable_acra_summary">有効にすると、クラッシュ発生時に報告するかを確認されます。クラッシュレポートが自動送信されることはありません。</string>
<string name="copy_value">値をコピー</string>
<string name="copied_to_clipboard">クリップボードへコピー</string>
<string name="nothing_to_copy">値が見つかりません</string>
</resources>

View File

@@ -76,16 +76,16 @@
<string name="leaveWithoutSaveConfirmation">Iziet nesaglabājot\?</string>
<string name="addFromImage">Atlasīt attēlu no galerijas</string>
<string name="card">Karte</string>
<string name="expiryDate">Derīguma termiņš</string>
<string name="expiryDate">Derīguma beigu datums</string>
<string name="never">Nekad</string>
<string name="chooseExpiryDate">Izvēlēties beigu datumu</string>
<string name="failedToRetrieveImageFile">Neizdevās iegūt attēla datni</string>
<string name="barcodeLongPressMessage">Galerijas lietotnē var atvērt tikai attēlus</string>
<string name="sort_by_expiry">Derīguma termiņš</string>
<string name="sort_by_expiry">Derīgums</string>
<string name="reverse">...apgrieztā secībā</string>
<string name="credits">Pateicības</string>
<string name="shortcutSelectCard">Atlasīt karti</string>
<string name="duplicateCard">Dublēt</string>
<string name="duplicateCard">Pavairot</string>
<string name="archive">Arhivēt</string>
<string name="translate_platform">Weblate</string>
<string name="starred">Izlase</string>
@@ -96,7 +96,7 @@
<item quantity="other">Neatgriezeniski dzēst šīs <xliff:g>%d</xliff:g> kartes\?</item>
</plurals>
<string name="about_title_fmt">Par <xliff:g id="app_name">%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Derīguma termiņš beidzās: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Derīgums beidzās: <xliff:g>%s</xliff:g></string>
<string name="selectColor">Atlasīt krāsu</string>
<string name="settings_catima_theme">Catima</string>
<string name="settings_pink_theme">Rozā</string>
@@ -129,7 +129,7 @@
<string name="source_repository">Pirmkoda glabātava</string>
<string name="rate_this_app">Novērtēt šo lietotni</string>
<string name="noGiftCardsGroup">Izveido kādas kartes, tad šeit pievieno tās kopai</string>
<string name="options">Parametri</string>
<string name="options">Iespējas</string>
<plurals name="groupCardCount">
<item quantity="zero"><xliff:g>%d</xliff:g> kartes</item>
<item quantity="one"><xliff:g>%d</xliff:g> karte</item>
@@ -165,7 +165,7 @@
<string name="group_updated">Kopa atjaunināta</string>
<string name="addManually">Pašrocīgi ievadīt svītrkodu</string>
<string name="groupsList">Kopas: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Derīguma termiņš: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Derīgums beigsies: <xliff:g>%s</xliff:g></string>
<string name="balanceSentence">Atlikums: <xliff:g>%s</xliff:g></string>
<string name="editBarcode">Labot svītrkodu</string>
<string name="importCatima">Ievietot no Catima</string>
@@ -182,10 +182,10 @@
<string name="intent_import_card_from_url_share_multiple_text">Vēlos ar Tevi kopīgot dažas kartes</string>
<string name="frontImageDescription">Priekšpuses attēls</string>
<string name="backImageDescription">Aizmugures attēls</string>
<string name="photos">Foto</string>
<string name="photos">Fotoattēli</string>
<string name="setFrontImage">Iestatīt priekšpuses attēlu</string>
<string name="setBackImage">Iestatīt aizmugures attēlu</string>
<string name="takePhoto">Fotografēt</string>
<string name="takePhoto">Uzņemt attēlu</string>
<string name="passwordRequired">Jāievada parole</string>
<string name="exportPassword">Iestatīt paroli, lai aizsargātu savu izguves datni (pēc izvēles)</string>
<string name="turn_flashlight_on">Ieslēgt zibspuldzi</string>
@@ -311,4 +311,7 @@
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> avārijas ziņojums</string>
<string name="pref_enable_acra">Vaicāt, lai nosūtītu ziņojumus par avārijām</string>
<string name="pref_enable_acra_summary">Kad iespējots, tiks vaicāts ziņot par avāriju, kad tā notiek. Ziņojumi par avārijām nekad netiks automātiski nosūtīti.</string>
<string name="copy_value">Ievietot vērtību starpliktuvē</string>
<string name="copied_to_clipboard">Ievietots starpliktuvē</string>
<string name="nothing_to_copy">Nav atrasta vērtība</string>
</resources>

View File

@@ -305,4 +305,7 @@
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> foutrapport</string>
<string name="pref_enable_acra">Vraag om foutrapporten te versturen</string>
<string name="pref_enable_acra_summary">Als dit aanstaat, zal je gevraagd worden om foutrapporten te sturen als de app crasht. Dit zal nooit automatisch gebeuren.</string>
<string name="nothing_to_copy">Geen waarde gevonden</string>
<string name="copied_to_clipboard">Gekopieerd naar klembord</string>
<string name="copy_value">Kopieer waarde</string>
</resources>

View File

@@ -311,4 +311,7 @@
<string name="acra_crash_email_subject">Relatório de falha em <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Solicitar o envio de relatórios de falhas</string>
<string name="pref_enable_acra_summary">Quando ativado, você será solicitado a relatar uma falha quando isto ocorrer. Os relatórios de falhas nunca são enviados automaticamente.</string>
<string name="copy_value">Copiar valor</string>
<string name="copied_to_clipboard">Copiado para a área de transferência</string>
<string name="nothing_to_copy">Nenhum valor encontrado</string>
</resources>

View File

@@ -5,7 +5,7 @@
<string name="note">Notă</string>
<string name="storeName">Numele</string>
<string name="noMatchingGiftCards">Nu au fost găsite rezultate. Încercați să schimbați termenii de căutare.</string>
<string name="noGiftCards">Faceți clic pe butonul + plus pentru a adăuga o carte sau importați mai întâi una din meniul ⋮.</string>
<string name="noGiftCards">Faceți clic pe butonul + plus pentru a adăuga o carte sau mai întâi importați una din meniu.</string>
<string name="action_add">Adăugați</string>
<string name="action_search">Căutare</string>
<string name="sendLabel">Trimiteți…</string>
@@ -88,7 +88,7 @@
<string name="passwordRequired">Vă rugăm, introduceți parola</string>
<string name="unsupportedBarcodeType">Acest tip de cod de bare nu poate fi afișat. Este posibil ca acesta să se poată afișa într-o versiune mai nouă a aplicației.</string>
<string name="photos">Imagini</string>
<string name="noGiftCardsGroup">Adăugați câteva carduri, iar apoi atribuiți-le grupului aici.</string>
<string name="noGiftCardsGroup">Creați câteva carduri, iar apoi atribuiți-le grupului de aici.</string>
<string name="importCatima">Importați din Catima</string>
<string name="intent_import_card_from_url_share_multiple_text">Aș dori să partajez niște carduri cu tine</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Drepturi de autor © 2019<xliff:g>%d</xliff:g> Sylvia van Os și contribuitorii</string>

View File

@@ -317,4 +317,7 @@
<string name="acra_crash_email_subject">Отчёт об ошибке в <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Запрашивать отправку отчётов об ошибках</string>
<string name="pref_enable_acra_summary">Если включено, то в случае сбоя вам будет предложено отправить отчёт о нём. Отчёты никогда не отправляются автоматически.</string>
<string name="copy_value">Скопировать значение</string>
<string name="copied_to_clipboard">Скопировано в буфер обмена</string>
<string name="nothing_to_copy">Значение не найдено</string>
</resources>

View File

@@ -18,9 +18,9 @@
<string name="cardShortcut">Skratka karty</string>
<string name="noCardsMessage">Najprv pridajte kartu</string>
<string name="noCardExistsError">Nepodarilo sa nájsť túto kartu</string>
<string name="importExport">Import/Export</string>
<string name="importExport">Import/export</string>
<string name="exportName">Export</string>
<string name="importExportHelp">Zálohovanie vašich údajov umožňuje ich presun na iné zariadenie.</string>
<string name="importExportHelp">Zálohovanie vašich údajov umožňuje ich presun na iné zariadenie</string>
<string name="importSuccessfulTitle">Úspešne importované</string>
<string name="importFailedTitle">Import zlyhal</string>
<string name="importFailed">Nemožno vykonať import</string>
@@ -30,7 +30,7 @@
<string name="importing">Importujem…</string>
<string name="exporting">Exportujem…</string>
<string name="importOptionFilesystemTitle">Import zo súborového systému</string>
<string name="importOptionFilesystemExplanation">Vyberte súbor zo súborového systému.</string>
<string name="importOptionFilesystemExplanation">Vyberte uložený súbor</string>
<string name="importOptionFilesystemButton">Zo súborového systému</string>
<string name="about">O aplikácii</string>
<string name="app_license">Slobodný softvér s copyleft licenciou GPLv3+</string>
@@ -62,11 +62,11 @@
<string name="leaveWithoutSaveTitle">Ukončiť</string>
<string name="moveDown">Pohyb smerom nadol</string>
<string name="moveUp">Pohyb smerom nahor</string>
<string name="failedOpeningFileManager">Najprv nainštalujte správcu súborov.</string>
<string name="failedOpeningFileManager">Nepodarilo sa otvoriť správcu súborov</string>
<string name="deleteConfirmationGroup">Vymazať skupinu\?</string>
<string name="all">Všetky</string>
<string name="noGroupCards">Táto skupina je prázdna</string>
<string name="noGroups">Kliknutím na tlačidlo + plus pridáte skupiny na kategorizáciu.</string>
<string name="noGroups">Kliknutím na tlačidlo + (plus) pridáte skupiny na kategorizáciu</string>
<string name="groups">Skupiny</string>
<string name="enter_group_name">Zadajte názov skupiny</string>
<string name="exportSuccessful">Údaje exportované</string>
@@ -80,7 +80,7 @@
<string name="settings_system_theme">Podľa nastavení systému</string>
<string name="settings_theme">Téma</string>
<string name="starImage">Obľúbená hviezda</string>
<string name="exportOptionExplanation">Údaje sa zapíšu na vami zvolené miesto.</string>
<string name="exportOptionExplanation">Údaje budú uložené na vami zvolené miesto</string>
<string name="failedParsingImportUriError">Nepodarilo sa analyzovať import URI</string>
<string name="share">Zdieľať</string>
<string name="barcodeImageDescriptionWithType">Obrázok čiarového kódu <xliff:g>%s</xliff:g></string>
@@ -115,10 +115,9 @@
<string name="balanceSentence">Zostatok: <xliff:g>%s</xliff:g></string>
<string name="importCatima">Import z aplikácie Catima</string>
<string name="settings_theme_color">Farba témy</string>
<string name="app_libraries">Slobodné knižnice tretích strán: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Slobodné zdroje tretích strán: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="importCatimaMessage">Vyberte svoj <i>catima.zip</i> export z aplikácie Catima, ktorý chcete importovať.
\nVytvorte ho z ponuky Import/Export inej aplikácie Catima tak, že stlačíte tlačidlo Exportovať.</string>
<string name="app_libraries">Knižnice tretích strán: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Zdroje tretích strán: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="importCatimaMessage">Vyberte svoj export z aplikácie Catima, ktorý chcete importovať. \nVytvorte ho z ponuky Import/export inej aplikácie Catima tak, že stlačíte tlačidlo Exportovať.</string>
<string name="accept">Prijať</string>
<string name="importLoyaltyCardKeychain">Import z aplikácie Loyalty Card Keychain</string>
<string name="importFidme">Import z aplikácie FidMe</string>
@@ -154,7 +153,7 @@
<string name="rate_this_app">Ohodnoťte túto aplikáciu</string>
<string name="exportPassword">Nastavte heslo na ochranu exportu (voliteľné)</string>
<string name="exportPasswordHint">Zadajte heslo</string>
<string name="failedGeneratingShareURL">Nepodarilo sa vygenerovať zdieľateľnú adresu URL. Nahláste to, prosím.</string>
<string name="failedGeneratingShareURL">Nepodarilo sa vygenerovať zdieľateľnú adresu URL</string>
<string name="turn_flashlight_off">Vypnúť svetlo</string>
<string name="settings_locale">Jazyk</string>
<string name="settings_system_locale">Systém</string>
@@ -179,7 +178,7 @@
<string name="updateBarcodeQuestionTitle">Aktualizovať hodnotu čiarového kódu\?</string>
<string name="updateBarcodeQuestionText">Zmenili ste ID. Chcete aktualizovať aj čiarový kód, aby používal rovnakú hodnotu\?</string>
<string name="no">Nie</string>
<string name="passwordRequired">Zadajte prosím heslo</string>
<string name="passwordRequired">Zadajte heslo</string>
<string name="noGiftCardsGroup">Zatiaľ nemáte žiadne vernostné karty. Keď nejaké pridáte, môžete ich priradiť ku skupine tu</string>
<string name="noCameraPermissionDirectToSystemSetting">Na skenovanie čiarových kódov potrebuje Catima prístup k fotoaparátu. Ťuknite sem a zmeňte nastavenia oprávnení.</string>
<string name="importCards">Importovať karty</string>
@@ -211,10 +210,8 @@
<string name="chooseValidFromDate">Zvoliť dátum platné od</string>
<string name="validFromSentence">Platnosť od: <xliff:g>%s</xliff:g></string>
<string name="cameraPermissionRequired">Pre túto akciu je potrebné oprávnenie na prístup k fotoaparátu…</string>
<string name="importLoyaltyCardKeychainMessage">Vyberte svoj export <i>LoyaltyCardKeychain.csv</i> z Kľúčenky vernostných kariet, ktorý chcete importovať.
\nVytvorte ho z ponuky Import/Export v aplikácii Loyalty Card Keychain tak, že tam najprv stlačíte tlačidlo Exportovať.</string>
<string name="importVoucherVaultMessage">Vyberte svoj <i>vouchervault.json</i> export z Trezoru poukážok pre import.
\nNajprv ho vytvorte stlačením tlačidla Export v aplikácii Voucher Vault.</string>
<string name="importLoyaltyCardKeychainMessage">Vyberte svoj export z aplikácie Loyalty Card Keychain, ktorý chcete importovať. \nVytvorte ho z ponuky Import/Export v aplikácii Loyalty Card Keychain tak, že tam stlačíte tlačidlo Exportovať.</string>
<string name="importVoucherVaultMessage">Vyberte svoj export z aplikácie Voucher Vault pre import.\nNajprv ho vytvorte stlačením tlačidla Export v aplikácii Voucher Vault.</string>
<string name="shortcutSelectCard">Vybrať kartu</string>
<string name="include_if_asking_support">Ak chcete požiadať o podporu, uveďte nasledujúce informácie:</string>
<plurals name="groupCardCountWithArchived">
@@ -225,13 +222,12 @@
<string name="barcodeLongPressMessage">V aplikácii galéria je možné otvoriť iba obrázky</string>
<string name="cameraPermissionDeniedTitle">Nepodarilo sa získať prístup k fotoaparátu</string>
<string name="storageReadPermissionRequired">Pre túto akciu je potrebné oprávnenie na čítanie úložiska…</string>
<string name="importFidmeMessage">Vyberte svoj <i>fidme-export-request-xxxxxx.zip</i> export zo služby FidMe pre import a potom vyberte typy čiarových kódov ručne.
\nVytvorte ho z profilu FidMe tak, že najprv vyberiete položku Ochrana údajov a potom stlačíte tlačidlo Extrahovať moje údaje.</string>
<string name="importFidmeMessage">Vyberte svoj export zo služby FidMe pre import a potom vyberte typy čiarových kódov ručne.\nVytvorte ho z profilu FidMe tak, že vyberiete položku Ochrana údajov a potom stlačíte tlačidlo Extrahovať moje údaje.</string>
<string name="currentBalanceSentence">Aktuálny zostatok: <xliff:g>%s</xliff:g></string>
<string name="intent_import_card_from_url_share_multiple_text">Chcem sa s vami zdielať karty</string>
<string name="app_contributors">Podporili: <xliff:g id="app_contributors">%s</xliff:g></string>
<string name="newBalanceSentence">Nový zostatok: <xliff:g>%s</xliff:g></string>
<string name="failedLaunchingPhotoPicker">Nepodarilo sa nájsť podporovanú aplikáciu galérie</string>
<string name="failedLaunchingPhotoPicker">Nepodarilo sa nájsť podporovanú aplikáciu pre výber obrázkov</string>
<string name="show_note">Zobraziť poznámku</string>
<string name="icon_header_click_text">Dlhým stlačením upravíte miniatúru</string>
<string name="settings_category_title_general">Všeobecné</string>
@@ -239,13 +235,13 @@
<string name="settings_keep_screen_on_summary">Ponechať obrazovku aktívnu počas prezerania karty</string>
<string name="settings_display_barcode_max_brightness_summary">Pre zaistenie čitateľnosti pre niektoré skenery</string>
<string name="settings_allow_content_provider_read_summary">Aplikácie budú musieť stále žiadať o povolenie, aby im bol udelený prístup</string>
<string name="openBackImageInGalleryApp">Otvorenie zadného obrázka v aplikácii galéria</string>
<string name="openFrontImageInGalleryApp">Otvorenie predného obrázka v aplikácii galéria</string>
<string name="openBackImageInGalleryApp">Otvorenie zadného obrázka v prehliadači obrázkov</string>
<string name="openFrontImageInGalleryApp">Otvorenie predného obrázka v prehliadači obrázkov</string>
<string name="setBarcodeHeight">Nastavenie výšky čiarového kódu</string>
<string name="show_balance">Ukážte zostatok</string>
<string name="show_name_below_image_thumbnail">Zobraziť názov pod miniatúrou obrázka</string>
<string name="show_validity">Zobraziť platnosť</string>
<string name="permissionReadCardsLabel">Načítať Catima karty</string>
<string name="permissionReadCardsLabel">Čítanie kariet Catima</string>
<string name="permissionReadCardsDescription">čítať svoje Catima karty a všetky jeho podrobnosti, vrátane poznámky a obrázkov</string>
<string name="switchToBackImage">Prepnutie na zadný obrázok</string>
<string name="height">Výška</string>
@@ -275,7 +271,7 @@
<string name="receive">Prijaté</string>
<string name="amountParsingFailed">Neplatná hodnota</string>
<string name="add_manually_warning_title">Skenovanie je odporúčané</string>
<string name="add_manually_warning_message">V niektorých obchodoch sa hodnota čiarového kódu líši od čísla uvedeného na karte. Z tohto dôvodu nemusí manuálne zadanie čiarového kódu vždy fungovať. Dôrazne odporúčame naskenovať čiarový kód pomocou fotoaparátu. Chcete napriek tomu pokračovať?</string>
<string name="add_manually_warning_message">Pri niektorých kartách sa hodnota čiarového kódu líši od čísla uvedeného na karte. Z tohto dôvodu nemusí manuálne zadanie čiarového kódu vždy fungovať. Odporúčame naskenovať čiarový kód pomocou fotoaparátu. Chcete napriek tomu pokračovať?</string>
<string name="addFromPdfFile">Vyberte súbor PDF</string>
<string name="errorReadingFile">Súbor sa nepodarilo prečítať</string>
<string name="failedLaunchingFileManager">Nepodarilo sa nájsť podporovaného správcu súborov</string>
@@ -286,7 +282,7 @@
<string name="exportCancelled">Export zrušený</string>
<string name="useFrontImage">Použiť obrázok prednej strany</string>
<string name="useBackImage">Použiť obrázok zadnej strany</string>
<string name="generic_error_please_retry">Prepáčte, niečo sa pokazilo, skúste to znova...</string>
<string name="generic_error_please_retry">Niečo sa pokazilo</string>
<string name="settings_category_title_cards_overview">Prehľad kariet</string>
<string name="width">Šírka</string>
<string name="setBarcodeWidth">Nastaviť šírku čiarového kódu</string>
@@ -308,5 +304,14 @@
<string name="card_list_widget_name">Zoznam kariet</string>
<string name="card_list_widget_empty">Po pridaní vernostných kariet do Catima sa zobrazia tu. Ak máte karty, uistite sa, že nie sú všetky archivované.</string>
<string name="cardWithNumber">Karta <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Karta <xliff:g>%d</xliff:g> (%s)</string>
<string name="cardWithNumberAndLocale">Karta <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="pleaseDoNotRotateTheDevice">Neotáčajte zariadenie, inak akciu prerušíte</string>
<string name="acra_catima_has_crashed">Ospravedlňujeme sa, aplikácia <xliff:g id="app_name">%s</xliff:g> spadla. Pomôžte nám tento problém opraviť zaslaním hlásenia o chybe.</string>
<string name="acra_explain_crash">Ak je to možné, uveďte viac podrobností o tom, čo ste práve robili:</string>
<string name="acra_crash_email_subject">Hlásenie o páde aplikácie <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Poskytovať možnosť zasielať hlásenia o chybách</string>
<string name="pref_enable_acra_summary">Keď túto možnosť zapnete, pri páde aplikácie vás požiadame o zaslanie hlásenia o chybe. Tie sa nikdy nezasielajú automaticky.</string>
<string name="copy_value">Kopírovať hodnotu</string>
<string name="copied_to_clipboard">Skopírované do schránky</string>
<string name="nothing_to_copy">Nenašla sa žiadna hodnota</string>
</resources>

View File

@@ -316,4 +316,7 @@
<string name="acra_crash_email_subject">Poročilo o sesutju aplikacije <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Prosi za pošiljanje poročila o sesutju</string>
<string name="pref_enable_acra_summary">Ko je ta možnost omogočena, boš ob pojavu sesutja pozvan, da prijaviš napako. Poročilo o sesutju se nikoli ne pošilja samodejno.</string>
<string name="copy_value">Kopiraj vrednost</string>
<string name="copied_to_clipboard">Kopirano v odložišče</string>
<string name="nothing_to_copy">Nobena vrednost ni najdena</string>
</resources>

View File

@@ -13,8 +13,8 @@
<item quantity="one"><xliff:g>%d</xliff:g> valt</item>
<item quantity="other"><xliff:g>%d</xliff:g> valda</item>
</plurals>
<string name="app_loyalty_card_keychain">Nyckelring för bonuskort</string>
<string name="importLoyaltyCardKeychainMessage">Välj den exporterade från Loyalty Card Keychain som du vill importera.\nSkapa den från Import/Export-menyn i Loyalty Card Keychain genom att trycka på Exportera.</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="importLoyaltyCardKeychainMessage">Välj det exporterade från Loyalty Card Keychain som du vill importera.\nSkapa det från Import/Export-menyn i Loyalty Card Keychain genom att trycka på Exportera.</string>
<string name="importVoucherVaultMessage">Välj den exporterade från Voucher Vault som du vill importera. \nSkapa den genom att trycka på Exportera i Voucher Vault.</string>
<string name="enter_group_name">Ange gruppnamn</string>
<string name="groups">Grupper</string>
@@ -24,7 +24,7 @@
</plurals>
<string name="all">Alla</string>
<string name="deleteConfirmationGroup">Ta bort grupp\?</string>
<string name="failedOpeningFileManager">Installera en filhanterare först.</string>
<string name="failedOpeningFileManager">Kunde ej öppna en filhanterare</string>
<string name="moveUp">Flytta uppåt</string>
<string name="moveDown">Flytta nedåt</string>
<string name="leaveWithoutSaveTitle">Avsluta</string>
@@ -192,7 +192,7 @@
</plurals>
<string name="include_if_asking_support">Om du vill be om hjälp, inkludera då följande information:</string>
<string name="settings_oled_dark">Helsvart bakgrund för mörkt tema</string>
<string name="failedLaunchingPhotoPicker">Kunde inte hitta kompatibelt bildprogram</string>
<string name="failedLaunchingPhotoPicker">Kunde ej hitta kompatibel bildväljare</string>
<string name="unarchive">Ta tillbaks från arkiv</string>
<string name="archived">Kort arkiverat</string>
<string name="duplicateCard">Kopiera</string>
@@ -215,14 +215,14 @@
<string name="storageReadPermissionRequired">Tillstånd att läsa lagring behövs för denna åtgärd…</string>
<string name="currentBalanceSentence">Nuvarande balans: <xliff:g>%s</xliff:g></string>
<string name="validFromDate">Giltig från</string>
<string name="cameraPermissionRequired">Tillstånd att komma åt kameran krävs för denna åtgärd…</string>
<string name="cameraPermissionRequired">Behörighet att komma åt kameran krävs för denna åtgärd…</string>
<string name="updateBalance">Uppdatera balans</string>
<string name="failedToRetrieveImageFile">Misslyckades att hämta bildfil</string>
<string name="barcodeLongPressMessage">Endast bilder kan öppnas i galleri app</string>
<string name="barcodeLongPressMessage">Endast bilder kan öppnas i galleriappen</string>
<string name="updateBalanceTitle">Hur mycket spenderade du eller fick du?</string>
<string name="updateBalanceHint">Ange summa</string>
<string name="newBalanceSentence">Ny balans: <xliff:g>%s</xliff:g></string>
<string name="openFrontImageInGalleryApp">Öppna bilden på framsidan i galleri-appen</string>
<string name="openFrontImageInGalleryApp">Öppna bilden på framsidan i bildvisningsappen</string>
<string name="show_name_below_image_thumbnail">Visa namnet nedanför bildens miniatyr</string>
<string name="show_validity">Visa giltighet</string>
<string name="view_online">Visa på internet</string>
@@ -230,7 +230,7 @@
<string name="settings_category_title_general">Generellt</string>
<string name="switchToBarcode">Byt till streckkod</string>
<string name="settings_disable_lockscreen_while_viewing_card_summary">Stänger av skärmlåset medans kort visas</string>
<string name="permissionReadCardsDescription">Se dina kort med alla dess detaljer, inklusive anteckningar och bilder</string>
<string name="permissionReadCardsDescription">läsa dina Catima-kort med alla dess detaljer, inklusive anteckningar och bilder</string>
<string name="action_display_options">Visningsalternativ</string>
<string name="settings_display_barcode_max_brightness_summary">Nödvändigt för att en del skannrar ska fungera</string>
<string name="settings_oled_dark_summary">Reducerar batterianvändning på OLED-skärmar</string>
@@ -242,10 +242,10 @@
<string name="switchToFrontImage">Byt till bilden på framsidan</string>
<string name="settings_allow_content_provider_read_summary">Appar måste fortfarande begära åtkomst för att få tillgång</string>
<string name="setBarcodeHeight">Ställ in streckkodens höjd</string>
<string name="openBackImageInGalleryApp">Öppna bilden på baksidan i galleri-appen</string>
<string name="openBackImageInGalleryApp">Öppna bilden på baksidan i bildvisningsappen</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Kopieringsskydd © 2019<xliff:g>%d</xliff:g> Sylvia van Os och medverkande</string>
<string name="settings_allow_content_provider_read_title">Tillåt andra appar att komma åt min data</string>
<string name="permissionReadCardsLabel">Läs Catima-kort</string>
<string name="permissionReadCardsLabel">Läsa Catima-kort</string>
<string name="donate">Donera</string>
<string name="show_archived_cards">Visa arkiverade kort</string>
<string name="settings_category_title_privacy">Sekretess</string>
@@ -296,4 +296,16 @@
<string name="generic_error_please_retry">Ett fel uppstod</string>
<string name="cardWithNumber">Kort <xliff:g>%d</xliff:g></string>
<string name="cardWithNumberAndLocale">Kort <xliff:g>%d</xliff:g> (<xliff:g>%s</xliff:g>)</string>
<string name="setBarcodeWidth">Ange streckkodsbredd</string>
<string name="card_list_widget_empty">Bonuskort som du lägger till i Catima kommer att dyka upp här. Om du redan har kort, kontrollera att de alla inte är arkiverade.</string>
<string name="add_manually_warning_message">För vissa kort skiljer sig streckkodsvärdet från numret som är skrivet på kortet. På grund av detta kan det ibland ej vara möjligt att ange en streckkod manuellt. Det rekommenderas att istället skanna streckkoden med din kamera. Vill du ändå fortsätta?</string>
<string name="pleaseDoNotRotateTheDevice">Var vänlig rotera ej enheten, då detta kommer att avbryta åtgärden</string>
<string name="acra_catima_has_crashed">Vi beklagar, men <xliff:g id="app_name">%s</xliff:g> har kraschat. Vänligen hjälp oss att lösa detta problem genom att skicka en felrapport till oss.</string>
<string name="acra_crash_email_subject">Kraschrapport för <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Be om att skicka kraschrapporter</string>
<string name="pref_enable_acra_summary">När det är aktiverat kommer du att bli ombedd att rapportera en krasch när den inträffar. Kraschrapporter skickas aldrig automatiskt.</string>
<string name="acra_explain_crash">Om möjligt, var vänlig lägg till fler detaljer om vad du höll på med här:</string>
<string name="copy_value">Kopiera värde</string>
<string name="copied_to_clipboard">Kopierade till urklipp</string>
<string name="nothing_to_copy">Inget värde hittades</string>
</resources>

View File

@@ -305,4 +305,7 @@
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> çökme raporu</string>
<string name="pref_enable_acra_summary">Etkinleştirildiğinde, bir çökmeyi şikayet etmeniz istenecektir. Çökme raporları hiç bir zaman otomatik olarak gönderilmez.</string>
<string name="pref_enable_acra">Çökme bildirimlerini göndermeyi iste</string>
<string name="copy_value">Değeri kopyala</string>
<string name="copied_to_clipboard">Panoya kopyalandı</string>
<string name="nothing_to_copy">Değer bulunamadı</string>
</resources>

View File

@@ -317,4 +317,7 @@
<string name="acra_crash_email_subject">Звіт про збій <xliff:g id="app_name">%s</xliff:g></string>
<string name="pref_enable_acra">Запит на надсилання звітів про збої</string>
<string name="pref_enable_acra_summary">Якщо цю функцію ввімкнено, вам буде запропоновано повідомити про збій, коли він станеться. Звіти про збої ніколи не надсилаються автоматично.</string>
<string name="copy_value">Копіювати значення</string>
<string name="copied_to_clipboard">Скопійовано в буфер обміну</string>
<string name="nothing_to_copy">Значення не знайдено</string>
</resources>

View File

@@ -155,7 +155,7 @@
<string name="updateBalance">Cập nhật số dư</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Bản quyền © 2019<xliff:g>%d</xliff:g> Sylvia van Os và các cộng sự</string>
<string name="sort_by_most_recently_used">Sửa dụng gần đây nhất</string>
<string name="noGiftCards">Bấm nút dấu cộng + để thêm thẻ, hoặc nhập dữ liệu từ menu ⋮.</string>
<string name="noGiftCards">Bấm nút dấu cộng + để thêm thẻ, hoặc nhập dữ liệu từ menu</string>
<string name="settings_theme_color">Chủ đề màu</string>
<string name="importVoucherVault">Nhập dữ liệu từ Voucher Vault</string>
<string name="barcodeId">Giá trị mã vạch</string>

View File

@@ -299,4 +299,7 @@
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> 崩溃报告</string>
<string name="pref_enable_acra">请求发送崩溃报告</string>
<string name="pref_enable_acra_summary">开启后,会在发生崩溃时请求报告崩溃。应用永远不会自动发送崩溃报告。</string>
<string name="copy_value">复制值</string>
<string name="copied_to_clipboard">已复制到剪贴板</string>
<string name="nothing_to_copy">没找到值</string>
</resources>

View File

@@ -298,4 +298,7 @@
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> 當機報告</string>
<string name="pref_enable_acra">詢問是否傳送當機報告</string>
<string name="pref_enable_acra_summary">啟用後,當發生當機時系統會詢問您是否要回報。當機報告絕不會自動傳送。</string>
<string name="copy_value">複製值</string>
<string name="copied_to_clipboard">已複製到剪貼簿</string>
<string name="nothing_to_copy">未找到值</string>
</resources>

View File

@@ -358,4 +358,8 @@
<string name="acra_crash_email_subject"><xliff:g id="app_name">%s</xliff:g> crash report</string>
<string name="pref_enable_acra">Ask to send crash reports</string>
<string name="pref_enable_acra_summary">When enabled, you will be asked to report a crash when it happens. Crash reports are never sent automatically.</string>
<string name="copy_value">Copy value</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="nothing_to_copy">No value found</string>
<string name="back">Back</string>
</resources>

View File

@@ -1,5 +1,4 @@
<resources>
<style name="AppTheme" parent="Theme.Material3.Light.NoActionBar">
<item name="colorPrimary">@color/md_theme_light_primary</item>
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>

View File

@@ -1,171 +0,0 @@
package protect.card_locker
import android.content.Intent
import android.net.Uri
import android.view.View
import android.widget.TextView
import androidx.core.view.isVisible
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.shadows.ShadowActivity
import org.robolectric.shadows.ShadowLog
import java.lang.reflect.Method
@RunWith(RobolectricTestRunner::class)
class AboutActivityTest {
private lateinit var activityController: org.robolectric.android.controller.ActivityController<AboutActivity>
private lateinit var activity: AboutActivity
private lateinit var shadowActivity: ShadowActivity
@Before
fun setUp() {
ShadowLog.stream = System.out
activityController = Robolectric.buildActivity(AboutActivity::class.java)
activity = activityController.get()
shadowActivity = shadowOf(activity)
}
@Test
fun testActivityCreation() {
activityController.create().start().resume()
// Verify activity title is set correctly
assertEquals(activity.title.toString(),
activity.getString(R.string.about_title_fmt, activity.getString(R.string.app_name)))
// Check key elements are initialized
assertNotNull(activity.findViewById(R.id.toolbar))
assertNotNull(activity.findViewById(R.id.credits_sub))
assertNotNull(activity.findViewById(R.id.version_history_sub))
}
@Test
fun testDisplayOptionsBasedOnConfig() {
activityController.create().start().resume()
// Test Google Play rate button visibility based on BuildConfig
val rateButton = activity.findViewById<View>(R.id.rate)
assertEquals(BuildConfig.showRateOnGooglePlay, rateButton.isVisible)
// Test donate button visibility based on BuildConfig
val donateButton = activity.findViewById<View>(R.id.donate)
assertEquals(BuildConfig.showDonate, donateButton.isVisible)
}
@Test
fun testClickListeners() {
activityController.create().start().resume()
// Test clicking on a link that opens external browser
val repoButton = activity.findViewById<View>(R.id.repo)
repoButton.performClick()
val startedIntent = shadowActivity.nextStartedActivity
assertEquals(Intent.ACTION_VIEW, startedIntent.action)
assertEquals(Uri.parse("https://github.com/CatimaLoyalty/Android/"),
startedIntent.data)
}
@Test
fun testActivityDestruction() {
activityController.create().start().resume()
// Verify a view exists before destruction
assertNotNull(activity.findViewById(R.id.credits_sub))
activityController.pause().stop().destroy()
// Verify activity was destroyed
assertTrue(activity.isDestroyed)
}
@Test
fun testDialogContentMethods() {
activityController.create().start().resume()
// Use reflection to test private methods
try {
val showCreditsMethod: Method = AboutActivity::class.java.getDeclaredMethod("showCredits")
showCreditsMethod.isAccessible = true
showCreditsMethod.invoke(activity) // Should not throw exception
val showHistoryMethod: Method = AboutActivity::class.java.getDeclaredMethod("showHistory", View::class.java)
showHistoryMethod.isAccessible = true
showHistoryMethod.invoke(activity, activity.findViewById(R.id.version_history)) // Should not throw exception
} catch (e: Exception) {
fail("Exception when calling dialog methods: ${e.message}")
}
}
@Test
fun testExternalBrowserWithDifferentURLs() {
activityController.create().start().resume()
try {
// Get access to the private method
val openExternalBrowserMethod: Method = AboutActivity::class.java.getDeclaredMethod("openExternalBrowser", View::class.java)
openExternalBrowserMethod.isAccessible = true
// Create test URLs
val testUrls = arrayOf(
"https://hosted.weblate.org/engage/catima/",
"https://github.com/CatimaLoyalty/Android/blob/main/LICENSE",
"https://catima.app/privacy-policy/",
"https://github.com/CatimaLoyalty/Android/issues"
)
for (url in testUrls) {
// Create a View with the URL as tag
val testView = View(activity)
testView.tag = url
// Call the method directly
openExternalBrowserMethod.invoke(activity, testView)
// Verify the intent
val intent = shadowActivity.nextStartedActivity
assertNotNull("No intent launched for URL: $url", intent)
assertEquals(Intent.ACTION_VIEW, intent.action)
assertEquals(Uri.parse(url), intent.data)
}
} catch (e: Exception) {
fail("Exception during reflection: ${e.message}")
}
}
@Test
fun testButtonVisibilityBasedOnBuildConfig() {
activityController.create().start().resume()
// Get the current values from BuildConfig
val showRateOnGooglePlay = BuildConfig.showRateOnGooglePlay
val showDonate = BuildConfig.showDonate
// Test that the visibility matches the BuildConfig values
assertEquals(showRateOnGooglePlay, activity.findViewById<View>(R.id.rate).isVisible)
assertEquals(showDonate, activity.findViewById<View>(R.id.donate).isVisible)
}
@Test
fun testAboutScreenTextContent() {
activityController.create().start().resume()
// Verify that text fields contain the expected content
val creditsSub = activity.findViewById<TextView>(R.id.credits_sub)
assertNotNull(creditsSub.text)
assertFalse(creditsSub.text.toString().isEmpty())
val versionHistorySub = activity.findViewById<TextView>(R.id.version_history_sub)
assertNotNull(versionHistorySub.text)
assertFalse(versionHistorySub.text.toString().isEmpty())
}
}

View File

@@ -54,7 +54,7 @@ Supported barcodes:
# Moving data from other apps
Within the app you can import cards and codes from files, Catima, FidMe, Loyalty Card Keychain, Voucher Vault, and Stocard.
Within the app you can import cards and codes from files, Catima, FidMe, Loyalty Card Keychain and Voucher Vault.
For FidMe you need to select the barcode type for each entry afterwards.
# Building

View File

@@ -3,11 +3,12 @@
# When releasing, do the following:
1. Press "Commit" and "Push" on Weblate to ensure all translations are up to date
2. Merge Weblate pull request
3. Update `CHANGELOG.md` with the new version name and the release date
4. Update `app/build.gradle.kts` with the new `versionCode` and `versionName`
5. Create a commit for the new release: `git add CHANGELOG.md app/build.gradle.kts && git commit -m "Release Catima <VERSION>"`
6. Build the new .apks: `KEYSTORE=/path/to/keystore KEYSTORE_ALIAS=catima ./build.sh`
7. Upload `app/build/outputs/apk/gplay/release/app-gplay-release.apk` to Google Play Open Testing
8. Push the version update commit: `git push`
9. Create a new release on GitHub and attach the `app/build/outputs/apk/foss/release/app-foss-release.apk` and `SHA256SUMS` files
10. When pushing the release to Google Play Production, update the metadata there: `bundle exec fastlane supply --version_code <VERSION_CODE>`
3. Make sure to pull the `main` branch locally
4. Update `CHANGELOG.md` with the new version name and the release date
5. Update `app/build.gradle.kts` with the new `versionCode` and `versionName`
6. Create a commit for the new release: `git add CHANGELOG.md app/build.gradle.kts && git commit -m "Release Catima <VERSION>"`
7. Build the new .apks: `KEYSTORE=/path/to/keystore KEYSTORE_ALIAS=catima ./build.sh`
8. Upload `app/build/outputs/apk/gplay/release/app-gplay-release.apk` to Google Play Open Testing
9. Push the version update commit: `git push`
10. Create a new release on GitHub and attach the `app/build/outputs/apk/foss/release/app-foss-release.apk` and `SHA256SUMS` files
11. When pushing the release to Google Play Production, update the metadata there: `bundle exec fastlane supply --version_code <VERSION_CODE>`

View File

@@ -0,0 +1,2 @@
- Zkopírování ID karty do schránky z dialogu zobrazení nebo po dlouhém podržení
- Prohození polí zůstatku a měny pro snížení nechtěného zaokrouhlování

View File

@@ -0,0 +1,2 @@
- Kopiere die Karten-ID aus dem "Ansichtsdialog" oder durch "langes Klicken" in die Zwischenablage
- Tausch der Felder Guthaben und Währung, um hoffentlich so unbeabsichtigte Konvertierungen zu reduzieren

View File

@@ -0,0 +1,2 @@
- Copy card ID to clipboard from view dialog or long press
- Swap balance and currency fields to hopefully reduce unintended rounding

View File

@@ -0,0 +1 @@
- Add duplicate option to main screen and reorder options slightly

View File

@@ -1 +1 @@
- Correcciones varias de RTL
- Varias correcciones de RTL

View File

@@ -1,4 +1,4 @@
- Mejoras en el renderizado de los códigos de barras
- Interoperabilidad básica con aplicaciones externas (Android 6.0+)
- Mejoras en el análisis de los códigos de barras
- Interoperatividad básica con aplicaciones externas (Android 6.0+)
- Pantalla de ajustes reorganizada
- Corrección al importar desde algunos navegadores que añaden una / al final de la URL al compartir

View File

@@ -1 +1 @@
- Correción de error inusual
- Corrección de error inusual

View File

@@ -1,3 +1,3 @@
- Se movió "Modo Archivo" al menú de "Opciones de pantalla" (antes llamado "Mostrar detalles")
- Se movió «Modo Archivador» al menú de «Opciones de pantalla» (antes llamado «Mostrar detalles»)
- Soporte al lenguaje por aplicación de Android 13
- Incorporación de la política de privacidad, registro de cambios y licencia en la aplicación
- Incorporación de la directiva de privacidad, registro de cambios y licencia en la aplicación

View File

@@ -1,4 +1,4 @@
- Se mejoró el flujo de trabajo "Agregar tarjeta"
- Se mejoró el flujo de trabajo «Agregar tarjeta»
- Mejoras en el proceso de validación.
- Se corrigió un error que causaba que la interfaz de usuario entrara en un estado no válido al cambiar la vista del archivo.
- El color del sistema o del mapa se utiliza para la barra de navegación (Android 8.1+)

View File

@@ -0,0 +1,4 @@
- Soporte para crear una tarjeta al compartir texto plano
- Exhibir tipo de imagen en lugar de código de barras debajo de imágenes
- Repara el accidente posible al intentar importar una copia de respaldo de la aplicación Nextcloud
- Mejora del mantenimiento para dispositivos sin cámara

View File

@@ -0,0 +1 @@
- Reparar el gesto hacia atrás en la pantalla principal desestimando el teclado y la búsqueda en Android 13+

View File

@@ -1,3 +1,3 @@
- Opción para navegar por las tarjetas usando los botones de volumen
- Corregir la importación de Stocard
- Corregir el mensaje "Importación cancelada" que aparece después de una importación exitosa
- Corregir el mensaje «Importación cancelada» que aparece después de una importación correcta

View File

@@ -0,0 +1 @@
Corregir el ajuste de texto en el cuadro de diálogo de agregar

View File

@@ -0,0 +1,4 @@
Cambiar el número de columnas predeterminado a 4 en pantallas panorámicas
Permitir modificar el número de columnas para los modos vertical y horizontal en la configuración
Mantener el filtro de búsqueda de la pantalla principal al girar la pantalla o abrir una tarjeta
Limitar la longitud máxima de las notas que se muestran en la pantalla principal

View File

@@ -0,0 +1,3 @@
Se agregó compatibilidad con Passbook (.pkpass)
Se corrigió la importación de archivos PDF transparentes
Se mejoró la visualización de miniaturas transparentes

View File

@@ -0,0 +1 @@
Se soluciona el cuelgue que se producía al abrir archivos pkpass no válidos

View File

@@ -0,0 +1 @@
Mejorar la exhibición de iconos de archivador/estrellas

View File

@@ -0,0 +1,3 @@
Compatible con Android 15
Se corrigió un error que provocaba que el teclado cubriera el botón de guardar en la pantalla de edición
Se corrigió un error que impedía la detección de algunos archivos pkpass como tales (compatibilidad con el tipo MIME application/vnd-com.apple.pkpass)

View File

@@ -0,0 +1,2 @@
Posibilidad de ordenar las tarjetas por fecha de inicio de validez
Se ha vuelto temporalmente a la versión para Android 14 para solucionar algunos problemas de la interfaz de usuario

View File

@@ -0,0 +1,3 @@
Compatible con Android 15
Se corrigió el fallo que se producía al leer archivos pkpass no compatibles
Se mejoró la compatibilidad con pkpass

View File

@@ -0,0 +1,4 @@
Se agregó la opción de elegir el ancho del código de barras en la vista de pantalla completa.
Se eliminó la función de importación desde la aplicación que resultaba confusa.
Se corrigieron varios errores de escaneo.
Se corrigió el error que provocaba el cierre inesperado de la aplicación al cargar un archivo pkpass sin código de barras

View File

@@ -0,0 +1 @@
Actualizaciones de dependencias y traducciones

View File

@@ -1,2 +1,2 @@
- Soporte para atajos de aplicación añadidos (Android 7.1+), donde las tarjetas usadas recientemente aparecerán como atajos. (pull #145 (https://github.com/brarcher/loyalty-card-locker/pull/145))
- Widget añadido que trabaja como un atajo de aplicación fijado, para dispositivos debajo de Android 7.1. (pull #142 (https://github.com/brarcher/loyalty-card-locker/pull/142))
Soporte para atajos de aplicación añadidos (Android 7.1+), donde las tarjetas usadas recientemente aparecerán como atajos. (pull #145 (https://github.com/brarcher/loyalty-card-locker/pull/145))
Añade un Widget que funciona como un atajo de aplicación pinchado, para dispositivos debajo de Android 7.1. (pull #142 (https://github.com/brarcher/loyalty-card-locker/pull/142))

View File

@@ -0,0 +1,2 @@
Agregar un widget que muestre todas las tarjetas no archivadas
Evitar que el teclado tape el botón de guardar en las pantallas de edición y de grupo

View File

@@ -0,0 +1,2 @@
Nuevo diseño del logotipo de Catima
Actualizaciones de traducción

View File

@@ -0,0 +1,3 @@
Se agregó compatibilidad con archivos .pkpasses
Se eliminó el importador Stocard (Stocard ya no existe)
Se deshabilitaron temporalmente las imágenes de widgets en versiones anteriores a Android 12L (solución alternativa para un problema de bloqueo)

View File

@@ -0,0 +1,4 @@
Compatible con Android 16
Se corrigió un posible fallo tras eliminar una imagen de la tarjeta
Se eliminó la función «Orientación de pantalla» (Google eliminó la capacidad de las aplicaciones para controlar la rotación de pantalla al desarrollar para Android 16)
Se añadió un informe de fallos a la compilación FOSS (no se usa en la versión de Google Play, solo en otras tiendas de aplicaciones)

View File

@@ -0,0 +1 @@
Se corrigió un posible fallo que podía ocurrir con las tarjetas que no tenían información de color en la base de datos

View File

@@ -0,0 +1 @@
Preparativos para futuras mejoras (se reescribieron muchas clases a Kotlin)

View File

@@ -0,0 +1,2 @@
Añadir compatibilidad para agregar accesos directos a la tarjeta de fidelización desde el lanzador/pantalla de inicio. (pull #161 (https://github.com/brarcher/loyalty-card-locker/pull/161))
Se elimina la compatibilidad para añadir accesos directos a tarjetas de fidelización desde la propia aplicación. Esto elimina la necesidad del permiso para accesos directos. (pull #163 (https://github.com/brarcher/loyalty-card-locker/pull/163))

View File

@@ -0,0 +1,2 @@
Se ha corregido un problema en Android SDK 24+ en el que el uso de la opción de importación del selector de archivos provocaba un fallo. (pull #170 (https://github.com/brarcher/loyalty-card-locker/pull/170))
Nuevo icono y esquema de colores. (pull #171 (https://github.com/brarcher/loyalty-card-locker/pull/171))

View File

@@ -1,3 +1,3 @@
- Traducciones en italiano
- Soporte para todos los tipos de códigos de barras 1D. (Originalmente sólo tenian soporte los productos con códigos de barras 1D)
- Añadir permiso obligatorio para la cámara, que inicialmente faltaba.
Traducciones en italiano
Soporte para todos los tipos de códigos de barras 1D. (Originalmente sólo tenían soporte los productos con códigos de barras 1D)
Añadir permiso obligatorio para la cámara, que inicialmente faltaba.

View File

@@ -1 +1 @@
- Solución alternativa al fallo durante la instalación en algunas versiones de Android (como Android 5 o menor), (pull #184 (https://github.com/brarcher/loyalty-card-locker/pull/184))
Solución alternativa al fallo durante la instalación en algunas versiones de Android (como Android 5 o menor), (pull #184 (https://github.com/brarcher/loyalty-card-locker/pull/184))

View File

@@ -1,2 +1,2 @@
- Mejorada la distribución en la lista de tarjetas. (pull #188 (https://github.com/brarcher/loyalty-card-locker/pull/188))
- Mejorada la distribución al ver una tarjeta. (pull #190 (https://github.com/brarcher/loyalty-card-locker/pull/190))
Mejorada la distribución en la lista de tarjetas. (pull #188 (https://github.com/brarcher/loyalty-card-locker/pull/188))
Mejorada la distribución al ver una tarjeta. (pull #190 (https://github.com/brarcher/loyalty-card-locker/pull/190))

View File

@@ -1 +1,2 @@
- Cambios al mostrar una nota en la vista de tarjeta, permitir que el ID de tarjeta ocupe varias líneas, y mostrar el nomber de la tienda. (pull #197 (https://github.com/brarcher/loyalty-card-locker/pull/197))
Cambios al mostrar una nota en la vista de tarjeta, permitir que el ID de tarjeta ocupe varias líneas, y mostrar el nombre de la tienda.
(pull #197 (https://github.com/brarcher/loyalty-card-locker/pull/197))

View File

@@ -0,0 +1,2 @@
- Reducir el espacio en el encabezado al visualizar una tarjeta.
- Desactivar el pitido al escanear un código de barras.

View File

@@ -0,0 +1,2 @@
- Reducir el SDK mínimo de 17 a 15. (https://github.com/brarcher/loyalty-card-locker/pull/226))
- Eliminar el uso de la biblioteca Apache heredada, que solo se utilizaba en las pruebas unitarias pero que ya no es necesaria. (pull #225 (https://github.com/brarcher/loyalty-card-locker/pull/225))

View File

@@ -1,4 +1,4 @@
- Nueva opción del control del brillo al mostrar un código de barras (pull #259 (https://github.com/brarcher/loyalty-card-locker/pull/259))
- Traducciones a griego (pull #252 (https://github.com/brarcher/loyalty-card-locker/pull/252))
- Traducciones a esloveno (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260))
- Traducciones actualizadas (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260), pull #254 (https://github.com/brarcher/loyalty-card-locker/pull/254))
Nueva opción del control del brillo al mostrar un código de barras (pull #259 (https://github.com/brarcher/loyalty-card-locker/pull/259))
Traducciones a griego (pull #252 (https://github.com/brarcher/loyalty-card-locker/pull/252))
Traducciones a esloveno (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260))
Traducciones actualizadas (pull #260 (https://github.com/brarcher/loyalty-card-locker/pull/260), pull #254 (https://github.com/brarcher/loyalty-card-locker/pull/254))

View File

@@ -0,0 +1,4 @@
Ordenar la lista de tarjetas sin distinguir entre mayúsculas y minúsculas
(solicitud n.º 266 (https://github.com/brarcher/loyalty-card-locker/pull/266))
Añadir opción para bloquear la orientación de todas las tarjetas
(solicitud n.º 269 (https://github.com/brarcher/loyalty-card-locker/pull/269))

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