Compare commits

...

200 Commits

Author SHA1 Message Date
Sylvia van Os
05dfd48f57 Release Catima 2.2.0 2021-08-02 23:42:32 +02:00
Sylvia van Os
ef955b866d Remove unused imports 2021-08-02 23:09:49 +02:00
Sylvia van Os
a843b5a1b9 Make lint happier 2021-08-02 22:58:06 +02:00
Sylvia van Os
8305d58ccc Fix maximize button appearing on no barcode 2021-08-02 22:57:55 +02:00
Sylvia van Os
6af79d5f41 General cleanups 2021-08-02 20:47:48 +02:00
Sylvia van Os
3b326d6f9c Also remove from AboutActivity 2021-08-02 20:37:13 +02:00
Sylvia van Os
1456e5073e Get rid of barely used dependency 2021-08-02 20:36:20 +02:00
Sylvia van Os
6a13dbf66a Comma-separate group names in loyalty card view 2021-08-01 23:03:56 +02:00
Sylvia van Os
c5b1718e8e Pre-select active group on creating new card 2021-08-01 22:33:44 +02:00
Sylvia van Os
e2bcc24867 Make links in notes clickable 2021-08-01 21:39:48 +02:00
Sylvia van Os
c86cc22a93 Release v2.1.0 2021-08-01 21:00:12 +02:00
Sylvia van Os
765577ae9e Merge pull request #306 from weblate/weblate-catima-catima
Translations update from Weblate
2021-08-01 20:50:42 +02:00
Heimen Stoffels
d1d15644b5 Translated using Weblate (Dutch)
Currently translated at 100.0% (172 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-08-01 20:36:29 +02:00
Sylvia van Os
5d41b213db Remove translations to break lint 2021-08-01 20:36:17 +02:00
Sylvia van Os
aa515b1192 Be more explicit in quantity
Translators still keep triggering the linter with errors such as the
following:
Error: The quantity 'one' matches more than one specific number in this locale (1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …), but the message did not include a formatting argument (such as %d). This is usually an internationalization error. See full issue explanation for more. [ImpliedQuantity]
2021-08-01 12:50:56 +02:00
Sylvia van Os
068660fe3f Merge pull request #305 from weblate/weblate-catima-catima
Translations update from Weblate
2021-08-01 12:42:14 +02:00
solokot
99ffaf97d1 Translated using Weblate (Russian)
Currently translated at 100.0% (172 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-08-01 12:32:51 +02:00
Heimen Stoffels
b53999c1cf Translated using Weblate (Dutch)
Currently translated at 100.0% (172 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-08-01 12:32:51 +02:00
Allan Nordhøy
57f87d7dc0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.6% (156 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2021-08-01 12:32:50 +02:00
J. Lavoie
760f6a873d Translated using Weblate (Italian)
Currently translated at 100.0% (172 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2021-08-01 12:32:50 +02:00
J. Lavoie
675bd43ff8 Translated using Weblate (French)
Currently translated at 100.0% (172 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2021-08-01 12:32:50 +02:00
J. Lavoie
07532ce001 Translated using Weblate (Spanish)
Currently translated at 100.0% (172 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2021-08-01 12:32:50 +02:00
J. Lavoie
908a7055c7 Translated using Weblate (German)
Currently translated at 100.0% (172 of 172 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2021-08-01 12:32:49 +02:00
Sylvia van Os
2be371caed Forgot number 2021-07-30 19:46:10 +02:00
Sylvia van Os
f4f3f2e307 Increase i18n flexibility
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. Therefore, we use the plain string when meaning exactly 1, and otherwise use the plural forms.
2021-07-30 19:40:43 +02:00
Sylvia van Os
edbf76c7dc Merge pull request #303 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-30 19:02:48 +02:00
109247019824
f8615f45f0 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2021-07-30 18:18:55 +02:00
IllusiveMan196
c3bc3e9911 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/uk/
2021-07-30 18:18:55 +02:00
solokot
8adcca1df2 Translated using Weblate (Russian)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-07-30 18:18:55 +02:00
Heimen Stoffels
bdde8669ac Translated using Weblate (Dutch)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-07-30 18:18:55 +02:00
solokot
59db2df525 Translated using Weblate (Russian)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-07-30 18:18:55 +02:00
Sylvia van Os
3b83ff5b0e Fix syntax 2021-07-30 18:18:32 +02:00
Heimen Stoffels
19fb290a51 Translated using Weblate (Dutch)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-07-30 18:15:39 +02:00
Allan Nordhøy
e1e823d9e0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.0% (153 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2021-07-30 18:15:30 +02:00
Sylvia van Os
7ed477b670 Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-29 22:16:47 +02:00
Sylvia van Os
1960fb0b6a Fix fullscreen loss on rotation 2021-07-29 22:16:36 +02:00
Sylvia van Os
e913482790 Merge pull request #299 from comradekingu/patch-7
New strings reworked
2021-07-29 21:54:43 +02:00
Allan Nordhøy
2e5bd76d31 New strings reworked 2021-07-29 19:47:10 +00:00
Sylvia van Os
876ae979da Simplify updateTempState
Hopefully fixes the few weird crashes on Google Play
2021-07-29 21:42:21 +02:00
Sylvia van Os
d918c15ad6 Prevent ArithmeticException 2021-07-29 21:18:21 +02:00
Sylvia van Os
d57cb307c3 Support multi-deleting cards 2021-07-29 20:38:37 +02:00
Sylvia van Os
cbcf1bcd99 Use desugaring to upgrade zxing without having to increase minSdk
As documented by the zxing team on https://github.com/zxing/zxing/wiki/Frequently-Asked-Questions#it-doesnt-work-with-java-7--no-interface-method-sortljavautilcomparator
2021-07-28 23:05:27 +02:00
Sylvia van Os
96bc10583f Fix colour dialog selected colour 2021-07-27 22:04:09 +02:00
Sylvia van Os
5ca4d29a36 Release Catima 2.0.4 2021-07-27 21:42:04 +02:00
Sylvia van Os
2efb666fae Fix loyalty card colour changing 2021-07-27 21:38:54 +02:00
Sylvia van Os
89dce1068f Shortcut fixes 2021-07-27 21:26:24 +02:00
Sylvia van Os
9854125af9 Make lint happy 2021-07-26 23:37:17 +02:00
Sylvia van Os
6e307ab1f0 Update dependencies 2021-07-26 23:10:51 +02:00
Sylvia van Os
c4f0d1bef6 Release Catima 2.0.3 2021-07-25 22:32:40 +02:00
Sylvia van Os
5201788818 Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-25 21:52:10 +02:00
Sylvia van Os
8472bc9755 Make SpotBugs happy 2021-07-25 21:51:46 +02:00
Sylvia van Os
ce0f531831 Merge pull request #289 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-25 21:41:26 +02:00
Quentin PAGÈS
18e9c3ccb5 Translated using Weblate (Occitan)
Currently translated at 32.3% (55 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/oc/
2021-07-25 21:40:25 +02:00
Sylvia van Os
19afe8e69c Simplify edit activity and fix some status bugs 2021-07-25 21:40:15 +02:00
Sylvia van Os
8e5bace7cc Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-23 23:52:58 +02:00
Sylvia van Os
1ca85e9d7b Create unit test for broken onResume behaviour 2021-07-23 23:52:45 +02:00
Sylvia van Os
eb24af8266 Add Zip4j to used libraries list 2021-07-22 12:14:39 +02:00
Sylvia van Os
12ba01eb87 Merge pull request #287 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-21 23:32:51 +02:00
Quentin PAGÈS
a0e30bdccc Added translation using Weblate (Occitan) 2021-07-21 23:26:38 +02:00
Sylvia van Os
4663e22128 Rename Spanish metadata to something fastlane supply can push to Google Play 2021-07-21 18:42:27 +02:00
Sylvia van Os
7bd1d16d24 Merge pull request #286 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-21 18:37:32 +02:00
Diego
fd838cfd43 Translated using Weblate (Spanish)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/es/
2021-07-21 18:32:48 +02:00
Diego
1eca79b4cb Translated using Weblate (Spanish)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2021-07-21 18:32:48 +02:00
Sylvia van Os
1d694f5f2c Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-21 18:23:05 +02:00
Sylvia van Os
96a046b165 Fix share/import URL crash on Android 6 2021-07-21 18:22:50 +02:00
Sylvia van Os
5ff9c9c469 Merge pull request #282 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-19 07:02:06 +02:00
Nyatsuki
836cdd87bc Translated using Weblate (Japanese)
Currently translated at 99.4% (169 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ja/
2021-07-19 06:33:37 +02:00
109247019824
0af6dd4d44 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2021-07-17 23:32:59 +02:00
IllusiveMan196
034d62a643 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/uk/
2021-07-17 23:32:59 +02:00
IllusiveMan196
417224602e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (170 of 170 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/uk/
2021-07-17 23:32:58 +02:00
Sylvia van Os
c711aeae7f Fix build 2021-07-16 20:44:51 +02:00
109247019824
1f12543e3e Translated using Weblate (Bulgarian)
Currently translated at 94.7% (162 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2021-07-16 20:34:00 +02:00
Nyatsuki
dc31175b5d Translated using Weblate (Japanese)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ja/
2021-07-16 20:34:00 +02:00
Nyatsuki
719b8112eb Translated using Weblate (Japanese)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ja/
2021-07-16 20:34:00 +02:00
solokot
f19a3c507b Translated using Weblate (Russian)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/ru/
2021-07-16 20:34:00 +02:00
solokot
2416ec396a Translated using Weblate (Russian)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-07-16 20:34:00 +02:00
Heimen Stoffels
d1fe92e967 Translated using Weblate (Dutch)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-07-16 20:34:00 +02:00
Gediminas Murauskas
ff5fd49b89 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/lt/
2021-07-16 20:34:00 +02:00
J. Lavoie
1015e0d94e Translated using Weblate (Italian)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2021-07-16 20:34:00 +02:00
J. Lavoie
d872828e7d Translated using Weblate (French)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2021-07-16 20:34:00 +02:00
J. Lavoie
8f6ad6d1bd Translated using Weblate (German)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2021-07-16 20:34:00 +02:00
Sylvia van Os
05fd629ad4 Fix bad strings 2021-07-16 20:33:46 +02:00
Sylvia van Os
efdb0dd6bb Consolidate 2 very similar strings 2021-07-16 20:28:25 +02:00
Sylvia van Os
2e0482beef Always move import/export to overflow menu 2021-07-15 23:03:28 +02:00
Sylvia van Os
0f25743da4 Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-15 22:45:32 +02:00
Sylvia van Os
e970bf185a Lowercase .zip filename 2021-07-15 22:45:12 +02:00
Sylvia van Os
60f3547b01 Merge pull request #275 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-15 22:18:09 +02:00
109247019824
ae09db428b Translated using Weblate (Bulgarian)
Currently translated at 21.0% (36 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2021-07-15 22:09:55 +02:00
Sylvia van Os
ebca7ca150 Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-15 22:09:44 +02:00
Sylvia van Os
b6ef6806a0 Make selected in main screen selectable 2021-07-15 22:09:31 +02:00
Sylvia van Os
20a9cb30c4 Merge pull request #273 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-15 21:37:14 +02:00
109247019824
a43112f469 Translated using Weblate (Bulgarian)
Currently translated at 11.1% (19 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/bg/
2021-07-15 21:33:48 +02:00
109247019824
2a95b2f530 Added translation using Weblate (Bulgarian) 2021-07-15 21:30:47 +02:00
Sylvia van Os
cb979bfbec Release Catima 2.0 2021-07-14 20:26:39 +02:00
Sylvia van Os
feabe353b0 Fix loyalty card viewer appbar top margin 2021-07-14 20:08:28 +02:00
Sylvia van Os
7987932100 Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-14 19:39:34 +02:00
Sylvia van Os
e496c69e15 Fix FAB being below other elements in Android 4 2021-07-14 19:39:19 +02:00
Sylvia van Os
e0300f8f21 Merge pull request #269 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-14 12:04:15 +02:00
phlostically
64cbcb2ef7 Translated using Weblate (Esperanto)
Currently translated at 40.9% (70 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/eo/
2021-07-14 11:32:33 +02:00
J. Lavoie
7e24f02a73 Translated using Weblate (French)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fr/
2021-07-14 11:32:32 +02:00
J. Lavoie
ed89eab782 Translated using Weblate (Italian)
Currently translated at 97.0% (166 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2021-07-14 11:32:31 +02:00
J. Lavoie
17863e8920 Translated using Weblate (French)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2021-07-14 11:32:31 +02:00
J. Lavoie
6e82e6fc5d Translated using Weblate (German)
Currently translated at 97.0% (166 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2021-07-14 11:32:31 +02:00
Sylvia van Os
64b1103f13 Merge pull request #268 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-13 23:59:44 +02:00
phlostically
e4274df941 Added translation using Weblate (Esperanto) 2021-07-13 23:58:37 +02:00
Sylvia van Os
c786b4b5a7 Merge pull request #267 from TheLastProject/dependabot/bundler/addressable-2.8.0
Bump addressable from 2.7.0 to 2.8.0
2021-07-13 21:48:18 +02:00
dependabot[bot]
95405270cb Bump addressable from 2.7.0 to 2.8.0
Bumps [addressable](https://github.com/sporkmonger/addressable) from 2.7.0 to 2.8.0.
- [Release notes](https://github.com/sporkmonger/addressable/releases)
- [Changelog](https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sporkmonger/addressable/compare/addressable-2.7.0...addressable-2.8.0)

---
updated-dependencies:
- dependency-name: addressable
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 19:41:48 +00:00
Sylvia van Os
9ef8eb2934 Merge pull request #265 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-12 21:44:33 +02:00
Gediminas Murauskas
4f70b06edb Translated using Weblate (Lithuanian)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/lt/
2021-07-12 20:32:56 +02:00
Heimen Stoffels
5677aa2b38 Translated using Weblate (Dutch)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/nl/
2021-07-12 20:32:56 +02:00
solokot
44997e1bbd Translated using Weblate (Russian)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-07-12 20:32:55 +02:00
Heimen Stoffels
a96c569314 Translated using Weblate (Dutch)
Currently translated at 100.0% (171 of 171 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-07-12 20:32:55 +02:00
Sylvia van Os
5545220d53 Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-11 19:34:05 +02:00
Sylvia van Os
4f47ee30ba Update supported import list 2021-07-11 19:33:52 +02:00
Sylvia van Os
4f9f318960 Merge pull request #264 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-11 19:14:42 +02:00
solokot
5dcfcf0ad0 Translated using Weblate (Russian)
Currently translated at 100.0% (169 of 169 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-07-11 19:10:51 +02:00
Heimen Stoffels
eb809974c4 Translated using Weblate (Dutch)
Currently translated at 100.0% (169 of 169 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-07-11 19:10:51 +02:00
Allan Nordhøy
815b037be7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 89.9% (152 of 169 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2021-07-11 19:10:51 +02:00
Sylvia van Os
3eff150835 Implement flashlight toggle on scan 2021-07-11 19:10:38 +02:00
Sylvia van Os
e191e8bbe6 Update supported type list 2021-07-11 16:16:14 +02:00
Sylvia van Os
5aaf135325 Update screenshots 2021-07-11 16:14:18 +02:00
Sylvia van Os
6fd4f617e5 Fix .csv import fallback 2021-07-11 15:52:22 +02:00
Sylvia van Os
621e4ce162 Merge pull request #259 from comradekingu/patch-6
App strings reworked 4
2021-07-11 13:40:31 +02:00
Sylvia van Os
bf07e8935f Merge pull request #262 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-11 13:37:41 +02:00
Sylvia van Os
e69d6a453b Consistent phrasing 2021-07-11 13:35:53 +02:00
Gediminas Murauskas
579c2fc640 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/lt/
2021-07-11 13:32:42 +02:00
solokot
ba870481dd Translated using Weblate (Russian)
Currently translated at 100.0% (167 of 167 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-07-11 13:32:42 +02:00
Heimen Stoffels
0c10936e75 Translated using Weblate (Dutch)
Currently translated at 100.0% (167 of 167 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-07-11 13:32:42 +02:00
Gediminas Murauskas
26cf4250ad Translated using Weblate (Lithuanian)
Currently translated at 31.7% (53 of 167 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/lt/
2021-07-11 13:32:42 +02:00
Sylvia van Os
a8d5f38d5c Update privacy policy URL 2021-07-11 13:32:32 +02:00
Sylvia van Os
5e1bac4bcf Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker 2021-07-11 13:31:15 +02:00
Sylvia van Os
6df8b7b1ea Generate catima.app share links 2021-07-11 13:31:03 +02:00
Sylvia van Os
964c603405 Only list new format 2021-07-11 12:31:51 +02:00
Sylvia van Os
f64eea6470 Update app/src/main/res/values/strings.xml
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
2021-07-11 12:31:22 +02:00
Sylvia van Os
aa97ad49d2 Create CNAME 2021-07-11 12:20:33 +02:00
Sylvia van Os
6221da72f8 Delete CNAME 2021-07-11 05:48:05 +02:00
Sylvia van Os
fe8ec38bf3 Create CNAME 2021-07-11 00:42:20 +02:00
Sylvia van Os
7324353d74 Use location hash instead of query parameters in share URL for increased privacy 2021-07-10 18:33:54 +02:00
Sylvia van Os
c5f0ee3a66 Fix bottom sheet alignment issue 2021-07-10 13:06:00 +02:00
Sylvia van Os
a8b4b25afb Merge pull request #260 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-10 05:58:57 +02:00
solokot
9744ede674 Translated using Weblate (Russian)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-07-10 05:00:16 +02:00
Heimen Stoffels
43505d427d Translated using Weblate (Dutch)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-07-10 05:00:16 +02:00
Allan Nordhøy
65e54c63ef Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.9% (151 of 166 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2021-07-10 05:00:15 +02:00
J. Lavoie
31b306d432 Translated using Weblate (Italian)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2021-07-10 05:00:15 +02:00
J. Lavoie
25bd1de09d Translated using Weblate (French)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2021-07-10 05:00:15 +02:00
J. Lavoie
c82e0d82a9 Translated using Weblate (German)
Currently translated at 100.0% (166 of 166 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2021-07-10 05:00:14 +02:00
Sylvia van Os
6625488d84 Merge pull request #258 from TheLastProject/feature/catimaImportExportZip
Import and export .csv and images as .zip
2021-07-09 22:26:13 +02:00
Sylvia van Os
a1f632ace3 Set CharSet 2021-07-09 22:20:00 +02:00
Sylvia van Os
b9ea7fd1ed Create export->import test 2021-07-09 22:08:46 +02:00
Allan Nordhøy
8dff8d392e Fixes to import dialogs 2021-07-08 22:07:47 +00:00
Allan Nordhøy
b10d50d5a0 Revert accidental deletion 2021-07-08 22:05:38 +00:00
Allan Nordhøy
0b54d87f4f App strings reworked 4 2021-07-08 20:47:35 +00:00
Sylvia van Os
a9568d6adb Import and export .csv and images as .zip 2021-07-08 22:27:40 +02:00
Sylvia van Os
18c5aa4707 Support for new Voucher Vault export 2021-07-08 19:59:27 +02:00
Sylvia van Os
f53c61bbec Merge pull request #245 from TheLastProject/feature/StocardImport
Stocard import
2021-07-08 19:41:22 +02:00
Sylvia van Os
25c9c67ca2 Make spotBugs happy 2021-07-08 19:37:04 +02:00
Sylvia van Os
2efbf664c9 Finish Stocard importing 2021-07-08 19:29:52 +02:00
Sylvia van Os
44023969a7 Merge pull request #255 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-08 01:38:36 +02:00
Allan Nordhøy
0e5216b010 Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.1% (147 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2021-07-08 01:33:02 +02:00
Sylvia van Os
f258506936 Merge pull request #253 from TheLastProject/feature/upce
Add UPC-E support
2021-07-07 23:19:59 +02:00
Sylvia van Os
8bad342374 Add UPC-E support 2021-07-07 19:14:11 +02:00
Sylvia van Os
476a219bec getOrDefault requires API level 24 2021-07-06 23:36:21 +02:00
Sylvia van Os
24e19e26ba Merge branch 'master' of github.com:TheLastProject/loyalty-card-locker into feature/StocardImport 2021-07-06 23:29:50 +02:00
Sylvia van Os
3cb16ae3e9 Update CHANGELOG 2021-07-06 23:26:50 +02:00
Sylvia van Os
c7ddf957fa Clean up unused imports 2021-07-06 23:25:55 +02:00
Sylvia van Os
0de8cd93ad Ask password when import fails with BadPassword 2021-07-06 23:25:05 +02:00
Sylvia van Os
614e5bac2d Mark Fidme and Stocard as beta 2021-07-06 22:44:02 +02:00
Sylvia van Os
e76bd9363c Make Stocard import work 2021-07-06 21:54:19 +02:00
Sylvia van Os
97c508c920 Stocard import skeleton 2021-07-06 00:20:35 +02:00
Sylvia van Os
0f178c1cac Merge pull request #250 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-05 09:03:10 +02:00
String E. Fighter
8eab852e1a Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/
2021-07-05 08:33:19 +02:00
String E. Fighter
539f8846d8 Translated using Weblate (German)
Currently translated at 100.0% (163 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2021-07-05 08:33:19 +02:00
Sylvia van Os
fb239b6974 Merge pull request #247 from weblate/weblate-catima-catima
Translations update from Weblate
2021-07-02 09:36:29 +02:00
J. Lavoie
85b553e17b Translated using Weblate (Finnish)
Currently translated at 100.0% (163 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fi/
2021-07-02 09:33:32 +02:00
J. Lavoie
f085f1e9e6 Translated using Weblate (Italian)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/it/
2021-07-02 09:33:32 +02:00
J. Lavoie
56b387c725 Translated using Weblate (German)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/
2021-07-02 09:33:32 +02:00
J. Lavoie
daf0cdaa71 Translated using Weblate (French)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fr/
2021-07-02 09:33:31 +02:00
J. Lavoie
6a7bebdbcd Translated using Weblate (Italian)
Currently translated at 100.0% (163 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/it/
2021-07-02 09:33:31 +02:00
J. Lavoie
7f72c3bcaf Translated using Weblate (French)
Currently translated at 100.0% (163 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fr/
2021-07-02 09:33:31 +02:00
J. Lavoie
52bff53756 Translated using Weblate (German)
Currently translated at 100.0% (163 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/de/
2021-07-02 09:33:30 +02:00
Sylvia van Os
73743b7f1e Merge pull request #246 from weblate/weblate-catima-catima
Translations update from Weblate
2021-06-30 07:29:13 +02:00
solokot
acfcd3d2d2 Translated using Weblate (Russian)
Currently translated at 100.0% (163 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/ru/
2021-06-30 05:33:04 +02:00
Heimen Stoffels
24e8d12b73 Translated using Weblate (Dutch)
Currently translated at 100.0% (163 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nl/
2021-06-30 05:33:04 +02:00
Allan Nordhøy
28068dcddc Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.1% (147 of 163 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2021-06-30 05:33:03 +02:00
Sylvia van Os
3e5ab76636 Update CHANGELOG 2021-06-29 22:40:36 +02:00
Sylvia van Os
3dceec8ec0 Support for password-protected zip files 2021-06-29 22:40:02 +02:00
Sylvia van Os
0cc409d087 Add Fidme test 2021-06-29 20:02:49 +02:00
Sylvia van Os
346acfa3f5 Fix CHANGELOG 2021-06-28 20:55:04 +02:00
Sylvia van Os
8d48da431e Adding and viewing front/back images (#215)
* Adding and viewing front/back images

* Fix import and export

* Fix unit tests

* Smaller preview pictures but clickable to make big

* Implement removing image

* Add card photo direct from camera

* Read Exif rotation info from picture taken

* Fix bad copy-paste

* Refactor to use local file system

* Delete card images when deleting card

* Prepare for image-based unit tests in ViewActivityTest
2021-06-28 20:40:52 +02:00
Sylvia van Os
b913fad847 Merge pull request #243 from weblate/weblate-catima-catima
Translations update from Weblate
2021-06-22 18:34:37 +02:00
IllusiveMan196
c900642f8e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/uk/
2021-06-22 18:33:24 +02:00
IllusiveMan196
c7d0da9e20 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (155 of 155 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/uk/
2021-06-22 18:33:24 +02:00
Sylvia van Os
f66b368cc2 Merge pull request #242 from weblate/weblate-catima-catima
Translations update from Weblate
2021-06-21 18:07:18 +02:00
huuhaa
4dad96472f Translated using Weblate (Finnish)
Currently translated at 100.0% (155 of 155 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fi/
2021-06-21 15:34:11 +02:00
Sylvia van Os
01bb4f0fc4 Merge pull request #240 from weblate/weblate-catima-catima
Translations update from Weblate
2021-06-20 08:09:08 +02:00
Allan Nordhøy
9ae5d74c7c Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.9% (141 of 155 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/nb_NO/
2021-06-20 02:44:38 +02:00
Sylvia van Os
5af6d9b61c Fix language names for Google Play 2021-06-17 19:04:02 +02:00
Sylvia van Os
eb89a04c72 Merge pull request #239 from weblate/weblate-catima-catima
Translations update from Weblate
2021-06-15 18:05:47 +02:00
huuhaa
1019e40987 Translated using Weblate (Finnish)
Currently translated at 100.0% (3 of 3 strings)

Translation: Catima/Fastlane
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/fi/
2021-06-15 12:32:35 +02:00
huuhaa
175d860885 Translated using Weblate (Finnish)
Currently translated at 100.0% (155 of 155 strings)

Translation: Catima/Catima
Translate-URL: https://hosted.weblate.org/projects/catima/catima/fi/
2021-06-15 12:32:35 +02:00
125 changed files with 3947 additions and 1294 deletions

View File

@@ -14,10 +14,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
- name: set up JDK 11
uses: actions/setup-java@v2
with:
java-version: 1.8
distribution: 'adopt'
java-version: '11'
- name: Build
run: ./gradlew assembleRelease
- name: Check lint

View File

@@ -1,5 +1,68 @@
# Changelog
## v2.2.0 (2021-08-02)
Changes:
- Make links in notes clickable
- Pre-select group the user is currently in when creating a new card
- Comma-separate group names in loyalty card view
- Fix maximize button appearing on no barcode
## v2.1.0 (2021-08-01)
Changes:
- Fix selected colour in colour changing dialog
- Support for deleting multiple cards at once
- Fix possible ArithmeticException when resizing image
- Fix fullscreen is closed when rotating device
## v2.0.4 (2021-07-27)
Changes:
- Fix shortcut creation
- Generate card-specific shortcut icon
- Fix ability to change loyalty card colour
## v2.0.3 (2021-07-25)
Changes:
- Fix loading photos when editing existing card
## v2.0.2 (2021-07-25)
Changes:
- Fix inability to configure photos in new loyalty card
## v2.0.1 (2021-07-21)
Changes:
- Several minor translation and UI fixes
- Fix crash in import/sharing loyalty card on Android 6
## v2.0 (2021-07-14)
Breaking changes:
- The backup format changed, see https://github.com/TheLastProject/Catima/wiki/Export-format
- The URL sharing format changed, see https://github.com/TheLastProject/Catima/wiki/Card-sharing-URL-format
Changes:
- Make it possible to enable or disable the flashlight while scanning
- Add UPC-E support
- Support adding a front and back photo to each card
- Support importing password-protected zip files
- Support importing from Stocard (Beta)
- Fix useless whitespace in notes from Fidme import
- Support new Voucher Vault export format
- Fix Floating Action Buttons being behind other UI elements on Android 4
- Fix loyalty card viewer appbar top margin
## v1.14.1 (2021-06-14)
Changes:

View File

@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.2)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.1.0)

View File

@@ -18,10 +18,11 @@ android {
applicationId "me.hackerchick.catima"
minSdkVersion 19
targetSdkVersion 30
versionCode 69
versionName "1.14.1"
versionCode 76
versionName "2.2.0"
vectorDrawables.useSupportLibrary true
multiDexEnabled true
}
buildTypes {
@@ -38,6 +39,10 @@ android {
compileOptions {
encoding "UTF-8"
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
@@ -47,6 +52,12 @@ android {
"MissingTranslation", "MissingPrefix"
}
sourceSets {
test {
resources.srcDirs += ['src/test/res']
}
}
// Starting with Android Studio 3 Robolectric is unable to find resources.
// The following allows it to find the resources.
testOptions {
@@ -63,29 +74,28 @@ android {
dependencies {
// AndroidX
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// Third-party
implementation 'com.journeyapps:zxing-android-embedded:4.1.0@aar'
// Do not upgrade past 3.3.3! Causes a crash on versions before Android Nougat
//noinspection GradleDependency
implementation 'com.google.zxing:core:3.3.3'
implementation 'com.google.zxing:core:3.4.1'
implementation 'org.apache.commons:commons-csv:1.8'
implementation 'com.jaredrummler:colorpicker:1.1.0'
implementation 'com.google.guava:guava:30.1.1-jre'
implementation 'com.github.invissvenska:NumberPickerPreference:1.0.2'
implementation 'net.lingala.zip4j:zip4j:2.8.0'
// SpotBugs
implementation 'io.wcm.tooling.spotbugs:io.wcm.tooling.spotbugs.annotations:1.0.0'
// Testing
testImplementation 'androidx.test:core:1.3.0'
testImplementation 'androidx.test:core:1.4.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:robolectric:4.6.1'
}
tasks.withType(SpotBugsTask) {

View File

@@ -60,13 +60,16 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "https://github.com/brarcher/loyalty-card-locker/” -->
<!-- Listen to known card sharing URIs -->
<data android:scheme="https"
android:host="@string/intent_import_card_from_url_host"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix" />
android:host="@string/intent_import_card_from_url_host_catima_app"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_catima_app" />
<data android:scheme="https"
android:host="@string/intent_import_card_from_url_host_old"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_old" />
android:host="@string/intent_import_card_from_url_host_thelastproject"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_thelastproject" />
<data android:scheme="https"
android:host="@string/intent_import_card_from_url_host_brarcher"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_brarcher" />
</intent-filter>
</activity>
<activity

View File

@@ -35,12 +35,12 @@ public class AboutActivity extends AppCompatActivity
}
final List<ThirdPartyInfo> USED_LIBRARIES = new ArrayList<>();
USED_LIBRARIES.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("Commons CSV", "https://commons.apache.org/proper/commons-csv/", "Apache 2.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("Guava", "https://github.com/google/guava", "Apache 2.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("NumberPickerPreference", "https://github.com/invissvenska/NumberPickerPreference", "GNU LGPL 3.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("Zip4j", "https://github.com/srikanth-lingala/zip4j", "Apache 2.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("ZXing", "https://github.com/zxing/zxing", "Apache 2.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("ZXing Android Embedded", "https://github.com/journeyapps/zxing-android-embedded", "Apache 2.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
USED_LIBRARIES.add(new ThirdPartyInfo("NumberPickerPreference", "https://github.com/invissvenska/NumberPickerPreference", "GNU LGPL 3.0"));
final List<ThirdPartyInfo> USED_ASSETS = new ArrayList<>();
USED_ASSETS.add(new ThirdPartyInfo("Android icons", "https://fonts.google.com/icons?selected=Material+Icons", "Apache 2.0"));
@@ -104,4 +104,4 @@ public class AboutActivity extends AppCompatActivity
return super.onOptionsItemSelected(item);
}
}
}

View File

@@ -133,6 +133,8 @@ class BarcodeImageWriterTask extends AsyncTask<Void, Void, Bitmap>
return "1003";
case UPC_A:
return "123456789012";
case UPC_E:
return "0123456";
default:
throw new IllegalArgumentException("No fallback known for this barcode type");
}

View File

@@ -16,12 +16,12 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.collect.ImmutableMap;
import com.google.zxing.BarcodeFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
@@ -58,7 +58,8 @@ public class BarcodeSelectorActivity extends AppCompatActivity
BarcodeFormat.ITF.name(),
BarcodeFormat.PDF_417.name(),
BarcodeFormat.QR_CODE.name(),
BarcodeFormat.UPC_A.name()
BarcodeFormat.UPC_A.name(),
BarcodeFormat.UPC_E.name()
));
private Map<String, Pair<Integer, Integer>> barcodeViewMap;
@@ -78,19 +79,19 @@ public class BarcodeSelectorActivity extends AppCompatActivity
actionBar.setDisplayHomeAsUpEnabled(true);
}
barcodeViewMap = ImmutableMap.<String, Pair<Integer, Integer>>builder()
.put(BarcodeFormat.AZTEC.name(), new Pair<>(R.id.aztecBarcode, R.id.aztecBarcodeText))
.put(BarcodeFormat.CODE_39.name(), new Pair<>(R.id.code39Barcode, R.id.code39BarcodeText))
.put(BarcodeFormat.CODE_128.name(), new Pair<>(R.id.code128Barcode, R.id.code128BarcodeText))
.put(BarcodeFormat.CODABAR.name(), new Pair<>(R.id.codabarBarcode, R.id.codabarBarcodeText))
.put(BarcodeFormat.DATA_MATRIX.name(), new Pair<>(R.id.datamatrixBarcode, R.id.datamatrixBarcodeText))
.put(BarcodeFormat.EAN_8.name(), new Pair<>(R.id.ean8Barcode, R.id.ean8BarcodeText))
.put(BarcodeFormat.EAN_13.name(), new Pair<>(R.id.ean13Barcode, R.id.ean13BarcodeText))
.put(BarcodeFormat.ITF.name(), new Pair<>(R.id.itfBarcode, R.id.itfBarcodeText))
.put(BarcodeFormat.PDF_417.name(), new Pair<>(R.id.pdf417Barcode, R.id.pdf417BarcodeText))
.put(BarcodeFormat.QR_CODE.name(), new Pair<>(R.id.qrcodeBarcode, R.id.qrcodeBarcodeText))
.put(BarcodeFormat.UPC_A.name(), new Pair<>(R.id.upcaBarcode, R.id.upcaBarcodeText))
.build();
barcodeViewMap = new HashMap<>();
barcodeViewMap.put(BarcodeFormat.AZTEC.name(), new Pair<>(R.id.aztecBarcode, R.id.aztecBarcodeText));
barcodeViewMap.put(BarcodeFormat.CODE_39.name(), new Pair<>(R.id.code39Barcode, R.id.code39BarcodeText));
barcodeViewMap.put(BarcodeFormat.CODE_128.name(), new Pair<>(R.id.code128Barcode, R.id.code128BarcodeText));
barcodeViewMap.put(BarcodeFormat.CODABAR.name(), new Pair<>(R.id.codabarBarcode, R.id.codabarBarcodeText));
barcodeViewMap.put(BarcodeFormat.DATA_MATRIX.name(), new Pair<>(R.id.datamatrixBarcode, R.id.datamatrixBarcodeText));
barcodeViewMap.put(BarcodeFormat.EAN_8.name(), new Pair<>(R.id.ean8Barcode, R.id.ean8BarcodeText));
barcodeViewMap.put(BarcodeFormat.EAN_13.name(), new Pair<>(R.id.ean13Barcode, R.id.ean13BarcodeText));
barcodeViewMap.put(BarcodeFormat.ITF.name(), new Pair<>(R.id.itfBarcode, R.id.itfBarcodeText));
barcodeViewMap.put(BarcodeFormat.PDF_417.name(), new Pair<>(R.id.pdf417Barcode, R.id.pdf417BarcodeText));
barcodeViewMap.put(BarcodeFormat.QR_CODE.name(), new Pair<>(R.id.qrcodeBarcode, R.id.qrcodeBarcodeText));
barcodeViewMap.put(BarcodeFormat.UPC_A.name(), new Pair<>(R.id.upcaBarcode, R.id.upcaBarcodeText));
barcodeViewMap.put(BarcodeFormat.UPC_E.name(), new Pair<>(R.id.upceBarcode, R.id.upceBarcodeText));
EditText cardId = findViewById(R.id.cardId);
cardId.addTextChangedListener(new TextWatcher()

View File

@@ -2,8 +2,8 @@ package protect.card_locker;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
@@ -12,6 +12,8 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
/**
@@ -47,6 +49,11 @@ public class CardShortcutConfigure extends AppCompatActivity implements LoyaltyC
}
final RecyclerView cardList = findViewById(R.id.list);
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
cardList.setLayoutManager(mLayoutManager);
cardList.setItemAnimator(new DefaultItemAnimator());
cardList.setVisibility(View.VISIBLE);
Cursor cardCursor = db.getLoyaltyCardCursor();
@@ -72,11 +79,12 @@ public class CardShortcutConfigure extends AppCompatActivity implements LoyaltyC
bundle.putBoolean("view", true);
shortcutIntent.putExtras(bundle);
Parcelable icon = Intent.ShortcutIconResource.fromContext(CardShortcutConfigure.this, R.mipmap.ic_launcher);
Bitmap iconBitmap = Utils.generateIcon(CardShortcutConfigure.this, loyaltyCard, true).getLetterTile();
Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, loyaltyCard.store);
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, icon);
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, iconBitmap);
setResult(RESULT_OK, intent);
finish();

View File

@@ -9,6 +9,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import com.google.zxing.BarcodeFormat;
import java.io.FileNotFoundException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Currency;
@@ -52,9 +53,13 @@ public class DBHelper extends SQLiteOpenHelper
public static final String groupID = "groupId";
}
private Context mContext;
public DBHelper(Context context)
{
super(context, DATABASE_NAME, null, DATABASE_VERSION);
mContext = context;
}
@Override
@@ -78,7 +83,7 @@ public class DBHelper extends SQLiteOpenHelper
LoyaltyCardDbIds.CARD_ID + " TEXT not null," +
LoyaltyCardDbIds.BARCODE_ID + " TEXT," +
LoyaltyCardDbIds.BARCODE_TYPE + " TEXT," +
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0' )");
LoyaltyCardDbIds.STAR_STATUS + " INTEGER DEFAULT '0')");
// create associative table for cards in groups
db.execSQL("create table " + LoyaltyCardDbIdsGroups.TABLE + "(" +
@@ -269,7 +274,7 @@ public class DBHelper extends SQLiteOpenHelper
return newId;
}
public boolean insertLoyaltyCard(final SQLiteDatabase db, final String store,
public long insertLoyaltyCard(final SQLiteDatabase db, final String store,
final String note, final Date expiry, final BigDecimal balance,
final Currency balanceType, final String cardId,
final String barcodeId, final BarcodeFormat barcodeType,
@@ -287,10 +292,10 @@ public class DBHelper extends SQLiteOpenHelper
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
contentValues.put(LoyaltyCardDbIds.STAR_STATUS,starStatus);
final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
return (newId != -1);
return newId;
}
public boolean insertLoyaltyCard(final SQLiteDatabase db, final int id, final String store,
public long insertLoyaltyCard(final SQLiteDatabase db, final int id, final String store,
final String note, final Date expiry, final BigDecimal balance,
final Currency balanceType, final String cardId,
final String barcodeId, final BarcodeFormat barcodeType,
@@ -309,7 +314,7 @@ public class DBHelper extends SQLiteOpenHelper
contentValues.put(LoyaltyCardDbIds.HEADER_COLOR, headerColor);
contentValues.put(LoyaltyCardDbIds.STAR_STATUS,starStatus);
final long newId = db.insert(LoyaltyCardDbIds.TABLE, null, contentValues);
return (newId != -1);
return newId;
}
public boolean updateLoyaltyCard(final int id, final String store, final String note,
@@ -426,7 +431,7 @@ public class DBHelper extends SQLiteOpenHelper
}
}
public boolean deleteLoyaltyCard (final int id)
public boolean deleteLoyaltyCard(final int id)
{
SQLiteDatabase db = getWritableDatabase();
// Delete card
@@ -439,6 +444,14 @@ public class DBHelper extends SQLiteOpenHelper
LoyaltyCardDbIdsGroups.cardID + " = ? ",
new String[]{String.format("%d", id)});
// Also wipe card images associated with this card
try {
Utils.saveCardImage(mContext, null, id, true);
Utils.saveCardImage(mContext, null, id, false);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return (rowsDeleted == 1);
}
@@ -741,6 +754,9 @@ public class DBHelper extends SQLiteOpenHelper
db.endTransaction();
}
// Reorder after delete to ensure no bad order IDs
reorderGroups(getGroups());
return success;
}

View File

@@ -5,16 +5,18 @@ import android.database.Cursor;
public class Group
{
public final String _id;
public final int order;
public Group(final String _id)
{
public Group(final String _id, final int order) {
this._id = _id;
this.order = order;
}
public static Group toGroup(Cursor cursor)
{
String _id = cursor.getString(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbGroups.ID));
int order = cursor.getInt(cursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbGroups.ORDER));
return new Group(_id);
return new Group(_id, order);
}
}

View File

@@ -5,52 +5,86 @@ import android.database.Cursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.recyclerview.widget.RecyclerView;
import protect.card_locker.preferences.Settings;
class GroupCursorAdapter extends CursorAdapter
class GroupCursorAdapter extends BaseCursorAdapter<GroupCursorAdapter.GroupListItemViewHolder>
{
Settings settings;
DBHelper db;
Settings mSettings;
private final Cursor mCursor;
private final Context mContext;
private final GroupCursorAdapter.GroupAdapterListener mListener;
DBHelper mDb;
public GroupCursorAdapter(Context context, Cursor cursor)
{
super(context, cursor, 0);
settings = new Settings(context);
db = new DBHelper(context);
public GroupCursorAdapter(Context inputContext, Cursor inputCursor, GroupCursorAdapter.GroupAdapterListener inputListener) {
super(inputCursor);
mSettings = new Settings(inputContext);
mCursor = inputCursor;
mContext = inputContext;
mListener = inputListener;
mDb = new DBHelper(inputContext);
}
// The newView method is used to inflate a new view and return it,
// you don't bind any data to the view at this point.
@NonNull
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent)
public GroupCursorAdapter.GroupListItemViewHolder onCreateViewHolder(ViewGroup inputParent, int inputViewType)
{
return LayoutInflater.from(context).inflate(R.layout.group_layout, parent, false);
View itemView = LayoutInflater.from(inputParent.getContext()).inflate(R.layout.group_layout, inputParent, false);
return new GroupCursorAdapter.GroupListItemViewHolder(itemView);
}
// The bindView method is used to bind all data to a given view
// such as setting the text on a TextView.
@Override
public void bindView(View view, Context context, Cursor cursor)
public Cursor getCursor()
{
// Find fields to populate in inflated template
TextView nameField = view.findViewById(R.id.name);
TextView countField = view.findViewById(R.id.cardCount);
return mCursor;
}
// Extract properties from cursor
Group group = Group.toGroup(cursor);
public void onBindViewHolder(GroupCursorAdapter.GroupListItemViewHolder inputHolder, Cursor inputCursor) {
Group group = Group.toGroup(inputCursor);
Integer groupCardCount = db.getGroupCardCount(group._id);
inputHolder.mName.setText(group._id);
// Populate fields with extracted properties
nameField.setText(group._id);
int groupCardCount = mDb.getGroupCardCount(group._id);
inputHolder.mCardCount.setText(mContext.getResources().getQuantityString(R.plurals.groupCardCount, groupCardCount, groupCardCount));
countField.setText(context.getResources().getQuantityString(R.plurals.groupCardCount, groupCardCount, groupCardCount));
inputHolder.mName.setTextSize(mSettings.getFontSizeMax(mSettings.getMediumFont()));
inputHolder.mCardCount.setTextSize(mSettings.getFontSizeMax(mSettings.getSmallFont()));
nameField.setTextSize(settings.getFontSizeMax(settings.getMediumFont()));
countField.setTextSize(settings.getFontSizeMax(settings.getSmallFont()));
applyClickEvents(inputHolder);
}
private void applyClickEvents(GroupListItemViewHolder inputHolder)
{
inputHolder.mMoveDown.setOnClickListener(view -> mListener.onMoveDownButtonClicked(inputHolder.itemView));
inputHolder.mMoveUp.setOnClickListener(view -> mListener.onMoveUpButtonClicked(inputHolder.itemView));
inputHolder.mEdit.setOnClickListener(view -> mListener.onEditButtonClicked(inputHolder.itemView));
inputHolder.mDelete.setOnClickListener(view -> mListener.onDeleteButtonClicked(inputHolder.itemView));
}
public interface GroupAdapterListener
{
void onMoveDownButtonClicked(View view);
void onMoveUpButtonClicked(View view);
void onEditButtonClicked(View view);
void onDeleteButtonClicked(View view);
}
public class GroupListItemViewHolder extends RecyclerView.ViewHolder
{
public TextView mName, mCardCount;
public AppCompatImageButton mMoveUp, mMoveDown, mEdit, mDelete;
public GroupListItemViewHolder(View inputView) {
super(inputView);
mName = inputView.findViewById(R.id.name);
mCardCount = inputView.findViewById(R.id.cardCount);
mMoveUp = inputView.findViewById(R.id.moveUp);
mMoveDown = inputView.findViewById(R.id.moveDown);
mEdit = inputView.findViewById(R.id.edit);
mDelete = inputView.findViewById(R.id.delete);
}
}
}

View File

@@ -8,10 +8,12 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.InputType;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.io.File;
@@ -20,6 +22,8 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
@@ -27,6 +31,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
public class ImportExportActivity extends AppCompatActivity
{
@@ -72,8 +78,8 @@ public class ImportExportActivity extends AppCompatActivity
// Check that there is a file manager available
final Intent intentCreateDocumentAction = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intentCreateDocumentAction.addCategory(Intent.CATEGORY_OPENABLE);
intentCreateDocumentAction.setType("text/csv");
intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "Catima.csv");
intentCreateDocumentAction.setType("application/zip");
intentCreateDocumentAction.putExtra(Intent.EXTRA_TITLE, "catima.zip");
Button exportButton = findViewById(R.id.exportButton);
exportButton.setOnClickListener(new View.OnClickListener()
@@ -115,9 +121,22 @@ public class ImportExportActivity extends AppCompatActivity
}
private void chooseImportType(Intent baseIntent) {
List<CharSequence> betaImportOptions = new ArrayList<>();
betaImportOptions.add("Fidme");
betaImportOptions.add("Stocard");
List<CharSequence> importOptions = new ArrayList<>();
for (String importOption : getResources().getStringArray(R.array.import_types_array)) {
if (betaImportOptions.contains(importOption)) {
importOption = importOption + " (BETA)";
}
importOptions.add(importOption);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.chooseImportType)
.setItems(R.array.import_types_array, (dialog, which) -> {
.setItems(importOptions.toArray(new CharSequence[importOptions.size()]), (dialog, which) -> {
switch (which) {
// Catima
case 0:
@@ -137,8 +156,14 @@ public class ImportExportActivity extends AppCompatActivity
importAlertMessage = getString(R.string.importLoyaltyCardKeychainMessage);
importDataFormat = DataFormat.Catima;
break;
// Voucher Vault
// Stocard
case 3:
importAlertTitle = getString(R.string.importStocard);
importAlertMessage = getString(R.string.importStocardMessage);
importDataFormat = DataFormat.Stocard;
break;
// Voucher Vault
case 4:
importAlertTitle = getString(R.string.importVoucherVault);
importAlertMessage = getString(R.string.importVoucherVaultMessage);
importDataFormat = DataFormat.VoucherVault;
@@ -162,19 +187,19 @@ public class ImportExportActivity extends AppCompatActivity
builder.show();
}
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat)
private void startImport(final InputStream target, final Uri targetUri, final DataFormat dataFormat, final char[] password)
{
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener()
{
@Override
public void onTaskComplete(boolean success)
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat)
{
onImportComplete(success, targetUri);
onImportComplete(result, targetUri, dataFormat);
}
};
importExporter = new ImportExportTask(ImportExportActivity.this,
dataFormat, target, listener);
dataFormat, target, password, listener);
importExporter.execute();
}
@@ -183,9 +208,9 @@ public class ImportExportActivity extends AppCompatActivity
ImportExportTask.TaskCompleteListener listener = new ImportExportTask.TaskCompleteListener()
{
@Override
public void onTaskComplete(boolean success)
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat)
{
onExportComplete(success, targetUri);
onExportComplete(result, targetUri);
}
};
@@ -195,23 +220,20 @@ public class ImportExportActivity extends AppCompatActivity
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)
{
if(requestCode == PERMISSIONS_EXTERNAL_STORAGE)
{
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSIONS_EXTERNAL_STORAGE) {
// If request is cancelled, the result arrays are empty.
boolean success = grantResults.length > 0;
for(int grant : grantResults)
{
if(grant != PackageManager.PERMISSION_GRANTED)
{
for (int grant : grantResults) {
if (grant != PackageManager.PERMISSION_GRANTED) {
success = false;
}
}
if(success == false)
{
if (!success) {
// External storage permission rejected, inform user that
// import/export is prevented
Toast.makeText(getApplicationContext(), R.string.noExternalStoragePermissionError,
@@ -245,20 +267,43 @@ public class ImportExportActivity extends AppCompatActivity
return super.onOptionsItemSelected(item);
}
private void onImportComplete(boolean success, Uri path)
{
private void retryWithPassword(DataFormat dataFormat, Uri uri) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.passwordRequired);
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
builder.setView(input);
builder.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
activityResultParser(IMPORT, RESULT_OK, uri, input.getText().toString().toCharArray());
});
builder.setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.cancel());
builder.show();
}
private void onImportComplete(ImportExportResult result, Uri path, DataFormat dataFormat) {
if (result == ImportExportResult.BadPassword) {
retryWithPassword(dataFormat, path);
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if(success)
int messageId;
if (result == ImportExportResult.Success)
{
builder.setTitle(R.string.importSuccessfulTitle);
messageId = R.string.importSuccessful;
}
else
{
builder.setTitle(R.string.importFailedTitle);
messageId = R.string.importFailed;
}
int messageId = success ? R.string.importSuccessful : R.string.importFailed;
final String message = getResources().getString(messageId);
builder.setMessage(message);
@@ -274,53 +319,44 @@ public class ImportExportActivity extends AppCompatActivity
builder.create().show();
}
private void onExportComplete(boolean success, final Uri path)
private void onExportComplete(ImportExportResult result, final Uri path)
{
AlertDialog.Builder builder = new AlertDialog.Builder(this);
if(success)
int messageId;
if(result == ImportExportResult.Success)
{
builder.setTitle(R.string.exportSuccessfulTitle);
messageId = R.string.exportSuccessful;
}
else
{
builder.setTitle(R.string.exportFailedTitle);
messageId = R.string.exportFailed;
}
int messageId = success ? R.string.exportSuccessful : R.string.exportFailed;
final String message = getResources().getString(messageId);
builder.setMessage(message);
builder.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
dialog.dismiss();
}
});
builder.setNeutralButton(R.string.ok, (dialog, which) -> dialog.dismiss());
if(success)
if(result == ImportExportResult.Success)
{
final CharSequence sendLabel = ImportExportActivity.this.getResources().getText(R.string.sendLabel);
builder.setPositiveButton(sendLabel, new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
sendIntent.setType("text/csv");
builder.setPositiveButton(sendLabel, (dialog, which) -> {
Intent sendIntent = new Intent(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, path);
sendIntent.setType("text/csv");
// set flag to give temporary permission to external app to use the FileProvider
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// set flag to give temporary permission to external app to use the FileProvider
sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
sendLabel));
ImportExportActivity.this.startActivity(Intent.createChooser(sendIntent,
sendLabel));
dialog.dismiss();
}
dialog.dismiss();
});
}
@@ -340,18 +376,13 @@ public class ImportExportActivity extends AppCompatActivity
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
private void activityResultParser(int requestCode, int resultCode, Uri uri, char[] password) {
if (resultCode != RESULT_OK)
{
Log.w(TAG, "Failed onActivityResult(), result=" + resultCode);
return;
}
Uri uri = data.getData();
if(uri == null)
{
Log.e(TAG, "Activity returned a NULL URI");
@@ -389,7 +420,7 @@ public class ImportExportActivity extends AppCompatActivity
Log.e(TAG, "Starting file import with: " + uri.toString());
startImport(reader, uri, importDataFormat);
startImport(reader, uri, importDataFormat, password);
}
}
catch(FileNotFoundException e)
@@ -397,12 +428,26 @@ public class ImportExportActivity extends AppCompatActivity
Log.e(TAG, "Failed to import/export file: " + uri.toString(), e);
if (requestCode == CHOOSE_EXPORT_LOCATION)
{
onExportComplete(false, uri);
onExportComplete(ImportExportResult.GenericFailure, uri);
}
else
{
onImportComplete(false, uri);
onImportComplete(ImportExportResult.GenericFailure, uri, importDataFormat);
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
if(data == null)
{
Log.e(TAG, "Activity returned NULL data");
return;
}
activityResultParser(requestCode, resultCode, data.getData(), null);
}
}

View File

@@ -2,6 +2,7 @@ package protect.card_locker;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.util.Log;
@@ -10,12 +11,14 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.MultiFormatExporter;
import protect.card_locker.importexport.MultiFormatImporter;
class ImportExportTask extends AsyncTask<Void, Void, Boolean>
class ImportExportTask extends AsyncTask<Void, Void, ImportExportResult>
{
private static final String TAG = "Catima";
@@ -24,6 +27,7 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
private DataFormat format;
private OutputStream outputStream;
private InputStream inputStream;
private char[] password;
private TaskCompleteListener listener;
private ProgressDialog progress;
@@ -45,7 +49,7 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
/**
* Constructor which will setup a task for importing from the given InputStream.
*/
ImportExportTask(Activity activity, DataFormat format, InputStream input,
ImportExportTask(Activity activity, DataFormat format, InputStream input, char[] password,
TaskCompleteListener listener)
{
super();
@@ -53,29 +57,27 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
this.doImport = true;
this.format = format;
this.inputStream = input;
this.password = password;
this.listener = listener;
}
private boolean performImport(InputStream stream, DBHelper db)
private ImportExportResult performImport(Context context, InputStream stream, DBHelper db, char[] password)
{
boolean result = false;
ImportExportResult importResult = MultiFormatImporter.importData(context, db, stream, format, password);
Log.i(TAG, "Import result: " + importResult.name());
result = MultiFormatImporter.importData(db, stream, format);
Log.i(TAG, "Import result: " + result);
return result;
return importResult;
}
private boolean performExport(OutputStream stream, DBHelper db)
private ImportExportResult performExport(Context context, OutputStream stream, DBHelper db)
{
boolean result = false;
ImportExportResult result = ImportExportResult.GenericFailure;
try
{
OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
result = MultiFormatExporter.exportData(db, writer, format);
OutputStreamWriter writer = new OutputStreamWriter(stream, StandardCharsets.UTF_8);
result = MultiFormatExporter.exportData(context, db, stream, format);
writer.close();
}
catch (IOException e)
@@ -105,26 +107,26 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
progress.show();
}
protected Boolean doInBackground(Void... nothing)
protected ImportExportResult doInBackground(Void... nothing)
{
final DBHelper db = new DBHelper(activity);
boolean result;
ImportExportResult result;
if(doImport)
{
result = performImport(inputStream, db);
result = performImport(activity.getApplicationContext(), inputStream, db, password);
}
else
{
result = performExport(outputStream, db);
result = performExport(activity.getApplicationContext(), outputStream, db);
}
return result;
}
protected void onPostExecute(Boolean result)
protected void onPostExecute(ImportExportResult result)
{
listener.onTaskComplete(result);
listener.onTaskComplete(result, format);
progress.dismiss();
Log.i(TAG, (doImport ? "Import" : "Export") + " Complete");
@@ -137,7 +139,7 @@ class ImportExportTask extends AsyncTask<Void, Void, Boolean>
}
interface TaskCompleteListener
{
void onTaskComplete(boolean success);
void onTaskComplete(ImportExportResult result, DataFormat format);
}
}

View File

@@ -7,9 +7,14 @@ import android.net.Uri;
import com.google.zxing.BarcodeFormat;
import java.io.InvalidObjectException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Currency;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
public class ImportURIHelper {
@@ -21,29 +26,34 @@ public class ImportURIHelper {
private static final String CARD_ID = DBHelper.LoyaltyCardDbIds.CARD_ID;
private static final String BARCODE_ID = DBHelper.LoyaltyCardDbIds.BARCODE_ID;
private static final String BARCODE_TYPE = DBHelper.LoyaltyCardDbIds.BARCODE_TYPE;
private static final String HEADER_COLOR = DBHelper.LoyaltyCardDbIds.HEADER_COLOR;
private final Context context;
private final String host;
private final String path;
private final String oldHost;
private final String oldPath;
private final String[] hosts = new String[3];
private final String[] paths = new String[3];
private final String shareText;
private final String shareMultipleText;
public ImportURIHelper(Context context) {
this.context = context;
host = context.getResources().getString(R.string.intent_import_card_from_url_host);
path = context.getResources().getString(R.string.intent_import_card_from_url_path_prefix);
oldHost = "brarcher.github.io";
oldPath = "/loyalty-card-locker/share";
hosts[0] = context.getResources().getString(R.string.intent_import_card_from_url_host_catima_app);
paths[0] = context.getResources().getString(R.string.intent_import_card_from_url_path_prefix_catima_app);
hosts[1] = context.getResources().getString(R.string.intent_import_card_from_url_host_thelastproject);
paths[1] = context.getResources().getString(R.string.intent_import_card_from_url_path_prefix_thelastproject);
hosts[2] = context.getResources().getString(R.string.intent_import_card_from_url_host_brarcher);
paths[2] = context.getResources().getString(R.string.intent_import_card_from_url_path_prefix_brarcher);
shareText = context.getResources().getString(R.string.intent_import_card_from_url_share_text);
shareMultipleText = context.getResources().getString(R.string.intent_import_card_from_url_share_multiple_text);
}
private boolean isImportUri(Uri uri) {
return (uri.getHost().equals(host) && uri.getPath().equals(path)) || (uri.getHost().equals(oldHost) && uri.getPath().equals(oldPath));
for (int i = 0; i < hosts.length; i++) {
if (uri.getHost().equals(hosts[i]) && uri.getPath().equals(paths[i])) {
return true;
}
}
return false;
}
public LoyaltyCard parse(Uri uri) throws InvalidObjectException {
@@ -59,77 +69,114 @@ public class ImportURIHelper {
Currency balanceType = null;
Integer headerColor = null;
String store = uri.getQueryParameter(STORE);
String note = uri.getQueryParameter(NOTE);
String cardId = uri.getQueryParameter(CARD_ID);
String barcodeId = uri.getQueryParameter(BARCODE_ID);
if (store == null || note == null || cardId == null) throw new InvalidObjectException("Not a valid import URI");
// Store everything in a simple key/value hashmap
HashMap<String, String> kv = new HashMap<>();
String unparsedBarcodeType = uri.getQueryParameter(BARCODE_TYPE);
// First, grab all query parameters (backwards compatibility)
for (String key : uri.getQueryParameterNames()) {
kv.put(key, uri.getQueryParameter(key));
}
// Then, parse the new and more private fragment part
// Overriding old format entries if they exist
String fragment = uri.getFragment();
if (fragment != null) {
for (String fragmentPart : fragment.split("&")) {
String[] fragmentData = fragmentPart.split("=", 2);
kv.put(fragmentData[0], URLDecoder.decode(fragmentData[1], StandardCharsets.UTF_8.name()));
}
}
// Then use all values we care about
String store = kv.get(STORE);
String note = kv.get(NOTE);
String cardId = kv.get(CARD_ID);
String barcodeId = kv.get(BARCODE_ID);
if (store == null || note == null || cardId == null) throw new InvalidObjectException("Not a valid import URI: " + uri.toString());
String unparsedBarcodeType = kv.get(BARCODE_TYPE);
if(unparsedBarcodeType != null && !unparsedBarcodeType.equals(""))
{
barcodeType = BarcodeFormat.valueOf(unparsedBarcodeType);
}
String unparsedBalance = uri.getQueryParameter(BALANCE);
String unparsedBalance = kv.get(BALANCE);
if(unparsedBalance != null && !unparsedBalance.equals(""))
{
balance = new BigDecimal(unparsedBalance);
}
String unparsedBalanceType = uri.getQueryParameter(BALANCE_TYPE);
String unparsedBalanceType = kv.get(BALANCE_TYPE);
if (unparsedBalanceType != null && !unparsedBalanceType.equals(""))
{
balanceType = Currency.getInstance(unparsedBalanceType);
}
String unparsedExpiry = uri.getQueryParameter(EXPIRY);
String unparsedExpiry = kv.get(EXPIRY);
if(unparsedExpiry != null && !unparsedExpiry.equals(""))
{
expiry = new Date(Long.parseLong(unparsedExpiry));
}
String unparsedHeaderColor = uri.getQueryParameter(HEADER_COLOR);
String unparsedHeaderColor = kv.get(HEADER_COLOR);
if(unparsedHeaderColor != null)
{
headerColor = Integer.parseInt(unparsedHeaderColor);
}
return new LoyaltyCard(-1, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, 0);
} catch (NullPointerException | NumberFormatException ex) {
} catch (NullPointerException | NumberFormatException | UnsupportedEncodingException ex) {
throw new InvalidObjectException("Not a valid import URI");
}
}
private StringBuilder appendFragment(StringBuilder fragment, String key, String value) throws UnsupportedEncodingException {
if (fragment.length() > 0) {
fragment.append("&");
}
// Double-encode the value to make sure it can't accidentally contain symbols that'll break the parser
fragment.append(key).append("=").append(URLEncoder.encode(value, StandardCharsets.UTF_8.name()));
return fragment;
}
// Protected for usage in tests
protected Uri toUri(LoyaltyCard loyaltyCard) {
protected Uri toUri(LoyaltyCard loyaltyCard) throws UnsupportedEncodingException {
Uri.Builder uriBuilder = new Uri.Builder();
uriBuilder.scheme("https");
uriBuilder.authority(host);
uriBuilder.path(path);
uriBuilder.appendQueryParameter(STORE, loyaltyCard.store);
uriBuilder.appendQueryParameter(NOTE, loyaltyCard.note);
uriBuilder.appendQueryParameter(BALANCE, loyaltyCard.balance.toString());
uriBuilder.authority(hosts[0]);
uriBuilder.path(paths[0]);
// Use fragment instead of QueryParameter to not leak this data to the server
StringBuilder fragment = new StringBuilder();
fragment = appendFragment(fragment, STORE, loyaltyCard.store);
fragment = appendFragment(fragment, NOTE, loyaltyCard.note);
fragment = appendFragment(fragment, BALANCE, loyaltyCard.balance.toString());
if (loyaltyCard.balanceType != null) {
uriBuilder.appendQueryParameter(BALANCE_TYPE, loyaltyCard.balanceType.getCurrencyCode());
fragment = appendFragment(fragment, BALANCE_TYPE, loyaltyCard.balanceType.getCurrencyCode());
}
if (loyaltyCard.expiry != null) {
uriBuilder.appendQueryParameter(EXPIRY, String.valueOf(loyaltyCard.expiry.getTime()));
fragment = appendFragment(fragment, EXPIRY, String.valueOf(loyaltyCard.expiry.getTime()));
}
uriBuilder.appendQueryParameter(CARD_ID, loyaltyCard.cardId);
fragment = appendFragment(fragment, CARD_ID, loyaltyCard.cardId);
if(loyaltyCard.barcodeId != null) {
uriBuilder.appendQueryParameter(BARCODE_ID, loyaltyCard.barcodeId);
fragment = appendFragment(fragment, BARCODE_ID, loyaltyCard.barcodeId);
}
if(loyaltyCard.barcodeType != null) {
uriBuilder.appendQueryParameter(BARCODE_TYPE, loyaltyCard.barcodeType.toString());
fragment = appendFragment(fragment, BARCODE_TYPE, loyaltyCard.barcodeType.toString());
}
if(loyaltyCard.headerColor != null) {
uriBuilder.appendQueryParameter(HEADER_COLOR, loyaltyCard.headerColor.toString());
fragment = appendFragment(fragment, HEADER_COLOR, loyaltyCard.headerColor.toString());
}
//StarStatus will not be exported
// Star status will not be exported
// Front and back pictures are often too big to fit into a message in base64 nicely, not sharing either...
uriBuilder.fragment(fragment.toString());
return uriBuilder.build();
}
public void startShareIntent(List<LoyaltyCard> loyaltyCards) {
public void startShareIntent(List<LoyaltyCard> loyaltyCards) throws UnsupportedEncodingException {
int loyaltyCardCount = loyaltyCards.size();
StringBuilder text = new StringBuilder();

View File

@@ -10,8 +10,7 @@ import java.util.Date;
import androidx.annotation.Nullable;
public class LoyaltyCard
{
public class LoyaltyCard {
public final int id;
public final String store;
public final String note;

View File

@@ -111,53 +111,20 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
private void applyClickEvents(LoyaltyCardListItemViewHolder inputHolder, final int inputPosition)
{
inputHolder.mThumbnailContainer.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View inputView)
{
mListener.onIconClicked(inputPosition);
}
inputHolder.mThumbnailContainer.setOnClickListener(inputView -> mListener.onIconClicked(inputPosition));
inputHolder.mRow.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition));
inputHolder.mInformationContainer.setOnClickListener(inputView -> mListener.onRowClicked(inputPosition));
inputHolder.mRow.setOnLongClickListener(inputView -> {
mListener.onRowLongClicked(inputPosition);
inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return true;
});
inputHolder.mRow.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View inputView)
{
mListener.onRowClicked(inputPosition);
}
});
inputHolder.mInformationContainer.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View inputView)
{
mListener.onRowClicked(inputPosition);
}
});
inputHolder.mRow.setOnLongClickListener(new View.OnLongClickListener()
{
@Override
public boolean onLongClick(View inputView)
{
mListener.onRowLongClicked(inputPosition);
inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return true;
}
});
inputHolder.mInformationContainer.setOnLongClickListener(new View.OnLongClickListener()
{
@Override
public boolean onLongClick(View inputView)
{
mListener.onRowLongClicked(inputPosition);
inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return true;
}
inputHolder.mInformationContainer.setOnLongClickListener(inputView -> {
mListener.onRowLongClicked(inputPosition);
inputView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
return true;
});
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
package protect.card_locker;
public enum LoyaltyCardField {
id,
store,
note,
expiry,
balance,
balanceType,
cardId,
barcodeId,
barcodeType,
headerColor,
starStatus
}

View File

@@ -1,11 +1,10 @@
package protect.card_locker;
import android.app.Application;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.multidex.MultiDexApplication;
import protect.card_locker.preferences.Settings;
public class LoyaltyCardLockerApplication extends Application {
public class LoyaltyCardLockerApplication extends MultiDexApplication {
public void onCreate() {
super.onCreate();

View File

@@ -3,10 +3,12 @@ package protect.card_locker;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
@@ -18,6 +20,7 @@ import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
@@ -27,8 +30,10 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.zxing.BarcodeFormat;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -49,7 +54,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
TextView cardIdFieldView;
BottomSheetBehavior behavior;
View bottomSheet;
View bottomSheetContentWrapper;
ImageView bottomSheetButton;
View frontImageView;
ImageView frontImage;
View backImageView;
ImageView backImage;
TextView noteView;
TextView groupsView;
TextView balanceView;
@@ -76,9 +86,22 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
Guideline centerGuideline;
SeekBar barcodeScaler;
Bitmap frontImageBitmap;
Bitmap backImageBitmap;
boolean starred;
boolean backgroundNeedsDarkIcons;
boolean barcodeIsFullscreen = false;
FullscreenType fullscreenType = FullscreenType.NONE;
boolean isBarcodeSupported = true;
static final String STATE_FULLSCREENTYPE = "fullscreenType";
enum FullscreenType {
NONE,
BARCODE,
IMAGE_FRONT,
IMAGE_BACK
}
private void extractIntentFields(Intent intent)
{
@@ -109,6 +132,10 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
{
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
fullscreenType = FullscreenType.valueOf(savedInstanceState.getString(STATE_FULLSCREENTYPE));
}
settings = new Settings(this);
extractIntentFields(getIntent());
@@ -120,7 +147,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
cardIdFieldView = findViewById(R.id.cardIdView);
bottomSheet = findViewById(R.id.bottom_sheet);
bottomSheetContentWrapper = findViewById(R.id.bottomSheetContentWrapper);
bottomSheetButton = findViewById(R.id.bottomSheetButton);
frontImageView = findViewById(R.id.frontImageView);
frontImage = findViewById(R.id.frontImage);
backImageView = findViewById(R.id.backImageView);
backImage = findViewById(R.id.backImage);
noteView = findViewById(R.id.noteView);
groupsView = findViewById(R.id.groupsView);
balanceView = findViewById(R.id.balanceView);
@@ -144,7 +176,9 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
float scale = (float) progress / (float) barcodeScaler.getMax();
Log.d(TAG, "Scaling to " + scale);
redrawBarcodeAfterResize();
if (fullscreenType == FullscreenType.BARCODE) {
redrawBarcodeAfterResize();
}
centerGuideline.setGuidelinePercent(0.5f * scale);
}
@@ -162,18 +196,29 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
rotationEnabled = true;
// Allow making barcode fullscreen on tap
maximizeButton.setOnClickListener(v -> setFullscreen(true));
maximizeButton.setOnClickListener(v -> setFullscreen(FullscreenType.BARCODE));
barcodeImage.setOnClickListener(view -> {
if (barcodeIsFullscreen)
{
setFullscreen(false);
}
else
{
setFullscreen(true);
if (fullscreenType != FullscreenType.NONE) {
setFullscreen(FullscreenType.NONE);
} else {
setFullscreen(FullscreenType.BARCODE);
}
});
minimizeButton.setOnClickListener(v -> setFullscreen(false));
frontImageView.setOnClickListener(view -> {
if (fullscreenType != FullscreenType.IMAGE_FRONT) {
setFullscreen(FullscreenType.IMAGE_FRONT);
} else {
setFullscreen(FullscreenType.NONE);
}
});
backImageView.setOnClickListener(view -> {
if (fullscreenType != FullscreenType.IMAGE_BACK) {
setFullscreen(FullscreenType.IMAGE_BACK);
} else {
setFullscreen(FullscreenType.NONE);
}
});
minimizeButton.setOnClickListener(v -> setFullscreen(FullscreenType.NONE));
editButton = findViewById(R.id.fabEdit);
editButton.setOnClickListener(v -> {
@@ -185,6 +230,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
startActivity(intent);
finish();
});
editButton.bringToFront();
behavior = BottomSheetBehavior.from(bottomSheet);
behavior.addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@@ -197,7 +243,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
editButton.hide();
} else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
bottomSheetButton.setImageResource(R.drawable.ic_baseline_arrow_drop_up_24);
editButton.show();
if (fullscreenType == FullscreenType.NONE) {
editButton.show();
}
// Scroll bottomsheet content back to top
bottomSheetContentWrapper.setScrollY(0);
}
}
@@ -212,6 +263,25 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
// Fix bottom sheet content sizing
ViewTreeObserver viewTreeObserver = bottomSheet.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
bottomSheet.getViewTreeObserver().removeOnGlobalLayoutListener(this);
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
int height = displayMetrics.heightPixels;
int maxHeight = height - appBarLayout.getHeight() - bottomSheetButton.getHeight();
Log.d(TAG, "Button sheet should be " + maxHeight + " pixels high");
bottomSheetContentWrapper.setLayoutParams(
new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
maxHeight
)
);
}
});
}
@Override
@@ -223,6 +293,13 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
extractIntentFields(intent);
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putString(STATE_FULLSCREENTYPE, String.valueOf(fullscreenType));
super.onSaveInstanceState(savedInstanceState);
}
@Override
public void onResume()
{
@@ -275,6 +352,22 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
settings.getFontSizeMin(settings.getLargeFont()), settings.getFontSizeMax(settings.getLargeFont()),
1, TypedValue.COMPLEX_UNIT_SP);
frontImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, true);
if (frontImageBitmap != null) {
frontImageView.setVisibility(View.VISIBLE);
frontImage.setImageBitmap(frontImageBitmap);
} else {
frontImageView.setVisibility(View.GONE);
}
backImageBitmap = Utils.retrieveCardImage(this, loyaltyCard.id, false);
if (backImageBitmap != null) {
backImageView.setVisibility(View.VISIBLE);
backImage.setImageBitmap(backImageBitmap);
} else {
backImageView.setVisibility(View.GONE);
}
if(loyaltyCard.note.length() > 0)
{
noteView.setVisibility(View.VISIBLE);
@@ -289,14 +382,13 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
List<Group> loyaltyCardGroups = db.getLoyaltyCardGroups(loyaltyCardId);
if(loyaltyCardGroups.size() > 0) {
StringBuilder groupsString = new StringBuilder();
List<String> groupNames = new ArrayList<>();
for (Group group : loyaltyCardGroups) {
groupsString.append(group._id);
groupsString.append(" ");
groupNames.add(group._id);
}
groupsView.setVisibility(View.VISIBLE);
groupsView.setText(getString(R.string.groupsList, groupsString.toString()));
groupsView.setText(getString(R.string.groupsList, TextUtils.join(", ", groupNames)));
groupsView.setTextSize(settings.getFontSizeMax(settings.getMediumFont()));
}
else
@@ -331,7 +423,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
}
expiryView.setTag(loyaltyCard.expiry);
if (!barcodeIsFullscreen) {
if (fullscreenType == FullscreenType.NONE) {
makeBottomSheetVisibleIfUseful();
}
@@ -384,55 +476,20 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
// Set shadow colour of store text so even same color on same color would be readable
storeName.setShadowLayer(1, 1, 1, backgroundNeedsDarkIcons ? Color.BLACK : Color.WHITE);
Boolean isBarcodeSupported = true;
if (format != null && !BarcodeSelectorActivity.SUPPORTED_BARCODE_TYPES.contains(format.name())) {
isBarcodeSupported = false;
Toast.makeText(this, getString(R.string.unsupportedBarcodeType), Toast.LENGTH_LONG).show();
}
if(format != null && isBarcodeSupported)
{
if (!barcodeIsFullscreen) {
maximizeButton.setVisibility(View.VISIBLE);
}
barcodeImage.setVisibility(View.VISIBLE);
if(barcodeImage.getHeight() == 0)
{
Log.d(TAG, "ImageView size is not known known at start, waiting for load");
// The size of the ImageView is not yet available as it has not
// yet been drawn. Wait for it to be drawn so the size is available.
redrawBarcodeAfterResize();
}
else
{
Log.d(TAG, "ImageView size known known, creating barcode");
new BarcodeImageWriterTask(
barcodeImage,
barcodeIdString != null ? barcodeIdString : cardIdString,
format,
null,
false,
null)
.execute();
}
// Force redraw fullscreen state
setFullscreen(barcodeIsFullscreen);
}
else
{
maximizeButton.setVisibility(View.GONE);
barcodeImage.setVisibility(View.GONE);
}
setFullscreen(fullscreenType);
}
@Override
public void onBackPressed() {
if (barcodeIsFullscreen)
if (fullscreenType != FullscreenType.NONE)
{
setFullscreen(false);
setFullscreen(FullscreenType.NONE);
return;
}
@@ -488,7 +545,12 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
break;
case R.id.action_share:
importURIHelper.startShareIntent(Arrays.asList(loyaltyCard));
try {
importURIHelper.startShareIntent(Arrays.asList(loyaltyCard));
} catch (UnsupportedEncodingException e) {
Toast.makeText(LoyaltyCardViewActivity.this, R.string.failedGeneratingShareURL, Toast.LENGTH_LONG).show();
e.printStackTrace();
}
return true;
case R.id.action_lock_unlock:
@@ -566,7 +628,7 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
private void makeBottomSheetVisibleIfUseful()
{
if (noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || balanceView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) {
if (frontImageView.getVisibility() == View.VISIBLE || backImageView.getVisibility() == View.VISIBLE || noteView.getVisibility() == View.VISIBLE || groupsView.getVisibility() == View.VISIBLE || balanceView.getVisibility() == View.VISIBLE || expiryView.getVisibility() == View.VISIBLE) {
bottomSheet.setVisibility(View.VISIBLE);
}
else
@@ -604,14 +666,22 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
* The purpose of this function is to make sure the barcode can be scanned from the phone
* by machines which offer no space to insert the complete device.
*/
private void setFullscreen(boolean enable)
private void setFullscreen(FullscreenType fullscreenType)
{
ActionBar actionBar = getSupportActionBar();
if(enable)
{
Log.d(TAG, "Move into of fullscreen");
// Prepare redraw after size change
redrawBarcodeAfterResize();
if (fullscreenType != FullscreenType.NONE) {
Log.d(TAG, "Move into fullscreen");
if (fullscreenType == FullscreenType.IMAGE_FRONT) {
barcodeImage.setImageBitmap(frontImageBitmap);
barcodeImage.setVisibility(View.VISIBLE);
} else if (fullscreenType == FullscreenType.IMAGE_BACK) {
barcodeImage.setImageBitmap(backImageBitmap);
barcodeImage.setVisibility(View.VISIBLE);
} else {
// Prepare redraw after size change
redrawBarcodeAfterResize();
}
// Hide maximize and show minimize button and scaler
maximizeButton.setVisibility(View.GONE);
@@ -630,7 +700,6 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
// Or the barcode will be centered instead of on top of the screen
// Don't ask me why...
appBarLayout.setVisibility(View.INVISIBLE);
appBarLayout.setPadding(0, 0, 0, 0);
collapsingToolbarLayout.setVisibility(View.GONE);
findViewById(R.id.toolbar_landscape).setVisibility(View.GONE);
@@ -646,11 +715,8 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_FULLSCREEN
);
// Set current state
barcodeIsFullscreen = true;
}
else if(!enable)
else
{
Log.d(TAG, "Move out of fullscreen");
@@ -658,10 +724,16 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
barcodeScaler.setProgress(100);
// Prepare redraw after size change
redrawBarcodeAfterResize();
if (format != null && isBarcodeSupported) {
redrawBarcodeAfterResize();
} else {
barcodeImage.setVisibility(View.GONE);
}
// Show maximize and hide minimize button and scaler
maximizeButton.setVisibility(View.VISIBLE);
if (format != null && isBarcodeSupported) {
maximizeButton.setVisibility(View.VISIBLE);
}
minimizeButton.setVisibility(View.GONE);
barcodeScaler.setVisibility(View.GONE);
@@ -676,7 +748,6 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
appBarLayout.setVisibility(View.VISIBLE);
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
appBarLayout.setPadding(0, (int) Math.ceil(metrics.density * 24), 0, 0);
setupOrientation();
// Show other UI elements
@@ -690,9 +761,8 @@ public class LoyaltyCardViewActivity extends AppCompatActivity
& ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
& ~View.SYSTEM_UI_FLAG_FULLSCREEN
);
// Set current state
barcodeIsFullscreen = false;
}
this.fullscreenType = fullscreenType;
}
}

View File

@@ -22,8 +22,10 @@ import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import java.io.UnsupportedEncodingException;
import java.util.List;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
@@ -49,8 +51,7 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
private ActionMode.Callback mCurrentActionModeCallback = new ActionMode.Callback()
{
@Override
public boolean onCreateActionMode(ActionMode inputMode, Menu inputMenu)
{
public boolean onCreateActionMode(ActionMode inputMode, Menu inputMenu) {
inputMode.getMenuInflater().inflate(R.menu.card_longclick_menu, inputMenu);
return true;
}
@@ -62,10 +63,8 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
}
@Override
public boolean onActionItemClicked(ActionMode inputMode, MenuItem inputItem)
{
if (inputItem.getItemId() == R.id.action_copy_to_clipboard)
{
public boolean onActionItemClicked(ActionMode inputMode, MenuItem inputItem) {
if (inputItem.getItemId() == R.id.action_copy_to_clipboard) {
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
String clipboardData;
@@ -93,27 +92,64 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
Toast.makeText(MainActivity.this, cardCount > 1 ? R.string.copy_to_clipboard_multiple_toast : R.string.copy_to_clipboard_toast, Toast.LENGTH_LONG).show();
inputMode.finish();
return true;
}
else if (inputItem.getItemId() == R.id.action_share)
{
} else if (inputItem.getItemId() == R.id.action_share) {
final ImportURIHelper importURIHelper = new ImportURIHelper(MainActivity.this);
importURIHelper.startShareIntent(mAdapter.getSelectedItems());
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)
{
} 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("id", mAdapter.getSelectedItems().get(0).id);
bundle.putBoolean("update", true);
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 AlertDialog.Builder(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) -> {
DBHelper db = new DBHelper(MainActivity.this);
for (LoyaltyCard loyaltyCard : mAdapter.getSelectedItems()) {
Log.e(TAG, "Deleting card: " + loyaltyCard.id);
db.deleteLoyaltyCard(loyaltyCard.id);
ShortcutHelper.removeShortcut(MainActivity.this, loyaltyCard.id);
}
TabLayout.Tab tab = ((TabLayout) findViewById(R.id.groups)).getTabAt(selectedTab);
updateLoyaltyCardList(mFilter, tab != null ? tab.getTag() : null);
dialog.dismiss();
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
return true;
}
@@ -269,14 +305,16 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
// End of active tab logic
FloatingActionButton addButton = findViewById(R.id.fabAdd);
addButton.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
Intent i = new Intent(getApplicationContext(), ScanActivity.class);
startActivityForResult(i, Utils.BARCODE_SCAN);
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);
startActivityForResult(intent, Utils.BARCODE_SCAN);
});
addButton.bringToFront();
}
@Override
@@ -302,8 +340,12 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
if(!barcodeValues.isEmpty()) {
Intent newIntent = new Intent(getApplicationContext(), LoyaltyCardEditActivity.class);
Bundle newBundle = new Bundle();
newBundle.putString("barcodeType", barcodeValues.format());
newBundle.putString("cardId", barcodeValues.content());
newBundle.putString(LoyaltyCardEditActivity.BUNDLE_BARCODETYPE, barcodeValues.format());
newBundle.putString(LoyaltyCardEditActivity.BUNDLE_CARDID, barcodeValues.content());
Bundle inputBundle = intent.getExtras();
if (inputBundle != null && inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) != null) {
newBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, inputBundle.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP));
}
newIntent.putExtras(newBundle);
startActivity(newIntent);
}
@@ -417,7 +459,7 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
private void openPrivacyPolicy() {
Intent browserIntent = new Intent(
Intent.ACTION_VIEW,
Uri.parse("https://thelastproject.github.io/Catima/privacy-policy")
Uri.parse("https://catima.app/privacy-policy")
);
startActivity(browserIntent);
}
@@ -436,14 +478,9 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
searchView.setSubmitButtonEnabled(false);
searchView.setOnCloseListener(new SearchView.OnCloseListener()
{
@Override
public boolean onClose()
{
invalidateOptionsMenu();
return false;
}
searchView.setOnCloseListener(() -> {
invalidateOptionsMenu();
return false;
});
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener()
@@ -616,7 +653,7 @@ public class MainActivity extends AppCompatActivity implements LoyaltyCardCursor
if (count == 0) {
mCurrentActionMode.finish();
} else {
mCurrentActionMode.setTitle("Selected: " + count + " Cards");
mCurrentActionMode.setTitle(getResources().getQuantityString(R.plurals.selectedCardCount, count, count));
MenuItem editItem = mCurrentActionMode.getMenu().findItem(R.id.action_edit);
if (count == 1) {

View File

@@ -1,7 +1,6 @@
package protect.card_locker;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
@@ -10,8 +9,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -22,12 +19,16 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class ManageGroupsActivity extends AppCompatActivity
public class ManageGroupsActivity extends AppCompatActivity implements GroupCursorAdapter.GroupAdapterListener
{
private static final String TAG = "Catima";
private final DBHelper db = new DBHelper(this);
private final DBHelper mDb = new DBHelper(this);
GroupCursorAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState)
@@ -52,12 +53,8 @@ public class ManageGroupsActivity extends AppCompatActivity
updateGroupList();
FloatingActionButton addButton = findViewById(R.id.fabAdd);
addButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
createGroup();
}
});
addButton.setOnClickListener(v -> createGroup());
addButton.bringToFront();
}
@Override
@@ -67,25 +64,27 @@ public class ManageGroupsActivity extends AppCompatActivity
private void updateGroupList()
{
final ListView groupList = findViewById(R.id.list);
final RecyclerView groupList = findViewById(R.id.list);
final TextView helpText = findViewById(R.id.helpText);
final DBHelper db = new DBHelper(this);
if(db.getGroupCount() > 0)
{
groupList.setVisibility(View.VISIBLE);
helpText.setVisibility(View.GONE);
}
else
{
if (mDb.getGroupCount() == 0) {
groupList.setVisibility(View.GONE);
helpText.setVisibility(View.VISIBLE);
return;
}
Cursor groupCursor = db.getGroupCursor();
groupList.setVisibility(View.VISIBLE);
helpText.setVisibility(View.GONE);
final GroupCursorAdapter adapter = new GroupCursorAdapter(this, groupCursor);
groupList.setAdapter(adapter);
Cursor groupCursor = mDb.getGroupCursor();
RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
groupList.setLayoutManager(mLayoutManager);
groupList.setItemAnimator(new DefaultItemAnimator());
mAdapter = new GroupCursorAdapter(this, groupCursor, this);
groupList.setAdapter(mAdapter);
registerForContextMenu(groupList);
}
@@ -112,69 +111,6 @@ public class ManageGroupsActivity extends AppCompatActivity
return super.onOptionsItemSelected(item);
}
public void moveGroupUp(View view) {
moveGroup(view, true);
}
public void moveGroupDown(View view) {
moveGroup(view, false);
}
public void editGroup(View view) {
final String groupName = getGroupname(view);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.enter_group_name);
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(groupName);
builder.setView(input);
builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
db.updateGroup(groupName, input.getText().toString());
updateGroupList();
}
});
builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
AlertDialog dialog = builder.create();
dialog.show();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
}
public void deleteGroup(View view) {
final String groupName = getGroupname(view);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.deleteConfirmationGroup);
builder.setMessage(groupName);
builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
db.deleteGroup(groupName);
updateGroupList();
// Delete may change ordering, so invalidate
invalidateHomescreenActiveTab();
}
});
builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
private void createGroup() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.enter_group_name);
@@ -182,51 +118,28 @@ public class ManageGroupsActivity extends AppCompatActivity
input.setInputType(InputType.TYPE_CLASS_TEXT);
builder.setView(input);
builder.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
db.insertGroup(input.getText().toString());
updateGroupList();
}
});
builder.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
mDb.insertGroup(input.getText().toString());
updateGroupList();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
dialog.show();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
}
private String getGroupname(View view) {
LinearLayout parentRow = (LinearLayout) view.getParent().getParent();
TextView groupNameTextView = parentRow.findViewById(R.id.name);
private String getGroupName(View view) {
TextView groupNameTextView = view.findViewById(R.id.name);
return (String) groupNameTextView.getText();
}
private void moveGroup(View view, boolean up) {
final String groupName = getGroupname(view);
List<Group> groups = mDb.getGroups();
final String groupName = getGroupName(view);
List<Group> groups = db.getGroups();
int currentIndex = -1;
Integer newIndex;
// Get current index in group list
for (int i = 0; i < groups.size(); i++) {
if (groups.get(i)._id.equals(groupName)) {
currentIndex = i;
break;
}
}
if (currentIndex == -1) {
throw new IndexOutOfBoundsException();
}
int currentIndex = mDb.getGroup(groupName).order;
int newIndex;
// Reinsert group in correct position
if (up) {
@@ -244,12 +157,64 @@ public class ManageGroupsActivity extends AppCompatActivity
groups.add(newIndex, group);
// Update database
db.reorderGroups(groups);
mDb.reorderGroups(groups);
// Update UI
updateGroupList();
mAdapter.notifyItemMoved(currentIndex, newIndex);
// Ordering may have changed, so invalidate
invalidateHomescreenActiveTab();
}
@Override
public void onMoveDownButtonClicked(View view) {
moveGroup(view, false);
}
@Override
public void onMoveUpButtonClicked(View view) {
moveGroup(view, true);
}
@Override
public void onEditButtonClicked(View view) {
final String groupName = getGroupName(view);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.enter_group_name);
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(groupName);
builder.setView(input);
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
String newGroupName = input.getText().toString();
mDb.updateGroup(groupName, newGroupName);
updateGroupList();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
dialog.show();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
}
@Override
public void onDeleteButtonClicked(View view) {
final String groupName = getGroupName(view);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.deleteConfirmationGroup);
builder.setMessage(groupName);
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
mDb.deleteGroup(groupName);
updateGroupList();
// Delete may change ordering, so invalidate
invalidateHomescreenActiveTab();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
dialog.show();
}
}

View File

@@ -2,9 +2,11 @@ package protect.card_locker;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -34,10 +36,13 @@ public class ScanActivity extends AppCompatActivity {
private DecoratedBarcodeView barcodeScannerView;
private String cardId;
private String addGroup;
private boolean torch = false;
private void extractIntentFields(Intent intent) {
final Bundle b = intent.getExtras();
cardId = b != null ? b.getString("cardId") : null;
cardId = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_CARDID) : null;
addGroup = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null;
Log.d(TAG, "Scan activity: id=" + cardId);
}
@@ -55,6 +60,9 @@ public class ScanActivity extends AppCompatActivity {
extractIntentFields(getIntent());
findViewById(R.id.add_from_image).setOnClickListener(this::addFromImage);
findViewById(R.id.add_manually).setOnClickListener(this::addManually);
barcodeScannerView = findViewById(R.id.zxing_barcode_scanner);
// Even though we do the actual decoding with the barcodeScannerView
@@ -73,6 +81,9 @@ public class ScanActivity extends AppCompatActivity {
Bundle scanResultBundle = new Bundle();
scanResultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, result.getText());
scanResultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, result.getBarcodeFormat().toString());
if (addGroup != null) {
scanResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
}
scanResult.putExtras(scanResultBundle);
ScanActivity.this.setResult(RESULT_OK, scanResult);
finish();
@@ -114,6 +125,18 @@ public class ScanActivity extends AppCompatActivity {
return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
getMenuInflater().inflate(R.menu.scan_menu, menu);
}
barcodeScannerView.setTorchOff();
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item)
{
@@ -122,6 +145,19 @@ public class ScanActivity extends AppCompatActivity {
setResult(Activity.RESULT_CANCELED);
finish();
return true;
} else if (item.getItemId() == R.id.action_toggle_flashlight)
{
if (torch) {
torch = false;
barcodeScannerView.setTorchOff();
item.setTitle(R.string.turn_flashlight_on);
item.setIcon(R.drawable.ic_flashlight_off_white_24dp);
} else {
torch = true;
barcodeScannerView.setTorchOn();
item.setTitle(R.string.turn_flashlight_off);
item.setIcon(R.drawable.ic_flashlight_on_white_24dp);
}
}
return super.onOptionsItemSelected(item);
@@ -139,6 +175,9 @@ public class ScanActivity extends AppCompatActivity {
Bundle manualResultBundle = new Bundle();
manualResultBundle.putString(BarcodeSelectorActivity.BARCODE_CONTENTS, barcodeValues.content());
manualResultBundle.putString(BarcodeSelectorActivity.BARCODE_FORMAT, barcodeValues.format());
if (addGroup != null) {
manualResultBundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
}
manualResult.putExtras(manualResultBundle);
ScanActivity.this.setResult(RESULT_OK, manualResult);
finish();

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
@@ -37,6 +38,8 @@ class ShortcutHelper
ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
LinkedList<ShortcutInfo> list = new LinkedList<>(shortcutManager.getDynamicShortcuts());
DBHelper dbHelper = new DBHelper(context);
String shortcutId = Integer.toString(card.id);
// Sort the shortcuts by rank, so working with the relative order will be easier.
@@ -105,6 +108,14 @@ class ShortcutHelper
Intent shortcutIntent = prevShortcut.getIntent();
Bitmap iconBitmap = Utils.generateIcon(context, dbHelper.getLoyaltyCard(Integer.parseInt(prevShortcut.getId())), true).getLetterTile();
Icon icon;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
icon = Icon.createWithAdaptiveBitmap(iconBitmap);
} else {
icon = Icon.createWithBitmap(iconBitmap);
}
// Prevent instances of the view activity from piling up; if one exists let this
// one replace it.
shortcutIntent.setFlags(shortcutIntent.getFlags() | Intent.FLAG_ACTIVITY_SINGLE_TOP);
@@ -113,7 +124,7 @@ class ShortcutHelper
.setShortLabel(prevShortcut.getShortLabel())
.setLongLabel(prevShortcut.getLongLabel())
.setIntent(shortcutIntent)
.setIcon(Icon.createWithResource(context, R.drawable.circle))
.setIcon(icon)
.setRank(index)
.build();

View File

@@ -4,7 +4,10 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
@@ -17,6 +20,10 @@ import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.NumberFormat;
@@ -24,6 +31,7 @@ import java.util.Calendar;
import java.util.Currency;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import androidx.core.graphics.ColorUtils;
@@ -35,9 +43,19 @@ public class Utils {
public static final int SELECT_BARCODE_REQUEST = 2;
public static final int BARCODE_SCAN = 3;
public static final int BARCODE_IMPORT_FROM_IMAGE_FILE = 4;
public static final int CARD_IMAGE_FROM_CAMERA_FRONT = 5;
public static final int CARD_IMAGE_FROM_CAMERA_BACK = 6;
public static final int CARD_IMAGE_FROM_FILE_FRONT = 7;
public static final int CARD_IMAGE_FROM_FILE_BACK = 8;
static final double LUMINANCE_MIDPOINT = 0.5;
static final int BITMAP_SIZE_BIG = 512;
static public LetterBitmap generateIcon(Context context, LoyaltyCard loyaltyCard, boolean forShortcut) {
return generateIcon(context, loyaltyCard.store, loyaltyCard.headerColor, forShortcut);
}
static public LetterBitmap generateIcon(Context context, String store, Integer backgroundColor) {
return generateIcon(context, store, backgroundColor, false);
}
@@ -208,4 +226,121 @@ public class Utils {
// Parse as BigDecimal
return new BigDecimal(value);
}
static public byte[] bitmapToByteArray(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, bos);
return bos.toByteArray();
}
static public Bitmap resizeBitmap(Bitmap bitmap) {
if (bitmap == null) {
return null;
}
double maxSize = BITMAP_SIZE_BIG;
double width = bitmap.getWidth();
double height = bitmap.getHeight();
if (height > width) {
double scale = height / maxSize;
height = maxSize;
width = width / scale;
} else if (width > height) {
double scale = width / maxSize;
width = maxSize;
height = height / scale;
} else {
height = maxSize;
width = maxSize;
}
return Bitmap.createScaledBitmap(bitmap, (int) Math.round(width), (int) Math.round(height), true);
}
static public Bitmap rotateBitmap(Bitmap bitmap, ExifInterface exifInterface) {
switch (exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
case ExifInterface.ORIENTATION_ROTATE_90:
return rotateBitmap(bitmap, 90f);
case ExifInterface.ORIENTATION_ROTATE_180:
return rotateBitmap(bitmap, 180f);
case ExifInterface.ORIENTATION_ROTATE_270:
return rotateBitmap(bitmap, 270f);
default:
return bitmap;
}
}
static public Bitmap rotateBitmap(Bitmap bitmap, float rotation) {
if (rotation == 0) {
return bitmap;
}
Matrix matrix = new Matrix();
matrix.postRotate(rotation);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
static public String getCardImageFileName(int loyaltyCardId, boolean front) {
StringBuilder cardImageFileNameBuilder = new StringBuilder();
cardImageFileNameBuilder.append("card_");
cardImageFileNameBuilder.append(loyaltyCardId);
cardImageFileNameBuilder.append("_");
if (front) {
cardImageFileNameBuilder.append("front");
} else {
cardImageFileNameBuilder.append("back");
}
cardImageFileNameBuilder.append(".png");
return cardImageFileNameBuilder.toString();
}
static public void saveCardImage(Context context, Bitmap bitmap, String fileName) throws FileNotFoundException {
if (bitmap == null) {
context.deleteFile(fileName);
return;
}
FileOutputStream out = context.openFileOutput(fileName, Context.MODE_PRIVATE);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
}
static public void saveCardImage(Context context, Bitmap bitmap, int loyaltyCardId, boolean front) throws FileNotFoundException {
saveCardImage(context, bitmap, getCardImageFileName(loyaltyCardId, front));
}
static public Bitmap retrieveCardImage(Context context, String fileName) {
FileInputStream in;
try {
in = context.openFileInput(fileName);
} catch (FileNotFoundException e) {
return null;
}
return BitmapFactory.decodeStream(in);
}
static public Bitmap retrieveCardImage(Context context, int loyaltyCardId, boolean front) {
return retrieveCardImage(context, getCardImageFileName(loyaltyCardId, front));
}
static public Object hashmapGetOrDefault(HashMap hashMap, Object key, Object defaultValue, Class keyType) {
Object value = hashMap.get(keyType.cast(key));
if (value == null) {
return defaultValue;
}
return value;
}
static public Object hashmapGetOrDefault(HashMap hashMap, String key, Object defaultValue) {
return hashmapGetOrDefault(hashMap, key, defaultValue, String.class);
}
}

View File

@@ -0,0 +1,35 @@
package protect.card_locker;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public class ZipUtils {
static public String read(ZipInputStream zipInputStream) throws IOException {
StringBuilder stringBuilder = new StringBuilder();
Reader reader = new BufferedReader(new InputStreamReader(zipInputStream, StandardCharsets.UTF_8));
int c;
while ((c = reader.read()) != -1) {
stringBuilder.append((char) c);
}
return stringBuilder.toString();
}
static public Bitmap readImage(ZipInputStream zipInputStream) {
return BitmapFactory.decodeStream(zipInputStream);
}
static public JSONObject readJSON(ZipInputStream zipInputStream) throws IOException, JSONException {
return new JSONObject(read(zipInputStream));
}
}

View File

@@ -1,25 +1,92 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import net.lingala.zip4j.io.outputstream.ZipOutputStream;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.util.InternalZipConstants;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import protect.card_locker.DBHelper;
import protect.card_locker.Group;
import protect.card_locker.LoyaltyCard;
import protect.card_locker.Utils;
/**
* Class for exporting the database into CSV (Comma Separate Values)
* format.
*/
public class CsvDatabaseExporter implements DatabaseExporter
public class CatimaExporter implements Exporter
{
public void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException
public void exportData(Context context, DBHelper db, OutputStream output) throws IOException, InterruptedException
{
// Necessary vars
int readLen;
byte[] readBuffer = new byte[InternalZipConstants.BUFF_SIZE];
// Create zip output stream
ZipOutputStream zipOutputStream = new ZipOutputStream(output);
// Generate CSV
ByteArrayOutputStream catimaOutputStream = new ByteArrayOutputStream();
OutputStreamWriter catimaOutputStreamWriter = new OutputStreamWriter(catimaOutputStream, StandardCharsets.UTF_8);
writeCSV(db, catimaOutputStreamWriter);
// Add CSV to zip file
ZipParameters csvZipParameters = new ZipParameters();
csvZipParameters.setFileNameInZip("catima.csv");
zipOutputStream.putNextEntry(csvZipParameters);
InputStream csvInputStream = new ByteArrayInputStream(catimaOutputStream.toByteArray());
while ((readLen = csvInputStream.read(readBuffer)) != -1) {
zipOutputStream.write(readBuffer, 0, readLen);
}
zipOutputStream.closeEntry();
// Loop over all cards again
Cursor cardCursor = db.getLoyaltyCardCursor();
while(cardCursor.moveToNext())
{
// For each card
LoyaltyCard card = LoyaltyCard.toLoyaltyCard(cardCursor);
// Prepare looping over both front and back image
boolean[] frontValues = new boolean[2];
frontValues[0] = true;
frontValues[1] = false;
// For each image
for (boolean front : frontValues) {
// If it exists, add to the .zip file
Bitmap image = Utils.retrieveCardImage(context, card.id, front);
if (image != null) {
ZipParameters imageZipParameters = new ZipParameters();
imageZipParameters.setFileNameInZip(Utils.getCardImageFileName(card.id, front));
zipOutputStream.putNextEntry(imageZipParameters);
InputStream imageInputStream = new ByteArrayInputStream(Utils.bitmapToByteArray(image));
while ((readLen = imageInputStream.read(readBuffer)) != -1) {
zipOutputStream.write(readBuffer, 0, readLen);
}
zipOutputStream.closeEntry();
}
}
}
zipOutputStream.close();
}
private void writeCSV(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException {
CSVPrinter printer = new CSVPrinter(output, CSVFormat.RFC4180);
// Print the version
@@ -57,9 +124,9 @@ public class CsvDatabaseExporter implements DatabaseExporter
DBHelper.LoyaltyCardDbIds.BALANCE,
DBHelper.LoyaltyCardDbIds.BALANCE_TYPE,
DBHelper.LoyaltyCardDbIds.CARD_ID,
DBHelper.LoyaltyCardDbIds.HEADER_COLOR,
DBHelper.LoyaltyCardDbIds.BARCODE_ID,
DBHelper.LoyaltyCardDbIds.BARCODE_TYPE,
DBHelper.LoyaltyCardDbIds.HEADER_COLOR,
DBHelper.LoyaltyCardDbIds.STAR_STATUS);
Cursor cardCursor = db.getLoyaltyCardCursor();
@@ -75,9 +142,9 @@ public class CsvDatabaseExporter implements DatabaseExporter
card.balance,
card.balanceType,
card.cardId,
card.headerColor,
card.barcodeId,
card.barcodeType,
card.headerColor,
card.starStatus);
if(Thread.currentThread().isInterrupted())

View File

@@ -1,14 +1,20 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -22,6 +28,8 @@ import java.util.List;
import protect.card_locker.DBHelper;
import protect.card_locker.FormatException;
import protect.card_locker.Group;
import protect.card_locker.Utils;
import protect.card_locker.ZipUtils;
/**
* Class for importing a database from CSV (Comma Separate Values)
@@ -30,10 +38,36 @@ import protect.card_locker.Group;
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class CsvDatabaseImporter implements DatabaseImporter
public class CatimaImporter implements Importer
{
public void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException
{
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, InterruptedException {
InputStream bufferedInputStream = new BufferedInputStream(input);
bufferedInputStream.mark(100);
// First, check if this is a zip file
ZipInputStream zipInputStream = new ZipInputStream(bufferedInputStream);
LocalFileHeader localFileHeader = zipInputStream.getNextEntry();
if (localFileHeader == null) {
// This is not a zip file, try importing as bare CSV
bufferedInputStream.reset();
importCSV(context, db, bufferedInputStream);
return;
}
importZipFile(context, db, zipInputStream, localFileHeader);
}
public void importZipFile(Context context, DBHelper db, ZipInputStream input, LocalFileHeader localFileHeader) throws IOException, FormatException, InterruptedException {
String fileName = localFileHeader.getFileName();
if (fileName.equals("catima.csv")) {
importCSV(context, db, new ByteArrayInputStream(ZipUtils.read(input).getBytes(StandardCharsets.UTF_8)));
} else {
Utils.saveCardImage(context, ZipUtils.readImage(input), fileName);
}
}
public void importCSV(Context context, DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
bufferedReader.mark(100);
@@ -50,10 +84,10 @@ public class CsvDatabaseImporter implements DatabaseImporter
switch (version) {
case 1:
parseV1(db, bufferedReader);
parseV1(context, db, bufferedReader);
break;
case 2:
parseV2(db, bufferedReader);
parseV2(context, db, bufferedReader);
break;
default:
throw new FormatException(String.format("No code to parse version %s", version));
@@ -62,7 +96,7 @@ public class CsvDatabaseImporter implements DatabaseImporter
bufferedReader.close();
}
public void parseV1(DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException
public void parseV1(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException
{
final CSVParser parser = new CSVParser(input, CSVFormat.RFC4180.withHeader());
@@ -73,7 +107,7 @@ public class CsvDatabaseImporter implements DatabaseImporter
{
for (CSVRecord record : parser)
{
importLoyaltyCard(database, db, record);
importLoyaltyCard(context, database, db, record);
if(Thread.currentThread().isInterrupted())
{
@@ -95,7 +129,7 @@ public class CsvDatabaseImporter implements DatabaseImporter
}
}
public void parseV2(DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException
public void parseV2(Context context, DBHelper db, BufferedReader input) throws IOException, FormatException, InterruptedException
{
SQLiteDatabase database = db.getWritableDatabase();
database.beginTransaction();
@@ -116,7 +150,7 @@ public class CsvDatabaseImporter implements DatabaseImporter
parseV2Groups(db, database, stringPart);
break;
case 2:
parseV2Cards(db, database, stringPart);
parseV2Cards(context, db, database, stringPart);
break;
case 3:
parseV2CardGroups(db, database, stringPart);
@@ -164,14 +198,14 @@ public class CsvDatabaseImporter implements DatabaseImporter
}
}
public void parseV2Cards(DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException
public void parseV2Cards(Context context, DBHelper db, SQLiteDatabase database, String data) throws IOException, FormatException, InterruptedException
{
// Parse cards
final CSVParser cardParser = new CSVParser(new StringReader(data), CSVFormat.RFC4180.withHeader());
try {
for (CSVRecord record : cardParser) {
importLoyaltyCard(database, db, record);
importLoyaltyCard(context, database, db, record);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
@@ -208,7 +242,7 @@ public class CsvDatabaseImporter implements DatabaseImporter
* Import a single loyalty card into the database using the given
* session.
*/
private void importLoyaltyCard(SQLiteDatabase database, DBHelper helper, CSVRecord record)
private void importLoyaltyCard(Context context, SQLiteDatabase database, DBHelper helper, CSVRecord record)
throws IOException, FormatException
{
int id = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.ID, record, false);
@@ -270,10 +304,11 @@ public class CsvDatabaseImporter implements DatabaseImporter
try {
starStatus = CSVHelpers.extractInt(DBHelper.LoyaltyCardDbIds.STAR_STATUS, record, false);
} catch (FormatException _e ) {
// This field did not exist in versions 0.28 and before
// This field did not exist in versions 0.278 and before
// We catch this exception so we can still import old backups
}
if (starStatus != 1) starStatus = 0;
helper.insertLoyaltyCard(database, id, store, note, expiry, balance, balanceType, cardId, barcodeId, barcodeType, headerColor, starStatus);
}

View File

@@ -1,9 +1,10 @@
package protect.card_locker;
package protect.card_locker.importexport;
public enum DataFormat
{
Catima,
Fidme,
Stocard,
VoucherVault
;
}

View File

@@ -1,7 +1,9 @@
package protect.card_locker.importexport;
import android.content.Context;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.OutputStream;
import protect.card_locker.DBHelper;
@@ -9,11 +11,11 @@ import protect.card_locker.DBHelper;
* Interface for a class which can export the contents of the database
* in a given format.
*/
public interface DatabaseExporter
public interface Exporter
{
/**
* Export the database to the output stream in a given format.
* @throws IOException
*/
void exportData(DBHelper db, OutputStreamWriter output) throws IOException, InterruptedException;
void exportData(Context context, DBHelper db, OutputStream output) throws IOException, InterruptedException;
}

View File

@@ -1,9 +1,13 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
@@ -15,8 +19,6 @@ import java.io.StringReader;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import protect.card_locker.DBHelper;
import protect.card_locker.FormatException;
@@ -28,20 +30,20 @@ import protect.card_locker.FormatException;
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class FidmeImporter implements DatabaseImporter
public class FidmeImporter implements Importer
{
public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException {
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
// We actually retrieve a .zip file
ZipInputStream zipInputStream = new ZipInputStream(input);
ZipInputStream zipInputStream = new ZipInputStream(input, password);
StringBuilder loyaltyCards = new StringBuilder();
byte[] buffer = new byte[1024];
int read = 0;
ZipEntry zipEntry;
LocalFileHeader localFileHeader;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (zipEntry.getName().equals("loyalty_programs.csv")) {
while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
if (localFileHeader.getFileName().equals("loyalty_programs.csv")) {
while ((read = zipInputStream.read(buffer, 0, 1024)) >= 0) {
loyaltyCards.append(new String(buffer, 0, read, StandardCharsets.UTF_8));
}
@@ -102,18 +104,18 @@ public class FidmeImporter implements DatabaseImporter
}
// There seems to be no note field in the CSV? So let's combine other fields instead...
String program = CSVHelpers.extractString("Program", record, "");
String addedAt = CSVHelpers.extractString("Added At", record, "");
String firstName = CSVHelpers.extractString("Firstname", record, "");
String lastName = CSVHelpers.extractString("Lastname", record, "");
String program = CSVHelpers.extractString("Program", record, "").trim();
String addedAt = CSVHelpers.extractString("Added At", record, "").trim();
String firstName = CSVHelpers.extractString("Firstname", record, "").trim();
String lastName = CSVHelpers.extractString("Lastname", record, "").trim();
String combinedName = String.format("%s %s", firstName, lastName);
String combinedName = String.format("%s %s", firstName, lastName).trim();
StringBuilder noteBuilder = new StringBuilder();
if (!program.isEmpty()) noteBuilder.append(program).append('\n');
if (!addedAt.isEmpty()) noteBuilder.append(addedAt).append('\n');
if (!combinedName.isEmpty()) noteBuilder.append(combinedName).append('\n');
String note = noteBuilder.toString();
String note = noteBuilder.toString().trim();
// The ID is called reference
String cardId = CSVHelpers.extractString("Reference", record, "");
@@ -130,6 +132,8 @@ public class FidmeImporter implements DatabaseImporter
// No favourite data in the export either
int starStatus = 0;
// TODO: Front and back image
helper.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, null, starStatus);
}
}

View File

@@ -0,0 +1,9 @@
package protect.card_locker.importexport;
public enum ImportExportResult
{
Success,
GenericFailure,
BadPassword
;
}

View File

@@ -1,5 +1,7 @@
package protect.card_locker.importexport;
import android.content.Context;
import org.json.JSONException;
import java.io.IOException;
@@ -13,7 +15,7 @@ import protect.card_locker.FormatException;
* Interface for a class which can import the contents of a stream
* into the database.
*/
public interface DatabaseImporter
public interface Importer
{
/**
* Import data from the input stream in a given format into
@@ -21,5 +23,5 @@ public interface DatabaseImporter
* @throws IOException
* @throws FormatException
*/
void importData(DBHelper db, InputStream input) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, InterruptedException, JSONException, ParseException;
}

View File

@@ -1,12 +1,12 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.OutputStream;
import protect.card_locker.DBHelper;
import protect.card_locker.DataFormat;
public class MultiFormatExporter
{
@@ -18,18 +18,18 @@ public class MultiFormatExporter
*
* The output stream is closed on success.
*
* @return true if the database was successfully exported,
* false otherwise. If false, partial data may have been
* @return ImportExportResult.Success if the database was successfully exported,
* another ImportExportResult otherwise. If not Success, partial data may have been
* written to the output stream, and it should be discarded.
*/
public static boolean exportData(DBHelper db, OutputStreamWriter output, DataFormat format)
public static ImportExportResult exportData(Context context, DBHelper db, OutputStream output, DataFormat format)
{
DatabaseExporter exporter = null;
Exporter exporter = null;
switch(format)
{
case Catima:
exporter = new CsvDatabaseExporter();
exporter = new CatimaExporter();
break;
default:
Log.e(TAG, "Failed to export data, unknown format " + format.name());
@@ -40,8 +40,8 @@ public class MultiFormatExporter
{
try
{
exporter.exportData(db, output);
return true;
exporter.exportData(context, db, output);
return ImportExportResult.Success;
}
catch(IOException e)
{
@@ -52,12 +52,12 @@ public class MultiFormatExporter
Log.e(TAG, "Failed to export data", e);
}
return false;
return ImportExportResult.GenericFailure;
}
else
{
Log.e(TAG, "Unsupported data format exported: " + format.name());
return false;
return ImportExportResult.GenericFailure;
}
}
}

View File

@@ -1,7 +1,10 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.util.Log;
import net.lingala.zip4j.exception.ZipException;
import org.json.JSONException;
import java.io.IOException;
@@ -9,7 +12,6 @@ import java.io.InputStream;
import java.text.ParseException;
import protect.card_locker.DBHelper;
import protect.card_locker.DataFormat;
import protect.card_locker.FormatException;
public class MultiFormatImporter
@@ -23,22 +25,25 @@ public class MultiFormatImporter
* The input stream is not closed, and doing so is the
* responsibility of the caller.
*
* @return true if the database was successfully imported,
* false otherwise. If false, no data was written to
* @return ImportExportResult.Success if the database was successfully imported,
* or another result otherwise. If no Success, no data was written to
* the database.
*/
public static boolean importData(DBHelper db, InputStream input, DataFormat format)
public static ImportExportResult importData(Context context, DBHelper db, InputStream input, DataFormat format, char[] password)
{
DatabaseImporter importer = null;
Importer importer = null;
switch(format)
{
case Catima:
importer = new CsvDatabaseImporter();
importer = new CatimaImporter();
break;
case Fidme:
importer = new FidmeImporter();
break;
case Stocard:
importer = new StocardImporter();
break;
case VoucherVault:
importer = new VoucherVaultImporter();
break;
@@ -48,10 +53,14 @@ public class MultiFormatImporter
{
try
{
importer.importData(db, input);
return true;
importer.importData(context, db, input, password);
return ImportExportResult.Success;
}
catch(IOException | FormatException | InterruptedException | JSONException | ParseException e)
catch(ZipException e)
{
return ImportExportResult.BadPassword;
}
catch(IOException | FormatException | InterruptedException | JSONException | ParseException | NullPointerException e)
{
Log.e(TAG, "Failed to import data", e);
}
@@ -61,6 +70,7 @@ public class MultiFormatImporter
{
Log.e(TAG, "Unsupported data format imported: " + format.name());
}
return false;
return ImportExportResult.GenericFailure;
}
}

View File

@@ -0,0 +1,226 @@
package protect.card_locker.importexport;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import com.google.zxing.BarcodeFormat;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import protect.card_locker.DBHelper;
import protect.card_locker.FormatException;
import protect.card_locker.Utils;
import protect.card_locker.ZipUtils;
/**
* Class for importing a database from CSV (Comma Separate Values)
* formatted data.
*
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class StocardImporter implements Importer
{
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
HashMap<String, HashMap<String, Object>> loyaltyCardHashMap = new HashMap<>();
HashMap<String, String> providers = new HashMap<>();
ZipInputStream zipInputStream = new ZipInputStream(input, password);
String[] providersFileName = null;
String[] cardBaseName = null;
String cardName = "";
LocalFileHeader localFileHeader;
while ((localFileHeader = zipInputStream.getNextEntry()) != null) {
String fileName = localFileHeader.getFileName();
String[] nameParts = fileName.split("/");
if (providersFileName == null) {
providersFileName = new String[] {
nameParts[0],
"sync",
"data",
"users",
nameParts[0],
"analytics-properties.json"
};
cardBaseName = new String[] {
nameParts[0],
"sync",
"data",
"users",
nameParts[0],
"loyalty-cards"
};
}
if (startsWith(nameParts, providersFileName, 0) && !localFileHeader.isDirectory()) {
providers = parseProviders(zipInputStream);
} else if (startsWith(nameParts, cardBaseName, 1)) {
// Extract cardName
cardName = nameParts[cardBaseName.length].split("\\.", 2)[0];
// This is the card itself
if (nameParts.length == cardBaseName.length + 1) {
// Ignore the .txt file
if (fileName.endsWith(".json")) {
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
loyaltyCardHashMap = appendToLoyaltyCardHashMap(
loyaltyCardHashMap,
cardName,
"cardId",
jsonObject.getString("input_id")
);
loyaltyCardHashMap = appendToLoyaltyCardHashMap(
loyaltyCardHashMap,
cardName,
"_providerId",
jsonObject
.getJSONObject("input_provider_reference")
.getString("identifier")
.substring("/loyalty-card-providers/".length())
);
try {
loyaltyCardHashMap = appendToLoyaltyCardHashMap(
loyaltyCardHashMap,
cardName,
"barcodeType",
jsonObject.getString("input_barcode_format")
);
} catch (JSONException ignored) {}
}
} else if (fileName.endsWith("notes/default.json")) {
loyaltyCardHashMap = appendToLoyaltyCardHashMap(
loyaltyCardHashMap,
cardName,
"note",
ZipUtils.readJSON(zipInputStream)
.getString("content")
);
} else if (fileName.endsWith("/images/front.png")) {
loyaltyCardHashMap = appendToLoyaltyCardHashMap(
loyaltyCardHashMap,
cardName,
"frontImage",
ZipUtils.readImage(zipInputStream)
);
} else if (fileName.endsWith("/images/back.png")) {
loyaltyCardHashMap = appendToLoyaltyCardHashMap(
loyaltyCardHashMap,
cardName,
"backImage",
ZipUtils.readImage(zipInputStream)
);
}
}
}
if (loyaltyCardHashMap.keySet().size() == 0) {
throw new FormatException("Couldn't find any loyalty cards in this Stocard export.");
}
SQLiteDatabase database = db.getWritableDatabase();
database.beginTransaction();
for (HashMap<String, Object> loyaltyCardData : loyaltyCardHashMap.values()) {
String store = providers.get(loyaltyCardData.get("_providerId").toString());
String note = (String) Utils.hashmapGetOrDefault(loyaltyCardData, "note", "");
String cardId = (String) loyaltyCardData.get("cardId");
String barcodeTypeString = (String) Utils.hashmapGetOrDefault(loyaltyCardData, "barcodeType", null);
BarcodeFormat barcodeType = null;
if (barcodeTypeString != null) {
if (barcodeTypeString.equals("RSS_DATABAR_EXPANDED")) {
barcodeType = BarcodeFormat.RSS_EXPANDED;
} else {
barcodeType = BarcodeFormat.valueOf(barcodeTypeString);
}
}
long loyaltyCardInternalId = db.insertLoyaltyCard(database, store, note, null, BigDecimal.valueOf(0), null, cardId, null, barcodeType, null, 0);
if (loyaltyCardData.containsKey("frontImage")) {
Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("frontImage"), (int) loyaltyCardInternalId, true);
}
if (loyaltyCardData.containsKey("backImage")) {
Utils.saveCardImage(context, (Bitmap) loyaltyCardData.get("backImage"), (int) loyaltyCardInternalId, false);
}
}
database.setTransactionSuccessful();
database.endTransaction();
database.close();
zipInputStream.close();
}
private boolean startsWith(String[] full, String[] start, int minExtraLength) {
if (full.length - minExtraLength < start.length) {
return false;
}
for (int i = 0; i < start.length; i++) {
if (!start[i].contentEquals(full[i])) {
return false;
}
}
return true;
}
private HashMap<String, HashMap<String, Object>> appendToLoyaltyCardHashMap(HashMap<String, HashMap<String, Object>> loyaltyCardHashMap, String cardID, String key, Object value) {
HashMap<String, Object> loyaltyCardData = loyaltyCardHashMap.get(cardID);
if (loyaltyCardData == null) {
loyaltyCardData = new HashMap<>();
}
loyaltyCardData.put(key, value);
loyaltyCardHashMap.put(cardID, loyaltyCardData);
return loyaltyCardHashMap;
}
private HashMap<String, String> parseProviders(ZipInputStream zipInputStream) throws IOException, JSONException {
// FIXME: This is probably completely wrong, but it works for the one and only test file I have
JSONObject jsonObject = ZipUtils.readJSON(zipInputStream);
JSONArray providerIdList = jsonObject.getJSONArray("provider_id_list");
JSONArray providerList = jsonObject.getJSONArray("provider_list");
// Resort, put IDs with - in them after IDs without any -
List<String> providerIds = new ArrayList<>();
List<String> customProviderIds = new ArrayList<>();
for (int i = 0; i < providerIdList.length(); i++) {
String providerId = providerIdList.get(i).toString();
if (providerId.contains("-")) {
customProviderIds.add(providerId);
} else {
providerIds.add(providerId);
}
}
providerIds.addAll(customProviderIds);
HashMap<String, String> providers = new HashMap<>();
for (int i = 0; i < jsonObject.getInt("number_of_cards"); i++) {
providers.put(providerIds.get(i), providerList.get(i).toString());
}
return providers;
}
}

View File

@@ -1,6 +1,7 @@
package protect.card_locker.importexport;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Color;
@@ -32,9 +33,9 @@ import protect.card_locker.FormatException;
* The database's loyalty cards are expected to appear in the CSV data.
* A header is expected for the each table showing the names of the columns.
*/
public class VoucherVaultImporter implements DatabaseImporter
public class VoucherVaultImporter implements Importer
{
public void importData(DBHelper db, InputStream input) throws IOException, FormatException, JSONException, ParseException {
public void importData(Context context, DBHelper db, InputStream input, char[] password) throws IOException, FormatException, JSONException, ParseException {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
@@ -60,11 +61,13 @@ public class VoucherVaultImporter implements DatabaseImporter
expiry = dateFormat.parse(jsonCard.getString("expires"));
}
BigDecimal balance;
if (!jsonCard.isNull("balance")) {
BigDecimal balance = new BigDecimal("0");
if (jsonCard.has("balanceMilliunits")) {
if (!jsonCard.isNull("balanceMilliunits")) {
balance = new BigDecimal(String.valueOf(jsonCard.getInt("balanceMilliunits") / 1000.0));
}
} else if (!jsonCard.isNull("balance")) {
balance = new BigDecimal(String.valueOf(jsonCard.getDouble("balance")));
} else {
balance = new BigDecimal("0");
}
Currency balanceType = Currency.getInstance("USD");
@@ -122,7 +125,7 @@ public class VoucherVaultImporter implements DatabaseImporter
headerColor = Color.YELLOW;
break;
default:
throw new FormatException("Unknown colour type foun: " + colorFromJSON);
throw new FormatException("Unknown colour type found: " + colorFromJSON);
}
db.insertLoyaltyCard(store, "", expiry, balance, balanceType, cardId, null, barcodeType, headerColor, 0);

View File

@@ -1,66 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="192dp"
android:height="192dp"
android:viewportWidth="50.8"
android:viewportHeight="50.8">
<path
android:pathData="M14.3354,20.1954l20.7318,-9.2304l5.7612,12.9398l-20.7318,9.2304z"
android:strokeLineJoin="miter"
android:strokeWidth="0.529167"
android:fillColor="#f0f0f0"
android:strokeColor="#c80000"/>
<path
android:pathData="M14.8755,10.9648l23.2041,10.3311l-6.8874,15.4694l-23.2041,-10.3311z"
android:strokeLineJoin="miter"
android:strokeWidth="0.529167"
android:fillColor="#f0f0f0"
android:strokeColor="#c80000"/>
<path
android:pathData="M16.5599,16.1348l26.5459,7.6119l-4.5489,15.8639l-26.5459,-7.6119z"
android:strokeLineJoin="round"
android:strokeWidth="1.5875"
android:fillColor="#c80000"
android:strokeColor="#c80000"
android:fillType="evenOdd"/>
<path
android:pathData="M12.011,15.4955h27.6157v16.5032h-27.6157z"
android:strokeLineJoin="round"
android:strokeWidth="1.5875"
android:fillColor="#ff0000"
android:strokeColor="#ff0000"
android:fillType="evenOdd"/>
<path
android:pathData="M7.8471,23.7471a4.3659,8.5899 0,1 0,8.7317 0a4.3659,8.5899 0,1 0,-8.7317 0z"
android:strokeLineJoin="round"
android:strokeWidth="0.91078"
android:fillColor="#ff0000"
android:strokeColor="#ff0000"/>
<path
android:pathData="m24.4983,25.781a1.6711,1.6711 0,0 1,-1.3809 1.6457,1.6711 1.6711,0 0,1 -1.8605,-1.0741"
android:strokeLineJoin="round"
android:strokeWidth="0.529167"
android:fillColor="#00000000"
android:strokeColor="#f0f0f0"
android:strokeLineCap="round"/>
<path
android:pathData="m27.7991,26.333a1.6711,1.6711 0,0 1,-1.8605 1.0741,1.6711 1.6711,0 0,1 -1.3809,-1.6457"
android:strokeLineJoin="round"
android:strokeWidth="0.529167"
android:fillColor="#00000000"
android:strokeColor="#f0f0f0"
android:strokeLineCap="round"/>
<path
android:pathData="m16.0606,22.271 l2.6458,-2.6458 2.6458,2.6458"
android:strokeLineJoin="miter"
android:strokeWidth="0.529167"
android:fillColor="#00000000"
android:strokeColor="#f0f0f0"
android:strokeLineCap="butt"/>
<path
android:pathData="m27.7023,22.271 l2.6458,-2.6458 2.6458,2.6458"
android:strokeLineJoin="miter"
android:strokeWidth="0.529167"
android:fillColor="#00000000"
android:strokeColor="#f0f0f0"
android:strokeLineCap="butt"/>
</vector>

View File

@@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,3h-1L19,1h-2v2L7,3L7,1L5,1v2L4,3c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,21L4,21L4,8h16v13z"/>
</vector>

View File

@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,5l0,-3l-12,0l0,1.17l1.83,1.83z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M16,11l2,-3l0,-1l-8.17,0l6.17,6.17z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M2.81,2.81L1.39,4.22L8,10.83V22h8v-3.17l3.78,3.78l1.41,-1.41L2.81,2.81z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,2h12v3h-12z"
android:fillColor="#FFFFFF"/>
<path
android:pathData="M6,7v1l2,3v11h8V11l2,-3V7H6zM12,15.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5S12.83,15.5 12,15.5z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/darker_gray"/>
<size android:height="1dp"/>
</shape>

View File

@@ -304,6 +304,27 @@
android:textStyle="bold"
android:layout_weight="1.0" />
</LinearLayout>
<LinearLayout android:orientation="vertical"
android:padding="10.0dp"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<ImageView
android:layout_width="match_parent"
android:layout_height="@dimen/barcode_disp_height"
android:layout_gravity="center_horizontal"
android:id="@+id/upceBarcode"
android:contentDescription="@string/barcodeImageDescription"
android:layout_weight="1.0"/>
<TextView
android:id="@+id/upceBarcodeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10.0dip"
android:gravity="center"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
android:layout_weight="1.0" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -30,14 +30,12 @@
android:id="@+id/add_from_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="addFromImage"
android:text="@string/addFromImage" />
<Button
android:id="@+id/add_manually"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="addManually"
android:text="@string/addManually" />
</LinearLayout>
</merge>

View File

@@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:baselineAligned="false"
android:padding="@dimen/activity_margin">
@@ -44,8 +44,7 @@
android:layout_weight="1"
app:srcCompat="@drawable/ic_baseline_arrow_drop_up_24"
android:contentDescription="@string/moveUp"
app:tint="@color/iconColor"
android:onClick="moveGroupUp"/>
app:tint="@color/iconColor"/>
<ImageButton
android:id="@+id/moveDown"
@@ -54,8 +53,7 @@
android:layout_weight="1"
app:srcCompat="@drawable/ic_baseline_arrow_drop_down_24"
android:contentDescription="@string/moveDown"
app:tint="@color/iconColor"
android:onClick="moveGroupDown"/>
app:tint="@color/iconColor"/>
<ImageButton
android:id="@+id/edit"
@@ -64,8 +62,7 @@
android:layout_weight="1"
app:srcCompat="@drawable/ic_mode_edit_white_24dp"
android:contentDescription="@string/edit"
app:tint="@color/iconColor"
android:onClick="editGroup" />
app:tint="@color/iconColor"/>
<ImageButton
android:id="@+id/delete"
@@ -74,8 +71,7 @@
android:layout_weight="1"
app:srcCompat="@drawable/ic_delete_white_24dp"
android:contentDescription="@string/delete"
app:tint="@color/iconColor"
android:onClick="deleteGroup"/>
app:tint="@color/iconColor"/>
</LinearLayout>

View File

@@ -17,9 +17,10 @@
android:text="@string/noGroups"
android:visibility="gone"/>
<ListView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:visibility="gone"/>
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:visibility="gone" />
</RelativeLayout>

View File

@@ -1,7 +1,8 @@
<?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"
android:layout_width="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
@@ -38,6 +39,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/barcode"/>
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/photos"/>
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
@@ -326,6 +331,84 @@
android:layout_weight="1.0"/>
</LinearLayout>
</TableLayout>
<TableLayout
android:id="@+id/picturesPart"
android:visibility="gone"
tools:visibility="visible">
<!-- Front image -->
<LinearLayout
android:id="@+id/frontImageHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/inputPadding"
android:paddingTop="@dimen/inputPadding">
<!-- Front image -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingHorizontal="@dimen/inputPadding"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/frontImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:minHeight="50dp"
android:background="@color/colorPrimary"
android:contentDescription="@string/frontImageDescription"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_camera_white" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<!-- Back image -->
<LinearLayout
android:id="@+id/backImageHolder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingHorizontal="@dimen/inputPadding"
android:paddingTop="@dimen/inputPadding">
<!-- Back image -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingHorizontal="@dimen/inputPadding"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/backImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:minHeight="50dp"
android:background="@color/colorPrimary"
android:contentDescription="@string/backImageDescription"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_camera_white" />
</androidx.cardview.widget.CardView>
</LinearLayout>
</TableLayout>
</TableLayout>
</ScrollView>

View File

@@ -113,6 +113,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@android:color/black" />
app:tint="#000000" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -45,6 +45,7 @@
<ImageButton
android:id="@+id/maximizeButton"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginStart="15.0dip"
@@ -53,7 +54,7 @@
android:padding="0dp"
app:srcCompat="@drawable/ic_baseline_arrow_drop_up_24"
android:contentDescription="@string/moveBarcodeToTopOfScreen"
android:tint="@android:color/white"
app:tint="#ffffff"
android:background="@color/colorPrimary"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/barcode"
@@ -84,7 +85,7 @@
android:padding="0dp"
app:srcCompat="@drawable/ic_baseline_arrow_drop_down_24"
android:contentDescription="@string/moveBarcodeToCenterOfScreen"
android:tint="@android:color/white"
app:tint="#ffffff"
android:background="@color/colorPrimary"
app:layout_constraintTop_toBottomOf="@+id/barcode"
app:layout_constraintEnd_toEndOf="parent"
@@ -134,60 +135,123 @@
android:id="@+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/inputBackground"
android:fitsSystemWindows="false"
android:orientation="vertical"
android:padding="20dp"
android:visibility="gone"
app:behavior_hideable="false"
app:behavior_peekHeight="104dp"
app:behavior_peekHeight="80dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
tools:visibility="visible"
android:fitsSystemWindows="true">
tools:visibility="visible">
<ImageButton
android:id="@+id/bottomSheetButton"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_gravity="top|start"
android:background="@color/colorPrimary"
android:scaleType="fitCenter"
android:layout_gravity="top|start"
android:tint="@android:color/white"
app:tint="#ffffff"
app:srcCompat="@drawable/ic_baseline_arrow_drop_up_24" />
<TextView
android:id="@+id/noteView"
<androidx.core.widget.NestedScrollView
android:id="@+id/bottomSheetContentWrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/inputBackground"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
android:layout_height="wrap_content">
<TextView
android:id="@+id/groupsView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/inputBackground"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/balanceView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/inputBackground"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/expiryView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/inputBackground"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
<androidx.cardview.widget.CardView
android:id="@+id/frontImageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingHorizontal="@dimen/inputPadding"
android:layout_weight="1"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/frontImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/frontImageDescription"
android:scaleType="fitCenter" />
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/backImageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/activity_margin"
android:layout_marginTop="@dimen/activity_margin"
android:layout_marginEnd="@dimen/activity_margin"
android:layout_marginBottom="@dimen/activity_margin"
android:paddingHorizontal="@dimen/inputPadding"
android:layout_weight="1"
app:cardCornerRadius="4dp"
app:cardElevation="0dp">
<ImageView
android:id="@+id/backImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@string/backImageDescription"
android:scaleType="fitCenter" />
</androidx.cardview.widget.CardView>
</LinearLayout>
<TextView
android:id="@+id/noteView"
android:autoLink="all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
<TextView
android:id="@+id/groupsView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
<TextView
android:id="@+id/balanceView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
<TextView
android:id="@+id/expiryView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="20dp"
android:textSize="@dimen/singleCardNoteTextSizeMin" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>
<com.google.android.material.appbar.AppBarLayout
@@ -197,7 +261,6 @@
android:clipToPadding="false"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingTop="24dp"
android:weightSum="1.0"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
@@ -243,6 +306,7 @@
android:layout_width="fill_parent"
android:layout_height="?actionBarSize"
app:contentInsetStart="72.0dip"
app:layout_collapseMode="pin" />
app:layout_collapseMode="pin"
android:paddingTop="6dp" />
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -7,20 +7,27 @@
android:title="@string/copy_to_clipboard"
android:icon="@drawable/ic_copy"
android:titleCondensed="@string/copy_to_clipboard"
app:showAsAction="always"/>
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_share"
android:title="@string/share"
android:icon="@drawable/ic_share"
android:titleCondensed="@string/share"
app:showAsAction="always"/>
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_edit"
android:icon="@drawable/ic_edit"
android:title="@string/editCardTitle"
android:titleCondensed="@string/editCardTitle"
app:showAsAction="always"/>
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete_white_24dp"
android:title="@string/delete"
android:titleCondensed="@string/delete"
app:showAsAction="ifRoom"/>
</menu>

View File

@@ -17,7 +17,7 @@
android:id="@+id/action_import_export"
android:icon="@drawable/ic_import_export_white_24dp"
android:title="@string/importExport"
app:showAsAction="ifRoom"/>
app:showAsAction="never"/>
<item
android:id="@+id/action_settings"
android:title="@string/settings"

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_toggle_flashlight"
android:icon="@drawable/ic_flashlight_off_white_24dp"
android:title="@string/turn_flashlight_on"
app:showAsAction="always"/>
</menu>

View File

@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Добавяне</string>
<string name="action_search">Търсене</string>
<string name="leaveWithoutSaveTitle">Изход</string>
<string name="confirm">Потвърждаване</string>
<string name="delete">Премахване</string>
<string name="edit">Редактиране</string>
<string name="save">Запазване</string>
<string name="cancel">Отказ</string>
<string name="unstar">Премахва от любими</string>
<string name="star">Добавя към любими</string>
<string name="noBarcode">Без щрихкод</string>
<string name="barcodeNoBarcode">Картата няма щрихкод</string>
<string name="barcodeType">Вид на щрихкод</string>
<string name="cardId">Идентификатор на карта</string>
<string name="note">Бележка</string>
<string name="storeName">Наименование</string>
<string name="noMatchingGiftCards">Няма резултати. Променете критериите за търсене.</string>
<string name="noGiftCards">Докоснете бутона +, за да добавите карта или внесете от менюто ⋮.</string>
<string name="all">Всички</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> карта</item>
<item quantity="other"><xliff:g>%d</xliff:g> карти</item>
</plurals>
<string name="failedOpeningFileManager">Инсталирайте приложение за управление на файлове.</string>
<string name="app_license">Свободен софтуер с авторски права, лицензиран под GPLv3+.</string>
<string name="frontImageDescription">Снимка на предната страна на карта</string>
<string name="backImageDescription">Снимка на задната страна на карта</string>
<string name="parsingBalanceFailed"><xliff:g>%s</xliff:g> не изглежда истинска наличност.</string>
<string name="barcodeImageDescription">Изображение на щрихкода на картата</string>
<string name="no">Не</string>
<string name="yes">Да</string>
<string name="setBackImage">Снимка на задната страна</string>
<string name="setFrontImage">Снимка на предната страна</string>
<string name="photos">Снимки</string>
<string name="importOptionApplicationExplanation">Изберете файл чрез друго приложение.</string>
<string name="noExternalStoragePermissionError">Дайте разрешение за достъп до хранилището, за да работи внасянето и изнасянето</string>
<string name="noCardExistsError">Картата не е намерена</string>
<string name="updateBarcodeQuestionText">Идентификаторът е променен. Желаете ли с неговата стойност да бъде променен и щрихкода\?</string>
<string name="updateBarcodeQuestionTitle">Обновяване на щрихкода\?</string>
<string name="noCardIdError">Не е въведен идентификатор</string>
<string name="noCardsMessage">Добавете карта</string>
<string name="cardShortcut">Пряк път до карта</string>
<string name="addCardTitle">Добавяне на карта</string>
<string name="removeImage">Премахване на изображение</string>
<string name="takePhoto">Снимане</string>
<string name="copy_to_clipboard_multiple_toast">Идентификаторите са копирани в междинната памет</string>
<string name="intent_import_card_from_url_share_multiple_text">Искам да споделя тези карти с вас</string>
<string name="wrongValueForBarcodeType">Стойността е невалидна за избрания вид щрихкод</string>
<string name="setBarcodeId">Задаване на стойност</string>
<string name="sameAsCardId">Същата като идентификатора</string>
<string name="barcodeId">Стойност на щрихкода</string>
<string name="importLoyaltyCardKeychain">Внасяне от Loyalty Card Keychain</string>
<string name="importFidmeMessage">Изберете файла <i>fidme-export-request-xxxxxx.zip</i>, предварително изнесен от FidMe и ръчно изберете вида на щрихкодовете.
\nИли създайте такъв файл от Data Protection в менюто на профила във FidMe и изберете „Extract my data“.</string>
<string name="importFidme">Внасяне от FidMe</string>
<string name="exportOptionExplanation">Данните ще бъдат запазени на място по ваш избор.</string>
<string name="accept">Приемане</string>
<string name="privacy_policy_popup_text">Политика за личните данни (необходима от някои магазини за приложения):
\n
\nНЕ СЕ СЪБИРАТ ИЗОБЩО НИКАКВИ ДАННИ, което може да бъде потвърдено, защото приложението е със свободен код.</string>
<string name="privacy_policy">Политика за личните данни</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="turn_flashlight_off">Изключва светкавицата</string>
<string name="turn_flashlight_on">Включва светкавицата</string>
<string name="passwordRequired">Въведете паролата</string>
<string name="points">Точки</string>
<string name="currency">Валута</string>
<string name="balance">Наличност</string>
<string name="errorReadingImage">Изображението е нечетимо</string>
<string name="noBarcodeFound">Не е намерен щрихкод</string>
<string name="moveBarcodeToCenterOfScreen">Премества щрихкода в центъра на екрана</string>
<string name="moveBarcodeToTopOfScreen">Премества щрихкода най-горе на екрана</string>
<string name="never">Не изтича</string>
<string name="chooseExpiryDate">Дата на изтичане</string>
<string name="expiryDate">Валидност</string>
<string name="editBarcode">Редактиране на щрихкод</string>
<string name="barcode">Щрихкод</string>
<string name="card">Карта</string>
<string name="groupsList">Списъци: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Валидност: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Изтекла: <xliff:g>%s</xliff:g></string>
<string name="balancePoints"><xliff:g>%s</xliff:g> точки</string>
<string name="balanceSentence">Наличност: <xliff:g>%s</xliff:g></string>
<string name="noGroups">Докоснете бутона +, за да добавите списък.</string>
<string name="noStoreError">Не е въведено наименование</string>
<string name="groups">Списъци</string>
<string name="enter_group_name">Въведете име на списък</string>
<string name="intent_import_card_from_url_share_text">Искам да споделя тази карта с вас</string>
<string name="settings_display_barcode_max_brightness">Увеличаване на яркостта при видим щрихкод</string>
<string name="settings_lock_barcode_orientation">Спиране на завъртането на щрихкода</string>
<string name="settings_keep_screen_on">Поддържане на екрана включен</string>
<string name="settings_disable_lockscreen_while_viewing_card">Предотвратяване на заключване на екрана</string>
<string name="settings_max_font_size_scale">Максимален размер на шрифта</string>
<string name="settings_dark_theme">Тъмна</string>
<string name="settings_light_theme">Светла</string>
<string name="settings_system_theme">Системна</string>
<string name="settings_theme">Тема</string>
<string name="settings_category_title_ui">Потребителски интерфейс</string>
<string name="settings">Настройки</string>
<string name="starImage">Звезда за любимо</string>
<string name="thumbnailDescription">Миниатюра на картата</string>
<string name="copy_to_clipboard_toast">Идентификаторът на картата е копиран в междинната памет</string>
<string name="enterBarcodeInstructions">Въведете идентификатор на картата или като изберете вида на щрихкода или докоснете бутона „Картата няма щрихкод“.</string>
<string name="selectBarcodeTitle">Избиране на щрихкод</string>
<string name="importOptionApplicationButton">Избиране чрез приложение</string>
<string name="importing">Внасяне…</string>
<string name="exporting">Изнасяне…</string>
<string name="exportFailed">Картите не могат да бъдат изнесени</string>
<string name="exportFailedTitle">Грешка при изнасяне</string>
<string name="importFailed">Картите не могат да бъдат внесени</string>
<string name="importFailedTitle">Грешка при внасяне</string>
<string name="exportSuccessfulTitle">Резултат от изнасяне</string>
<string name="importSuccessfulTitle">Резултат от внасяне</string>
<string name="importExportHelp">Резервните копия на картите ви дават възможност да ги преместите на друго устройство.</string>
<string name="exportName">Изнасяне</string>
<string name="importExport">Внасяне/изнасяне</string>
<string name="sendLabel">Изпращане…</string>
<string name="scanCardBarcode">Сканиране на щрихкод от карта</string>
<string name="editCardTitle">Редактиране на карта</string>
<string name="share">Споделя</string>
<string name="copy_to_clipboard">Копира идентификатора в междинната памет</string>
<string name="ok">Добре</string>
<string name="importSuccessful">Картите са внесени успешно</string>
<string name="chooseImportType">От къде ще внесете\?</string>
<string name="importCatimaMessage">Изберете файла <i>catima.zip</i>, предварително изнесен от Catima.
\nИли създайте такъв файл от меню Внасяне/изнасяне от друго устройство със Catima като изберете Изнасяне.</string>
<string name="importOptionApplicationTitle">От друго приложение</string>
<string name="importOptionFilesystemButton">Избиране от файлова система</string>
<string name="importOptionFilesystemExplanation">Изберете определен файл от файловата система.</string>
<string name="app_resources">Свободни ресурси: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Свободни библиотеки: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_revision_fmt">Компилация: <xliff:g id="app_revision_url">%s</xliff:g></string>
<string name="debug_version_fmt">Издание: <xliff:g id="version">%s</xliff:g></string>
<string name="about_title_fmt">Относно <xliff:g id="app_name">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Всички права запазени © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="app_copyright_old">На основата на Loyalty Card Keychain
\nвсички права запазени © 20162020 Branden Archer.</string>
<string name="about">Относно</string>
<string name="importOptionFilesystemTitle">Внасяне от файловата система</string>
<string name="importCatima">Внасяне от Catima</string>
<string name="exportSuccessful">Картите са изнесени успешно</string>
<string name="unlockScreen">Разрешава автоматичното завъртане</string>
<string name="lockScreen">Спира автоматичното завъртане</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> избрана карта</item>
<item quantity="other"><xliff:g>%d</xliff:g> избрани карти</item>
</plurals>
<string name="deleteConfirmationGroup">Изтриване на група\?</string>
<string name="moveDown">Преместване надолу</string>
<string name="moveUp">Преместване нагоре</string>
<string name="addFromImage">Избор от галерията</string>
<string name="addManually">Ръчно въвеждане</string>
<string name="leaveWithoutSaveConfirmation">Оставяте промените незапазени\?</string>
<string name="unsupportedBarcodeType">Щрихкод от този вид не може да бъде показан. Може да бъде поддържан в следващо издание.</string>
<string name="importStocard">Внасяне от Stocard</string>
<string name="importVoucherVault">Внасяне от Voucher Vault</string>
<string name="importVoucherVaultMessage">Изберете файла <i>vouchervault.json</i>, предварително изнесен от Voucher Vault.
\nИли създайте такъв файл от меню Изнасяне от Voucher Vault.</string>
<string name="importStocardMessage">Изберете файла <i>***-sync.zip</i>, предварително изнесен от Stocard и ръчно изберете вида на щрихкодовете.
\nИли получете такъв файл като пишете до support@stocardapp.com с молба за копие от вашата информация.</string>
<string name="importLoyaltyCardKeychainMessage">Изберете файла <i>LoyaltyCardKeychain.csv</i>, предварително изнесен от Loyalty Card Keychain.
\nИли създайте такъв файл от меню Внасяне/изнасяне от друго устройство със Loyalty Card Keychain като изберете Изнасяне.</string>
<string name="failedParsingImportUriError">Препратката не може да бъде анализирана за внасяне</string>
<string name="card_ids_copied">[не превеждай този низ, https://github.com/TheLastProject/Catima/issues/278]</string>
<string name="failedGeneratingShareURL">Грешка при създаване на адрес за споделяне. Изпратете доклад за дефект.</string>
<string name="deleteTitle">Премахване на карта</string>
<plurals name="deleteCardsTitle">
<item quantity="one">Премахване на карта</item>
<item quantity="other">Премахване на карти</item>
</plurals>
<string name="deleteConfirmation">Потвърдете премахване на карта</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Желаете ли картата да бъде премахната\?</item>
<item quantity="other">Желаете ли <xliff:g>%d</xliff:g> картати да бъдат премахнати\?</item>
</plurals>
</resources>

View File

@@ -12,8 +12,6 @@
<string name="edit">Editovat</string>
<string name="delete">Smazat</string>
<string name="confirm">Potvrdit</string>
<string name="deleteTitle">Odstzranit věrnostní kartu</string>
<string name="deleteConfirmation">Opravdu chcete smazat tuto věrnostní kartu?</string>
<string name="ok">Ano</string>
<string name="copy_to_clipboard">Kopírovat ID do schránky</string>
<string name="sendLabel">Odeslat&#8230;</string>
@@ -51,4 +49,7 @@
<string name="selectBarcodeTitle">Vyberte čárový kód</string>
<string name="copy_to_clipboard_toast">ID karty zkopírováno do schránky</string>
<string name="deleteTitle">Odstzranit věrnostní kartu</string>
<string name="deleteConfirmation">Opravdu chcete smazat tuto věrnostní kartu?</string>
</resources>

View File

@@ -16,8 +16,6 @@
<string name="unlockScreen">Rotation zulassen</string>
<string name="star">Zu den Favoriten hinzufügen</string>
<string name="unstar">Aus der Favoritenliste entfernen</string>
<string name="deleteTitle">Karte entfernen</string>
<string name="deleteConfirmation">Diese Karte löschen\?</string>
<string name="ok">OK</string>
<string name="copy_to_clipboard">Nummer in die Zwischenablage kopieren</string>
<string name="sendLabel">Senden </string>
@@ -93,7 +91,7 @@
<item quantity="other"><xliff:g>%d</xliff:g> Karten</item>
</plurals>
<string name="groupsList">Gruppen: <xliff:g>%s</xliff:g></string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="app_loyalty_card_keychain">Bonuskartenschlüsselring</string>
<string name="chooseImportType">Daten importieren aus\?</string>
<string name="parsingBalanceFailed"><xliff:g>%s</xliff:g> scheint kein gültiges Guthaben zu sein.</string>
<string name="points">Punkte</string>
@@ -118,17 +116,17 @@
\nKEINE DATEN WERDEN GESAMMELT, was jeder bestätigen kann, da unsere Anwendung eine freie Software ist.</string>
<string name="accept">Annehmen</string>
<string name="privacy_policy">Datenschutzrichtlinie</string>
<string name="importVoucherVaultMessage">Suchen Sie eine Datei, die wahrscheinlich <i>vouchervault.json</i> heißt, um sie zu importieren.
\nOder erstellen Sie sie, indem Sie zuerst in Voucher Vault auf Export drücken.</string>
<string name="importVoucherVaultMessage">Wählen Sie Ihren <i>vouchervault.json</i>-Export aus Voucher Vault zum Importieren aus.
\nOder erstellen Sie ihn, indem Sie zuerst auf Export in Voucher Vault drücken.</string>
<string name="importVoucherVault">Aus Voucher Vault importieren</string>
<string name="importLoyaltyCardKeychainMessage">Suchen Sie eine Datei, die höchstwahrscheinlich <i>LoyaltyCardKeychain.csv</i> heißt, um sie zu importieren.
\nOder erstellen Sie sie über das Menü Import/Export in Loyalty Card Keychain, indem Sie zuerst auf Export drücken.</string>
<string name="importLoyaltyCardKeychainMessage">Wählen Sie Ihren <i>LoyaltyCardKeychain.csv</i>-Export aus Loyalty Card Keychain zum Importieren aus.
\nOder erstellen Sie ihn über das Menü Import/Export in Loyalty Card Keychain, indem Sie dort zuerst auf Export drücken.</string>
<string name="importLoyaltyCardKeychain">Aus Loyalty Card Keychain importieren</string>
<string name="importFidmeMessage">Suchen Sie eine Datei, die wahrscheinlich <i>fidme-export-request-xxxxxx.zip</i> heißt, um sie zu importieren, und wählen Sie anschließend die Strichcodetypen manuell aus.
\nOder erstellen Sie sie aus Ihrem FidMe-Profil, indem Sie Datenschutz wählen und dann zuerst auf Extrahiere meine Daten drücken.</string>
<string name="importFidmeMessage">Wählen Sie Ihren <i>fidme-export-request-xxxxxx.zip</i>-Export aus FidMe zum Importieren aus und wählen Sie anschließend die Strichcodetypen manuell aus.
\nOder erstellen Sie ihn aus Ihrem FidMe-Profil, indem Sie Datenschutz wählen und dann zuerst auf Meine Daten extrahieren drücken.</string>
<string name="importFidme">Aus FidMe importieren</string>
<string name="importCatimaMessage">Suchen Sie eine Datei, die wahrscheinlich <i>Catima.csv</i> heißt, um sie zu importieren.
\nOder erstellen Sie sie aus dem Import/Export-Menü einer anderen Catima-Anwendung, indem Sie dort zuerst Export drücken.</string>
<string name="importCatimaMessage">Wählen Sie Ihren <i>catima.zip</i>-Export aus Catima zum Importieren aus.
\nOder erstellen Sie ihn aus dem Import/Export-Menü einer anderen Catima-Anwendung, indem Sie dort zuerst Export drücken.</string>
<string name="importCatima">Aus Catima importieren</string>
<string name="setBarcodeId">Strichcodewert setzen</string>
<string name="sameAsCardId">Gleich wie Karten-ID</string>
@@ -137,7 +135,7 @@
<string name="noBarcodeFound">Kein Strichcode gefunden</string>
<string name="addFromImage">Bild aus der Galerie auswählen</string>
<string name="settings_max_font_size_scale">Max. Schriftgröße</string>
<string name="unsupportedBarcodeType">Dieser Strichcodetyp kann noch nicht angezeigt werden. Er wird möglicherweise in einer neueren Version der Anwendung unterstützt.</string>
<string name="unsupportedBarcodeType">Dieser Strichcodetyp kann noch nicht angezeigt werden. Er wird möglicherweise in einer späteren Version der Anwendung unterstützt.</string>
<string name="wrongValueForBarcodeType">Der Wert ist für den gewählten Strichcodetyp nicht gültig</string>
<string name="app_resources">Freie Ressourcen von Drittanbietern: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Freie Bibliotheken von Drittanbietern: <xliff:g id="app_libraries_list">%s</xliff:g></string>
@@ -145,9 +143,36 @@
<string name="intent_import_card_from_url_share_multiple_text">Ich möchte Karten mit dir teilen</string>
<string name="copy_to_clipboard_multiple_toast">Kartennummern in die Zwischenablage kopiert</string>
<string name="card_ids_copied">Kartennummer(n) kopiert</string>
<string name="card_selected">"Ausgewählt: "</string>
<string name="no">Nein</string>
<string name="yes">Ja</string>
<string name="updateBarcodeQuestionText">Sie haben die Karten-ID geändert. Möchten Sie auch den Strichcode aktualisieren, um denselben Wert zu verwenden\?</string>
<string name="updateBarcodeQuestionTitle">Strichcodewert aktualisieren\?</string>
<string name="takePhoto">Foto aufnehmen</string>
<string name="removeImage">Bild entfernen</string>
<string name="setBackImage">Rückseitenbild einstellen</string>
<string name="setFrontImage">Vorderseitenbild einstellen</string>
<string name="photos">Fotos</string>
<string name="frontImageDescription">Bild der Kartenvorderseite</string>
<string name="backImageDescription">Bild der Kartenrückseite</string>
<string name="passwordRequired">Bitte geben Sie das Passwort ein</string>
<string name="importStocardMessage">Wählen Sie Ihren <i>***-sync.zip</i>-Export aus Stocard zum Importieren aus, und wählen Sie anschließend die Strichcodetypen manuell aus.
\nOder Sie erhalten ihn, indem Sie eine E-Mail an support@stocardapp.com senden und um einen Export Ihrer Daten bitten.</string>
<string name="importStocard">Von Stocard importieren</string>
<string name="turn_flashlight_off">Taschenlampe ausschalten</string>
<string name="turn_flashlight_on">Taschenlampe einschalten</string>
<string name="failedGeneratingShareURL">Keine teilbare URL konnte generiert werden. Bitte melden Sie dies.</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> Karte ausgewählt</item>
<item quantity="other"><xliff:g>%d</xliff:g> Karten ausgewählt</item>
</plurals>
<string name="deleteTitle">Karte entfernen</string>
<string name="deleteConfirmation">Diese Karte löschen\?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Diese <xliff:g>%d</xliff:g> Karte endgültig löschen\?</item>
<item quantity="other">Diese <xliff:g>%d</xliff:g> Karten endgültig löschen\?</item>
</plurals>
<plurals name="deleteCardsTitle">
<item quantity="one">Karte löschen</item>
<item quantity="other">Karten löschen</item>
</plurals>
</resources>

View File

@@ -12,8 +12,6 @@
<string name="confirm">Επιβεβαίωση</string>
<string name="lockScreen">Αποκλεισμός Περιστροφής</string>
<string name="unlockScreen">Περιστροφή</string>
<string name="deleteTitle">Αφαίρεση Κάρτας</string>
<string name="deleteConfirmation">Παρακαλώ επιβεβαιώστε ότι θέλετε να διαγράψετε αυτή την κάρτα.</string>
<string name="ok">Εντάξει</string>
<string name="copy_to_clipboard">Αντιγραφή κωδικού στο πρόχειρο</string>
<string name="sendLabel">Αποστολή…</string>
@@ -58,4 +56,6 @@
<string name="settings_light_theme">Φωτεινό</string>
<string name="settings_system_theme">Σύστημα</string>
<string name="barcode">Γραμμικός κώδικας</string>
<string name="deleteTitle">Αφαίρεση Κάρτας</string>
<string name="deleteConfirmation">Παρακαλώ επιβεβαιώστε ότι θέλετε να διαγράψετε αυτή την κάρτα.</string>
</resources>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="barcodeNoBarcode">Strekokodo mankas al karto</string>
<string name="delete">Forigi</string>
<string name="noBarcode">Sen strekokodo</string>
<string name="barcodeType">Tipo de strekokodo</string>
<string name="cardId">Identigilo de karto</string>
<string name="settings_category_title_ui">Fasado</string>
<string name="settings">Agordoj</string>
<string name="selectBarcodeTitle">Elekti strekokodon</string>
<string name="debug_version_fmt">Versio: <xliff:g id="version">%s</xliff:g></string>
<string name="about_title_fmt">Pri <xliff:g id="app_name">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Kopirajto © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="importOptionFilesystemButton">El dosiersistemo</string>
<string name="importOptionFilesystemTitle">Enporti el dosiersistemo</string>
<string name="exportFailedTitle">Elportado malsukcesis</string>
<string name="exportSuccessfulTitle">Elportado sukcesis</string>
<string name="importFailedTitle">Enportado malsukcesis</string>
<string name="importSuccessfulTitle">Enportado sukcesis</string>
<string name="exporting">Elportante…</string>
<string name="importing">Enportante…</string>
<string name="exportName">Elporti</string>
<string name="importExport">Enporti/elporti</string>
<string name="addCardTitle">Aldoni karton</string>
<string name="editCardTitle">Redakti karton</string>
<string name="sendLabel">Sendi…</string>
<string name="takePhoto">Foti</string>
<string name="no">Ne</string>
<string name="yes">Jes</string>
<string name="photos">Fotoj</string>
<string name="points">Poentoj</string>
<string name="currency">Valuto</string>
<string name="editBarcode">Redakti strekokodon</string>
<string name="barcode">Strekokodo</string>
<string name="card">Karto</string>
<string name="never">Neniam</string>
<string name="groupsList">Grupoj: <xliff:g>%s</xliff:g></string>
<string name="groups">Grupoj</string>
<string name="settings_dark_theme">Malhela</string>
<string name="settings_light_theme">Hela</string>
<string name="settings_system_theme">Sistema</string>
<string name="settings_theme">Etoso</string>
<string name="about">Pri</string>
<string name="note">Noto</string>
<string name="storeName">Nomo</string>
<string name="confirm">Konfirmi</string>
<string name="ok">Bone</string>
<string name="edit">Redakti</string>
<string name="save">Konservi</string>
<string name="cancel">Nuligi</string>
<string name="action_add">Aldoni</string>
<string name="action_search">Serĉi</string>
<string name="deleteConfirmation">Ĉu forigi ĉi tiun karton\?</string>
<string name="deleteTitle">Forigi karton</string>
</resources>

View File

@@ -12,8 +12,6 @@
<string name="confirm">Confirmar</string>
<string name="lockScreen">Bloquear giro</string>
<string name="unlockScreen">Desbloquear giro</string>
<string name="deleteTitle">Eliminar tarjeta</string>
<string name="deleteConfirmation">¿Quiere eliminar esta tarjeta\?</string>
<string name="ok">Aceptar</string>
<string name="copy_to_clipboard">Copiar id. en portapapeles</string>
<string name="sendLabel">Enviar…</string>
@@ -28,7 +26,7 @@
<string name="noCardExistsError">No se ha podido encontrar la tarjeta</string>
<string name="importExport">Importar/exportar</string>
<string name="exportName">Exportar</string>
<string name="importExportHelp">La copia de respaldo le permite transferir sus tarjetas a otro dispositivo.</string>
<string name="importExportHelp">El respaldo le permite transferir sus tarjetas a otro dispositivo.</string>
<string name="importSuccessfulTitle">Importado</string>
<string name="importFailedTitle">Falló la importación</string>
<string name="importFailed">No se han podido importar tarjetas</string>
@@ -48,18 +46,18 @@
<string name="app_license">Programa libre con «copyleft», disponible en virtud de la licencia GPLv3+.</string>
<string name="about_title_fmt">Acerca de <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">Versión: <xliff:g id="version">%s</xliff:g></string>
<string name="settings">Configuración</string>
<string name="settings">Ajustes</string>
<string name="settings_category_title_ui">Interfaz de usuario</string>
<string name="settings_display_barcode_max_brightness">Iluminar vista del código de barras</string>
<string name="exportSuccessful">Datos de las tarjetas exportados</string>
<string name="importSuccessful">Datos de las tarjetas importados</string>
<string name="intent_import_card_from_url_share_text">Quiero compartirle una tarjeta</string>
<string name="settings_lock_barcode_orientation">Bloquear orientación del código de barras</string>
<string name="intent_import_card_from_url_share_text">Quiero compartirte una tarjeta</string>
<string name="settings_lock_barcode_orientation">Bloquear giro en el código de barras</string>
<string name="settings_dark_theme">Oscuro</string>
<string name="settings_light_theme">Claro</string>
<string name="settings_system_theme">Sistema</string>
<string name="settings_theme">Tema</string>
<string name="enterBarcodeInstructions">Introduzca el identificador de tarjeta y seleccione la imagen que represente el código de barras que se utilizará, o bien, elija «Esta tarjeta no tiene código de barras» para no utilizar ninguno.</string>
<string name="enterBarcodeInstructions">Introduzca el identificador de tarjeta y seleccione el código de barras que se utilizará, o de lo contrario, elija «Esta tarjeta no tiene código de barras».</string>
<string name="app_copyright_old">Basado en Loyalty Card Keychain
\nderechos de autor © 2016-2020 de Branden Archer.</string>
<string name="exportOptionExplanation">Los datos se guardarán en la ubicación que elija.</string>
@@ -73,7 +71,7 @@
<string name="noGroups">Primero pulse en el botón «+» para añadir grupos de categorización.</string>
<string name="starImage">Favorito</string>
<string name="thumbnailDescription">Miniatura de la tarjeta</string>
<string name="copy_to_clipboard_toast">Se copió el identificador de tarjeta en el portapapeles</string>
<string name="copy_to_clipboard_toast">Se copió la id. de tarjeta en el portapapeles</string>
<string name="selectBarcodeTitle">Seleccionar el código de barras</string>
<string name="unstar">Eliminar de favoritos</string>
<string name="noBarcode">Sin código de barras</string>
@@ -94,8 +92,8 @@
<item quantity="other"><xliff:g>%d</xliff:g> tarjetas</item>
</plurals>
<string name="points">Puntos</string>
<string name="moveBarcodeToCenterOfScreen">Centrar el código de barras en la pantalla</string>
<string name="moveBarcodeToTopOfScreen">Mover el código de barras a la zona superior de la pantalla</string>
<string name="moveBarcodeToCenterOfScreen">Centre el código de barras en la pantalla</string>
<string name="moveBarcodeToTopOfScreen">Mueva el código de barras a la zona superior de la pantalla</string>
<string name="chooseExpiryDate">Elegir fecha de caducidad</string>
<string name="never">Nunca</string>
<string name="expiryDate">Fecha de caducidad</string>
@@ -108,4 +106,73 @@
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Derechos de autor © 2019-<xliff:g>%d</xliff:g> de Sylvia van Os.</string>
<string name="app_resources">Recursos de terceros libres: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliotecas de terceros libres: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="settings_max_font_size_scale">Tam. de fuente máximo</string>
<string name="importCatimaMessage">Seleccione su <i>catima.zip</i> exportado desde Catima para importarlo.
\nO créalo primero desde el menú Importar/Exportar de otra app de Catima al presionar Exportar desde allí.</string>
<string name="importFidmeMessage">Seleccione su <i>fidme-export-request-xxxxxx.zip</i> exportado desde FidMe para importar, y luego escoja los tipos de códigos de barras manualmente.
\nO créalo primero desde tu perfil de FidMe eligiendo Protección de datos y pulsa Extraer mis datos.</string>
<string name="importLoyaltyCardKeychainMessage">Seleccione su <i>LoyaltyCardKeychain.csv</i> exportado desde Loyalty Card Keychain para importarlo.
\nO créalo primero desde el menú Importar/Exportar en Loyalty Card Keychain pulsando Exportar desde allí.</string>
<string name="importStocardMessage">Seleccione su <i>*-sync.zip</i> exportado de Stocard para importarla, y selecciona los tipos de códigos de barras manualmente después.
\nO consígalo enviando un correo electrónico a support@stocardapp.com solicitando una exportación de sus datos.</string>
<string name="importVoucherVaultMessage">Seleccione su <i>vouchervault.json</i> exportado desde Voucher Vault para importarlo.
\nO créalo pulsando primero Exportar en Voucher Vault.</string>
<string name="failedGeneratingShareURL">No se ha podido generar una URL compartible. Por favor, informe de ello.</string>
<string name="passwordRequired">Por favor, introduzca la contraseña</string>
<string name="updateBarcodeQuestionText">Ha cambiado la Id. de la tarjeta. ¿Desea actualizar también el código de barras para usar el mismo valor\?</string>
<string name="intent_import_card_from_url_share_multiple_text">Quiero compartirte algunas tarjetas</string>
<string name="setBackImage">Establecer imagen anversa</string>
<string name="card_ids_copied">Id. de tarjetas copiadas</string>
<string name="turn_flashlight_off">Apagar linterna</string>
<string name="turn_flashlight_on">Encender linterna</string>
<string name="no">No</string>
<string name="yes"></string>
<string name="updateBarcodeQuestionTitle">¿Actualizar valor de código de barras\?</string>
<string name="takePhoto">Tomar una foto</string>
<string name="removeImage">Quitar imagen</string>
<string name="setFrontImage">Establecer imagen frontal</string>
<string name="photos">Fotos</string>
<string name="backImageDescription">Imagen del reverso de la tarjeta</string>
<string name="frontImageDescription">Imagen frontal de la tarjeta</string>
<string name="copy_to_clipboard_multiple_toast">ID de tarjetas copiadas al portapapeles</string>
<string name="wrongValueForBarcodeType">El valor no es válido para el tipo de código de barras seleccionado</string>
<string name="unsupportedBarcodeType">Este tipo de código de barras todavía no se puede visualizar. Es posible que se admita en una futura versión de la aplicación.</string>
<string name="setBarcodeId">Establecer valor de código de barra</string>
<string name="sameAsCardId">Igual que la ID de tarjeta</string>
<string name="barcodeId">Valor de código de barra</string>
<string name="importVoucherVault">Importar desde Voucher Vault</string>
<string name="importStocard">Importar desde Stocard</string>
<string name="importLoyaltyCardKeychain">Importar desde Loyalty Card Keychain</string>
<string name="importFidme">Importar desde FidMe</string>
<string name="importCatima">Importar desde Catima</string>
<string name="accept">Aceptar</string>
<string name="privacy_policy_popup_text">Aviso de política de privacidad (requerido por algunas tiendas de apps):
\n
\nNINGÚN DATO SE RECOPILA, cualquiera puede confirmar ya que nuestra aplicación es software libre.</string>
<string name="privacy_policy">Política de privacidad</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="chooseImportType">¿De dónde importar datos\?</string>
<string name="parsingBalanceFailed"><xliff:g>%s</xliff:g> no tendría un saldo válido.</string>
<string name="currency">Moneda</string>
<string name="balance">Saldo</string>
<string name="errorReadingImage">No se pudo leer la imagen</string>
<string name="noBarcodeFound">No se encontró código de barras</string>
<string name="balanceSentence">Saldo: <xliff:g>%s</xliff:g></string>
<string name="addFromImage">Selecciona imagen desde galería</string>
<string name="settings_disable_lockscreen_while_viewing_card">Evitar bloqueo de pantalla</string>
<string name="settings_keep_screen_on">Mantener pantalla encendida</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> tarjeta seleccionada</item>
<item quantity="other"><xliff:g>%d</xliff:g> tarjetas seleccionadas</item>
</plurals>
<string name="deleteTitle">Eliminar tarjeta</string>
<string name="deleteConfirmation">¿Quiere eliminar esta tarjeta\?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">¿Borrar esta tarjeta <xliff:g>%d</xliff:g> permanentemente\?</item>
<item quantity="other">¿Borrar estas tarjetas <xliff:g>%d</xliff:g> permanentemente\?</item>
</plurals>
<plurals name="deleteCardsTitle">
<item quantity="one">Borrar la tarjeta</item>
<item quantity="other">Borrar las tarjetas</item>
</plurals>
</resources>

View File

@@ -22,14 +22,14 @@
\nTai luo se Tuo/Vie toiminnolla FidMe profiilistasi, valitsemalla Tietotosuoja ja sitten valitsemalla Vie tietoni.</string>
<string name="importFidme">Tuo FidMe varmuuskopiotiedostosta</string>
<string name="importCatimaMessage">Etsi tiedostoa joka on todennäköisesti nimetty nimellä <i>Catima.csv</i> tuotavaksi.
\nTai luo se Tuo/Vie toiminnolla toisen puhelimen Catima sovelluksessa, valitsemalla valikosta Vie.</string>
\nTai luo se Tuo/Vie toiminnolla Catima sovelluksessa, valitsemalla valikosta Vie.</string>
<string name="importCatima">Tuo Catima varmuuskopiotiedostosta</string>
<string name="accept">Hyväksy</string>
<string name="privacy_policy_popup_text">Tietosuojaseloste (joidenkin sovelluskauppojen vaatimus):
\n
\nMITÄÄN TIETOJA EI KERÄTÄ LAINKAAN, minkä kuka tahansa voi vahvistaa, koska sovelluksemma on vapaa ohjelmisto.</string>
<string name="privacy_policy">Tietosuojakäytäntö</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain varmuuskopiotiedostosta</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="chooseImportType">Tuo tietoja kohteesta\?</string>
<string name="parsingBalanceFailed"><xliff:g>%s</xliff:g> ei vaikuta olevan kelvollinen saldo.</string>
<string name="points">Pisteet</string>
@@ -128,8 +128,6 @@
<string name="share">Jaa</string>
<string name="copy_to_clipboard">Kopioi ID-tunnus leikepöydälle</string>
<string name="ok">OK</string>
<string name="deleteConfirmation">Poista tämä kortti\?</string>
<string name="deleteTitle">Poista kortti</string>
<string name="unlockScreen">Poista kierron esto</string>
<string name="lockScreen">Estä kierto</string>
<string name="confirm">Vahvista</string>
@@ -147,7 +145,15 @@
<string name="storeName">Nimi</string>
<string name="noMatchingGiftCards">Ei hakutuloksia, kokeile toisella hakutermillä.</string>
<string name="noGiftCards">Lisää ensin kortti napsauttamalla + plus-painiketta, tai mene ⋮ valikkoon tuodaksesi varmuuskopiosta.</string>
<string name="card_selected">"Valittu: "</string>
<string name="action_add">Lisää</string>
<string name="action_search">Hae</string>
<string name="takePhoto">Ota valokuva</string>
<string name="removeImage">Poista kuva</string>
<string name="setBackImage">Aseta takakuva</string>
<string name="setFrontImage">Aseta etukuva</string>
<string name="photos">Valokuvat</string>
<string name="backImageDescription">Kortin takakuva</string>
<string name="frontImageDescription">Kortin etukuva</string>
<string name="deleteConfirmation">Poista tämä kortti\?</string>
<string name="deleteTitle">Poista kortti</string>
</resources>

View File

@@ -12,8 +12,6 @@
<string name="confirm">Confirmer</string>
<string name="lockScreen">Désactiver la rotation</string>
<string name="unlockScreen">Activer la rotation</string>
<string name="deleteTitle">Supprimer la carte de fidélité</string>
<string name="deleteConfirmation">Supprimer cette carte \?</string>
<string name="ok">OK</string>
<string name="copy_to_clipboard">Copier le numéro dans le presse-papier</string>
<string name="sendLabel">Envoyer…</string>
@@ -100,7 +98,7 @@
<string name="privacy_policy">Politique de confidentialité</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="chooseImportType">Importer les données depuis \?</string>
<string name="parsingBalanceFailed">&lt;xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\"&gt;%s&lt;/xliff:g&gt; ne semble pas être un solde valide.</string>
<string name="parsingBalanceFailed"><xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">%s</xliff:g> ne semble pas être un solde valide.</string>
<string name="points">Points</string>
<string name="currency">Monnaie</string>
<string name="balance">Solde</string>
@@ -112,23 +110,23 @@
<string name="editBarcode">Modifier le code-barres</string>
<string name="barcode">Code-barres</string>
<string name="card">Carte</string>
<string name="balancePoints">&lt;xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\"&gt;%s&lt;/xliff:g&gt; points</string>
<string name="balanceSentence">Solde : &lt;xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\"&gt;%s&lt;/xliff:g&gt;</string>
<string name="expiryStateSentenceExpired">Expiré : &lt;xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\"&gt;%s&lt;/xliff:g&gt;</string>
<string name="expiryStateSentence">Expire : &lt;xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\"&gt;%s&lt;/xliff:g&gt;</string>
<string name="balancePoints"><xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">%s</xliff:g> points</string>
<string name="balanceSentence">Solde : <xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Expiré : <xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">%s</xliff:g></string>
<string name="expiryStateSentence">Expire : <xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">%s</xliff:g></string>
<string name="settings_disable_lockscreen_while_viewing_card">Empêcher le verrouillage de lécran</string>
<string name="settings_keep_screen_on">Garder lécran allumé</string>
<string name="importVoucherVaultMessage">Trouvez un fichier probablement nommé <i>vouchervault.json</i> à importer.
\nOu créez-le en appuyant dabord sur Exporter dans Voucher Vault.</string>
<string name="importVoucherVaultMessage">Sélectionnez votre exportation <i>vouchervault.json</i> de Voucher Vault à importer.
\nOu créez-la en appuyant dabord sur Exporter dans Voucher Vault.</string>
<string name="importVoucherVault">Importer depuis Voucher Vault</string>
<string name="importLoyaltyCardKeychainMessage">Trouvez un fichier probablement nommé <i>LoyaltyCardKeychain.csv</i> à importer.
\nOu créez-le à partir du menu Importer/Exporter du Loyalty Card Keychain en appuyant dabord sur Exporter.</string>
<string name="importLoyaltyCardKeychainMessage">Sélectionnez votre exportation <i>LoyaltyCardKeychain.csv</i> à partir de Loyalty Card Keychain pour limporter.
\nOu créez-la à partir du menu Importer/Exporter du Loyalty Card Keychain en appuyant dabord sur Exporter.</string>
<string name="importLoyaltyCardKeychain">Importer depuis Loyalty Card Keychain</string>
<string name="importFidmeMessage">Trouvez un fichier probablement nommé <i>fidme-export-request-xxxxxx.zip</i> pour limporter, et sélectionnez ensuite manuellement les types de codes-barres.
\nVous pouvez aussi le créer à partir de votre profil FidMe en choisissant Protection des données, puis en cliquant sur Extraire mes données.</string>
<string name="importFidmeMessage">Sélectionnez votre exportation <i>fidme-export-request-xxxxxx.zip</i> de FidMe pour limporter, puis sélectionnez manuellement les types de codes-barres.
\nOu créez-la à partir de votre profil FidMe en choisissant Protection des données, puis en cliquant sur Extraire mes données dabord.</string>
<string name="importFidme">Importer depuis FidMe</string>
<string name="importCatimaMessage">Trouvez un fichier probablement nommé <i>Catima.csv</i> à importer.
\nOu créez-le à partir du menu Importer/Exporter dune autre application Catima en appuyant dabord sur Exporter.</string>
<string name="importCatimaMessage">Sélectionnez votre exportation <i>catima.zip</i> depuis Catima à importer.
\nOu créez-la à partir du menu Importer/Exporter dune autre application Catima en appuyant dabord sur Exporter.</string>
<string name="importCatima">Importer depuis Catima</string>
<string name="addFromImage">Sélectionner dans la galerie</string>
<string name="errorReadingImage">Impossible de lire l\'image</string>
@@ -137,17 +135,40 @@
<string name="sameAsCardId">Identique à lidentifiant de la carte</string>
<string name="barcodeId">Valeur du code-barres</string>
<string name="settings_max_font_size_scale">Taille max. de la police</string>
<string name="unsupportedBarcodeType">Ce type de code-barres ne peut pas encore être affiché. Il sera peut-être pris en charge dans une version plus récente de l\'application.</string>
<string name="unsupportedBarcodeType">Ce type de code-barres ne peut pas encore être affiché. Il sera peut-être pris en charge dans une version ultérieure de lapplication.</string>
<string name="wrongValueForBarcodeType">La valeur n\'est pas valide pour le type de code-barres sélectionné</string>
<string name="app_resources">Ressources libres tierces : <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliothèques libres tierces : <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Ressources tierces libres : <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Bibliothèques tierces libres : <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="intent_import_card_from_url_share_multiple_text">Je veux partager des cartes avec vous</string>
<string name="copy_to_clipboard_multiple_toast">Nméros des cartes copiés dans le presse-papier</string>
<string name="copy_to_clipboard_multiple_toast">Identifiants des cartes copiés dans le presse-papiers</string>
<string name="card_ids_copied">Num. de la carte copié(s)</string>
<string name="card_selected">"Sélectionnée : "</string>
<string name="updateBarcodeQuestionText">Vous avez changé l\'identifiant de la carte. Voulez-vous également mettre à jour le code-barres pour utiliser la même valeur \?</string>
<string name="no">Non</string>
<string name="yes">Oui</string>
<string name="updateBarcodeQuestionTitle">Mettre à jour la valeur du code-barres \?</string>
</resources>
<string name="takePhoto">Prendre une photo</string>
<string name="removeImage">Retirer limage</string>
<string name="setBackImage">Définir limage verso</string>
<string name="setFrontImage">Définir limage recto</string>
<string name="photos">Photos</string>
<string name="backImageDescription">Image verso de la carte</string>
<string name="frontImageDescription">Image recto de la carte</string>
<string name="passwordRequired">Veuillez entrer le mot de passe</string>
<string name="importStocardMessage">Sélectionnez votre exportation <i>***-sync.zip</i> de Stocard pour limporter, et sélectionnez les types de codes-barres manuellement par la suite.
\nVous pouvez aussi lobtenir en envoyant un courriel à support@stocardapp.com pour demander une exportation de vos données.</string>
<string name="importStocard">Importer depuis Stocard</string>
<string name="turn_flashlight_off">Éteindre la lampe de poche</string>
<string name="turn_flashlight_on">Allumer la lampe de poche</string>
<string name="failedGeneratingShareURL">Impossible de générer une URL partageable. Veuillez signaler ceci.</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> carte sélectionnée</item>
<item quantity="other"><xliff:g>%d</xliff:g> cartes sélectionnées</item>
</plurals>
<string name="deleteTitle">Supprimer la carte de fidélité</string>
<string name="deleteConfirmation">Supprimer cette carte \?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Supprimer cette <xliff:g>%d</xliff:g> carte définitivement \?</item>
<item quantity="other">Supprimer ces <xliff:g>%d</xliff:g> cartes définitivement \?</item>
</plurals>
</resources>

View File

@@ -15,8 +15,6 @@
<string name="confirm">Conferma</string>
<string name="lockScreen">Blocca rotazione</string>
<string name="unlockScreen">Sblocca rotazione</string>
<string name="deleteTitle">Rimuovi carta fedeltà</string>
<string name="deleteConfirmation">Eliminare questa carta\?</string>
<string name="ok">OK</string>
<string name="copy_to_clipboard">Copia ID negli appunti</string>
<string name="share">Condividi</string>
@@ -93,7 +91,7 @@
<item quantity="one"><xliff:g>%d</xliff:g> carta</item>
<item quantity="other"><xliff:g>%d</xliff:g> carte</item>
</plurals>
<string name="parsingBalanceFailed">&lt;xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\"&gt;%s&lt;/xliff:g&gt; non sembra un saldo corretto.</string>
<string name="parsingBalanceFailed"><xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">%s</xliff:g> non sembra un saldo corretto.</string>
<string name="points">Punti</string>
<string name="currency">Valuta</string>
<string name="balance">Saldo</string>
@@ -118,17 +116,17 @@
\n
\nNESSUN DATO VIENE RACCOLTO, cosa che chiunque può confermare dato che la nostra applicazione è un software libero.</string>
<string name="privacy_policy">Informativa sulla riservatezza</string>
<string name="importVoucherVaultMessage">Trova un file probabilmente chiamato <i>vouchervault.json</i> da importare.
<string name="importVoucherVaultMessage">Seleziona la tua esportazione <i>vouchervault.json</i> da Voucher Vault da importare.
\nOppure crealo premendo prima Esporta in Voucher Vault.</string>
<string name="importVoucherVault">Importa da Voucher Vault</string>
<string name="importLoyaltyCardKeychainMessage">Trova un file probabilmente chiamato <i>LoyaltyCardKeychain.csv</i> da importare.
\nOppure crealo dal menù Importa/Esporta nel Loyalty Card Keychain premendo prima Esporta.</string>
<string name="importLoyaltyCardKeychainMessage">Seleziona la tua esportazione <i>LoyaltyCardKeychain.csv</i> da Loyalty Card Keychain per importarla.
\nOppure createlo dal menù Importazione/Esportazione in Loyalty Card Keychain premendo prima su Esporta.</string>
<string name="importLoyaltyCardKeychain">Importa da Loyalty Card Keychain</string>
<string name="importFidmeMessage">Trova un file probabilmente chiamato <i>fidme-export-request-xxxxxx.zip</i> da importare, e poi seleziona i tipi di codice a barre manualmente in seguito.
\nOppure crearlo dal tuo profilo FidMe scegliendo Protezione dati e poi premendo Estrai i miei dati prima.</string>
<string name="importFidmeMessage">Seleziona la tua esportazione <i>fidme-export-request-xxxxxx.zip</i> da FidMe per importare, e seleziona i tipi di codice a barre manualmente dopo.
\nOppure crearlo dal tuo profilo FidMe scegliendo Protezione Dati e poi premendo Estrai i miei dati prima.</string>
<string name="importFidme">Importa da FidMe</string>
<string name="importCatimaMessage">Trova un file probabilmente chiamato <i>Catima.csv</i> da importare.
\nOppure crealo dal menù Importa/Esporta di unaltra applicazione Catima premendo prima Esporta.</string>
<string name="importCatimaMessage">Seleziona la tua esportazione <i>catima.zip</i> da Catima per importarla.
\nOppure crealo dal menù Importazione/Esportazione di un\'altra applicazione Catima premendo prima Esporta.</string>
<string name="importCatima">Importa da Catima</string>
<string name="setBarcodeId">Imposta il valore del codice a barre</string>
<string name="sameAsCardId">Uguale all\'ID della carta</string>
@@ -137,7 +135,7 @@
<string name="noBarcodeFound">Nessun codice a barre trovato</string>
<string name="addFromImage">Seleziona immagine dalla galleria</string>
<string name="settings_max_font_size_scale">Dimensione mass. caratteri</string>
<string name="unsupportedBarcodeType">Questo tipo di codice a barre non può ancora essere visualizzato. Potrebbe essere supportato in una versione più recente dell\'applicazione.</string>
<string name="unsupportedBarcodeType">Questo tipo di codice a barre non può ancora essere visualizzato. Potrebbe essere supportato in una versione successiva dell\'applicazione.</string>
<string name="wrongValueForBarcodeType">Il valore non è valido per il tipo di codice a barre selezionato</string>
<string name="app_resources">Risorse libre di terze parti: <xliff:g id="app_resources_list"> %s </xliff:g></string>
<string name="app_libraries">Librerie libre di terze parti: <xliff:g id="app_libraries_list"> %s </xliff:g></string>
@@ -145,9 +143,36 @@
<string name="intent_import_card_from_url_share_multiple_text">Voglio condividere alcune carte con te</string>
<string name="copy_to_clipboard_multiple_toast">Numeri delle carte copiati negli appunti</string>
<string name="card_ids_copied">Numero/i della carta copiato/i</string>
<string name="card_selected">"Selezionata: "</string>
<string name="no">No</string>
<string name="yes"></string>
<string name="updateBarcodeQuestionText">Hai cambiato l\'ID della carta. Vuoi anche aggiornare il codice a barre per usare lo stesso valore\?</string>
<string name="updateBarcodeQuestionTitle">Aggiornare il valore del codice a barre\?</string>
<string name="takePhoto">Scatta una foto</string>
<string name="removeImage">Rimuovi limmagine</string>
<string name="setBackImage">Imposta immagine posteriore</string>
<string name="setFrontImage">Imposta immagine frontale</string>
<string name="photos">Foto</string>
<string name="backImageDescription">Immagine posteriore della carta</string>
<string name="frontImageDescription">Immagine frontale della carta</string>
<string name="passwordRequired">Si prega di inserire la password</string>
<string name="importStocardMessage">Seleziona la tua esportazione <i>***-sync.zip</i> da Stocard per importare, e seleziona i tipi di codice a barre manualmente dopo.
\nOppure ottenerlo inviando un\'e-mail a support@stocardapp.com chiedendo un\'esportazione dei tuoi dati.</string>
<string name="importStocard">Importa da Stocard</string>
<string name="turn_flashlight_off">Spegni la torcia</string>
<string name="turn_flashlight_on">Accendi la torcia</string>
<string name="failedGeneratingShareURL">Impossibile generare un URL condivisibile. Si prega di segnalarlo.</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> carta selezionata</item>
<item quantity="other"><xliff:g>%d</xliff:g> carte selezionate</item>
</plurals>
<string name="deleteTitle">Rimuovi carta fedeltà</string>
<string name="deleteConfirmation">Eliminare questa carta\?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Eliminare definitivamente questa scheda <xliff:g>%d</xliff:g>\?</item>
<item quantity="other">Eliminare definitivamente queste schede <xliff:g>%d</xliff:g>\?</item>
</plurals>
<plurals name="deleteCardsTitle">
<item quantity="one">Elimina la carta</item>
<item quantity="other">Elimina le carte</item>
</plurals>
</resources>

View File

@@ -3,15 +3,15 @@
<string name="wrongValueForBarcodeType">選択したバーコード形式ではこの番号は使用できません</string>
<string name="unsupportedBarcodeType">このバーコード形式は表示できません。将来のアップデートにより対応するかもしれません。</string>
<string name="setBarcodeId">バーコード番号を設定</string>
<string name="importLoyaltyCardKeychainMessage">インポートするには <i>LoyaltyCardKeychain.csv</i> のような名前のファイルを選択してください。
\nまたは、あらかじめ Loyalty Card Keychainアプリからファイルをエクスポートしてください。</string>
<string name="importLoyaltyCardKeychain">Loyalty Card Keychain からインポート</string>
<string name="importFidmeMessage">インポートするには <i>fidme-export-request-xxxxxx.zip</i> のような名前のファイルを選択してください。そのあと手動でバーコード形式を選択してください。
\nまたは 、あらかじめFidMeからファイルを作成してください。</string>
<string name="importFidme">FidMe からインポート</string>
<string name="importCatimaMessage">インポートするには <i>Catima.csv</i> のような名前のファイルを選択してください。
\nまたは、あらかじめ他のCatima アプリからファイルをエクスポートしてください。</string>
<string name="importCatima">Catima からインポート</string>
<string name="importLoyaltyCardKeychainMessage">インポートするにはLoyalty Card Keychainでエクスポートした <i>LoyaltyCardKeychain.csv</i>ファイルを選択してください。
\nファイルがない場合、 Loyalty Card Keychainアプリからファイルをエクスポートしてください。</string>
<string name="importLoyaltyCardKeychain">Loyalty Card Keychainからインポート</string>
<string name="importFidmeMessage">インポートするにはFindMeでエクスポートした <i>fidme-export-request-xxxxxx.zip</i>ファイルを選択してください。そのあと手動でバーコード形式を選択してください。
\nファイルがない場合、FidMeファイルを作成してください。</string>
<string name="importFidme">FidMeからインポート</string>
<string name="importCatimaMessage">インポートするにはCatimaでエクスポートした<i>Catima.zip</i>ファイルを選択してください。
\nファイルがない場合、他のCatimaアプリファイルをエクスポートしてください。</string>
<string name="importCatima">Catimaからインポート</string>
<string name="accept">承認</string>
<string name="privacy_policy_popup_text">プライバシーポリシーの案内:
\n
@@ -35,7 +35,7 @@
<string name="card">カード</string>
<string name="balancePoints"><xliff:g>%s</xliff:g> ポイント</string>
<string name="balanceSentence">残高: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">期限: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">期限切れ: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">期限: <xliff:g>%s</xliff:g></string>
<string name="groupsList">グループ: <xliff:g>%s</xliff:g></string>
<string name="addFromImage">ギャラリーから画像を選択</string>
@@ -55,7 +55,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_lock_barcode_orientation">バーコード表示画面を自動回転しない</string>
@@ -65,14 +65,14 @@
<string name="settings_light_theme">ライト</string>
<string name="settings_system_theme">システムに従う</string>
<string name="settings_theme">テーマ</string>
<string name="settings_category_title_ui">ユーザーインターフェイス</string>
<string name="settings_category_title_ui">外観</string>
<string name="settings">設定</string>
<string name="starImage">お気に入りのスター</string>
<string name="thumbnailDescription">カードのサムネイル</string>
<string name="copy_to_clipboard_toast">カード番号をクリップボードにコピーしました</string>
<string name="enterBarcodeInstructions">カード番号を入力し、バーコード形式を選択してください。</string>
<string name="selectBarcodeTitle">バーコード選択</string>
<string name="app_libraries">Third-party libre libraries: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_libraries">Libre third-party libraries: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_revision_fmt">Revision Info: <xliff:g id="app_revision_url">%s</xliff:g></string>
<string name="debug_version_fmt">Version: <xliff:g id="version">%s</xliff:g></string>
<string name="about_title_fmt">About <xliff:g id="app_name">%s</xliff:g></string>
@@ -80,7 +80,7 @@
<string name="app_copyright_old">Based on Loyalty Card Keychain
\ncopyright © 20162020 Branden Archer.</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Copyright © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="app_resources">Third-party libre resources: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_resources">Libre third-party resources: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="about">Catimaについて</string>
<string name="importOptionApplicationButton">外部のアプリを使う</string>
<string name="importOptionApplicationExplanation">任意のアプリやお気に入りのファイルマネージャーからファイルを開く。</string>
@@ -93,17 +93,17 @@
<string name="exporting">エクスポート中…</string>
<string name="importing">インポート中…</string>
<string name="exportFailed">カードをエクスポートできませんでした</string>
<string name="exportFailedTitle">エクスポート失敗</string>
<string name="exportFailedTitle">エクスポート失敗しました</string>
<string name="exportSuccessfulTitle">エクスポートしました</string>
<string name="sameAsCardId">カードの表記と同一</string>
<string name="barcodeId">バーコード番号</string>
<string name="importVoucherVaultMessage">インポートするには <i>vouchervault.json</i> のような名前のファイルを選択してください。
\nまたは、あらかじめVoucher Vault でファイルを作成してください。</string>
<string name="importVoucherVault">Voucher Vault からインポート</string>
<string name="importVoucherVaultMessage">Voucher Vaultでエクスポートした <i>vouchervault.json</i>ファイルを選択してください。
\nファイルがない場合、Voucher Vaultでファイルをエクスポートしてください。</string>
<string name="importVoucherVault">Voucher Vaultからインポート</string>
<string name="importFailed">カードをインポートできません</string>
<string name="importFailedTitle">インポート失敗</string>
<string name="importFailedTitle">インポート失敗しました</string>
<string name="importSuccessfulTitle">インポートしました</string>
<string name="importExportHelp">カードをバックアップすると、カードを他のデバイスに移すことができます。</string>
<string name="importExportHelp">カードをバックアップすると、他のデバイスにカードを移すことができます。</string>
<string name="exportName">エクスポート</string>
<string name="importExport">インポート/エクスポート</string>
<string name="failedParsingImportUriError">インポートURIを解析できません</string>
@@ -118,10 +118,8 @@
<string name="editCardTitle">カードの編集</string>
<string name="sendLabel">送信先を選択…</string>
<string name="share">共有</string>
<string name="copy_to_clipboard">カード番号をクリップボードにコピー</string>
<string name="ok">はい</string>
<string name="deleteConfirmation">選択したカードを削除しますか?</string>
<string name="deleteTitle">カードを削除</string>
<string name="copy_to_clipboard">カード番号をクリップボードにコピーする</string>
<string name="ok">確定</string>
<string name="unlockScreen">自動回転を無効にしない</string>
<string name="lockScreen">自動回転を無効にする</string>
<string name="confirm">確認</string>
@@ -133,7 +131,7 @@
<string name="star">お気に入りに追加</string>
<string name="noBarcode">バーコードなし</string>
<string name="barcodeNoBarcode">バーコード指定なし</string>
<string name="barcodeType">形式</string>
<string name="barcodeType">バーコード形式</string>
<string name="cardId">カード番号</string>
<string name="note">メモ</string>
<string name="storeName">名前</string>
@@ -141,8 +139,30 @@
<string name="noGiftCards">まず初めに+ボタンを押してカードを追加するか、メニューから以前のカードをインポートしてください。</string>
<string name="action_add">追加</string>
<string name="action_search">検索</string>
<string name="intent_import_card_from_url_share_multiple_text">あなたとカードを共有したいです</string>
<string name="intent_import_card_from_url_share_multiple_text">カードを共有しましょう</string>
<string name="copy_to_clipboard_multiple_toast">カード番号をクリップボードにコピーしました</string>
<string name="card_ids_copied">コピーしたカード</string>
<string name="card_selected">"選択: "</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>
<string name="yes">はい</string>
<string name="updateBarcodeQuestionText">カード番号を変更しました。バーコード番号も同じ値に変更しますか?</string>
<string name="updateBarcodeQuestionTitle">バーコードの番号を変更しますか?</string>
<string name="takePhoto">写真を撮影する</string>
<string name="removeImage">画像を削除</string>
<string name="setBackImage">裏面の画像を設定</string>
<string name="setFrontImage">表面の画像を設定</string>
<string name="photos">フォト</string>
<string name="backImageDescription">裏面</string>
<string name="frontImageDescription">表面</string>
<string name="importStocardMessage">Stocardでエクスポートした<i>***-sync.zip</i>ファイルを選択し、手動でバーコード形式を選択してください。
\nファイルがない場合、e-mailing support@stocardapp.comにデータのエクスポートを要求してください。</string>
<string name="importStocard">Stocardからインポート</string>
<plurals name="selectedCardCount">
<item quantity="other">選択済み: <xliff:g>%d</xliff:g></item>
</plurals>
<string name="deleteConfirmation">このカードを削除しますか?</string>
<string name="deleteTitle">カードの削除</string>
</resources>

View File

@@ -15,7 +15,7 @@
<string name="settings_category_title_ui">사용자 인터페이스</string>
<string name="settings">설정</string>
<string name="enterBarcodeInstructions">카드 ID를 입력하고 카드에서 사용하는 바코드 이미지를 선택하세요. 바코드를 사용하지 않는 경우 “이 카드는 바코드가 없음”을 선택하세요.</string>
<string name="app_revision_fmt">리비전 정보: &lt;xliff:g xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\" id=\"app_revision_url\"&gt;%s&lt;/xliff:g&gt;</string>
<string name="app_revision_fmt">리비전 정보: <xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" id="app_revision_url">%s</xliff:g></string>
<string name="selectBarcodeTitle">바코드 선택</string>
<string name="about">정보</string>
<string name="exporting">내보내는 중…</string>
@@ -31,8 +31,6 @@
<string name="share">공유</string>
<string name="copy_to_clipboard">ID를 클립보드에 복사</string>
<string name="ok">확인</string>
<string name="deleteConfirmation">정말 이 카드를 삭제하시겠습니까\?</string>
<string name="deleteTitle">카드 제거</string>
<string name="unlockScreen">회전 잠금 해제</string>
<string name="lockScreen">회전 잠금</string>
<string name="confirm">확인</string>
@@ -78,4 +76,6 @@
<string name="starImage">즐겨찾기 별</string>
<string name="settings_display_barcode_max_brightness">바코드를 표시할 때 화면 밝기 높이기</string>
<string name="barcode">바코드</string>
</resources>
<string name="deleteConfirmation">정말 이 카드를 삭제하시겠습니까\?</string>
<string name="deleteTitle">카드 제거</string>
</resources>

View File

@@ -1,10 +1,8 @@
<resources
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Pridėti</string>
<string name="noGiftCards">Šiuo metu neturite nė vienos įvestos lojalumo kortelės. Paspauskite "+" (pliuso) pliuso mygtuką, kad pradėtumėte.\n\nLoyalty Card Locker leidžia Jums visada nešiotis lojalumo kortelių informaciją savo telefone ar planšetėje, taip jos visada pasiekiamos.</string>
<string name="storeName">Parduotuvė</string>
<string name="noGiftCards">Norėdami pridėti kortelę, spustelėkite mygtuką + plius arba pirmiausia importuokite kortelę iš ⋮ meniu.</string>
<string name="storeName">Pavadinimas</string>
<string name="note">Užrašas</string>
<string name="cardId">Kortelės ID</string>
<string name="cancel">Atšaukti</string>
@@ -12,30 +10,163 @@
<string name="edit">Redaguoti</string>
<string name="delete">Ištrinti</string>
<string name="confirm">Patvirtinti</string>
<string name="deleteTitle">Panaikinti lojalumo kortelę</string>
<string name="deleteConfirmation">Prašome patvirtinti jog Jūs norite panaikinti šią lojalumo kortelę.</string>
<string name="ok">Gerai</string>
<string name="copy_to_clipboard">Nukopijuoti ID į iškarpinę</string>
<string name="editCardTitle">Redaguoti lojalumo kortelę</string>
<string name="addCardTitle">Pridėti lojalumo kortelę</string>
<string name="scanCardBarcode">Nuskanuokite kortelės brūkšninį kodą</string>
<string name="barcodeImageDescription">Kortelės brūkšninio kodo paveikslėlis</string>
<string name="noStoreError">Parduotuvė neįvesta</string>
<string name="scanCardBarcode">Skenuoti kortelės brūkšninį kodą</string>
<string name="barcodeImageDescription">Kortelės brūkšninio kodo vaizdas</string>
<string name="noStoreError">Neįvestas pavadinimas</string>
<string name="noCardIdError">Neįvestas kortelės ID</string>
<string name="importExport">Importuoti/Exportuoti</string>
<string name="exportName">Exportuoti</string>
<string name="importFailed">Nepavyko importuoti</string>
<string name="exportFailed">Nepavyko eksportuoti</string>
<string name="importing">Importuoja&#8230;</string>
<string name="exporting">Eksportuoja&#8230;</string>
<string name="noExternalStoragePermissionError">Negalima importuoti/eksportuoti kortelių be išorinės atminties leidimo</string>
<string name="importFailed">Nepavyko importuoti kortelių</string>
<string name="exportFailed">Nepavyko eksportuoti kortelių</string>
<string name="importing">Importuoja</string>
<string name="exporting">Eksportuoja</string>
<string name="noExternalStoragePermissionError">Pirmiausia suteikite išorinės saugyklos leidimą, kad galėtumėte importuoti arba eksportuoti korteles</string>
<string name="about">Apie</string>
<string name="app_license">Licenzijuota pagal GPLv3.</string>
<string name="app_license">Copylefted libre programinė įranga, licencijuota GPLv3+.</string>
<string name="about_title_fmt">Apie <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">Versija: <xliff:g id="version">%s</xliff:g></string>
<string name="app_revision_fmt">Revizijos informacija: <xliff:g id="app_revision_url">%s</xliff:g></string>
<string name="app_revision_fmt">Revizijos info: <xliff:g id="app_revision_url">%s</xliff:g></string>
<string name="selectBarcodeTitle">Pasirinkite brūkšninį kodą</string>
<string name="copy_to_clipboard_toast">Kortelės ID nukopijuota į iškarpinę</string>
</resources>
<string name="card_ids_copied">Nukopijuotos kortelės ID</string>
<string name="noCardsMessage">Pirmiausia pridėkite kortelę</string>
<string name="sendLabel">Siųsti…</string>
<string name="unlockScreen">Atblokuoti pasukimą</string>
<string name="lockScreen">Blokuoti pasukimą</string>
<string name="unstar">Pašalinti iš mėgstamiausių</string>
<string name="star">Pridėti prie mėgstamiausių</string>
<string name="noBarcode">Nėra brūkšninio kodo</string>
<string name="barcodeNoBarcode">Ši kortelė neturi brūkšninio kodo</string>
<string name="barcodeType">Brūkšninio kodo tipas</string>
<string name="noMatchingGiftCards">Nieko nerasta. Pabandykite pakeisti paiešką.</string>
<string name="action_search">Ieškoti</string>
<string name="cardShortcut">Kortelės sparčioji nuoroda</string>
<string name="importVoucherVaultMessage">Pasirinkite savo <i>vouchervault.json</i> eksportą iš Voucher Vault, kurį norite importuoti.
\nArba sukurkite jį pirmiausia paspausdami Eksportuoti Voucher Vault.</string>
<string name="importVoucherVault">Importuoti iš Voucher Vault</string>
<string name="importLoyaltyCardKeychainMessage">Pasirinkite savo <i> LoyaltyCardKeychain.csv</i> eksportą iš Loyalty Card Keychain, kurį norite importuoti.
\nArba sukurkite jį iš Loyalty Card Keychain meniu Importas/Eksportas, pirmiausia paspausdami Eksportuoti.</string>
<string name="importLoyaltyCardKeychain">Importuoti iš Loyalty Card Keychain</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="parsingBalanceFailed">Panašu, kad <xliff:g> %s </xliff:g> reikšmė nėra tinkama.</string>
<string name="moveBarcodeToCenterOfScreen">Centruoti brūkšninį kodą ekrane</string>
<string name="moveBarcodeToTopOfScreen">Perkelti brūkšninį kodą į ekrano viršų</string>
<string name="settings_display_barcode_max_brightness">Šviesinti brūkšninio kodo rodinį</string>
<string name="failedParsingImportUriError">Nepavyko iššifruoti importo URI</string>
<string name="turn_flashlight_off">Išjunkti žibintuvėlį</string>
<string name="turn_flashlight_on">Įjunkti žibintuvėlį</string>
<string name="failedGeneratingShareURL">Nepavyko sugeneruoti bendrinamo URL. Praneškite apie šią klaidą!</string>
<string name="passwordRequired">Įveskite slaptažodį</string>
<string name="no">Ne</string>
<string name="yes">Taip</string>
<string name="updateBarcodeQuestionText">Pakeitėte kortelės ID. Ar norite atnaujinti ir brūkšninį kodą, kad būtų naudojama ta pati reikšmė\?</string>
<string name="updateBarcodeQuestionTitle">Atnaujinti brūkšninio kodo reikšmę\?</string>
<string name="takePhoto">Nufotografuoti</string>
<string name="removeImage">Pašalinti vaizdą</string>
<string name="setBackImage">Nustatyti galinį vaizdą</string>
<string name="setFrontImage">Nustatyti priekinį vaizdą</string>
<string name="photos">Nuotraukos</string>
<string name="backImageDescription">Kortelės galinis vaizdas</string>
<string name="frontImageDescription">Kortelės priekinis vaizdas</string>
<string name="intent_import_card_from_url_share_multiple_text">Noriu su jumis pasidalyti keliomis kortelėmis</string>
<string name="copy_to_clipboard_multiple_toast">Kortelės ID nukopijuotas į iškarpinę</string>
<string name="wrongValueForBarcodeType">Vertė netinkama pasirinktam brūkšninio kodo tipui</string>
<string name="unsupportedBarcodeType">Šio brūkšninio kodo tipo dar negalima rodyti. Galbūt jis bus palaikomas vėlesnėje programėlės versijoje.</string>
<string name="setBarcodeId">Nustatyti brūkšninio kodo reikšmę</string>
<string name="sameAsCardId">Tokia pat kaip kortelės ID</string>
<string name="barcodeId">Brūkšninio kodo reikšmė</string>
<string name="importStocardMessage">Pasirinkite <i>***-sync.zip</i> eksportą iš Stocard, kad galėtumėte importuoti, o vėliau brūkšninių kodų tipus pasirinkite rankiniu būdu.
\nArba gaukite susisiekę el. paštu support@stocardapp.com, prašydami eksportuoti jūsų duomenis.</string>
<string name="importStocard">Importuoti iš Stocard</string>
<string name="importFidmeMessage">Pasirinkite <i>fidme-export-request-xxxxxx.zip</i> eksportą iš FidMe, kurį norite importuoti, ir po to brūkšninių kodų tipus pasirinkite rankiniu būdu.
\nArba sukurkite jį iš savo FidMe profilio, pasirinkę Duomenų apsauga ir pirmiausia paspaudę Išgauti mano duomenis.</string>
<string name="importFidme">Importuoti iš FidMe</string>
<string name="importCatimaMessage">Norėdami importuoti, pasirinkite savo <i> catima.zip </i> eksportą iš Catima.
\nArba sukurkite ją iš kitos Catima programos importavimo / eksportavimo meniu, pirmiausia paspausdami Eksportuoti ten.</string>
<string name="importCatima">Importuoti iš Catima</string>
<string name="accept">Priimti</string>
<string name="privacy_policy_popup_text">Privatumo politikos pranešimas (kurio reikalaujama kai kuriose programėlių parduotuvėse):
\n
\nJOKIE DUOMENYS NĖRA RENKAMI, o tai gali patvirtinti bet kas, nes mūsų programėlė yra libre programinė įranga.</string>
<string name="privacy_policy">Privatumo politika</string>
<string name="chooseImportType">Importuoti duomenis iš\?</string>
<string name="points">Taškai</string>
<string name="currency">Valiuta</string>
<string name="balance">Balansas</string>
<string name="errorReadingImage">Nepavyko nuskaityti paveikslėlio</string>
<string name="noBarcodeFound">Nerastas joks brūkšninis kodas</string>
<string name="chooseExpiryDate">Pasirinkite galiojimo datą</string>
<string name="never">Niekada</string>
<string name="expiryDate">Galiojimo data</string>
<string name="editBarcode">Redaguoti brūkšninį kodą</string>
<string name="barcode">Brūkšninis kodas</string>
<string name="card">Kortelė</string>
<string name="balancePoints"><xliff:g>%s</xliff:g> taškai</string>
<string name="balanceSentence">Balansas: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Galiojimas baigėsi: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Nustoja galioti: <xliff:g>%s</xliff:g></string>
<string name="groupsList">Grupės: <xliff:g> %s </xliff:g></string>
<string name="addFromImage">Pasirinkti vaizdą iš galerijos</string>
<string name="addManually">Rankiniu būdu įvesti kortelės ID</string>
<string name="leaveWithoutSaveConfirmation">Išeiti neišsaugojus\?</string>
<string name="leaveWithoutSaveTitle">Išeiti</string>
<string name="moveDown">Judėti žemyn</string>
<string name="moveUp">Judėti aukštyn</string>
<string name="failedOpeningFileManager">Pirmiausia įdiekite failų tvarkyklę.</string>
<string name="deleteConfirmationGroup">Ištrinti grupę\?</string>
<string name="all">Visos</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> kortelę</item>
<item quantity="few"><xliff:g>%d</xliff:g> kortelės</item>
<item quantity="other"><xliff:g>%d</xliff:g> kortelių</item>
</plurals>
<string name="noGroups">Spustelėkite + pliuso mygtuką, kad pirmiausia pridėtumėte grupes kategorizavimui.</string>
<string name="groups">Grupės</string>
<string name="enter_group_name">Įvesti grupės pavadinimą</string>
<string name="exportSuccessful">Kortelės duomenys eksportuoti</string>
<string name="importSuccessful">Kortelės duomenys importuoti</string>
<string name="intent_import_card_from_url_share_text">Noriu pasidalyti su jumis kortele</string>
<string name="settings_disable_lockscreen_while_viewing_card">Neleisti užrakinti ekrano</string>
<string name="settings_keep_screen_on">Laikyti ekraną įjungtą</string>
<string name="settings_lock_barcode_orientation">Užrakinti brūkšninio kodo orientaciją</string>
<string name="settings_max_font_size_scale">Didžiausias šrifto dydis</string>
<string name="settings_dark_theme">Tamsi</string>
<string name="settings_light_theme">Šviesi</string>
<string name="settings_system_theme">Sistema</string>
<string name="settings_theme">Tema</string>
<string name="settings_category_title_ui">Vartotojo sąsaja</string>
<string name="settings">Nustatymai</string>
<string name="starImage">Mėgstamiausia žvaigždė</string>
<string name="thumbnailDescription">Kortelės miniatiūra</string>
<string name="enterBarcodeInstructions">Įveskite kortelės ID ir toliau pasirinkite jos brūkšninio kodo tipą arba \"Ši kortelė neturi brūkšninio kodo\".</string>
<string name="app_resources">Libre trečiųjų šalių ištekliai: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Libre trečiųjų šalių bibliotekos: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_copyright_old">Paremta Loyalty Card Keychain
\nautorinės teisės © 20162020 Branden Archer.</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Autorinės teisės © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="importOptionApplicationButton">Naudoti kitą programą</string>
<string name="importOptionApplicationExplanation">Norėdami atidaryti failą, naudokite bet kurią programą arba mėgstamą failų tvarkyklę.</string>
<string name="importOptionApplicationTitle">Naudoti kitą programą</string>
<string name="importOptionFilesystemButton">Iš failų sistemos</string>
<string name="importOptionFilesystemExplanation">Pasirinkite konkretų failą iš failų sistemos.</string>
<string name="importOptionFilesystemTitle">Importuoti iš failų sistemos</string>
<string name="exportOptionExplanation">Duomenys bus įrašyti į jūsų pasirinktą vietą.</string>
<string name="exportFailedTitle">Eksportuoti nepavyko</string>
<string name="exportSuccessfulTitle">Eksportuota</string>
<string name="importFailedTitle">Importuoti nepavyko</string>
<string name="importSuccessfulTitle">Importuota</string>
<string name="importExportHelp">Atsarginių kopijų darymas leidžia perkelti korteles į kitą įrenginį.</string>
<string name="noCardExistsError">Nepavyko rasti kortelės</string>
<string name="share">Dalintis</string>
<plurals name="selectedCardCount">
<item quantity="one">Pasirinkta: <xliff:g>%d</xliff:g> kortelė</item>
<item quantity="few">Pasirinktos: <xliff:g>%d</xliff:g> kortelės</item>
<item quantity="other">Pasirinkta: <xliff:g>%d</xliff:g> kortelių</item>
</plurals>
<string name="deleteTitle">Panaikinti lojalumo kortelę</string>
<string name="deleteConfirmation">Ištrinti šią kortelę\?</string>
</resources>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="action_add">Legg til</string>
<string name="noGiftCards">Klikk på «+» (pluss)-knappen for å legge til eller importere kort fra -menyen først-.</string>
<string name="storeName">Butikk</string>
<string name="noGiftCards">Klikk på «+» (pluss)-knappen for å legge til eller importer kort fra «⋮»-menyen først.</string>
<string name="storeName">Navn</string>
<string name="note">Merknad</string>
<string name="cardId">Kort-ID</string>
<string name="barcodeType">Strekkodetype</string>
@@ -13,8 +13,6 @@
<string name="confirm">Bekreft</string>
<string name="lockScreen">Ingen rotering</string>
<string name="unlockScreen">Skru på rotering</string>
<string name="deleteTitle">Fjern kundekort</string>
<string name="deleteConfirmation">Slett dette kortet\?</string>
<string name="ok">OK</string>
<string name="copy_to_clipboard">Kopier ID til utklippstavle</string>
<string name="sendLabel">Send…</string>
@@ -24,7 +22,7 @@
<string name="cardShortcut">Kort-snarvei</string>
<string name="noCardsMessage">Legg til et kort først</string>
<string name="barcodeImageDescription">Bilde av kortets strekkode</string>
<string name="noStoreError">Ingen butikk angitt</string>
<string name="noStoreError">Navn ikke angitt</string>
<string name="noCardIdError">Ingen kort-ID innskrevet</string>
<string name="noCardExistsError">Kunne ikke finne kort</string>
<string name="importExport">Import/eksport</string>
@@ -43,16 +41,16 @@
<string name="importOptionFilesystemTitle">Importer fra filsystem</string>
<string name="importOptionFilesystemExplanation">Velg spesifikk fil fra filsystemet.</string>
<string name="importOptionFilesystemButton">Fra filsystem</string>
<string name="importOptionApplicationTitle">Bruk eksternt program</string>
<string name="importOptionApplicationTitle">Bruk et annet program</string>
<string name="importOptionApplicationExplanation">Bruk hvilket som helst program, eller din favoritt-filutforsker for å åpne en fil.</string>
<string name="importOptionApplicationButton">Bruk eksternt program</string>
<string name="importOptionApplicationButton">Bruk et annet program</string>
<string name="about">Om</string>
<string name="app_license">Gemenhetslig fri programvare, lisensiert GPLv3+.</string>
<string name="about_title_fmt">Om <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">Versjon: <xliff:g id="version">%s</xliff:g></string>
<string name="app_revision_fmt">Utgivelsesinfo: <xliff:g id="app_revision_url">%s</xliff:g></string>
<string name="selectBarcodeTitle">Velg strekkode</string>
<string name="enterBarcodeInstructions">Skriv inn strekkodeverdien og velg så bildet som representerer strekkoden du ønsker å bruke.</string>
<string name="enterBarcodeInstructions">Skriv inn kortets ID, og enten velg dens strekkode nedenfor, eller «Dette kortet har ingen strekkode».</string>
<string name="copy_to_clipboard_toast">Kort-ID kopiert til utklippstavle</string>
<string name="thumbnailDescription">Miniatyrbilde for kort</string>
<string name="settings">Innstillinger</string>
@@ -86,8 +84,8 @@
<string name="leaveWithoutSaveConfirmation">Forlat uten å lagre\?</string>
<string name="leaveWithoutSaveTitle">Avslutt</string>
<string name="addManually">Skriv inn kort-ID manuelt</string>
<string name="moveDown">Flytt nedover i listen</string>
<string name="moveUp">Flytt oppover i listen</string>
<string name="moveDown">Flytt nedover</string>
<string name="moveUp">Flytt oppover</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> kort</item>
<item quantity="other"><xliff:g>%d</xliff:g> kort</item>
@@ -110,12 +108,12 @@
<string name="balancePoints"><xliff:g>%s</xliff:g> poeng</string>
<string name="balanceSentence">Saldo: <xliff:g>%s</xliff:g></string>
<string name="chooseImportType">Importer data fra\?</string>
<string name="app_loyalty_card_keychain">Kundekortsknippe</string>
<string name="settings_disable_lockscreen_while_viewing_card">Skru av låseskjerm under kortvisning</string>
<string name="settings_keep_screen_on">Behold skjerm påslått under kortvisning</string>
<string name="privacy_policy_popup_text">Mange programbutikker krever at programmer viser personvernspraksisen sin ved første oppstart. Her er vår:
<string name="app_loyalty_card_keychain">Kundekortknippe</string>
<string name="settings_disable_lockscreen_while_viewing_card">Forhindre skjermlås</string>
<string name="settings_keep_screen_on">Behold skjerm påslått</string>
<string name="privacy_policy_popup_text">Personvernspraksis-notis (påkrevd av noen programbutikker):
\n
\nVi SAMLER IKKE IN NOEN DATA overhodet, og programmet vårt er fri programvare, så alle kan bekrefte at dette stemmer.</string>
\nINGEN DATA SAMLES INN, noe alle kan bekreftes siden programmet vårt er fri programvare.</string>
<string name="accept">Godta</string>
<string name="privacy_policy">Personvernspraksis</string>
<string name="importFidme">Importer fra FidMe</string>
@@ -133,17 +131,48 @@
<string name="importLoyaltyCardKeychainMessage">Finn en fil som antagelig heter <i>LoyaltyCardKeychain.csv</i> å importere.
\nEller opprett den i Import/eksport-menyen i Kundekortknippe ved å trykke «Eksporter» der først.</string>
<string name="importLoyaltyCardKeychain">Importer fra Kundekortknippe</string>
<string name="importFidmeMessage">Finn en fil som antagelig heter <i>fidme.export-request-xxxxx.zip</i> å importere, for så å velge strekkodetypene manuelt etterpå-
<string name="importFidmeMessage">Finn en fil som antagelig heter <i>fidme.export-request-xxxxx.zip</i> å importere, for så å velge strekkodetypene manuelt etterpå-
\nEller opprett den i din FidMe-profil ved å velge «Databeskyttelse», for så å trykke «Pakk ut dataen min» først.</string>
<string name="importCatimaMessage">Finn en fil som antagelig heter <i>Catima.csv</i> å importere.
\nEller opprett den i Import/eksport-menyen i et annet Catima-program ved å trykke «Eksporter» der først.</string>
<string name="settings_max_font_size_scale">Maksimal skriftstørrelse</string>
<string name="wrongValueForBarcodeType">Verdien er ikke gyldig for valgt strekkodetype</string>
<string name="intent_import_card_from_url_share_multiple_text">Jeg ønsker å dele noen kort med deg</string>
<string name="copy_to_clipboard_multiple_toast">Kopierte kort-ID til utklippstavle</string>
<string name="copy_to_clipboard_multiple_toast">Kopierte kort-ID-er til utklippstavle</string>
<string name="app_resources">Frie tredjepartsressurser: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Frie tredjepartsbibliotek: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="card_selected">"Valgt: "</string>
<string name="card_ids_copied">Kopierte kort-ID(er)</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Opphavsrett © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="updateBarcodeQuestionText">Du har endret kortets ID. Ønsker du å også oppdatere strekkoden til samme verdi\?</string>
<string name="no">Nei</string>
<string name="yes">Ja</string>
<string name="updateBarcodeQuestionTitle">Oppdater strekkodeverdi\?</string>
<string name="takePhoto">Ta et bilde</string>
<string name="removeImage">Fjern bilde</string>
<string name="setBackImage">Sett bakside</string>
<string name="setFrontImage">Sett forside</string>
<string name="photos">Bilder</string>
<string name="backImageDescription">Kortets bakside</string>
<string name="frontImageDescription">Kortets forside</string>
<string name="importStocardMessage">Velg din <i>***-sync.zip</i>-eksport fra Stocard å importere, og velg strekkodetypene manuelt etterpå.
\nEller få den ved å sende e-post til support@stocardapp.com der du etterspør eksport av dataen din.</string>
<string name="passwordRequired">Skriv inn passordet</string>
<string name="importStocard">Importer fra Stocard</string>
<string name="failedGeneratingShareURL">Klarte ikke å lage delbar nettadresse. Rapporter denne feilen.</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> kort valgt</item>
<item quantity="other"><xliff:g>%d</xliff:g> kort valgt</item>
</plurals>
<string name="deleteConfirmation">Slett dette kortet\?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Slett dette kortet for godt\?</item>
<item quantity="other">Slett disse <xliff:g xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">%d</xliff:g> kortene for godt\?</item>
</plurals>
<string name="turn_flashlight_on">Skru på lommelykten</string>
<string name="turn_flashlight_off">Skru av lommelykten</string>
<string name="deleteTitle">Fjern kundekort</string>
<plurals name="deleteCardsTitle">
<item quantity="one">Slett kort</item>
<item quantity="other">Slett kort</item>
</plurals>
</resources>

View File

@@ -16,8 +16,6 @@
<string name="confirm">Bevestigen</string>
<string name="lockScreen">Draaien niet toestaan</string>
<string name="unlockScreen">Draaien toestaan</string>
<string name="deleteTitle">Kaart verwijderen</string>
<string name="deleteConfirmation">Kaart verwijderen\?</string>
<string name="ok">Oké</string>
<string name="copy_to_clipboard">Kaartnummer kopiëren naar klembord</string>
<string name="share">Delen</string>
@@ -118,15 +116,15 @@
\nER WORDEN GEEN GEGEVENS VERZAMELD. Bovendien is onze app open source, zodat een ieder met eigen ogen kan zien wat de app wel of niet doet.</string>
<string name="privacy_policy">Privacybeleid</string>
<string name="accept">Accepteren</string>
<string name="importVoucherVaultMessage">Kies het te importeren Voucher Vault-exportbestand genaamd <i>vouchervault.json</i>.
\nOf ga naar het exportmenu van Voucher Vault om exportbestand samen te stellen.</string>
<string name="importVoucherVaultMessage">Kies het te importeren <i>vouchervault.json</i>-exportbestand.
\nOf ga naar het exportmenu van Voucher Vault om een exportbestand samen te stellen.</string>
<string name="importVoucherVault">Importeren uit Voucher Vault</string>
<string name="importLoyaltyCardKeychainMessage">Kies het te importeren Klantenkaartkluis-exportbestand - genaamd <i>LoyaltyCardKeychain.csv</i>.
<string name="importLoyaltyCardKeychainMessage">Kies het te importeren genaamd <i>LoyaltyCardKeychain.csv</i>-exportbestand.
\nOf ga naar het import-/exportmenu van Klantenkaartkluis om een exportbestand samen te stellen.</string>
<string name="importLoyaltyCardKeychain">Importeren uit Klantenkaartkluis</string>
<string name="importFidmeMessage">Kies het te importeren FidMe-exportbestand genaamd <i>fidme-export-request-xxxxxx.zip</i>.
<string name="importFidmeMessage">Kies het te importeren <i>fidme-export-request-xxxxxx.zip</i>-exportbestand en kies nadien de juiste barcodes.
\nOf ga naar je FidMe-profiel en druk op Gegevensbescherming om een exportbestand samen te stellen.</string>
<string name="importCatimaMessage">Kies het te importeren exportbestand genaamd <i>Catima.csv</i>.
<string name="importCatimaMessage">Kies het te importeren <i>Catima.zip</i>-exportbestand.
\nOf ga naar het import-/exportmenu van Catima op een ander apparaat om een exportbestand samen te stellen.</string>
<string name="importFidme">Importeren uit FidMe</string>
<string name="importCatima">Importeren uit Catima</string>
@@ -139,15 +137,42 @@
<string name="settings_max_font_size_scale">Max. tekstgrootte</string>
<string name="unsupportedBarcodeType">Dit type barcode kan nog niet worden getoond - we hopen hiervoor in een nieuwere versie ondersteuning toe te voegen.</string>
<string name="wrongValueForBarcodeType">Deze waarde komt niet overeen met het gekozen barcodetype</string>
<string name="app_resources">Externe vrije bronnen: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Externe vrije bibliotheken: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Vrije externe bronnen: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Vrije externe bibliotheken: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Auteursrecht © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="intent_import_card_from_url_share_multiple_text">Ik wil kaarten met je delen</string>
<string name="copy_to_clipboard_multiple_toast">De kaart-id\'s zijn gekopieerd naar het klembord</string>
<string name="card_ids_copied">De kaart-ids zijn gekopieerd</string>
<string name="card_selected">"Geselecteerd: "</string>
<string name="no">Nee</string>
<string name="yes">Ja</string>
<string name="updateBarcodeQuestionText">Je hebt de kaart-id aangepast. Wil je dezelfde waarde toekennen aan de barcode\?</string>
<string name="updateBarcodeQuestionTitle">Barcodewaarde bijwerken\?</string>
<string name="takePhoto">Foto maken</string>
<string name="removeImage">Afbeelding verwijderen</string>
<string name="setFrontImage">Voorzijde kiezen</string>
<string name="setBackImage">Achterzijde kiezen</string>
<string name="photos">Afbeeldingen</string>
<string name="backImageDescription">Achterzijde van de kaart</string>
<string name="frontImageDescription">Voorzijde van de kaart</string>
<string name="passwordRequired">Voer het wachtwoord in</string>
<string name="importStocardMessage">Kies het te importeren Stocard-exportbestand genaamd <i>***-sync.zip</i> en kies nadien de juiste barcodes.
\nOf stuur een e-mail naar support@stocardapp.com waarin je vraagt om een exportbestand.</string>
<string name="importStocard">Importeren uit Stocard</string>
<string name="failedGeneratingShareURL">De te delen link kan niet worden gegenereerd. Meld deze fout.</string>
<string name="turn_flashlight_off">Zaklamp uitzetten</string>
<string name="turn_flashlight_on">Zaklamp aanzetten</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> kaart geselecteerd</item>
<item quantity="other"><xliff:g>%d</xliff:g> kaarten geselecteerd</item>
</plurals>
<string name="deleteTitle">Kaart verwijderen</string>
<plurals name="deleteCardsTitle">
<item quantity="one"><xliff:g>%d</xliff:g> kaart verwijderen</item>
<item quantity="other"><xliff:g>%d</xliff:g> kaarten verwijderen</item>
</plurals>
<string name="deleteConfirmation">Kaart verwijderen\?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Weet je zeker dat je deze <xliff:g>%d</xliff:g> kaart wilt verwijderen\?</item>
<item quantity="other">Weet je zeker dat je deze <xliff:g>%d</xliff:g> kaarten wilt verwijderen\?</item>
</plurals>
</resources>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="exporting">Exportacion…</string>
<string name="importing">Importacion…</string>
<string name="exportFailed">Exportacion pas possibla</string>
<string name="exportSuccessfulTitle">Exportat</string>
<string name="importFailed">Importacion de vòstras cartas impossibla</string>
<string name="importFailedTitle">Fracàs de limportacion</string>
<string name="importSuccessfulTitle">Importat</string>
<string name="importExportHelp">Exportar vòstras cartas vos permet de las recuperar sus un autre aparelh.</string>
<string name="exportName">Exportar</string>
<string name="importExport">Importar/Exportar</string>
<string name="failedParsingImportUriError">Analisi impossibla lURI dimportacion</string>
<string name="noCardExistsError">Cap de carta pas trobada</string>
<string name="noCardIdError">Cap de numèro de carta pas picat</string>
<string name="noStoreError">Cap de nom pas picat</string>
<string name="barcodeImageDescription">Imatge del còdi de barras</string>
<string name="card_ids_copied">Num. de la carta copiada</string>
<string name="noCardsMessage">Apondètz den primièr una carta</string>
<string name="cardShortcut">Acorchi de carta</string>
<string name="scanCardBarcode">Numerizar lo còdi de barras</string>
<string name="addCardTitle">Apondre una carta</string>
<string name="editCardTitle">Modificar la carta</string>
<string name="sendLabel">Enviar…</string>
<string name="share">Partejar</string>
<string name="unlockScreen">Activar la rotacion</string>
<string name="lockScreen">Desactivar la rotacion</string>
<string name="confirm">Confirmar</string>
<string name="delete">Suprimir</string>
<string name="edit">Modificar</string>
<string name="save">Enregistrar</string>
<string name="cancel">Anullar</string>
<string name="unstar">Tirar dels favorits</string>
<string name="star">Apondre als favorits</string>
<string name="barcodeType">Tipe còdi de barras</string>
<string name="cardId">Numèro</string>
<string name="note">Nòta</string>
<string name="storeName">Nom</string>
<string name="action_add">Apondre</string>
<string name="action_search">Recercar</string>
</resources>

View File

@@ -15,8 +15,6 @@
<string name="confirm">Potwierdź</string>
<string name="lockScreen">Zablokuj autoobracanie ekranu</string>
<string name="unlockScreen">Odblokuj autoobracanie ekranu</string>
<string name="deleteTitle">Usuń kartę lojalnościową</string>
<string name="deleteConfirmation">Potwierdź, że chcesz usunąć tę kartę.</string>
<string name="ok">OK</string>
<string name="copy_to_clipboard">Skopiuj identyfikator do schowka</string>
<string name="share">Udostępnij</string>
@@ -83,4 +81,6 @@
<string name="star">Dodaj do ulubionych</string>
<string name="noBarcode">Brak kodu kreskowego</string>
<string name="barcodeType">Typ kodu kreskowego</string>
<string name="deleteTitle">Usuń kartę lojalnościową</string>
<string name="deleteConfirmation">Potwierdź, że chcesz usunąć tę kartę.</string>
</resources>

View File

@@ -16,8 +16,6 @@
<string name="confirm">Подтвердить</string>
<string name="lockScreen">Блокировать поворот экрана</string>
<string name="unlockScreen">Автоповорот экрана</string>
<string name="deleteTitle">Удаление карты</string>
<string name="deleteConfirmation">Удалить карту\?</string>
<string name="ok">ОК</string>
<string name="copy_to_clipboard">Копировать номер карты</string>
<string name="share">Переслать</string>
@@ -120,15 +118,15 @@
<item quantity="other"><xliff:g>%d</xliff:g> карт</item>
</plurals>
<string name="accept">Принять</string>
<string name="importVoucherVaultMessage">Выберите файл для импортирования, обычно именуемый <i>vouchervault.json</i>.
<string name="importVoucherVaultMessage">Выберите файл для импортирования, именуемый <i>vouchervault.json</i>.
\nФайл экспорта можно создать в приложении Voucher Vault, нажав кнопку \"Экспорт\".</string>
<string name="importVoucherVault">Импорт из Voucher Vault</string>
<string name="importFidmeMessage">Выберите файл для импортирования, обычно именуемый <i>fidme-export-request-xxxxxx.zip</i>, а затем вручную выберите типы штрих-кодов.
<string name="importFidmeMessage">Выберите файл для импортирования, именуемый <i>fidme-export-request-xxxxxx.zip</i>, а затем вручную укажите типы штрих-кодов.
\nФайл экспорта можно создать в приложении FidMe, перейдя в свой профиль, выбрав функцию \"Защита данных\", и затем нажав кнопку \"Извлечь мои данные\".</string>
<string name="importLoyaltyCardKeychainMessage">Выберите файл экспорта, обычно именуемый <i>LoyaltyCardKeychain.csv</i>.
<string name="importLoyaltyCardKeychainMessage">Выберите файл экспорта, именуемый <i>LoyaltyCardKeychain.csv</i>.
\nФайл экспорта можно создать в приложении Loyalty Card Keychain, перейдя в меню \"Импорт/Экспорт\" и нажав кнопку \"Экспорт\".</string>
<string name="importLoyaltyCardKeychain">Импорт из Loyalty Card Keychain</string>
<string name="importCatimaMessage">Выберите файл для импортирования, обычно именуемый <i>Catima.csv</i>.
<string name="importCatimaMessage">Выберите файл для импортирования, именуемый <i>Catima.zip</i>.
\nФайл экспорта можно создать в другой копии Catima, перейдя в меню \"Импорт/Экспорт\" и нажав кнопку \"Экспорт\".</string>
<string name="importFidme">Импорт из FidMe</string>
<string name="importCatima">Импорт из Catima</string>
@@ -141,15 +139,42 @@
<string name="settings_max_font_size_scale">Максимальный размер шрифта</string>
<string name="unsupportedBarcodeType">В настоящее время данный тип штрих-кодов не отображается. Его поддержка может быть добавлена в следующих версиях приложения.</string>
<string name="wrongValueForBarcodeType">Недопустимое значение для выбранного типа штрих-кода</string>
<string name="app_resources">Сторонние свободные ресурсы: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Сторонние свободные библиотеки: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Свободные сторонние ресурсы: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Свободные сторонние библиотеки: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Авторские права © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="intent_import_card_from_url_share_multiple_text">Поделиться картами</string>
<string name="card_ids_copied">Скопированные номера карт</string>
<string name="copy_to_clipboard_multiple_toast">Номера карт скопированы в буфер обмена</string>
<string name="card_selected">"Выбрано: "</string>
<string name="updateBarcodeQuestionText">Вы изменили номер карты. Обновить также значение штрих-кода, чтобы использовалось одинаковое значение\?</string>
<string name="no">Нет</string>
<string name="yes">Да</string>
<string name="updateBarcodeQuestionTitle">Обновить значение штрих-кода\?</string>
</resources>
<string name="setBackImage">Изображение задней стороны</string>
<string name="setFrontImage">Изображение лицевой стороны</string>
<string name="takePhoto">Сфотографировать</string>
<string name="removeImage">Удалить изображение</string>
<string name="backImageDescription">Задняя сторона карты</string>
<string name="frontImageDescription">Лицевая сторона карты</string>
<string name="photos">Фотографии</string>
<string name="importStocardMessage">Выберите файл <i>***-sync.zip</i> для импортирования, а затем вручную укажите типы штрих-кодов.
\nЭтот файл можно получить по электронной почте от support@stocardapp.com, предварительно запросив экспорт ваших данных.</string>
<string name="passwordRequired">Введите пароль</string>
<string name="importStocard">Импорт из Stocard</string>
<string name="failedGeneratingShareURL">Невозможно создать URL для обмена. Пожалуйста, сообщите об этом.</string>
<string name="turn_flashlight_off">Отключить вспышку</string>
<string name="turn_flashlight_on">Включить вспышку</string>
<plurals name="selectedCardCount">
<item quantity="one">Выбрано: <xliff:g>%d</xliff:g> карта</item>
<item quantity="few">Выбрано: <xliff:g>%d</xliff:g> карты</item>
<item quantity="many">Выбрано: <xliff:g>%d</xliff:g> карт</item>
<item quantity="other">Выбрано: <xliff:g>%d</xliff:g> карт</item>
</plurals>
<string name="deleteTitle">Удаление карты</string>
<string name="deleteConfirmation">Удалить карту\?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Удалить эту <xliff:g>%d</xliff:g> карту безвозвратно\?</item>
<item quantity="few">Удалить эти <xliff:g>%d</xliff:g> карты безвозвратно\?</item>
<item quantity="many">Удалить эти <xliff:g>%d</xliff:g> карт безвозвратно\?</item>
<item quantity="other">Удалить эти <xliff:g>%d</xliff:g> карт безвозвратно\?</item>
</plurals>
</resources>

View File

@@ -14,8 +14,6 @@
<string name="confirm">Potvrdiť</string>
<string name="lockScreen">Zakázať rotáciu</string>
<string name="unlockScreen">Povoliť rotáciu</string>
<string name="deleteTitle">Odstrániť kartu</string>
<string name="deleteConfirmation">Naozaj chcete túto kartu odstrániť?</string>
<string name="ok">Áno</string>
<string name="copy_to_clipboard">Kopírovať ID do schránky</string>
<string name="sendLabel">Odoslať&#8230;</string>
@@ -64,4 +62,6 @@
<string name="settings_category_title_ui">Používateľské prostredie</string>
<string name="settings_display_barcode_max_brightness">Zvýšiť jas pri zobrazení čiarového kódu </string>
<string name="settings_lock_barcode_orientation">Uzamkni orientáciu čiarového kódu</string>
</resources>
<string name="deleteTitle">Odstrániť kartu</string>
<string name="deleteConfirmation">Naozaj chcete túto kartu odstrániť?</string>
</resources>

View File

@@ -12,8 +12,6 @@
<string name="confirm">Potrdi</string>
<string name="lockScreen">Onemogoči obračanje zaslona</string>
<string name="unlockScreen">Omogoči obračanje zaslona</string>
<string name="deleteTitle">Odstrani kartico zvestobe</string>
<string name="deleteConfirmation">Prosim potrdite, če želite izbrisati to kartico.</string>
<string name="ok">Vredu</string>
<string name="copy_to_clipboard">Kopirajte številko kartice</string>
<string name="sendLabel">Pošlji…</string>
@@ -56,4 +54,6 @@
<string name="settings_category_title_ui">Uporabniški vmesnik</string>
<string name="settings_display_barcode_max_brightness">Povečaj osvetljenost prikaza črtne kode</string>
<string name="settings_lock_barcode_orientation">Zakleni orientacijo črtne kode</string>
<string name="deleteTitle">Odstrani kartico zvestobe</string>
<string name="deleteConfirmation">Prosim potrdite, če želite izbrisati to kartico.</string>
</resources>

View File

@@ -1,4 +1,174 @@
<resources
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
</resources>
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<string name="privacy_policy_popup_text">Політика конфіденційності (вимагається деякими магазинами):
\n
\nЖОДНОЇ ІНФОРМАЦІЇ НЕ ЗБИРАЄТЬСЯ, що може підтвердити будь-хто, адже наша програма це вільне програмне забезпечення.</string>
<string name="noGiftCards">Натисніть + щоб додати карту чи спочатку імпортуйте з ⋮ меню.</string>
<string name="settings_display_barcode_max_brightness">Яскравіший штрих-код</string>
<string name="enterBarcodeInstructions">Введіть ID карти та оберіть тип штрих-коду чи \"Ця карта не має штрих-коду\".</string>
<string name="selectBarcodeTitle">Оберіть штрих-код</string>
<string name="barcodeImageDescription">Зображення штрих-коду карти</string>
<string name="scanCardBarcode">Відсканувати штрих-код карти</string>
<string name="noBarcode">Без штрих-коду</string>
<string name="barcodeNoBarcode">Ця карта не має штрих-коду</string>
<string name="barcodeType">Тип штрих-коду</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> картка</item>
<item quantity="few"><xliff:g>%d</xliff:g> карток</item>
<item quantity="many"><xliff:g>%d</xliff:g> картки</item>
<item quantity="other"><xliff:g>%d</xliff:g> картки</item>
</plurals>
<string name="no">НІ</string>
<string name="yes">Так</string>
<string name="updateBarcodeQuestionText">Ви змінили ID картки. Чи ви бажаєте оновити штрих-код для використання цього ж значення\?</string>
<string name="updateBarcodeQuestionTitle">Оновити значення штрих-коду\?</string>
<string name="wrongValueForBarcodeType">Значення не дійсне для обраного типу штрих-коду</string>
<string name="unsupportedBarcodeType">Цей тип штрих-коду поки що не відображається. Підтримку може бути додано в подальших версіях програми.</string>
<string name="setBarcodeId">Встановіть значення штрих-коду</string>
<string name="sameAsCardId">Таке ж як ID картки</string>
<string name="barcodeId">Значення штрих-коду</string>
<string name="importVoucherVaultMessage">Оберіть Voucher Vault експорт-файл названий <i>vouchervault.json</i> для імпортування.
\nЧи створіть його натиснувши \"Експорт\" у Voucher Vault.</string>
<string name="importVoucherVault">Імпорт з Voucher Vault</string>
<string name="importLoyaltyCardKeychainMessage">Знайдіть файл названий <i>LoyaltyCardKeychain.csv</i> для імпортування.
\nЧи створіть його у меню імпорту/експорту Loyalty Card Keychain натиснувши \"Експорт\".</string>
<string name="importCatimaMessage">Оберіть експорт-файл названий <i>catima.zip</i> для імпортування.
\nЧи створіть його з меню імпорту/експорту у іншій Catima, натиснувши \"Експорт\".</string>
<string name="importLoyaltyCardKeychain">Імпорт з Loyalty Card Keychain</string>
<string name="importFidmeMessage">Оберіть FidMe експорт-файл названий <i>fidme-export-request-xxxxxx.zip</i> для імпортування і оберіть типи штрих-кодів вручну пізніше.
\nЧи створіть його у вашому FidMe профілі обравши \"Захист даних -&gt; Витяг даних\".</string>
<string name="importFidme">Імпорт з FidMe</string>
<string name="importCatima">Імпорт з Catima</string>
<string name="accept">Прийняти</string>
<string name="privacy_policy">Політика конфіденційності</string>
<string name="app_loyalty_card_keychain">Loyalty Card Keychain</string>
<string name="chooseImportType">Імпортувати дані з\?</string>
<string name="parsingBalanceFailed"><xliff:g>%s</xliff:g> здається, не є дійсним залишком.</string>
<string name="points">Бали</string>
<string name="currency">Валюта</string>
<string name="balance">Баланс</string>
<string name="errorReadingImage">Неможливо прочитати зображення</string>
<string name="noBarcodeFound">Жодного штрих-коду не знайдено</string>
<string name="moveBarcodeToCenterOfScreen">Відцентрувати штрих-код на екрані</string>
<string name="moveBarcodeToTopOfScreen">Посунути штрих-код наверх екрану</string>
<string name="chooseExpiryDate">Оберіть дату закінчення терміну дії</string>
<string name="never">Ніколи</string>
<string name="expiryDate">Дата закінчення терміну дії</string>
<string name="editBarcode">Редагувати штрих-код</string>
<string name="barcode">Штрих-код</string>
<string name="card">Картка</string>
<string name="balancePoints"><xliff:g>%s</xliff:g> балів</string>
<string name="balanceSentence">Баланс: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Термін дії закінчився: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Термін дії закінчується: <xliff:g>%s</xliff:g></string>
<string name="groupsList">Групи: <xliff:g>%s</xliff:g></string>
<string name="addFromImage">Оберіть зображення з галереї</string>
<string name="addManually">Ввести ID карти вручну</string>
<string name="leaveWithoutSaveConfirmation">Вийти без збереження\?</string>
<string name="leaveWithoutSaveTitle">Вихід</string>
<string name="moveDown">Посунути донизу</string>
<string name="moveUp">Посунути вгору</string>
<string name="failedOpeningFileManager">Спочатку встановіть файловий менеджер.</string>
<string name="deleteConfirmationGroup">Видалити групу\?</string>
<string name="all">Усі</string>
<string name="noGroups">Спочатку натисніть + щоб додати групи для категоризації.</string>
<string name="groups">Групи</string>
<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="settings_disable_lockscreen_while_viewing_card">Не блокувати екран</string>
<string name="settings_keep_screen_on">Не вимикати екран</string>
<string name="settings_lock_barcode_orientation">Заблокувати орієнтацію штрих-коду</string>
<string name="settings_max_font_size_scale">Макс. розмір шрифту</string>
<string name="settings_dark_theme">Темна</string>
<string name="settings_light_theme">Світла</string>
<string name="settings_system_theme">Система</string>
<string name="settings_theme">Тема</string>
<string name="settings_category_title_ui">Інтерфейс користувача</string>
<string name="settings">Налаштування</string>
<string name="starImage">Улюблена зірка</string>
<string name="thumbnailDescription">Ескіз для карти</string>
<string name="intent_import_card_from_url_share_multiple_text">Я хочу поділитися деякими картами з тобою</string>
<string name="copy_to_clipboard_multiple_toast">ID карток скопійовано до буферу обміну</string>
<string name="copy_to_clipboard_toast">ID карти скопійовано до буферу обміну</string>
<string name="app_resources">Вільні ресурси третіх сторін: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Вільні бібліотеки третіх сторін: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_revision_fmt">Інформація про випуск: <xliff:g id="app_revision_url">%s</xliff:g></string>
<string name="debug_version_fmt">Версія: <xliff:g id="version">%s</xliff:g></string>
<string name="about_title_fmt">Про <xliff:g id="app_name">%s</xliff:g></string>
<string name="app_license">Копілефт вільне програмне забезпечення, ліцензоване під GPLv3+.</string>
<string name="app_copyright_old">Створено на основі Loyalty Card Keychain
\nавторські права © 20162020 Branden Archer.</string>
<string name="app_copyright_fmt" tools:ignore="PluralsCandidate">Авторські права © 2019<xliff:g>%d</xliff:g> Sylvia van Os.</string>
<string name="about">Про програму</string>
<string name="importOptionApplicationButton">Використайте іншу програму</string>
<string name="importOptionApplicationExplanation">Використайте іншу програму чи ваш улюблений файл-менеджер для відкриття файлу.</string>
<string name="importOptionApplicationTitle">Використати іншу програму</string>
<string name="importOptionFilesystemButton">З провідника</string>
<string name="importOptionFilesystemExplanation">Оберіть конкретний файл у провіднику.</string>
<string name="importOptionFilesystemTitle">Імпорт з провідника</string>
<string name="exportOptionExplanation">Дані буде записано до локації обраної вами.</string>
<string name="noExternalStoragePermissionError">Надайте дозвіл на доступ до пам\'яті пристрою для імпорту чи експорту карток</string>
<string name="exporting">Експортуємо…</string>
<string name="importing">Імпортуємо…</string>
<string name="exportFailed">Неможливо експортувати картки</string>
<string name="exportFailedTitle">Помилка експорту</string>
<string name="exportSuccessfulTitle">Експортовано</string>
<string name="importFailed">Неможливо імпортувати картки</string>
<string name="importFailedTitle">Помилка імпорту</string>
<string name="importSuccessfulTitle">Імпортовано</string>
<string name="importExportHelp">Створення резервної копії ваших карток дозволяє перемістити їх до іншого пристрою.</string>
<string name="exportName">Експорт</string>
<string name="importExport">Імпорт/Експорт</string>
<string name="failedParsingImportUriError">Неможливо опрацювати імпорт-URI</string>
<string name="noCardExistsError">Карту не знайдено</string>
<string name="noCardIdError">ID карти не введено</string>
<string name="noStoreError">Ім\'я не введено</string>
<string name="card_ids_copied">ID карти скопійовано</string>
<string name="noCardsMessage">Спочатку додайте карту</string>
<string name="cardShortcut">Швидкий виклик карти</string>
<string name="addCardTitle">Додати карту</string>
<string name="editCardTitle">Редагувати карту</string>
<string name="sendLabel">Відправити…</string>
<string name="share">Поділитися</string>
<string name="copy_to_clipboard">Копіювати ID до буферу обміну</string>
<string name="ok">ОК</string>
<string name="unlockScreen">Розблокувати обертання</string>
<string name="lockScreen">Блокувати обертання</string>
<string name="confirm">Підтвердити</string>
<string name="delete">Видалити</string>
<string name="edit">Редагувати</string>
<string name="save">Зберегти</string>
<string name="cancel">Відмінити</string>
<string name="unstar">Видалити з улюблених</string>
<string name="star">Додати до улюблених</string>
<string name="cardId">ID картки</string>
<string name="note">Замітка</string>
<string name="storeName">Ім\'я</string>
<string name="noMatchingGiftCards">Збігів не знайдено. Спробуйте змінити параметри пошуку.</string>
<string name="action_add">Додати</string>
<string name="action_search">Пошук</string>
<string name="turn_flashlight_off">Вимкнути спалах</string>
<string name="turn_flashlight_on">Увімкнути спалах</string>
<string name="failedGeneratingShareURL">Збій створення URL обміну. Будь ласка повідомте про цю помилку.</string>
<string name="passwordRequired">Будь ласка введіть пароль</string>
<string name="takePhoto">Зробити фото</string>
<string name="removeImage">Видалити зображення</string>
<string name="setBackImage">Встановити зображення тильної сторони</string>
<string name="setFrontImage">Встановити зображення лицьової сторони</string>
<string name="photos">Фотографії</string>
<string name="backImageDescription">Тильна сторона карти</string>
<string name="frontImageDescription">Лицьова сторона карти</string>
<string name="importStocardMessage">Оберіть Stocard експорт-файл, названий <i>***-sync.zip</i> для імпортування і оберіть типи штрих-кодів вручну пізніше.
\nЧи отримайте його надіславши електронного листа із запитом на експортування ваших даних до support@stocardapp.com.</string>
<string name="importStocard">Імпорт із Stocard</string>
<plurals name="selectedCardCount">
<item quantity="one">Обрано: <xliff:g>%d</xliff:g> карта</item>
<item quantity="few">Обрано: <xliff:g>%d</xliff:g> карти</item>
<item quantity="many">Обрано: <xliff:g>%d</xliff:g> карт</item>
<item quantity="other">Обрано: <xliff:g>%d</xliff:g> карт</item>
</plurals>
<string name="deleteTitle">Видалити карту</string>
<string name="deleteConfirmation">Бажаєте видалити карту\?</string>
</resources>

View File

@@ -123,8 +123,6 @@
<string name="share">分享</string>
<string name="copy_to_clipboard">复制卡号到剪贴板</string>
<string name="ok">确定</string>
<string name="deleteConfirmation">删除此卡?</string>
<string name="deleteTitle">移除卡片</string>
<string name="unlockScreen">解除旋转锁定</string>
<string name="lockScreen">锁定旋转</string>
<string name="confirm">确认</string>
@@ -141,7 +139,8 @@
<string name="storeName">名称</string>
<string name="noMatchingGiftCards">没有找到任何东西。尝试改变你的搜索。</string>
<string name="noGiftCards">点击 \"+\"加号按钮来添加卡片,或者先从⋮菜单中导入一些。</string>
<string name="card_selected">"选中: "</string>
<string name="action_add">添加</string>
<string name="action_search">搜索</string>
<string name="deleteConfirmation">删除此卡?</string>
<string name="deleteTitle">移除卡片</string>
</resources>

View File

@@ -4,6 +4,7 @@
<item>Catima</item>
<item>Fidme</item>
<item>@string/app_loyalty_card_keychain</item>
<item>Stocard</item>
<item>Voucher Vault</item>
</string-array>
</resources>

View File

@@ -4,7 +4,10 @@
<string name="action_search">Search</string>
<string name="action_add">Add</string>
<string name="card_selected">Selected:\u0020</string>
<plurals name="selectedCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> card selected</item>
<item quantity="other"><xliff:g>%d</xliff:g> cards selected</item>
</plurals>
<string name="noGiftCards">Click the + plus button to add a card, or import some from the ⋮ menu first.</string>
<string name="noMatchingGiftCards">Didn\'t find anything. Try changing your search.</string>
@@ -26,8 +29,22 @@
<string name="confirm">Confirm</string>
<string name="lockScreen">Block Rotation</string>
<string name="unlockScreen">Unblock Rotation</string>
<string name="deleteTitle">Remove Card</string>
<string name="deleteConfirmation">Delete this card?</string>
<!-- START NOTE: i18n oddness -->
<!-- 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 Catima, we use the plain string when meaning exactly 1, and otherwise use the plural forms -->
<string name="deleteTitle">Delete card</string>
<plurals name="deleteCardsTitle">
<item quantity="one">Delete <xliff:g>%d</xliff:g> card</item>
<item quantity="other">Delete <xliff:g>%d</xliff:g> cards</item>
</plurals>
<string name="deleteConfirmation">Delete this card permanently?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">Delete this <xliff:g>%d</xliff:g> card permanently?</item>
<item quantity="other">Delete these <xliff:g>%d</xliff:g> cards permanently?</item>
</plurals>
<!-- END NOTE: i18n oddness -->
<string name="ok">OK</string>
<string name="copy_to_clipboard">Copy ID to clipboard</string>
<string name="share">Share</string>
@@ -72,8 +89,8 @@
<string name="about_title_fmt">About <xliff:g id="app_name">%s</xliff:g></string>
<string name="debug_version_fmt">Version: <xliff:g id="version">%s</xliff:g></string>
<string name="app_revision_fmt">Revision Info: <xliff:g id="app_revision_url">%s</xliff:g></string>
<string name="app_libraries">Third-party libre libraries: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Third-party libre resources: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="app_libraries">Libre third-party libraries: <xliff:g id="app_libraries_list">%s</xliff:g></string>
<string name="app_resources">Libre third-party resources: <xliff:g id="app_resources_list">%s</xliff:g></string>
<string name="selectBarcodeTitle">Select Barcode</string>
<string name="enterBarcodeInstructions">Enter the card ID, and either pick its barcode type below, or &#8220;This card has no barcode&#8221;.</string>
@@ -109,10 +126,12 @@
<string name="sharedpreference_privacy_policy_shown" translatable="false">sharedpreference_privacy_policy_shown</string>
<string name="intent_import_card_from_url_share_text">I want to share a card with you</string>
<string name="intent_import_card_from_url_host" translatable="false">thelastproject.github.io</string>
<string name="intent_import_card_from_url_path_prefix" translatable="false">/Catima/share</string>
<string name="intent_import_card_from_url_host_old" translatable="false">brarcher.github.io</string>
<string name="intent_import_card_from_url_path_prefix_old" translatable="false">/loyalty-card-locker/share</string>
<string name="intent_import_card_from_url_host_catima_app" translatable="false">catima.app</string>
<string name="intent_import_card_from_url_path_prefix_catima_app" translatable="false">/share</string>
<string name="intent_import_card_from_url_host_thelastproject" translatable="false">thelastproject.github.io</string>
<string name="intent_import_card_from_url_path_prefix_thelastproject" translatable="false">/Catima/share</string>
<string name="intent_import_card_from_url_host_brarcher" translatable="false">brarcher.github.io</string>
<string name="intent_import_card_from_url_path_prefix_brarcher" translatable="false">/loyalty-card-locker/share</string>
<string name="importSuccessful">Card data imported</string>
<string name="exportSuccessful">Card data exported</string>
@@ -163,22 +182,35 @@
<string name="privacy_policy_popup_text">Privacy policy notice (required by some app stores):\n\nNO DATA IS COLLECTED AT ALL, which anyone can confirm since our app is libre software.</string>
<string name="accept">Accept</string>
<string name="importCatima">Import from Catima</string>
<string name="importCatimaMessage">Find a file likely named <i>Catima.csv</i> to import.\nOr create it from the Import/Export menu of another Catima app by pressing "Export" there first.</string>
<string name="importCatimaMessage">Select your <i>catima.zip</i> export from Catima to import.\nOr create it from the "Import/Export" menu of another Catima app by pressing "Export" there first.</string>
<string name="importFidme">Import from FidMe</string>
<string name="importFidmeMessage">Find a file likely named <i>fidme-export-request-xxxxxx.zip</i> to import, and then select the barcode types manually afterwards.\nOr create it from your FidMe profile by choosing "Data Protection" and then pressing "Extract my data" first.</string>
<string name="importFidmeMessage">Select your <i>fidme-export-request-xxxxxx.zip</i> export from FidMe to import, and select the barcode types manually afterwards.\nOr create it from your FidMe profile by choosing "Data Protection" and then pressing "Extract my data" first.</string>
<string name="importLoyaltyCardKeychain">Import from Loyalty Card Keychain</string>
<string name="importLoyaltyCardKeychainMessage">Find a file most likely named <i>LoyaltyCardKeychain.csv</i> to import.\nOr create it from the Import/Export menu in Loyalty Card Keychain by pressing "Export" first.</string>
<string name="importLoyaltyCardKeychainMessage">Select your <i>LoyaltyCardKeychain.csv</i> export from Loyalty Card Keychain to import.\nOr create it from the "Import/Export" menu in Loyalty Card Keychain by pressing "Export" there first.</string>
<string name="importStocard">Import from Stocard</string>
<string name="importStocardMessage">Select your <i>***-sync.zip</i> export from Stocard to import, and select the barcode types manually afterwards.\nOr get it by e-mailing support@stocardapp.com asking for an export of your data.</string>
<string name="importVoucherVault">Import from Voucher Vault</string>
<string name="importVoucherVaultMessage">Find a file likely named <i>vouchervault.json</i> to import.\nOr create it by pressing "Export" in Voucher Vault first.</string>
<string name="importVoucherVaultMessage">Select your <i>vouchervault.json</i> export from Voucher Vault to import.\nOr create it by pressing "Export" in Voucher Vault first.</string>
<string name="barcodeId">Barcode value</string>
<string name="sameAsCardId">Same as card ID</string>
<string name="setBarcodeId">Set barcode value</string>
<string name="unsupportedBarcodeType">This barcode type can\'t yet be displayed. It may be supported in a newer version of the app.</string>
<string name="unsupportedBarcodeType">This barcode type can\'t yet be displayed. It may be supported in a later version of the app.</string>
<string name="wrongValueForBarcodeType">The value is not valid for the selected barcode type</string>
<string name="copy_to_clipboard_multiple_toast">Copied card IDs to clipboard</string>
<string name="copy_to_clipboard_multiple_toast">Card IDs copied to clipboard</string>
<string name="intent_import_card_from_url_share_multiple_text">I want to share some cards with you</string>
<string name="frontImageDescription">Card\'s front image</string>
<string name="backImageDescription">Card\'s back image</string>
<string name="photos">Photos</string>
<string name="setFrontImage">Set front image</string>
<string name="setBackImage">Set back image</string>
<string name="removeImage">Remove image</string>
<string name="takePhoto">Take a photo</string>
<string name="updateBarcodeQuestionTitle">Update barcode value?</string>
<string name="updateBarcodeQuestionText">You changed the card ID. Do you want to also update the barcode to use the same value?</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="passwordRequired">Please enter the password</string>
<string name="failedGeneratingShareURL">Could not generate sharable URL. Please report this.</string>
<string name="turn_flashlight_on">Turn flashlight on</string>
<string name="turn_flashlight_off">Turn flashlight off</string>
</resources>

View File

@@ -2,9 +2,12 @@ package protect.card_locker;
import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.os.Environment;
import android.util.DisplayMetrics;
import com.google.zxing.BarcodeFormat;
@@ -16,6 +19,7 @@ import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLog;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -24,6 +28,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
@@ -33,8 +38,12 @@ import java.util.Arrays;
import java.util.Calendar;
import java.util.Currency;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import androidx.core.content.res.ResourcesCompat;
import protect.card_locker.importexport.DataFormat;
import protect.card_locker.importexport.ImportExportResult;
import protect.card_locker.importexport.MultiFormatExporter;
import protect.card_locker.importexport.MultiFormatImporter;
@@ -59,6 +68,8 @@ public class ImportExportTest
@Before
public void setUp()
{
ShadowLog.stream = System.out;
activity = Robolectric.setupActivity(MainActivity.class);
db = TestHelpers.getEmptyDb(activity);
nowMs = System.currentTimeMillis();
@@ -323,8 +334,8 @@ public class ImportExportTest
OutputStreamWriter outStream = new OutputStreamWriter(outData);
// Export data to CSV format
boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima);
assertEquals(ImportExportResult.Success, result);
outStream.close();
TestHelpers.getEmptyDb(activity);
@@ -332,8 +343,8 @@ public class ImportExportTest
ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray());
// Import the CSV data
result = MultiFormatImporter.importData(db, inData, DataFormat.Catima);
assertTrue(result);
result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(NUM_CARDS, db.getLoyaltyCardCount());
@@ -354,8 +365,8 @@ public class ImportExportTest
OutputStreamWriter outStream = new OutputStreamWriter(outData);
// Export data to CSV format
boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima);
assertEquals(ImportExportResult.Success, result);
outStream.close();
TestHelpers.getEmptyDb(activity);
@@ -363,8 +374,8 @@ public class ImportExportTest
ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray());
// Import the CSV data
result = MultiFormatImporter.importData(db, inData, DataFormat.Catima);
assertTrue(result);
result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(NUM_CARDS, db.getLoyaltyCardCount());
@@ -427,8 +438,8 @@ public class ImportExportTest
OutputStreamWriter outStream = new OutputStreamWriter(outData);
// Export data to CSV format
boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima);
assertEquals(ImportExportResult.Success, result);
outStream.close();
TestHelpers.getEmptyDb(activity);
@@ -436,8 +447,8 @@ public class ImportExportTest
ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray());
// Import the CSV data
result = MultiFormatImporter.importData(db, inData, DataFormat.Catima);
assertTrue(result);
result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(NUM_CARDS, db.getLoyaltyCardCount());
assertEquals(NUM_GROUPS, db.getGroupCount());
@@ -471,15 +482,15 @@ public class ImportExportTest
OutputStreamWriter outStream = new OutputStreamWriter(outData);
// Export into CSV data
boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima);
assertEquals(ImportExportResult.Success, result);
outStream.close();
ByteArrayInputStream inData = new ByteArrayInputStream(outData.toByteArray());
// Import the CSV data on top of the existing database
result = MultiFormatImporter.importData(db, inData, DataFormat.Catima);
assertTrue(result);
result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(NUM_CARDS, db.getLoyaltyCardCount());
@@ -502,8 +513,8 @@ public class ImportExportTest
OutputStreamWriter outStream = new OutputStreamWriter(outData);
// Export data to CSV format
boolean result = MultiFormatExporter.exportData(db, outStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatExporter.exportData(activity.getApplicationContext(), db, outData, DataFormat.Catima);
assertEquals(ImportExportResult.Success, result);
TestHelpers.getEmptyDb(activity);
@@ -516,8 +527,8 @@ public class ImportExportTest
ByteArrayInputStream inData = new ByteArrayInputStream((outData.toString() + corruptEntry).getBytes());
// Attempt to import the data
result = MultiFormatImporter.importData(db, inData, format);
assertEquals(false, result);
result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inData, format, null);
assertEquals(ImportExportResult.GenericFailure, result);
assertEquals(0, db.getLoyaltyCardCount());
@@ -527,11 +538,11 @@ public class ImportExportTest
class TestTaskCompleteListener implements ImportExportTask.TaskCompleteListener
{
Boolean success;
ImportExportResult result;
public void onTaskComplete(boolean success)
public void onTaskComplete(ImportExportResult result, DataFormat dataFormat)
{
this.success = success;
this.result = result;
}
}
@@ -557,8 +568,8 @@ public class ImportExportTest
Robolectric.flushBackgroundThreadScheduler();
// Check that the listener was executed
assertNotNull(listener.success);
assertEquals(true, listener.success);
assertNotNull(listener.result);
assertEquals(ImportExportResult.Success, listener.result);
TestHelpers.getEmptyDb(activity);
@@ -568,15 +579,15 @@ public class ImportExportTest
FileInputStream fileStream = new FileInputStream(exportFile);
task = new ImportExportTask(activity, DataFormat.Catima, fileStream, listener);
task = new ImportExportTask(activity, DataFormat.Catima, fileStream, null, listener);
task.execute();
// Actually run the task to completion
Robolectric.flushBackgroundThreadScheduler();
// Check that the listener was executed
assertNotNull(listener.success);
assertEquals(true, listener.success);
assertNotNull(listener.result);
assertEquals(ImportExportResult.Success, listener.result);
assertEquals(NUM_CARDS, db.getLoyaltyCardCount());
@@ -602,8 +613,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(1, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
@@ -640,8 +651,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(1, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
@@ -678,8 +689,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertEquals(false, result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.GenericFailure, result);
assertEquals(0, db.getLoyaltyCardCount());
TestHelpers.getEmptyDb(activity);
@@ -703,8 +714,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertEquals(true, result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(1, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
@@ -741,8 +752,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertEquals(true, result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(1, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
@@ -779,8 +790,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertEquals(true, result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(1, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
@@ -817,8 +828,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertTrue(result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(1, db.getLoyaltyCardCount());
csvText = "";
@@ -836,8 +847,8 @@ public class ImportExportTest
inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertTrue(result);
result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(1, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
@@ -857,33 +868,106 @@ public class ImportExportTest
}
@Test
public void exportV2()
public void exportImportV2Zip() throws FileNotFoundException
{
db.insertGroup("Example");
int loyaltyCard = (int) db.insertLoyaltyCard("Card 1", "Note 1", new Date(1618053234), new BigDecimal("100"), Currency.getInstance("USD"), "1234", "5432", BarcodeFormat.QR_CODE, 1, 0);
db.setLoyaltyCardGroups(loyaltyCard, Arrays.asList(db.getGroup("Example")));
// Prepare images
BitmapDrawable launcher = (BitmapDrawable) ResourcesCompat.getDrawableForDensity(activity.getResources(), R.mipmap.ic_launcher, DisplayMetrics.DENSITY_XXXHIGH, activity.getTheme());
BitmapDrawable roundLauncher = (BitmapDrawable) ResourcesCompat.getDrawableForDensity(activity.getResources(), R.mipmap.ic_launcher_round, DisplayMetrics.DENSITY_XXXHIGH, activity.getTheme());
Bitmap launcherBitmap = launcher.getBitmap();
Bitmap roundLauncherBitmap = roundLauncher.getBitmap();
// Set up cards and groups
HashMap<Integer, LoyaltyCard> loyaltyCardHashMap = new HashMap<>();
HashMap<Integer, List<Group>> loyaltyCardGroups = new HashMap<>();
HashMap<Integer, Bitmap> loyaltyCardFrontImages = new HashMap<>();
HashMap<Integer, Bitmap> loyaltyCardBackImages = new HashMap<>();
// Create card 1
int loyaltyCardId = (int) db.insertLoyaltyCard("Card 1", "Note 1", new Date(1618053234), new BigDecimal("100"), Currency.getInstance("USD"), "1234", "5432", BarcodeFormat.QR_CODE, 1, 0);
loyaltyCardHashMap.put(loyaltyCardId, db.getLoyaltyCard(loyaltyCardId));
db.insertGroup("One");
List<Group> groups = Arrays.asList(db.getGroup("One"));
db.setLoyaltyCardGroups(loyaltyCardId, groups);
loyaltyCardGroups.put(loyaltyCardId, groups);
Utils.saveCardImage(activity.getApplicationContext(), launcherBitmap, loyaltyCardId, true);
Utils.saveCardImage(activity.getApplicationContext(), roundLauncherBitmap, loyaltyCardId, false);
loyaltyCardFrontImages.put(loyaltyCardId, launcherBitmap);
loyaltyCardBackImages.put(loyaltyCardId, roundLauncherBitmap);
// Create card 2
loyaltyCardId = (int) db.insertLoyaltyCard("Card 2", "", null, new BigDecimal(0), null, "123456", null, null, 2, 1);
loyaltyCardHashMap.put(loyaltyCardId, db.getLoyaltyCard(loyaltyCardId));
// Export everything
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
MultiFormatExporter.exportData(activity.getApplicationContext(), db, outputStream, DataFormat.Catima);
MultiFormatExporter.exportData(db, outputStreamWriter, DataFormat.Catima);
// Wipe database
TestHelpers.getEmptyDb(activity);
String outputCsv = "2\r\n" +
"\r\n" +
"_id\r\n" +
"Example\r\n" +
"\r\n" +
"_id,store,note,expiry,balance,balancetype,cardid,headercolor,barcodeid,barcodetype,starstatus\r\n" +
"1,Card 1,Note 1,1618053234,100,USD,1234,1,5432,QR_CODE,0\r\n" +
"\r\n" +
"cardId,groupId\r\n" +
"1,Example\r\n";
// Import everything
MultiFormatImporter.importData(activity.getApplicationContext(), db, new ByteArrayInputStream(outputStream.toByteArray()), DataFormat.Catima, null);
assertEquals(outputCsv, outputStream.toString());
// Ensure everything is there
assertEquals(loyaltyCardHashMap.size(), db.getLoyaltyCardCount());
for (Integer loyaltyCardID : loyaltyCardHashMap.keySet()) {
LoyaltyCard loyaltyCard = loyaltyCardHashMap.get(loyaltyCardID);
LoyaltyCard dbLoyaltyCard = db.getLoyaltyCard(loyaltyCardID);
assertEquals(loyaltyCard.id, dbLoyaltyCard.id);
assertEquals(loyaltyCard.store, dbLoyaltyCard.store);
assertEquals(loyaltyCard.note, dbLoyaltyCard.note);
assertEquals(loyaltyCard.expiry, dbLoyaltyCard.expiry);
assertEquals(loyaltyCard.balance, dbLoyaltyCard.balance);
assertEquals(loyaltyCard.cardId, dbLoyaltyCard.cardId);
assertEquals(loyaltyCard.barcodeId, dbLoyaltyCard.barcodeId);
assertEquals(loyaltyCard.starStatus, dbLoyaltyCard.starStatus);
assertEquals(loyaltyCard.barcodeType, dbLoyaltyCard.barcodeType);
assertEquals(loyaltyCard.balanceType, dbLoyaltyCard.balanceType);
assertEquals(loyaltyCard.headerColor, dbLoyaltyCard.headerColor);
List<Group> emptyGroup = new ArrayList<>();
assertEquals(
groupsToGroupNames(
(List<Group>) Utils.hashmapGetOrDefault(
loyaltyCardGroups,
loyaltyCardID,
emptyGroup,
Integer.class
)
),
groupsToGroupNames(
db.getLoyaltyCardGroups(
loyaltyCardID
)
)
);
Bitmap expectedFrontImage = loyaltyCardFrontImages.get(loyaltyCardID);
Bitmap expectedBackImage = loyaltyCardBackImages.get(loyaltyCardID);
Bitmap actualFrontImage = Utils.retrieveCardImage(activity.getApplicationContext(), Utils.getCardImageFileName(loyaltyCardID, true));
Bitmap actualBackImage = Utils.retrieveCardImage(activity.getApplicationContext(), Utils.getCardImageFileName(loyaltyCardID, false));
if (expectedFrontImage != null) {
assertTrue(expectedFrontImage.sameAs(actualFrontImage));
} else {
assertNull(actualFrontImage);
}
if (expectedBackImage != null) {
assertTrue(expectedBackImage.sameAs(actualBackImage));
} else {
assertNull(actualBackImage);
}
}
}
@Test
public void importV2()
public void importV2CSV()
{
String csvText = "2\n" +
"\n" +
@@ -893,12 +977,13 @@ public class ImportExportTest
"Fashion\n" +
"\n" +
"_id,store,note,expiry,balance,balancetype,cardid,barcodeid,headercolor,barcodetype,starstatus\n" +
"8,Clothes Store,Note about store,,0,,a,,-5317,,0\n" +
"2,Department Store,,1618041729,0,,A,,-9977996,,0\n" +
"3,Grocery Store,,,150,,dhd,,-9977996,,0\n" +
"4,Pharmacy,,,0,,dhshsvshs,,-10902850,,1\n" +
"5,Restaurant,Note about restaurant here,,0,,98765432,23456,-10902850,CODE_128,0\n" +
"6,Shoe Store,,,12.50,EUR,a,-5317,,AZTEC,0\n" +
"1,Card 1,Note 1,1618053234,100,USD,1234,5432,1,QR_CODE,0,\r\n" +
"8,Clothes Store,Note about store,,0,,a,,-5317,,0,\n" +
"2,Department Store,,1618041729,0,,A,,-9977996,,0,\n" +
"3,Grocery Store,,,150,,dhd,,-9977996,,0,\n" +
"4,Pharmacy,,,0,,dhshsvshs,,-10902850,,1,\n" +
"5,Restaurant,Note about restaurant here,,0,,98765432,23456,-10902850,CODE_128,0,\n" +
"6,Shoe Store,,,12.50,EUR,a,-5317,,AZTEC,0,\n" +
"\n" +
"cardId,groupId\n" +
"8,Fashion\n" +
@@ -910,9 +995,9 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(csvText.getBytes(StandardCharsets.UTF_8));
// Import the CSV data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.Catima);
assertEquals(true, result);
assertEquals(6, db.getLoyaltyCardCount());
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Catima, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(7, db.getLoyaltyCardCount());
assertEquals(3, db.getGroupCount());
// Check all groups
@@ -932,6 +1017,21 @@ public class ImportExportTest
assertEquals(Arrays.asList(8, 6), db.getGroupCardIds("Fashion"));
// Check all cards
LoyaltyCard card1 = db.getLoyaltyCard(1);
assertEquals("Card 1", card1.store);
assertEquals("Note 1", card1.note);
assertEquals(new Date(1618053234), card1.expiry);
assertEquals(new BigDecimal("100"), card1.balance);
assertEquals(Currency.getInstance("USD"), card1.balanceType);
assertEquals("1234", card1.cardId);
assertEquals("5432", card1.barcodeId);
assertEquals(BarcodeFormat.QR_CODE, card1.barcodeType);
assertEquals(1, (long) card1.headerColor);
assertEquals(0, card1.starStatus);
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card1.id, true));
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card1.id, false));
LoyaltyCard card8 = db.getLoyaltyCard(8);
assertEquals("Clothes Store", card8.store);
@@ -944,6 +1044,8 @@ public class ImportExportTest
assertEquals(null, card8.barcodeType);
assertEquals(-5317, (long) card8.headerColor);
assertEquals(0, card8.starStatus);
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card8.id, true));
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card8.id, false));
LoyaltyCard card2 = db.getLoyaltyCard(2);
@@ -957,6 +1059,8 @@ public class ImportExportTest
assertEquals(null, card2.barcodeType);
assertEquals(-9977996, (long) card2.headerColor);
assertEquals(0, card2.starStatus);
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card2.id, true));
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card2.id, false));
LoyaltyCard card3 = db.getLoyaltyCard(3);
@@ -970,6 +1074,8 @@ public class ImportExportTest
assertEquals(null, card3.barcodeType);
assertEquals(-9977996, (long) card3.headerColor);
assertEquals(0, card3.starStatus);
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card3.id, true));
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card3.id, false));
LoyaltyCard card4 = db.getLoyaltyCard(4);
@@ -983,6 +1089,8 @@ public class ImportExportTest
assertEquals(null, card4.barcodeType);
assertEquals(-10902850, (long) card4.headerColor);
assertEquals(1, card4.starStatus);
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card4.id, true));
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card4.id, false));
LoyaltyCard card5 = db.getLoyaltyCard(5);
@@ -996,6 +1104,8 @@ public class ImportExportTest
assertEquals(BarcodeFormat.CODE_128, card5.barcodeType);
assertEquals(-10902850, (long) card5.headerColor);
assertEquals(0, card5.starStatus);
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card5.id, true));
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card5.id, false));
LoyaltyCard card6 = db.getLoyaltyCard(6);
@@ -1009,6 +1119,119 @@ public class ImportExportTest
assertEquals(BarcodeFormat.AZTEC, card6.barcodeType);
assertEquals(null, card6.headerColor);
assertEquals(0, card6.starStatus);
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card6.id, true));
assertEquals(null, Utils.retrieveCardImage(activity.getApplicationContext(), card6.id, false));
TestHelpers.getEmptyDb(activity);
}
@Test
public void importFidme() {
InputStream inputStream = getClass().getResourceAsStream("fidme.zip");
// Import the Fidme data
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Fidme, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(3, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
assertEquals("Hema", card.store);
assertEquals("2021-03-24 18:35:08 UTC", card.note);
assertEquals(null, card.expiry);
assertEquals(new BigDecimal("0"), card.balance);
assertEquals(null, card.balanceType);
assertEquals("82825292629272726", card.cardId);
assertEquals(null, card.barcodeId);
assertEquals(null, card.barcodeType);
assertEquals(0, card.starStatus);
card = db.getLoyaltyCard(2);
assertEquals("test", card.store);
assertEquals("Test\n2021-03-24 18:34:19 UTC", card.note);
assertEquals(null, card.expiry);
assertEquals(new BigDecimal("0"), card.balance);
assertEquals(null, card.balanceType);
assertEquals("123456", card.cardId);
assertEquals(null, card.barcodeId);
assertEquals(null, card.barcodeType);
assertEquals(0, card.starStatus);
card = db.getLoyaltyCard(3);
assertEquals("Albert Heijn", card.store);
assertEquals("Bonus Kaart\n2021-03-24 16:47:47 UTC\nFirst Last", card.note);
assertEquals(null, card.expiry);
assertEquals(new BigDecimal("0"), card.balance);
assertEquals(null, card.balanceType);
assertEquals("123435363634", card.cardId);
assertEquals(null, card.barcodeId);
assertEquals(null, card.barcodeType);
assertEquals(0, card.starStatus);
TestHelpers.getEmptyDb(activity);
}
@Test
public void importStocard() throws IOException {
InputStream inputStream = getClass().getResourceAsStream("stocard.zip");
// Import the Stocard data
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Stocard, null);
assertEquals(ImportExportResult.BadPassword, result);
assertEquals(0, db.getLoyaltyCardCount());
inputStream = getClass().getResourceAsStream("stocard.zip");
result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.Stocard, "da811b40a4dac56f0cbb2d99b21bbb9a".toCharArray());
assertEquals(ImportExportResult.Success, result);
assertEquals(3, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);
assertEquals("GAMMA", card.store);
assertEquals("", card.note);
assertEquals(null, card.expiry);
assertEquals(new BigDecimal("0"), card.balance);
assertEquals(null, card.balanceType);
assertEquals("55555", card.cardId);
assertEquals(null, card.barcodeId);
assertEquals(null, card.barcodeType);
assertEquals(0, card.starStatus);
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 1, true));
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 1, false));
card = db.getLoyaltyCard(2);
assertEquals("Air Miles", card.store);
assertEquals("szjsbs", card.note);
assertEquals(null, card.expiry);
assertEquals(new BigDecimal("0"), card.balance);
assertEquals(null, card.balanceType);
assertEquals("7649484", card.cardId);
assertEquals(null, card.barcodeId);
assertEquals(null, card.barcodeType);
assertEquals(0, card.starStatus);
assertTrue(BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-front.jpg")).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), 2, true)));
assertTrue(BitmapFactory.decodeStream(getClass().getResourceAsStream("stocard-back.jpg")).sameAs(Utils.retrieveCardImage(activity.getApplicationContext(), 2, false)));
card = db.getLoyaltyCard(3);
assertEquals("", card.store);
assertEquals("", card.note);
assertEquals(null, card.expiry);
assertEquals(new BigDecimal("0"), card.balance);
assertEquals(null, card.balanceType);
assertEquals("(01)09010374000019(21)02097564604859211217(10)01231287693", card.cardId);
assertEquals(null, card.barcodeId);
assertEquals(BarcodeFormat.RSS_EXPANDED, card.barcodeType);
assertEquals(0, card.starStatus);
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 3, true));
assertNull(Utils.retrieveCardImage(activity.getApplicationContext(), 3, false));
TestHelpers.getEmptyDb(activity);
}
@@ -1024,6 +1247,7 @@ public class ImportExportTest
" \"expires\": null,\n" +
" \"removeOnceExpired\": true,\n" +
" \"balance\": null,\n" +
" \"balanceMilliunits\": null,\n" +
" \"color\": \"GREY\"\n" +
" },\n" +
" {\n" +
@@ -1033,7 +1257,8 @@ public class ImportExportTest
" \"codeType\": \"CODE39\",\n" +
" \"expires\": \"2021-03-26T00:00:00.000\",\n" +
" \"removeOnceExpired\": true,\n" +
" \"balance\": 3.5,\n" +
" \"balance\": null,\n" +
" \"balanceMilliunits\": 3500,\n" +
" \"color\": \"PURPLE\"\n" +
" }\n" +
"]";
@@ -1041,8 +1266,8 @@ public class ImportExportTest
ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonText.getBytes(StandardCharsets.UTF_8));
// Import the Voucher Vault data
boolean result = MultiFormatImporter.importData(db, inputStream, DataFormat.VoucherVault);
assertTrue(result);
ImportExportResult result = MultiFormatImporter.importData(activity.getApplicationContext(), db, inputStream, DataFormat.VoucherVault, null);
assertEquals(ImportExportResult.Success, result);
assertEquals(2, db.getLoyaltyCardCount());
LoyaltyCard card = db.getLoyaltyCard(1);

View File

@@ -14,6 +14,7 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.InvalidObjectException;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.util.Currency;
import java.util.Date;
@@ -37,12 +38,11 @@ public class ImportURITest {
}
@Test
public void ensureNoDataLoss() throws InvalidObjectException
{
public void ensureNoDataLoss() throws InvalidObjectException, UnsupportedEncodingException {
// Generate card
Date date = new Date();
db.insertLoyaltyCard("store", "note", date, new BigDecimal("100"), null, BarcodeFormat.UPC_E.toString(), BarcodeFormat.UPC_A.toString(), BarcodeFormat.QR_CODE, Color.BLACK, 1);
db.insertLoyaltyCard("store", "This note contains evil symbols like & and = that will break the parser if not escaped right $#!%()*+;:á", date, new BigDecimal("100"), null, BarcodeFormat.UPC_E.toString(), BarcodeFormat.UPC_A.toString(), BarcodeFormat.QR_CODE, Color.BLACK, 1);
// Get card
LoyaltyCard card = db.getLoyaltyCard(1);
@@ -68,8 +68,7 @@ public class ImportURITest {
}
@Test
public void ensureNoCrashOnMissingHeaderFields() throws InvalidObjectException
{
public void ensureNoCrashOnMissingHeaderFields() throws InvalidObjectException, UnsupportedEncodingException {
// Generate card
db.insertLoyaltyCard("store", "note", null, new BigDecimal("10.00"), Currency.getInstance("EUR"), BarcodeFormat.UPC_A.toString(), null, BarcodeFormat.QR_CODE, null, 0);
@@ -110,35 +109,48 @@ public class ImportURITest {
@Test
public void failToParseBadData()
{
try {
//"stare" instead of store
importURIHelper.parse(Uri.parse("https://brarcher.github.io/loyalty-card-locker/share?stare=store&note=note&cardid=12345&barcodetype=ITF&headercolor=-416706"));
assertTrue(false); // Shouldn't get here
} catch(InvalidObjectException ex) {
// Desired behaviour
String[] urls = new String[3];
urls[0] = "https://brarcher.github.io/loyalty-card-locker/share?stare=store&note=note&cardid=12345&barcodetype=ITF&headercolor=-416706";
urls[1] = "https://thelastproject.github.io/Catima/share#stare%3Dstore%26note%3Dnote%26balance%3D0%26cardid%3D12345%26barcodetype%3DITF%26headercolor%3D-416706";
urls[2] = "https://catima.app/share#stare%3Dstore%26note%3Dnote%26balance%3D0%26cardid%3D12345%26barcodetype%3DITF%26headercolor%3D-416706";
for (String url : urls) {
try {
//"stare" instead of store
importURIHelper.parse(Uri.parse(url));
assertTrue(false); // Shouldn't get here
} catch (InvalidObjectException ex) {
// Desired behaviour
}
}
}
@Test
public void parseAdditionalUnforeseenData()
{
LoyaltyCard parsedCard = null;
try {
parsedCard = importURIHelper.parse(Uri.parse("https://brarcher.github.io/loyalty-card-locker/share?store=store&note=note&cardid=12345&barcodetype=ITF&headercolor=-416706&headertextcolor=-1&notforeseen=no"));
} catch (InvalidObjectException e) {
e.printStackTrace();
}
public void parseAdditionalUnforeseenData() {
String[] urls = new String[3];
urls[0] = "https://brarcher.github.io/loyalty-card-locker/share?store=store&note=note&cardid=12345&barcodetype=ITF&headercolor=-416706&headertextcolor=-1&notforeseen=no";
urls[1] = "https://thelastproject.github.io/Catima/share#store%3Dstore%26note%3Dnote%26balance%3D0%26cardid%3D12345%26barcodetype%3DITF%26headercolor%3D-416706%26notforeseen%3Dno";
urls[2] = "https://catima.app/share#store%3Dstore%26note%3Dnote%26balance%3D0%26cardid%3D12345%26barcodetype%3DITF%26headercolor%3D-416706%26notforeseen%3Dno";
// Compare everything
assertEquals("store", parsedCard.store);
assertEquals("note", parsedCard.note);
assertEquals(null, parsedCard.expiry);
assertEquals(new BigDecimal("0"), parsedCard.balance);
assertEquals(null, parsedCard.balanceType);
assertEquals("12345", parsedCard.cardId);
assertEquals(null, parsedCard.barcodeId);
assertEquals(BarcodeFormat.ITF, parsedCard.barcodeType);
assertEquals(Integer.valueOf(-416706), parsedCard.headerColor);
assertEquals(0, parsedCard.starStatus);
for (String url : urls) {
LoyaltyCard parsedCard = null;
try {
parsedCard = importURIHelper.parse(Uri.parse(url));
} catch (InvalidObjectException e) {
e.printStackTrace();
}
// Compare everything
assertEquals("store", parsedCard.store);
assertEquals("note", parsedCard.note);
assertEquals(null, parsedCard.expiry);
assertEquals(new BigDecimal("0"), parsedCard.balance);
assertEquals(null, parsedCard.balanceType);
assertEquals("12345", parsedCard.cardId);
assertEquals(null, parsedCard.barcodeId);
assertEquals(BarcodeFormat.ITF, parsedCard.barcodeType);
assertEquals(Integer.valueOf(-416706), parsedCard.headerColor);
assertEquals(0, parsedCard.starStatus);
}
}
}

View File

@@ -4,7 +4,6 @@ import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Color;
import android.view.View;
import android.widget.ImageView;
@@ -29,11 +28,9 @@ import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;
import protect.card_locker.preferences.Settings;
import static android.os.Looper.getMainLooper;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.robolectric.Shadows.shadowOf;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 23)

View File

@@ -9,7 +9,10 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
@@ -24,6 +27,7 @@ import android.widget.SeekBar;
import android.widget.TextView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.google.android.material.textfield.TextInputLayout;
import com.google.zxing.BarcodeFormat;
@@ -48,6 +52,7 @@ import java.text.ParseException;
import java.util.Currency;
import java.util.Date;
import androidx.appcompat.app.AlertDialog;
import androidx.core.widget.TextViewCompat;
import androidx.preference.PreferenceManager;
@@ -55,6 +60,7 @@ import static android.os.Looper.getMainLooper;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.robolectric.Shadows.shadowOf;
@@ -76,6 +82,13 @@ public class LoyaltyCardViewActivityTest
;
}
enum FieldTypeView
{
TextView,
TextInputLayout,
ImageView
}
@Before
public void setUp()
{
@@ -277,20 +290,27 @@ public class LoyaltyCardViewActivityTest
}
private void checkFieldProperties(final Activity activity, final int id, final int visibility,
final String contents)
final Object contents, final FieldTypeView fieldType)
{
final View view = activity.findViewById(id);
assertNotNull(view);
assertEquals(visibility, view.getVisibility());
if(contents != null)
{
try {
TextView textView = (TextView) view;
assertEquals(contents, textView.getText().toString());
} catch (ClassCastException e) {
TextInputLayout textView = (TextInputLayout) view;
assertEquals(contents, textView.getEditText().getText().toString());
if (fieldType == FieldTypeView.TextView) {
TextView textView = (TextView) view;
assertEquals(contents, textView.getText().toString());
} else if (fieldType == FieldTypeView.TextInputLayout) {
TextInputLayout textView = (TextInputLayout) view;
assertEquals(contents, textView.getEditText().getText().toString());
} else if (fieldType == FieldTypeView.ImageView) {
ImageView imageView = (ImageView) view;
Bitmap image = null;
if (imageView.getTag() != null) {
image = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
}
assertEquals(contents, image);
} else {
throw new UnsupportedOperationException();
}
}
@@ -298,25 +318,109 @@ public class LoyaltyCardViewActivityTest
final String store, final String note, final String expiryString,
final String balanceString, final String balanceTypeString,
final String cardId, final String barcodeId,
final String barcodeType)
final String barcodeType, final Bitmap frontImage,
final Bitmap backImage)
{
if(mode == ViewMode.VIEW_CARD)
{
checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId);
checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId, FieldTypeView.TextView);
}
else
{
int editVisibility = View.VISIBLE;
checkFieldProperties(activity, R.id.storeNameEdit, editVisibility, store);
checkFieldProperties(activity, R.id.noteEdit, editVisibility, note);
checkFieldProperties(activity, R.id.expiryView, editVisibility, expiryString);
checkFieldProperties(activity, R.id.balanceField, editVisibility, balanceString);
checkFieldProperties(activity, R.id.balanceCurrencyField, editVisibility, balanceTypeString);
checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId);
checkFieldProperties(activity, R.id.barcodeIdField, View.VISIBLE, barcodeId);
checkFieldProperties(activity, R.id.barcodeTypeField, View.VISIBLE, barcodeType);
checkFieldProperties(activity, R.id.barcode, View.VISIBLE, null);
checkFieldProperties(activity, R.id.storeNameEdit, editVisibility, store, FieldTypeView.TextView);
checkFieldProperties(activity, R.id.noteEdit, editVisibility, note, FieldTypeView.TextView);
checkFieldProperties(activity, R.id.expiryView, editVisibility, expiryString, FieldTypeView.TextInputLayout);
checkFieldProperties(activity, R.id.balanceField, editVisibility, balanceString, FieldTypeView.TextView);
checkFieldProperties(activity, R.id.balanceCurrencyField, editVisibility, balanceTypeString, FieldTypeView.TextView);
checkFieldProperties(activity, R.id.cardIdView, View.VISIBLE, cardId, FieldTypeView.TextView);
checkFieldProperties(activity, R.id.barcodeIdField, View.VISIBLE, barcodeId, FieldTypeView.TextView);
checkFieldProperties(activity, R.id.barcodeTypeField, View.VISIBLE, barcodeType, FieldTypeView.TextView);
//checkFieldProperties(activity, R.id.barcode, View.VISIBLE, null, FieldTypeView.ImageView);
checkFieldProperties(activity, R.id.frontImage, View.VISIBLE, frontImage, FieldTypeView.ImageView);
checkFieldProperties(activity, R.id.backImage, View.VISIBLE, backImage, FieldTypeView.ImageView);
}
}
@Test
public void noDataLossOnResume()
{
registerMediaStoreIntentHandler();
for(boolean newCard : new boolean[] {false, true}) {
System.out.println();
System.out.println("=====");
System.out.println("New card? " + newCard);
System.out.println("=====");
System.out.println();
ActivityController activityController;
if (!newCard) {
activityController = createActivityWithLoyaltyCard(true);
} else {
activityController = Robolectric.buildActivity(LoyaltyCardEditActivity.class).create();
}
Activity activity = (Activity) activityController.get();
final Context context = activity.getApplicationContext();
DBHelper db = TestHelpers.getEmptyDb(activity);
if (!newCard) {
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, Color.BLACK, 0);
}
activityController.start();
activityController.visible();
activityController.resume();
shadowOf(getMainLooper()).idle();
// Check default settings
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, newCard ? "" : "store", newCard ? "" : "note", context.getString(R.string.never), "0", context.getString(R.string.points), newCard ? "" : EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), newCard ? context.getString(R.string.noBarcode) : EAN_BARCODE_TYPE.name(), null, null);
// Change everything
final EditText storeField = activity.findViewById(R.id.storeNameEdit);
final EditText noteField = activity.findViewById(R.id.noteEdit);
final EditText expiryField = activity.findViewById(R.id.expiryField);
final EditText balanceField = activity.findViewById(R.id.balanceField);
final EditText balanceTypeField = activity.findViewById(R.id.balanceCurrencyField);
final EditText cardIdField = activity.findViewById(R.id.cardIdView);
final EditText barcodeField = activity.findViewById(R.id.barcodeIdField);
final EditText barcodeTypeField = activity.findViewById(R.id.barcodeTypeField);
final ImageView frontImageView = activity.findViewById(R.id.frontImage);
final ImageView backImageView = activity.findViewById(R.id.backImage);
Currency currency = Currency.getInstance("EUR");
Date expiryDate = new Date();
Bitmap frontBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.circle);
Bitmap backBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.save_24dp);
storeField.setText("correct store");
noteField.setText("correct note");
LoyaltyCardEditActivity.formatExpiryField(context, expiryField, expiryDate);
balanceField.setText("100");
balanceTypeField.setText(currency.getSymbol());
cardIdField.setText("12345678");
barcodeField.setText("87654321");
barcodeTypeField.setText(BarcodeFormat.QR_CODE.name());
LoyaltyCardEditActivity.setCardImage(frontImageView, frontBitmap);
LoyaltyCardEditActivity.setCardImage(backImageView, backBitmap);
shadowOf(getMainLooper()).idle();
// Check if changed
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", BarcodeFormat.QR_CODE.name(), frontBitmap, backBitmap);
// Resume
activityController.pause();
activityController.resume();
shadowOf(getMainLooper()).idle();
// Check if no changes lost
checkAllFields(activity, newCard ? ViewMode.ADD_CARD : ViewMode.UPDATE_CARD, "correct store", "correct note", DateFormat.getDateInstance(DateFormat.LONG).format(expiryDate), "100.00", currency.getSymbol(), "12345678", "87654321", BarcodeFormat.QR_CODE.name(), frontBitmap, backBitmap);
}
}
@@ -331,7 +435,7 @@ public class LoyaltyCardViewActivityTest
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never) , "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),"");
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never) , "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),context.getString(R.string.noBarcode), null, null);
}
@Test
@@ -391,12 +495,12 @@ public class LoyaltyCardViewActivityTest
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),"");
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),context.getString(R.string.noBarcode), null, null);
// Complete barcode capture successfully
captureBarcodeWithResult(activity, true);
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString(), null, null);
shadowOf(getMainLooper()).idle();
@@ -415,12 +519,14 @@ public class LoyaltyCardViewActivityTest
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId), "");
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId), context.getString(R.string.noBarcode), null, null);
// Complete barcode capture in failure
captureBarcodeWithResult(activity, false);
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),"");
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),context.getString(R.string.noBarcode), null, null);
}
@Test
@@ -434,12 +540,12 @@ public class LoyaltyCardViewActivityTest
LoyaltyCardEditActivity activity = (LoyaltyCardEditActivity) activityController.get();
final Context context = activity.getApplicationContext();
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),"");
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), "", context.getString(R.string.sameAsCardId),context.getString(R.string.noBarcode), null, null);
// Complete barcode capture successfully
captureBarcodeWithResult(activity, true);
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.ADD_CARD, "", "", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString(), null, null);
// Cancel the loyalty card creation
assertEquals(false, activity.isFinishing());
@@ -495,7 +601,7 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString(), null, null);
db.close();
}
@@ -514,7 +620,7 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.VIEW_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.VIEW_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString(), null, null);
db.close();
}
@@ -533,12 +639,12 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
// Complete barcode capture successfully
captureBarcodeWithResult(activity, true);
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString(), null, null);
db.close();
}
@@ -557,12 +663,12 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
// Complete barcode capture successfully
captureBarcodeWithResult(activity, true);
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", null, "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString(), null, null);
// Cancel the loyalty card creation
assertEquals(false, activity.isFinishing());
@@ -595,7 +701,7 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
// Set date to today
MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField);
@@ -609,7 +715,7 @@ public class LoyaltyCardViewActivityTest
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@@ -628,13 +734,13 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
// Set date to never
MaterialAutoCompleteTextView expiryField = activity.findViewById(R.id.expiryField);
expiryField.setText(expiryField.getAdapter().getItem(0).toString(), false);
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@@ -653,7 +759,7 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
// Set balance to 10 points
EditText balanceField = activity.findViewById(R.id.balanceField);
@@ -682,7 +788,7 @@ public class LoyaltyCardViewActivityTest
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "10.00", "", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", DateFormat.getDateInstance(DateFormat.LONG).format(new Date()), "10.00", "", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@@ -704,7 +810,7 @@ public class LoyaltyCardViewActivityTest
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "10.00", "$", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "10.00", "$", EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
shadowOf(getMainLooper()).idle();
@@ -726,7 +832,147 @@ public class LoyaltyCardViewActivityTest
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", "", EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", "", EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@Test
public void startWithLoyaltyCardSameAsCardIDUpdateBarcodeID()
{
ActivityController activityController = createActivityWithLoyaltyCard(true);
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
DBHelper db = TestHelpers.getEmptyDb(activity);
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, Color.BLACK, 0);
activityController.start();
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
// Change barcode ID
EditText barcodeField = activity.findViewById(R.id.barcodeIdField);
barcodeField.setText("123456");
// Switch away from card ID and ensure no dialog appears
TabLayout tabs = activity.findViewById(R.id.tabs);
tabs.getTabAt(2).select();
shadowOf(getMainLooper()).idle();
AlertDialog updateBarcodeIdDialog = (AlertDialog) (ShadowDialog.getLatestDialog());
assertNull(updateBarcodeIdDialog);
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, "123456", EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@Test
public void startWithLoyaltyCardSameAsCardIDUpdateCardID()
{
ActivityController activityController = createActivityWithLoyaltyCard(true);
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
DBHelper db = TestHelpers.getEmptyDb(activity);
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, null, EAN_BARCODE_TYPE, Color.BLACK, 0);
activityController.start();
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
// Change card ID
EditText cardIdField = activity.findViewById(R.id.cardIdView);
cardIdField.setText("123456");
// Switch away from card ID and ensure no dialog appears
TabLayout tabs = activity.findViewById(R.id.tabs);
tabs.getTabAt(2).select();
shadowOf(getMainLooper()).idle();
AlertDialog updateBarcodeIdDialog = (AlertDialog) (ShadowDialog.getLatestDialog());
assertNull(updateBarcodeIdDialog);
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), "123456", context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@Test
public void startWithLoyaltyCardDifferentFromCardIDUpdateCardIDUpdate()
{
ActivityController activityController = createActivityWithLoyaltyCard(true);
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
DBHelper db = TestHelpers.getEmptyDb(activity);
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, "123456", EAN_BARCODE_TYPE, Color.BLACK, 0);
activityController.start();
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, "123456", EAN_BARCODE_TYPE.toString(), null, null);
// Change card ID
EditText cardIdField = activity.findViewById(R.id.cardIdView);
cardIdField.setText("654321");
shadowOf(getMainLooper()).idle();
// Switch away from card ID and ensure the dialog appears
TabLayout tabs = activity.findViewById(R.id.tabs);
tabs.getTabAt(2).select();
shadowOf(getMainLooper()).idle();
AlertDialog updateBarcodeIdDialog = (AlertDialog) ShadowDialog.getLatestDialog();
assertNotNull(updateBarcodeIdDialog);
updateBarcodeIdDialog.getButton(DatePickerDialog.BUTTON_POSITIVE).performClick();
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), "654321", context.getString(R.string.sameAsCardId), EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@Test
public void startWithLoyaltyCardDifferentFromCardIDUpdateCardIDDoNotUpdate()
{
ActivityController activityController = createActivityWithLoyaltyCard(true);
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
DBHelper db = TestHelpers.getEmptyDb(activity);
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, EAN_BARCODE_DATA, "123456", EAN_BARCODE_TYPE, Color.BLACK, 0);
activityController.start();
activityController.visible();
activityController.resume();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), EAN_BARCODE_DATA, "123456", EAN_BARCODE_TYPE.toString(), null, null);
// Change card ID
EditText cardIdField = activity.findViewById(R.id.cardIdView);
cardIdField.setText("654321");
shadowOf(getMainLooper()).idle();
// Switch away from card ID and ensure the dialog appears
TabLayout tabs = activity.findViewById(R.id.tabs);
tabs.getTabAt(2).select();
shadowOf(getMainLooper()).idle();
AlertDialog updateBarcodeIdDialog = (AlertDialog) (ShadowDialog.getLatestDialog());
assertNotNull(updateBarcodeIdDialog);
updateBarcodeIdDialog.getButton(DatePickerDialog.BUTTON_NEGATIVE).performClick();
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), "654321", "123456", EAN_BARCODE_TYPE.toString(), null, null);
db.close();
}
@@ -869,13 +1115,13 @@ public class LoyaltyCardViewActivityTest
activityController.resume();
// First check if the card is as expected
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, null, BARCODE_TYPE.toString());
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), BARCODE_TYPE.toString(), null, null);
// Complete empty barcode selection successfully
selectBarcodeWithResult(activity, BARCODE_DATA, "", true);
// Check if the barcode type is NO_BARCODE as expected
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, null, context.getString(R.string.noBarcode));
checkAllFields(activity, ViewMode.UPDATE_CARD, "store", "note", context.getString(R.string.never), "0", context.getString(R.string.points), BARCODE_DATA, context.getString(R.string.sameAsCardId), context.getString(R.string.noBarcode), null, null);
assertEquals(View.GONE, activity.findViewById(R.id.barcodeLayout).getVisibility());
// Check if the special NO_BARCODE string doesn't get saved
@@ -966,7 +1212,7 @@ public class LoyaltyCardViewActivityTest
Activity activity = (Activity) activityController.get();
DBHelper db = TestHelpers.getEmptyDb(activity);
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, Color.BLACK,0);
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, null, BARCODE_TYPE, Color.BLACK, 0);
activityController.start();
activityController.visible();
activityController.resume();
@@ -1094,6 +1340,50 @@ public class LoyaltyCardViewActivityTest
db.close();
}
@Test
public void checkNoBarcodeFullscreenWorkflow()
{
ActivityController activityController = createActivityWithLoyaltyCard(false);
Activity activity = (Activity)activityController.get();
DBHelper db = TestHelpers.getEmptyDb(activity);
db.insertLoyaltyCard("store", "note", null, new BigDecimal("0"), null, BARCODE_DATA, null, null, Color.BLACK, 0);
activityController.start();
activityController.visible();
activityController.resume();
assertEquals(false, activity.isFinishing());
ImageView barcodeImage = activity.findViewById(R.id.barcode);
View collapsingToolbarLayout = activity.findViewById(R.id.collapsingToolbarLayout);
View bottomSheet = activity.findViewById(R.id.bottom_sheet);
ImageButton maximizeButton = activity.findViewById(R.id.maximizeButton);
ImageButton minimizeButton = activity.findViewById(R.id.minimizeButton);
FloatingActionButton editButton = activity.findViewById(R.id.fabEdit);
SeekBar barcodeScaler = activity.findViewById(R.id.barcodeScaler);
// Android should not be in fullscreen mode
int uiOptions = activity.getWindow().getDecorView().getSystemUiVisibility();
assertNotEquals(uiOptions | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY, uiOptions);
assertNotEquals(uiOptions | View.SYSTEM_UI_FLAG_FULLSCREEN, uiOptions);
// Elements should be visible (except minimize/maximize buttons and barcode and scaler)
assertEquals(View.VISIBLE, collapsingToolbarLayout.getVisibility());
assertEquals(View.VISIBLE, bottomSheet.getVisibility());
assertEquals(View.GONE, maximizeButton.getVisibility());
assertEquals(View.GONE, minimizeButton.getVisibility());
assertEquals(View.VISIBLE, editButton.getVisibility());
assertEquals(View.GONE, barcodeScaler.getVisibility());
// Pressing back when not in full screen should finish activity
activity.onBackPressed();
shadowOf(getMainLooper()).idle();
assertEquals(true, activity.isFinishing());
db.close();
}
@Test
public void importCard()
{
@@ -1115,7 +1405,7 @@ public class LoyaltyCardViewActivityTest
shadowOf(getMainLooper()).idle();
checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "$", "123456", null, "AZTEC");
checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", DateFormat.getDateInstance(DateFormat.LONG).format(date), "10.00", "$", "123456", context.getString(R.string.sameAsCardId), "AZTEC", null, null);
assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor());
}
@@ -1136,7 +1426,7 @@ public class LoyaltyCardViewActivityTest
Activity activity = (Activity)activityController.get();
final Context context = activity.getApplicationContext();
checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", context.getString(R.string.never), "0", context.getString(R.string.points), "123456", null, "AZTEC");
checkAllFields(activity, ViewMode.ADD_CARD, "Example Store", "", context.getString(R.string.never), "0", context.getString(R.string.points), "123456", context.getString(R.string.sameAsCardId), "AZTEC", null, null);
assertEquals(-416706, ((ColorDrawable) activity.findViewById(R.id.thumbnail).getBackground()).getColor());
}
}

View File

@@ -3,11 +3,9 @@ package protect.card_locker;
import android.app.Activity;
import android.content.ComponentName;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.graphics.Color;
import android.view.Menu;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import com.google.android.material.tabs.TabLayout;
@@ -20,7 +18,6 @@ import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowActivity;
import org.w3c.dom.Text;
import java.math.BigDecimal;
import java.util.ArrayList;

View File

@@ -1,11 +1,31 @@
package protect.card_locker;
import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import java.io.FileNotFoundException;
public class TestHelpers {
static public DBHelper getEmptyDb(Activity activity) {
DBHelper db = new DBHelper(activity);
// Make sure no files remain
Cursor cursor = db.getLoyaltyCardCursor();
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
int cardID = cursor.getColumnIndex(DBHelper.LoyaltyCardDbIds.ID);
try {
Utils.saveCardImage(activity.getApplicationContext(), null, cardID, true);
} catch (FileNotFoundException ignored) {}
try {
Utils.saveCardImage(activity.getApplicationContext(), null, cardID, false);
} catch (FileNotFoundException ignored) {}
cursor.moveToNext();
}
// Make sure DB is empty
SQLiteDatabase database = db.getWritableDatabase();
database.execSQL("delete from " + DBHelper.LoyaltyCardDbIds.TABLE);

View File

Binary file not shown.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

Binary file not shown.

View File

@@ -9,7 +9,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath 'com.android.tools.build:gradle:7.0.0'
classpath 'gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.0'
// NOTE: Do not place your application dependencies here; they belong
@@ -20,7 +20,6 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
maven { url "https://jitpack.io" }
gradlePluginPortal()
}

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
catima.app

View File

@@ -32,6 +32,7 @@ Supported barcodes:
- PDF_417
- QR_CODE
- UPC_A
- UPC_E
# Screenshots

View File

@@ -1,7 +1,23 @@
Bist du es auch leid, beim Bezahlen an der Kasse im Supermarkt jedes mal nach dieser Bonuskarte aus Plastik zu suchen? Genervt davon, dass die Brieftasche durch die ganzen Plastikkarten gefühlte fünf Meter dick ist? Wäre es nicht toll, eine <i>freie</i> Lösung für das Problem zu haben, die deine Daten nicht noch zusätzlich mit weiteren Anbietern teilt?
<i>Catima</i> ist eine Anwendung, die deine auf Strichcodes basierenden Kundenkarten auf deinem Smartphone verwaltet. <i>Catima</i> ist quelloffen und kann eines richtig gut: Deine Karten verwalten!
Neue Karten können mit Leichtigkeit hinzugefügt werden. Entweder nutzt du die Kamera deines Smartphones, um den Strichcode direkt einzulesen oder du gibst die unter selbigem befindliche Ziffernfolge von Hand ein. An der Kasse zeigt <i>Catima</i> sodann den Strichcode auf dem Bildschirm deines Mobiltelefones an, sodass er vom Scanner gelesen werden kann. Sollte der Scanner dabei doch einmal streiken (was selten vorkommt und wenn, dann meist nur bei älteren Scanner-Modellen), kann der Verkäufer die ebenfalls angezeigten Ziffern wiederum auch händisch erfassen.
Die Anwendung benötigt nur wenige Berechtigungen und greift nie auf das Internet zu. <i>Catima</i> bietet auch die Möglichkeit, deine Kartensammlung zu exportieren sowie zu importieren. Damit kannst du die Daten z. B. auch auf ein anderes Gerät übertragen oder etwa nach einem Werksreset wieder neu einlesen.
Schluss mit der Suche nach Plastik-Belohnungskarten beim Bezahlen im Geschäft oder Webshop.
<b>Scanne EAN-Codes mit der Kamera deines Geräts, du brauchst keine Karten mehr.</b>
😺
Du brauchst keine Geldbörse, oder mach sie federleicht für Wertsachen.
😺
Mit diesem unverzichtbaren (EDC)Werkzeug kannst du nutzloses Plastik durch Bargeld ersetzen.
😺
- Vermeide Spionage mit sehr wenigen Berechtigungen. Kein Internetzugang und keine Werbung.
- Füge Karten oder Codes mit Namen und anpassbaren Farben hinzu.
- Manuelle Code-Eingabe, wenn kein Barcode gespeichert ist oder nicht funktioniert.
- Importiere Karten und Codes aus Dateien, Catima, Loyalty Card Keychain, Voucher Vault und FidMe.
- Erstelle ein Backup aller Karten und übertrage diese auf ein neues Gerät, wenn du möchtest.
- Teile Gutscheine, exklusive Angebote, Werbeaktionscodes oder Karten und Codes mit jeder Applikation.
- Dunkler Modus und Zugänglichkeitsoptionen für sehbehinderte Nutzer.
- Von der Freien-Software-Gemeinschaft für alle Menschen gemacht.
- Lokalisierte, eigenhändige Übersetzungen für mehr als 20 Sprachen.
- Kostenlos, unterstützt durch Gemeinschaftsbeiträge.
- Verwende, studiere, ändere und teile es, wie du willst; <i>mit Allen</i>.
- Nicht nur freie und quelloffene Software. <i>Copylefted</i> freie Software (GPLv3+) Kartenverwaltung.
😺
Vereinfache dein Leben und Einkäufe, und verliere nie wieder eine Papierrechung, eine Geschenkkarte für die Bezahlung im Geschäft oder ein Flugticket.
Habe deine Prämien und Boni immer bei der Hand, und spare unterwegs.
😺

View File

@@ -1 +1 @@
Keine Plastikverschmutzung. Speichere Rabatte, Mitgliedskarten und Strichcodes.
Für Ihre EAN-Codes, Mitgliedschaften, Treueprogramme, Rabattmarkerl und Tickets.

View File

@@ -1 +1 @@
Catima Kundenkarten- und Ticketverwaltung
Catima — Das freie Kartenetui

View File

@@ -8,7 +8,7 @@ With this essential everyday carry (EDC) tool you can replace useless plastic wi
- Avoid spying with very few permissions. No Internet access and no ads.
- Add cards or codes with names and customizable colours.
- Manual code entry if there is no barcode to store, or it can't be used.
- Import cards and codes from files, Catima, Loyalty Card Keychain, Voucher Vault, and FidMe.
- Import cards and codes from files, Catima, FidMe, Loyalty Card Keychain, Stocard and Voucher Vault.
- Make a backup of all your cards and transfer them to a new device if you want.
- Share coupons, exclusive offers, promo codes, or cards and codes using any app.
- Dark theme and accessibility options for vision impaired users.

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 67 KiB

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