Compare commits
924
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9738fbe52e | ||
|
|
64e0d5de03
|
||
|
|
2cf32128ba
|
||
|
|
2b900afc30
|
||
|
|
d059d031bd
|
||
|
|
caff1a07cc
|
||
|
|
fe766c8820
|
||
|
|
1e6a374e96
|
||
|
|
02b92308d0
|
||
|
|
a9c8a89aac | ||
|
|
a69d5644a5 | ||
|
|
eab348d187 | ||
|
|
1877455c70 | ||
|
|
0b6074aaba | ||
|
|
875d809189 | ||
|
|
3960f5cb5d | ||
|
|
07368a408c | ||
|
|
844da3ac93 | ||
|
|
af27e53150 | ||
|
|
9bced78550 | ||
|
|
0129758f74 | ||
|
|
97657eeb0d | ||
|
|
99db62cada | ||
|
|
606ffe2aff | ||
|
|
d3cf4d69d6 | ||
|
|
1e4e24eef8 | ||
|
|
3036ffcc76 | ||
|
|
1a98af2844 | ||
|
|
7ecd655a53 | ||
|
|
d7f8939750 | ||
|
|
e3be95449e | ||
|
|
61a8ee5439 | ||
|
|
f0da771089 | ||
|
|
d39a750597 | ||
|
|
e32715554c | ||
|
|
731f84bf58 | ||
|
|
1c3d2b62c6 | ||
|
|
66a8b9a1a9 | ||
|
|
c165eacdeb | ||
|
|
36cfb7dc05 | ||
|
|
3ad9dfe8f4 | ||
|
|
82065515e6 | ||
|
|
74b278e972 | ||
|
|
f9ac6c35e5 | ||
|
|
975c60df63 | ||
|
|
8fed759a8a | ||
|
|
ca4f4b0833 | ||
|
|
68f9501e3b | ||
|
|
76efae59cc | ||
|
|
48c7516a66 | ||
|
|
cee26154b8 | ||
|
|
48eba6c069 | ||
|
|
ab599e7189 | ||
|
|
8b498bdc99 | ||
|
|
52b589959f | ||
|
|
7ce8a93c9f | ||
|
|
49a9bad969 | ||
|
|
92c41a264d | ||
|
|
9bdc5c9bdc | ||
|
|
f2047a62f4 | ||
|
|
af8af0db31 | ||
|
|
3d312b7f6d | ||
|
|
788686fd59 | ||
|
|
e1368b9e16 | ||
|
|
3900e74953 | ||
|
|
42f9b66ed0 | ||
|
|
fb162ff48f | ||
|
|
7c8b2f0f7d | ||
|
|
c8f5e4d8e1 | ||
|
|
a227f86d36 | ||
|
|
e3c84a795c | ||
|
|
32b62b5a22 | ||
|
|
6f747d9f85 | ||
|
|
7de915832d | ||
|
|
725a7d6282 | ||
|
|
65c0ed5d7a | ||
|
|
ed16b7dd21 | ||
|
|
2e425ad97f | ||
|
|
1399b98a73 | ||
|
|
f404c70998 | ||
|
|
91bbfc2db8 | ||
|
|
59d51fc35f | ||
|
|
fd5e163055 | ||
|
|
5820e164b0 | ||
|
|
3a4f8b58ff | ||
|
|
aa83c53db7 | ||
|
|
ceced8bfe5 | ||
|
|
b23f9f774c | ||
|
|
572bdaf9a7 | ||
|
|
a481d20f8e | ||
|
|
74812d5528 | ||
|
|
3e3c028fb8 | ||
|
|
8373aec5a4 | ||
|
|
618e9693ae | ||
|
|
3b2b18713f | ||
|
|
66d035546d | ||
|
|
d725eeea7f | ||
|
|
0ceaa500e5 | ||
|
|
6fc3b37076 | ||
|
|
90e7805363 | ||
|
|
de9b372543 | ||
|
|
0fa80e015b | ||
|
|
a1e55680e6 | ||
|
|
2ea4481422 | ||
|
|
d0226d9bc8 | ||
|
|
3ca6f59e0b | ||
|
|
21841da60d | ||
|
|
c78df673a8 | ||
|
|
7bec552612 | ||
|
|
a2d2a8edd3 | ||
|
|
0876ae5955 | ||
|
|
ab9e8f5063 | ||
|
|
864fe26c95 | ||
|
|
79ebc617a4 | ||
|
|
ff84cce5ec | ||
|
|
953ce75b26 | ||
|
|
7eb68a1436 | ||
|
|
ed6b4f99ee | ||
|
|
c29590c51e | ||
|
|
544ffa82fd | ||
|
|
133f0992f9 | ||
|
|
deb2fd536f | ||
|
|
46586dc2e4 | ||
|
|
b255ef907a | ||
|
|
cf9b1cc6e2
|
||
|
|
d78ef0b38b
|
||
|
|
30eb5657ce
|
||
|
|
4085149e88
|
||
|
|
1788d734d2 | ||
|
|
b53215e4ba | ||
|
|
df19f9640f | ||
|
|
31aa9630d0 | ||
|
|
b1019aeb0b | ||
|
|
63aa14e31a | ||
|
|
2d87507fa4
|
||
|
|
bb8dc15554 | ||
|
|
b3369f6390 | ||
|
|
46df3b5dbe | ||
|
|
3ab93c7342 | ||
|
|
a8c4529ee8 | ||
|
|
e7a3f09547 | ||
|
|
579cf75ca2 | ||
|
|
121b34c034 | ||
|
|
97014e9454 | ||
|
|
674106923f | ||
|
|
e21dad8ed7 | ||
|
|
b2cca2332b | ||
|
|
13ad82df95 | ||
|
|
2befea2325 | ||
|
|
f45cc57049 | ||
|
|
a7f05d5d80 | ||
|
|
269e844a4c | ||
|
|
818fc49e3d | ||
|
|
8a3f7f7621 | ||
|
|
8c50670a9d | ||
|
|
141ea67d2e | ||
|
|
291fae7aec
|
||
|
|
7a04440eca | ||
|
|
691d2e4ea1 | ||
|
|
dd5c9387f9 | ||
|
|
6038d132ec | ||
|
|
6a0ac3e5c7 | ||
|
|
327a6550b4 | ||
|
|
7027d0ffb1 | ||
|
|
15b33b59ed | ||
|
|
36787717ff | ||
|
|
c27aef23c0 | ||
|
|
a9e567d299 | ||
|
|
468fb4ab13 | ||
|
|
e952de3c17 | ||
|
|
da1793bc21 | ||
|
|
650a59b1ac | ||
|
|
5a9284749c | ||
|
|
981fc5ebf4 | ||
|
|
78f5fe7c35 | ||
|
|
bdf2911a1c | ||
|
|
40b2040f25 | ||
|
|
48c88d85a4 | ||
|
|
727c828128 | ||
|
|
cce6719158 | ||
|
|
13a79baa2b | ||
|
|
c956fb9876 | ||
|
|
339ac468f3 | ||
|
|
f1eacee394 | ||
|
|
a000fb4a3d | ||
|
|
16ff2e1bc8 | ||
|
|
6ee16f02ab | ||
|
|
fd05d9b6f5 | ||
|
|
eece41d0ba | ||
|
|
b609704cc1 | ||
|
|
39436f473a | ||
|
|
ce2721025a | ||
|
|
371a2aa35f | ||
|
|
0f986616ae | ||
|
|
eb948c2e59 | ||
|
|
4fe43c5fc6 | ||
|
|
eed23b35d4 | ||
|
|
e9d46d0824 | ||
|
|
ccd18f3717 | ||
|
|
1e78f269c1 | ||
|
|
0740c00014 | ||
|
|
740526104b | ||
|
|
f807fa62d2 | ||
|
|
18725261e3 | ||
|
|
cc1a0c1aca
|
||
|
|
f2a5efcec5
|
||
|
|
cfa784991b
|
||
|
|
14f8543d3d
|
||
|
|
5705749d12
|
||
|
|
1d0b814e77 | ||
|
|
ecc26f811a | ||
|
|
8cd93d866b | ||
|
|
6bf4034e1e | ||
|
|
977321ddb5 | ||
|
|
bee96b5d9b | ||
|
|
6d21f0da6d | ||
|
|
445ccbcac0 | ||
|
|
225e5bfdc8 | ||
|
|
0c446f0d59 | ||
|
|
7c0b9bda80 | ||
|
|
eaa0278764 | ||
|
|
eeac01f638 | ||
|
|
9f8d5aaa18 | ||
|
|
cdb9d51257 | ||
|
|
2d6b895f57 | ||
|
|
80e191a50d | ||
|
|
4114185a73 | ||
|
|
24fe56c02a | ||
|
|
d83234cf5b | ||
|
|
e608b22c0c | ||
|
|
f8ed30cd73
|
||
|
|
c50933908c
|
||
|
|
77edae3585 | ||
|
|
50c152084e | ||
|
|
b0a107c749 | ||
|
|
91050826b2 | ||
|
|
1004119fed | ||
|
|
d262331d1b | ||
|
|
7440e85987 | ||
|
|
2351512af8 | ||
|
|
ae2cde3545 | ||
|
|
1000319b8f
|
||
|
|
8c4c5e16e0
|
||
|
|
0ce1e604a8
|
||
|
|
757783534c
|
||
|
|
3eba48ea7d
|
||
|
|
cf886b9581 | ||
|
|
f2ff152c07 | ||
|
|
19b152db8c | ||
|
|
18dff78e5b | ||
|
|
9b16a30c9a | ||
|
|
62bf040093 | ||
|
|
60bafc6798 | ||
|
|
bf206efc1f | ||
|
|
c42f72a856 | ||
|
|
c6a890aee3 | ||
|
|
59450cf480 | ||
|
|
7f068ea10f | ||
|
|
f9763d7b53 | ||
|
|
f2e9f44dee | ||
|
|
1039e220fb | ||
|
|
a810345e2b | ||
|
|
4dc5213e6f | ||
|
|
5f313a20d5 | ||
|
|
01eae6daf9 | ||
|
|
7ae7ebb9b3 | ||
|
|
a4e64e43f3 | ||
|
|
a1e73f5844 | ||
|
|
a6189c77ef | ||
|
|
1b36ae84d7 | ||
|
|
c609cccdc6 | ||
|
|
1360c5eeef | ||
|
|
d087eddc29 | ||
|
|
1910a8672b | ||
|
|
787175ee37 | ||
|
|
90285d5efa | ||
|
|
66b06d608d | ||
|
|
cabaa02f59 | ||
|
|
ee60dffa17 | ||
|
|
98a2558ad2 | ||
|
|
d820c0e5fa | ||
|
|
61bba0c099 | ||
|
|
c58f1359cd | ||
|
|
942e3a6235 | ||
|
|
d43535f772 | ||
|
|
e12c7194e6 | ||
|
|
47277d3609 | ||
|
|
2a8bb6d1b4 | ||
|
|
7ad2eff27b | ||
|
|
b52c16c242 | ||
|
|
caeb0f69a5 | ||
|
|
74b4fe4ac4 | ||
|
|
504ae87174 | ||
|
|
06bba3ceaf | ||
|
|
70defaf916 | ||
|
|
dfcebe327f | ||
|
|
3b27613e99 | ||
|
|
bff39001a4 | ||
|
|
e77c8dc49e | ||
|
|
c4da784b95 | ||
|
|
cbf1871cec | ||
|
|
9ed40a67bc | ||
|
|
8e699d0b22 | ||
|
|
9424517eb4 | ||
|
|
acd6753112 | ||
|
|
6fa02d117b | ||
|
|
f83ff7aca1 | ||
|
|
89bd79b542 | ||
|
|
4db4757701 | ||
|
|
f57f29cda6 | ||
|
|
51ff021d37 | ||
|
|
c6f9bf7fc5 | ||
|
|
9385442542 | ||
|
|
19a185a6c3 | ||
|
|
725de6c7b3 | ||
|
|
5228ac2508 | ||
|
|
80d6693ef3 | ||
|
|
9d3ce7410a | ||
|
|
989fd1cea0 | ||
|
|
d3b11fbb02 | ||
|
|
9a8c13e148 | ||
|
|
4f65a86302 | ||
|
|
8c559f73f7 | ||
|
|
1e77399412 | ||
|
|
6f2d9cacc4 | ||
|
|
f6209562ff | ||
|
|
9e692dae6a | ||
|
|
16458b014f | ||
|
|
af262b0ad2 | ||
|
|
5a81ae5170 | ||
|
|
89931b9c32 | ||
|
|
5a07720457 | ||
|
|
7dd4049f89 | ||
|
|
6aa1253ab6 | ||
|
|
3391985d20 | ||
|
|
28bb0ae611 | ||
|
|
d7a3f5389e | ||
|
|
0552dc33c0 | ||
|
|
b0799070eb | ||
|
|
b000b2e921 | ||
|
|
10c5f9538f | ||
|
|
5a47c70a6f | ||
|
|
f768306651 | ||
|
|
ff22ea6355 | ||
|
|
f22ff6eb34 | ||
|
|
12812dcc2f | ||
|
|
c348a2c8a6 | ||
|
|
417f53860d | ||
|
|
7dc7381075 | ||
|
|
b86c1aeebe | ||
|
|
1d32687848 | ||
|
|
80738118f0 | ||
|
|
45776a2f9b | ||
|
|
b303bc6aa6 | ||
|
|
476dcddaa9 | ||
|
|
5229507771 | ||
|
|
9a16f1b2ce | ||
|
|
633a7b5d06 | ||
|
|
80ff0458a1 | ||
|
|
5d5d6c6962 | ||
|
|
2c94f7871b | ||
|
|
26a4aae265 | ||
|
|
477a8c94fd | ||
|
|
c00078e766 | ||
|
|
09ba61f3c5 | ||
|
|
71d48d87be | ||
|
|
9695f69e70 | ||
|
|
992116fcfe | ||
|
|
270946c082 | ||
|
|
6832f3ebee | ||
|
|
ce47bff4f5 | ||
|
|
47c8cf7812 | ||
|
|
5cd1b5f9db | ||
|
|
e3a6b11c67 | ||
|
|
8fb08205f1 | ||
|
|
863bba990a | ||
|
|
cc7f0f6a47 | ||
|
|
c4432e6494 | ||
|
|
a4237d102e | ||
|
|
5050468192 | ||
|
|
e526ea9974 | ||
|
|
36cd230e16 | ||
|
|
b55b834e35 | ||
|
|
f60ff9e9e6 | ||
|
|
b8618319b0 | ||
|
|
92e4fbeb7a | ||
|
|
0d1ff771dd | ||
|
|
486ccd2a63 | ||
|
|
560005a943 | ||
|
|
40fdf3f536 | ||
|
|
07172fded1 | ||
|
|
f7f499c4c7 | ||
|
|
3ed7eeebcd | ||
|
|
5b7e497bc6 | ||
|
|
5749aca451 | ||
|
|
fd57d75063 | ||
|
|
17be04d259 | ||
|
|
08fb6902db | ||
|
|
4cf95f81e3 | ||
|
|
11ab88e25c | ||
|
|
28e9eb20b9 | ||
|
|
c728e49731 | ||
|
|
4cb421ec7a | ||
|
|
ffe7f6c36d | ||
|
|
e65475aca7 | ||
|
|
1ae85ab23c | ||
|
|
2eb83d6e3d | ||
|
|
fbebc001f6 | ||
|
|
e3105a920b | ||
|
|
791cc5df34 | ||
|
|
92c6c26d49 | ||
|
|
db86caa4e4 | ||
|
|
17ce5da002 | ||
|
|
d514362719 | ||
|
|
71865d28f5 | ||
|
|
5b9ac2b00b | ||
|
|
41cf46f182 | ||
|
|
007361fa98 | ||
|
|
94fbad41e8 | ||
|
|
f525b5fb68 | ||
|
|
39dd9f76c5 | ||
|
|
d7980ee737 | ||
|
|
08ad167c12 | ||
|
|
e713357246 | ||
|
|
99b82b5d6e | ||
|
|
d7f12fdacb | ||
|
|
759645e31e | ||
|
|
5e50f5733c | ||
|
|
db523412c5 | ||
|
|
75a772f4fe | ||
|
|
e4fa5430ba | ||
|
|
db321c4d74 | ||
|
|
14bda79c07 | ||
|
|
0cf5511aad | ||
|
|
2e9aa07b71 | ||
|
|
2dd6722ac5 | ||
|
|
b15bd32edf | ||
|
|
a1daa4d1eb | ||
|
|
9914010eb6 | ||
|
|
6353a2b8b7 | ||
|
|
9f8c061f29 | ||
|
|
ef4e8f64dd | ||
|
|
2065e60c3b | ||
|
|
ac0eeefb26 | ||
|
|
22343cc7c6 | ||
|
|
c8840fe925 | ||
|
|
0bc2614c0e | ||
|
|
28400ede30 | ||
|
|
91c996a0f1 | ||
|
|
bb602ea38f | ||
|
|
fc33efecf7 | ||
|
|
832b7d0ee1 | ||
|
|
2755201920 | ||
|
|
7c953a706a | ||
|
|
d26adae47c | ||
|
|
940aba5863 | ||
|
|
a88ea3a5b8 | ||
|
|
af7b3e7b2c | ||
|
|
57007049b4 | ||
|
|
e52e8cba27 | ||
|
|
3a675af015 | ||
|
|
07c2926d47 | ||
|
|
bc29c93e72 | ||
|
|
a8b0c99b1c | ||
|
|
027af22a55 | ||
|
|
c5ea332f6e | ||
|
|
c80dcfc721 | ||
|
|
9c415da2cf | ||
|
|
52a4bc1c21 | ||
|
|
45128b0c41 | ||
|
|
97649f9f43 | ||
|
|
3de4677121 | ||
|
|
03f9777c9a | ||
|
|
1124d239d9 | ||
|
|
4efc445ac5 | ||
|
|
960a167b44 | ||
|
|
d4eeadc4f5 | ||
|
|
3936601599 | ||
|
|
453ba4c867 | ||
|
|
55d25db229 | ||
|
|
528789cbfe | ||
|
|
867584dbb0 | ||
|
|
e19c8eba7f | ||
|
|
867ec40141 | ||
|
|
dd0fd4041c | ||
|
|
b772d8769c | ||
|
|
92b6e98060 | ||
|
|
0919c237c8 | ||
|
|
afd79eb2c7 | ||
|
|
d953984df1 | ||
|
|
f606a5c57c | ||
|
|
3e1bfe44df | ||
|
|
63a552934c | ||
|
|
74faab692d | ||
|
|
23c866c743 | ||
|
|
8836c41622 | ||
|
|
f1249506a6 | ||
|
|
9b62593811 | ||
|
|
b201c770e8 | ||
|
|
94e64bcf0d | ||
|
|
12f7913e77 | ||
|
|
5866bf837c | ||
|
|
9c11c96c00 | ||
|
|
6a13d7a779 | ||
|
|
c71129d6a3 | ||
|
|
a588c8509d | ||
|
|
b93082dab1 | ||
|
|
495312e7cd | ||
|
|
01d7903fa6 | ||
|
|
cf30d5f265 | ||
|
|
6294502850 | ||
|
|
1b692865dc | ||
|
|
e20eee2f84 | ||
|
|
8d61249b6a | ||
|
|
2265e4f4cc | ||
|
|
e6b290d991 | ||
|
|
b4da9656d6 | ||
|
|
34079e700d | ||
|
|
910d1de178 | ||
|
|
e2296859c5 | ||
|
|
267126e664 | ||
|
|
fe0ee5c463 | ||
|
|
064be48564 | ||
|
|
b8a1a0e0bc | ||
|
|
fb30049f04 | ||
|
|
4c6a556cc8 | ||
|
|
2c45e4dfe4 | ||
|
|
c2eec3965f | ||
|
|
dad18f6c0c | ||
|
|
3bf8705179 | ||
|
|
c1df3f6a92 | ||
|
|
cfc5ba086a | ||
|
|
1431d384db | ||
|
|
9fad8233b2 | ||
|
|
b985b9dee0 | ||
|
|
22ced4b782 | ||
|
|
64090e0eb1 | ||
|
|
11cf1787b2 | ||
|
|
2ca47892c1 | ||
|
|
ec71180c95 | ||
|
|
db10def0fb | ||
|
|
38b0995523 | ||
|
|
a1384e7594 | ||
|
|
fac01dc0ca | ||
|
|
063649a5df | ||
|
|
bb6baa3fe8 | ||
|
|
6e0b2632ff | ||
|
|
33cd4c17da | ||
|
|
62eb91bdf3 | ||
|
|
6a717ca652 | ||
|
|
0eb8337089 | ||
|
|
a68eceab6d | ||
|
|
78fefa9bd2 | ||
|
|
dcf8f05657 | ||
|
|
0a327cbea1 | ||
|
|
240669f9f2 | ||
|
|
b1c1fe78f3 | ||
|
|
afab88a939 | ||
|
|
0b5669cb52 | ||
|
|
e7af455803 | ||
|
|
ca8231d988 | ||
|
|
1cf43f41d3 | ||
|
|
52d2aa37fa | ||
|
|
b1a96d1003 | ||
|
|
d858795a54 | ||
|
|
dbec6ade39 | ||
|
|
937cd31bac | ||
|
|
ad03d60934 | ||
|
|
55f9e52c19 | ||
|
|
af7ebfc6fb | ||
|
|
2b3c4cea80 | ||
|
|
16bfc6cbba | ||
|
|
aa0dfa23fa | ||
|
|
4c245337bd | ||
|
|
00adc184d7 | ||
|
|
32210875c4 | ||
|
|
63a10cbd12 | ||
|
|
43cca9e874 | ||
|
|
d97c63f75f | ||
|
|
50e5e74f59 | ||
|
|
94561e2f96 | ||
|
|
23513fc8fb | ||
|
|
bea9bb0b96 | ||
|
|
0da8f01a92 | ||
|
|
ede977452d | ||
|
|
e12494e5ed | ||
|
|
1c7c66ee50 | ||
|
|
88f86c75c8 | ||
|
|
036db580f5 | ||
|
|
6dcd4a9e25 | ||
|
|
da276fb8f8 | ||
|
|
05301b7bd5 | ||
|
|
78c35c28b3 | ||
|
|
863ede58b4 | ||
|
|
7d6692b17a | ||
|
|
4f31eadbd4 | ||
|
|
56d2d2684d | ||
|
|
0668c65bfe | ||
|
|
abe3d5e958 | ||
|
|
7379e27f90 | ||
|
|
be8b297235 | ||
|
|
64ad296815 | ||
|
|
17266f3c0b | ||
|
|
0d9cae6550 | ||
|
|
41c1eb1257 | ||
|
|
4a6164ac84 | ||
|
|
aec008e7be | ||
|
|
0d76b994a2 | ||
|
|
14bfaabb11 | ||
|
|
d77c5ec391 | ||
|
|
b793ac7a6e | ||
|
|
d04e78694c | ||
|
|
d6dd4e6b96 | ||
|
|
44e5c54c77 | ||
|
|
bdb49cd085 | ||
|
|
e76aca71eb | ||
|
|
98321b4c3b | ||
|
|
b082fc44dc | ||
|
|
0776814283 | ||
|
|
3a9718d1a7 | ||
|
|
d9ef921a43 | ||
|
|
c415e8a829 | ||
|
|
daa291d5de | ||
|
|
d2987ebe1e | ||
|
|
ed32fd1183 | ||
|
|
83b1ec9c92 | ||
|
|
5e982e93ee | ||
|
|
846081536d | ||
|
|
9f2f7844f9 | ||
|
|
9966bba33c | ||
|
|
ae59b054c9 | ||
|
|
8bed6b040b | ||
|
|
d1933ec661 | ||
|
|
b40932624a | ||
|
|
6c5e727c67 | ||
|
|
6420ae2f13 | ||
|
|
91b234777d | ||
|
|
b1a646f1f2 | ||
|
|
89134681f3 | ||
|
|
3b670b8ce2 | ||
|
|
b0ba07a7fb | ||
|
|
07adda1d74 | ||
|
|
6a86d05bad | ||
|
|
dc94c465d6 | ||
|
|
950bfbb42e | ||
|
|
927ac05f03 | ||
|
|
d6d84c6892 | ||
|
|
a34c1dbb11 | ||
|
|
6df13a044b | ||
|
|
68fb0023da | ||
|
|
686982c096 | ||
|
|
84898895df | ||
|
|
472237ba8f
|
||
|
|
2ae7e1fb8c | ||
|
|
ed4d751dce | ||
|
|
b5274d2113 | ||
|
|
d7ad8ebd74 | ||
|
|
43a521de16 | ||
|
|
4cf8c497a8 | ||
|
|
fd57b5dee4 | ||
|
|
49b80dc3b9 | ||
|
|
c0e3943700 | ||
|
|
f1608179f8 | ||
|
|
86aa888be6
|
||
|
|
9c1374c99f
|
||
|
|
6b2f786afe
|
||
|
|
2ded750c46 | ||
|
|
686be17fd2 | ||
|
|
7339e16e61 | ||
|
|
e43264b327 | ||
|
|
532a34cc7f | ||
|
|
59a02332d0 | ||
|
|
02a83a491f | ||
|
|
0a01aa95f1 | ||
|
|
438eae23b0 | ||
|
|
796faf393e | ||
|
|
9643d0ebb8 | ||
|
|
0999a0c512 | ||
|
|
7a3eb6c8f3 | ||
|
|
5a01c846fa | ||
|
|
4533f36bd2 | ||
|
|
ec45f36ec4 | ||
|
|
5396b570ec | ||
|
|
d0c656e3d0 | ||
|
|
adc416a5e3 | ||
|
|
b30430ec7d | ||
|
|
767d018d45 | ||
|
|
cbfc484ed8 | ||
|
|
39418809f6 | ||
|
|
40ab056bc0 | ||
|
|
ad64b289d4 | ||
|
|
5bdb16b48b | ||
|
|
89ca3200b4 | ||
|
|
54dbf74721 | ||
|
|
2cc7a69b0f | ||
|
|
37026870c7 | ||
|
|
8a9e389a53 | ||
|
|
7a2bae0e6b | ||
|
|
5b199aef8e | ||
|
|
99ff29af88 | ||
|
|
1d36d282f4 | ||
|
|
122dfe8981 | ||
|
|
0f9f428eed | ||
|
|
ff4a2513c3 | ||
|
|
14a3fb9ffd | ||
|
|
952bddf9e2 | ||
|
|
f346a93b1f | ||
|
|
7ac9dee6d8 | ||
|
|
767b98468b | ||
|
|
84c20349c7 | ||
|
|
097b86b6a5 | ||
|
|
30f6853d98 | ||
|
|
fe201a139d | ||
|
|
4da09b509f | ||
|
|
c009e750bb | ||
|
|
69ea4cf7c0 | ||
|
|
2f3c3ee84c | ||
|
|
dbd84a91bf | ||
|
|
34560b2da2 | ||
|
|
c8bb846d0f | ||
|
|
5bd6920857 | ||
|
|
5080498b96 | ||
|
|
3cc522aaa5 | ||
|
|
832b06f424 | ||
|
|
19dfd83351 | ||
|
|
75058f0705 | ||
|
|
da2d72246e | ||
|
|
e13517317f | ||
|
|
ca6d74b581 | ||
|
|
23b47df111 | ||
|
|
38422cc30a | ||
|
|
2f314e5082 | ||
|
|
27befa1790 | ||
|
|
86aaa3899b | ||
|
|
205f81bdab | ||
|
|
9899f8038b | ||
|
|
62b3ff8500 | ||
|
|
2dbb4a2d77 | ||
|
|
f4f56e0711 | ||
|
|
8b4ceae602 | ||
|
|
6619e7c891 | ||
|
|
0aac38c05e | ||
|
|
cb67901e41 | ||
|
|
4b418942da | ||
|
|
5e03bb33e3 | ||
|
|
22a75e9bc7 | ||
|
|
8ef3702f89 | ||
|
|
a89c916e1a | ||
|
|
6eeab1103a | ||
|
|
89c10a67fd | ||
|
|
aa2b503732 | ||
|
|
f01fdbad00 | ||
|
|
0fc5da8693 | ||
|
|
891a15c1f9 | ||
|
|
ff7e05a747 | ||
|
|
9e741fe04e | ||
|
|
e8307d79f5 | ||
|
|
ac694850da | ||
|
|
2aaf3661e5 | ||
|
|
b661a7da47 | ||
|
|
c38ca8d8c4 | ||
|
|
80a8c21cb0 | ||
|
|
944ff3529b | ||
|
|
fe4ae26428 | ||
|
|
bee13854f5 | ||
|
|
c02cfa4344 | ||
|
|
a4376e5513 | ||
|
|
731ef956d2 | ||
|
|
714abc2a96 | ||
|
|
542fc9b01f | ||
|
|
0e436cb1fc | ||
|
|
324e887d68 | ||
|
|
c6c4c072a8 | ||
|
|
02f289d4d0 | ||
|
|
fb89deabdf | ||
|
|
5ef6f76355 | ||
|
|
40edcff528 | ||
|
|
370fa6f15f | ||
|
|
1b2a2f5ff4 | ||
|
|
0c7490dea4 | ||
|
|
5ffc1cce50 | ||
|
|
bebfaf0921 | ||
|
|
21db9c6d23 | ||
|
|
5cb12372e1 | ||
|
|
1c45957cc3 | ||
|
|
86e233e93b | ||
|
|
2575e0f159 | ||
|
|
4442b94211 | ||
|
|
9a88c4a943 | ||
|
|
6bf9570916 | ||
|
|
1c7aca98c1 | ||
|
|
4982e168ff | ||
|
|
0b9e42ac6c | ||
|
|
05d44d274e | ||
|
|
25c06cd390 | ||
|
|
0e7a939172 | ||
|
|
0b8d699d68 | ||
|
|
34f9fb53bc | ||
|
|
ae782b1c32 | ||
|
|
bc78898f8e | ||
|
|
8ae862e292 | ||
|
|
2b1b82e163 | ||
|
|
edc5df342d | ||
|
|
7d32e267c6 | ||
|
|
fa8f4218be | ||
|
|
f7a166c9f2 | ||
|
|
8cc871203d | ||
|
|
4449b5cf8f | ||
|
|
0f1046fcd2 | ||
|
|
395fab45f4 | ||
|
|
0b44d67d61 | ||
|
|
d3242a1304 | ||
|
|
9a451764c2 | ||
|
|
898fe6e862 | ||
|
|
e357b60b14 | ||
|
|
cfb23ed1a0 | ||
|
|
4769901955 | ||
|
|
5c577ec763 | ||
|
|
0724c9a021 | ||
|
|
d031aa4ee4 | ||
|
|
6c7a82475e | ||
|
|
3bc34cef94 | ||
|
|
c0b489ae21 | ||
|
|
96f5e01d5f | ||
|
|
8c5793a75b | ||
|
|
858162ba47 | ||
|
|
191b3c3eff | ||
|
|
7ddc29275d | ||
|
|
0df7bb7f2c | ||
|
|
43fe9ab868 | ||
|
|
43f6acfab3 | ||
|
|
126cbfe7b1 | ||
|
|
4aad449a18 | ||
|
|
ee7fbf4d5a | ||
|
|
2d48cc8dae | ||
|
|
bdca9fe2a1 | ||
|
|
ff83b0abe3 | ||
|
|
dbe93b6884 | ||
|
|
53f2bd5a57 | ||
|
|
ff0714f942 | ||
|
|
ed067a616e | ||
|
|
e259d401ad | ||
|
|
9599933c5f | ||
|
|
00b0b6c746 | ||
|
|
adbae39d27 | ||
|
|
8097d25a18 | ||
|
|
fed3e55572 | ||
|
|
d76057f17c | ||
|
|
aab452f798 | ||
|
|
40fd4522ad | ||
|
|
fad65f76ee | ||
|
|
8ad3a26fb0 | ||
|
|
1118ed9b10 | ||
|
|
53db8be5f9 | ||
|
|
111a587793 | ||
|
|
cc5ade027a | ||
|
|
026fc7562f | ||
|
|
63a49455e7 | ||
|
|
3f59d876a1 | ||
|
|
05b78ed9a9 | ||
|
|
7241cdd5a1 | ||
|
|
8c65aeb2b9 | ||
|
|
4119518ff5 | ||
|
|
af839915cc
|
||
|
|
9fb11df99e | ||
|
|
9e18619271 | ||
|
|
755c0cd5c2 | ||
|
|
64c5f54eb8 | ||
|
|
18a037421c | ||
|
|
9660c19db7 | ||
|
|
bad189507d | ||
|
|
58ad43fffe | ||
|
|
54f4bb9138 | ||
|
|
4f578b027d | ||
|
|
cd4649b329 | ||
|
|
2a29237e26 | ||
|
|
17dd26b3b0 | ||
|
|
942f713a2f | ||
|
|
df4e01352a | ||
|
|
e7ab816c46 | ||
|
|
d171437e6f | ||
|
|
f33711f075 | ||
|
|
e854b50515 | ||
|
|
38d11574b1 | ||
|
|
96bb3e9d37 | ||
|
|
fb132f81a6 | ||
|
|
8bfc9c21eb | ||
|
|
bd7f61e1f7 | ||
|
|
108c805409 | ||
|
|
3875413fa4 | ||
|
|
9630608934 | ||
|
|
fcbef4b992 | ||
|
|
38397ac27b | ||
|
|
785c0491b9 | ||
|
|
24e547a294 | ||
|
|
f636b0c884 | ||
|
|
86b0ad59f8 | ||
|
|
436e793200 | ||
|
|
192179e3af | ||
|
|
6aedb64207 | ||
|
|
81ef0185b9 | ||
|
|
1636934e42 | ||
|
|
36bd6c9a44 | ||
|
|
e58df0291c | ||
|
|
7e87ed360c | ||
|
|
cb6ae76a67 | ||
|
|
4d519fc9a2 | ||
|
|
25abde0ba3
|
||
|
|
eda0bc19a0
|
||
|
|
8e2304f5fc | ||
|
|
603e933ba3 | ||
|
|
9488d85378 | ||
|
|
a1f7b7e803 | ||
|
|
4cb1bd9cd9 | ||
|
|
69bfd0ce56 | ||
|
|
dc0371ca41 | ||
|
|
f404f60a9a | ||
|
|
3041f03a89
|
||
|
|
1dd587d252
|
||
|
|
d17a2409f1 | ||
|
|
14b5562234 | ||
|
|
9b8142fe67 | ||
|
|
973039d4af
|
||
|
|
08a647a08b
|
@@ -23,8 +23,10 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: set up secrets
|
- name: set up secrets
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
|
echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc
|
||||||
@@ -32,7 +34,6 @@ jobs:
|
|||||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties
|
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch keystore.asc > app/keystore.properties
|
||||||
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks
|
gpg -d --passphrase "${{ secrets.RELEASE_KEYSTORE_PASSWORD }}" --batch key.asc > app/key.jks
|
||||||
|
|
||||||
- uses: gradle/wrapper-validation-action@v2
|
|
||||||
|
|
||||||
- name: create and checkout branch
|
- name: create and checkout branch
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
@@ -41,17 +42,38 @@ jobs:
|
|||||||
run: git checkout -B "$BRANCH"
|
run: git checkout -B "$BRANCH"
|
||||||
|
|
||||||
- name: set up JDK
|
- name: set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v6
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 21
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
cache: 'gradle'
|
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v6
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: ./gradlew testDebugUnitTest
|
||||||
|
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
run: ./gradlew assembleSignedRelease
|
run: |
|
||||||
|
VERSION_CODE=$(git rev-list --count HEAD)
|
||||||
|
VERSION_BASE=$(git describe --tags --abbrev=0 | sed 's/^v//')
|
||||||
|
VERSION_DEV=$(git rev-list --count $(git describe --tags --abbrev=0)..HEAD)
|
||||||
|
if [ $VERSION_DEV -gt 0 ]; then
|
||||||
|
VERSION_NAME="${VERSION_BASE}.${VERSION_DEV}"
|
||||||
|
else
|
||||||
|
VERSION_NAME="${VERSION_BASE}"
|
||||||
|
fi
|
||||||
|
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
||||||
|
|
||||||
|
- name: Upload APK
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
path: app/build/outputs/apk/signedRelease/app-signedRelease.apk
|
||||||
|
compression-level: 0
|
||||||
|
archive: false
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v3
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
+3
-3
@@ -20,8 +20,8 @@ app/build/
|
|||||||
app/debug/
|
app/debug/
|
||||||
app/release/
|
app/release/
|
||||||
captures/
|
captures/
|
||||||
.externalNativeBuild
|
|
||||||
.cxx
|
|
||||||
local.properties
|
local.properties
|
||||||
keystore.properties
|
keystore.properties
|
||||||
key.jks
|
key.jks
|
||||||
|
# generated map intermediates; the packed .bmap in assets is what ships
|
||||||
|
mapdata/*.geojson
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
<p>A virtual scratchmap of the world</p>
|
<p>A virtual scratchmap of the world</p>
|
||||||
|
|
||||||
|
<a href="https://ko-fi.com/I2I615VP5M"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="ko-fi"></a>
|
||||||
|
<br>
|
||||||
<img src="https://forthebadge.com/images/badges/built-for-android.svg" alt="Built for Android">
|
<img src="https://forthebadge.com/images/badges/built-for-android.svg" alt="Built for Android">
|
||||||
<img src="https://forthebadge.com/images/badges/built-with-love.svg" alt="Built with love">
|
<img src="https://forthebadge.com/images/badges/built-with-love.svg" alt="Built with love">
|
||||||
<br>
|
<br>
|
||||||
@@ -39,8 +41,11 @@
|
|||||||
## 📳 Installation
|
## 📳 Installation
|
||||||
|
|
||||||
<div style="display: flex; justify-content: center; align-items: center; flex-direction: row;">
|
<div style="display: flex; justify-content: center; align-items: center; flex-direction: row;">
|
||||||
<a href="https://f-droid.org/packages/net.helcel.beans/">
|
<!--<a href="https://f-droid.org/packages/net.helcel.beans/">
|
||||||
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" width="206">
|
<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png" alt="Get it on F-Droid" width="206">
|
||||||
|
</a>-->
|
||||||
|
<a href="https://apt.izzysoft.de/fdroid/index/apk/net.helcel.beans">
|
||||||
|
<img width="200" height="80" alt="Izzy Download" src=".github/images/izzy.png">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/helcel-net/beans/releases/latest">
|
<a href="https://github.com/helcel-net/beans/releases/latest">
|
||||||
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
|
<img width="200" height="84" alt="APK Download" src=".github/images/apk.png">
|
||||||
@@ -68,7 +73,7 @@ Thanks to all contributors, the developers of our dependencies, and our users.
|
|||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
```
|
```
|
||||||
Copyright (C) 2024 Helcel MYDOLI
|
Copyright (C) 2026 Helcel & MYDOLI
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
|
|||||||
+50
-30
@@ -1,24 +1,24 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.23'
|
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.10'
|
||||||
id 'com.mikepenz.aboutlibraries.plugin' version '11.1.3'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'net.helcel.beans'
|
namespace 'net.helcel.beans'
|
||||||
compileSdk 34
|
compileSdk = 37
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
buildConfigField("String", "APP_NAME", "\"Beans\"")
|
||||||
|
manifestPlaceholders["APP_NAME"] = "Beans"
|
||||||
applicationId 'net.helcel.beans'
|
applicationId 'net.helcel.beans'
|
||||||
minSdk 28
|
minSdk = 28
|
||||||
targetSdk 34
|
targetSdk = 37
|
||||||
versionCode 1
|
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.5"
|
||||||
versionName "1.0"
|
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
create("release") {
|
register("release") {
|
||||||
try {
|
try {
|
||||||
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
|
def keystorePropertiesFile = rootProject.file("app/keystore.properties")
|
||||||
def keystoreProperties = new Properties()
|
def keystoreProperties = new Properties()
|
||||||
@@ -33,20 +33,18 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
debuggable true
|
debuggable true
|
||||||
}
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources false
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
signedRelease {
|
signedRelease {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources false
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
signingConfig = signingConfigs.getByName("release")
|
signingConfig = signingConfigs.getByName("release")
|
||||||
}
|
}
|
||||||
@@ -54,17 +52,15 @@ android {
|
|||||||
compileOptions {
|
compileOptions {
|
||||||
coreLibraryDesugaringEnabled true
|
coreLibraryDesugaringEnabled true
|
||||||
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
encoding 'utf-8'
|
encoding 'utf-8'
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
|
compose true
|
||||||
|
buildConfig true
|
||||||
}
|
}
|
||||||
|
|
||||||
dependenciesInfo {
|
dependenciesInfo {
|
||||||
@@ -73,19 +69,43 @@ android {
|
|||||||
// Disables dependency metadata when building Android App Bundles.
|
// Disables dependency metadata when building Android App Bundles.
|
||||||
includeInBundle = false
|
includeInBundle = false
|
||||||
}
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = "2.2.20"
|
||||||
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
// The app ships English strings only; without this the libraries drag
|
||||||
|
// in some eighty locales of their own.
|
||||||
|
localeFilters += ["en"]
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
disable 'UsingMaterialAndMaterial3Libraries'
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
aboutLibraries {
|
|
||||||
exclusionPatterns = [~"androidx.*", ~"com.google.android.*", ~"org.jetbrains.*"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
|
implementation 'androidx.compose.material3:material3:1.4.0'
|
||||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
implementation "androidx.compose.material:material:1.12.0"
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'androidx.compose.material:material-icons-extended:1.7.8'
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
implementation 'androidx.navigation:navigation-compose:2.10.0'
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.5'
|
||||||
|
|
||||||
implementation 'com.caverock:androidsvg-aar:1.4'
|
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0'
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
|
||||||
implementation 'com.mikepenz:aboutlibraries:11.1.3'
|
implementation 'androidx.compose.ui:ui'
|
||||||
|
implementation "androidx.activity:activity-ktx:1.13.0"
|
||||||
|
|
||||||
|
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||||
|
|
||||||
|
|
||||||
|
implementation platform('androidx.compose:compose-bom:2026.08.00')
|
||||||
|
debugImplementation 'androidx.compose.ui:ui-tooling:1.12.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.configureEach { task ->
|
||||||
|
if (task.name.startsWith("merge") && task.name.endsWith("Assets")) {
|
||||||
|
task.outputs.upToDateWhen { false }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,50 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:hardwareAccelerated="false"
|
android:hardwareAccelerated="false"
|
||||||
android:icon="@mipmap/ic_launcher_round"
|
android:icon="@mipmap/ic_launcher_round"
|
||||||
android:label="@string/app_name"
|
android:label="${APP_NAME}"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true">
|
||||||
android:theme="@style/Theme.Beans"
|
|
||||||
tools:replace="android:allowBackup"
|
|
||||||
tools:targetApi="31">
|
|
||||||
<profileable android:shell="true" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activity.MainActivity"
|
android:name=".activity.MainScreen"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name=".activity.EditActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".activity.StatsActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".activity.SettingsActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.1 MiB |
+3103
-3102
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 6.1 MiB |
@@ -1,71 +0,0 @@
|
|||||||
package net.helcel.beans.activity
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.activity.addCallback
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.activity.adapter.ViewPagerAdapter
|
|
||||||
import net.helcel.beans.activity.fragment.EditGroupAddFragment
|
|
||||||
import net.helcel.beans.activity.fragment.EditPlaceColorFragment
|
|
||||||
import net.helcel.beans.activity.fragment.EditPlaceFragment
|
|
||||||
import net.helcel.beans.countries.World
|
|
||||||
import net.helcel.beans.databinding.ActivityEditBinding
|
|
||||||
import net.helcel.beans.helper.Data
|
|
||||||
import net.helcel.beans.helper.DialogCloser
|
|
||||||
import net.helcel.beans.helper.Settings
|
|
||||||
import net.helcel.beans.helper.Theme.createActionBar
|
|
||||||
|
|
||||||
|
|
||||||
class EditActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var _binding: ActivityEditBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
_binding = ActivityEditBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
setContentView(_binding.root)
|
|
||||||
createActionBar(this, getString(R.string.action_edit))
|
|
||||||
|
|
||||||
val adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, _binding.pager)
|
|
||||||
_binding.pager.adapter = adapter
|
|
||||||
adapter.addFragment(null, EditPlaceFragment(World.WWW, adapter))
|
|
||||||
|
|
||||||
TabLayoutMediator(_binding.tab, _binding.pager) { tab, position ->
|
|
||||||
tab.text = adapter.getLabel(position)
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
onBackPressedDispatcher.addCallback {
|
|
||||||
if (!adapter.backPressed()) {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
if (Settings.isSingleGroup(this)) {
|
|
||||||
menuInflater.inflate(R.menu.menu_edit, menu)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_color -> {
|
|
||||||
Data.groups.getUniqueEntry()?.let { group ->
|
|
||||||
EditGroupAddFragment(group.key, {
|
|
||||||
(_binding.pager.adapter as ViewPagerAdapter?)?.refreshColors(group.color)
|
|
||||||
}, {}, false).show(supportFragmentManager, "AddColorDialogFragment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> finish()
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package net.helcel.beans.activity
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import net.helcel.beans.R
|
||||||
|
import net.helcel.beans.activity.sub.EditPlaceScreen
|
||||||
|
import net.helcel.beans.countries.World
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EditScreen(onExit:()->Unit = {}) {
|
||||||
|
SysTheme {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.action_edit)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onExit) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
.padding(innerPadding) // ensures content is below the app bar
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier.height(2.dp).fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
)
|
||||||
|
EditPlaceScreen(World.WWW, onExit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package net.helcel.beans.activity
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.drawable.PictureDrawable
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.caverock.androidsvg.RenderOptions
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.countries.GeoLocImporter
|
|
||||||
import net.helcel.beans.databinding.ActivityMainBinding
|
|
||||||
import net.helcel.beans.helper.Data
|
|
||||||
import net.helcel.beans.helper.Settings
|
|
||||||
import net.helcel.beans.helper.Theme.colorWrapper
|
|
||||||
import net.helcel.beans.svg.CSSWrapper
|
|
||||||
import net.helcel.beans.svg.SVGWrapper
|
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
private lateinit var _binding: ActivityMainBinding
|
|
||||||
|
|
||||||
private lateinit var psvg: SVGWrapper
|
|
||||||
private lateinit var css: CSSWrapper
|
|
||||||
|
|
||||||
override fun onRestart() {
|
|
||||||
refreshProjection()
|
|
||||||
refreshMap()
|
|
||||||
super.onRestart()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
||||||
menuInflater.inflate(R.menu.menu_main, menu)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
val d = when (item.itemId) {
|
|
||||||
R.id.action_edit -> EditActivity::class.java
|
|
||||||
R.id.action_stats -> StatsActivity::class.java
|
|
||||||
R.id.action_settings -> SettingsActivity::class.java
|
|
||||||
else -> throw Exception("Non Existent Menu Item")
|
|
||||||
}
|
|
||||||
startActivity(Intent(this@MainActivity, d))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
_binding = ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
Settings.start(this)
|
|
||||||
|
|
||||||
setContentView(_binding.root)
|
|
||||||
|
|
||||||
_binding.photoView.minimumScale = 1f
|
|
||||||
_binding.photoView.maximumScale = 40f
|
|
||||||
|
|
||||||
GeoLocImporter.importStates(this)
|
|
||||||
Data.loadData(this, Int.MIN_VALUE)
|
|
||||||
|
|
||||||
refreshProjection()
|
|
||||||
refreshMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshMap() {
|
|
||||||
val opt: RenderOptions = RenderOptions.create()
|
|
||||||
opt.css(css.get())
|
|
||||||
_binding.photoView.setImageDrawable(PictureDrawable(psvg.get()?.renderToPicture(opt)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshProjection() {
|
|
||||||
psvg = SVGWrapper(this)
|
|
||||||
css = CSSWrapper(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package net.helcel.beans.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.Percent
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import net.helcel.beans.BuildConfig
|
||||||
|
import net.helcel.beans.activity.sub.EditPlaceDialog
|
||||||
|
import net.helcel.beans.activity.sub.MapPickDialog
|
||||||
|
import net.helcel.beans.activity.sub.applyDirectVisit
|
||||||
|
import net.helcel.beans.activity.sub.commitVisitDialog
|
||||||
|
import net.helcel.beans.countries.GeoLoc
|
||||||
|
import net.helcel.beans.countries.GeoLocImporter
|
||||||
|
import net.helcel.beans.countries.GeoLocTree
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.Settings
|
||||||
|
import net.helcel.beans.map.MapAssets
|
||||||
|
import net.helcel.beans.map.MapStyle
|
||||||
|
import net.helcel.beans.map.MapView
|
||||||
|
import net.helcel.beans.map.MapWorld
|
||||||
|
import net.helcel.beans.map.MapReader
|
||||||
|
|
||||||
|
|
||||||
|
class MainScreen : ComponentActivity() {
|
||||||
|
|
||||||
|
private var world by mutableStateOf<MapWorld?>(null)
|
||||||
|
private var loadToken = 0
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
actionBar?.hide()
|
||||||
|
Settings.start(this)
|
||||||
|
Data.loadData(this, Int.MIN_VALUE)
|
||||||
|
GeoLocImporter.importStates(this)
|
||||||
|
|
||||||
|
refreshProjection()
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
SysTheme {
|
||||||
|
// Both bars: without the navigation inset the system buttons sit on top of
|
||||||
|
// whatever is at the bottom of a screen, such as the About button.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colors.primary)
|
||||||
|
.statusBarsPadding()
|
||||||
|
.navigationBarsPadding(),
|
||||||
|
) {
|
||||||
|
AppNavHost()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppNavHost() {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
NavHost(navController, startDestination = "main") {
|
||||||
|
composable("main") { MainScreenC(world, navController) }
|
||||||
|
composable("settings") { SettingsMainScreen { navController.navigate("main") } }
|
||||||
|
composable("edit") { EditScreen { navController.navigate("main") } }
|
||||||
|
composable("stats") { StatsScreen { navController.navigate("main") } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MainScreenC(world: MapWorld?, nav: NavHostController){
|
||||||
|
SysTheme {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(BuildConfig.APP_NAME) },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { nav.navigate("edit") }) {
|
||||||
|
Icon(Icons.Default.Edit, contentDescription = "Edit")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { nav.navigate("stats") }){
|
||||||
|
Icon(Icons.Default.Percent, contentDescription = "Stats")
|
||||||
|
}
|
||||||
|
IconButton(onClick = { nav.navigate("settings") }) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = "Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
Box(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
|
||||||
|
if (world == null) LoadingMap() else MapScreen(world)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoadingMap() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
strokeWidth = 4.dp,
|
||||||
|
modifier = Modifier.size(50.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MapScreen(world: MapWorld) {
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
val visits by Data.visits.visitsFlow.collectAsState()
|
||||||
|
val groups by Data.groups.groupsFlow.collectAsState()
|
||||||
|
val land = MaterialTheme.colors.onBackground.toArgb()
|
||||||
|
val background = MaterialTheme.colors.background.toArgb()
|
||||||
|
val style = remember(visits, groups, land, background) {
|
||||||
|
MapStyle.build(ctx, land, background)
|
||||||
|
}
|
||||||
|
val touchRadius = Settings.getTouchRadius(ctx)
|
||||||
|
|
||||||
|
var candidates by remember { mutableStateOf<List<GeoLoc>>(emptyList()) }
|
||||||
|
var showColor by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// A place is either applied straight away, or the colour dialog picks
|
||||||
|
// the group for it, exactly as it does from the edit list.
|
||||||
|
fun select(loc: GeoLoc) {
|
||||||
|
if (!applyDirectVisit(ctx, loc)) showColor = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidates.isNotEmpty()) {
|
||||||
|
MapPickDialog(
|
||||||
|
candidates = candidates,
|
||||||
|
onPick = { candidates = emptyList(); select(it) },
|
||||||
|
onDismiss = { candidates = emptyList() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showColor) {
|
||||||
|
EditPlaceDialog(false) { cleared ->
|
||||||
|
showColor = false
|
||||||
|
commitVisitDialog(cleared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
factory = { MapView(it) },
|
||||||
|
update = { view ->
|
||||||
|
view.world = world
|
||||||
|
view.style = style
|
||||||
|
view.touchRadiusDp = touchRadius
|
||||||
|
// Always the tree, even for a single hit: landing on a region is
|
||||||
|
// just as often a way of reaching the country around it.
|
||||||
|
view.onPick = { picks ->
|
||||||
|
val locs = picks.mapNotNull { GeoLocTree.find(it.code) }
|
||||||
|
if (locs.isNotEmpty()) candidates = locs
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reloads the map asset, on a worker thread since it is a few megabytes. */
|
||||||
|
fun refreshProjection() {
|
||||||
|
val asset = MapAssets.assetFor(this)
|
||||||
|
val token = ++loadToken
|
||||||
|
world = null
|
||||||
|
Thread {
|
||||||
|
val parsed = assets.open(asset).use { MapReader.read(it) }
|
||||||
|
runOnUiThread { if (token == loadToken) world = parsed }
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package net.helcel.beans.activity
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.activity.fragment.AboutFragment
|
|
||||||
import net.helcel.beans.activity.fragment.LicenseFragment
|
|
||||||
import net.helcel.beans.activity.fragment.SettingsFragment
|
|
||||||
import net.helcel.beans.helper.Theme.createActionBar
|
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_settings)
|
|
||||||
createActionBar(this, getString(R.string.action_settings))
|
|
||||||
|
|
||||||
// Populate activity with settings fragment
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_view, SettingsFragment(), getString(R.string.action_settings))
|
|
||||||
.commit()
|
|
||||||
|
|
||||||
// Change title in action bar according to current fragment
|
|
||||||
supportFragmentManager.addFragmentOnAttachListener { _, _ ->
|
|
||||||
supportActionBar?.title =
|
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_view).let {
|
|
||||||
when (it) {
|
|
||||||
is LicenseFragment -> getString(R.string.licenses)
|
|
||||||
is AboutFragment -> getString(R.string.about)
|
|
||||||
else -> getString(R.string.action_settings)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
// Configure on back pressed
|
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_view).let {
|
|
||||||
when (it) {
|
|
||||||
is LicenseFragment, is AboutFragment -> {
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.remove(it)
|
|
||||||
.commit()
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(
|
|
||||||
R.id.fragment_view,
|
|
||||||
SettingsFragment(),
|
|
||||||
getString(R.string.action_settings)
|
|
||||||
)
|
|
||||||
.commit()
|
|
||||||
supportActionBar?.title = getString(R.string.action_settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
package net.helcel.beans.activity
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
import androidx.compose.material.Colors
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.RadioButton
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Slider
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.NavHost
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import net.helcel.beans.R
|
||||||
|
import net.helcel.beans.activity.sub.AboutScreen
|
||||||
|
import net.helcel.beans.activity.sub.EditPlaceColorDialog
|
||||||
|
import net.helcel.beans.countries.GeoLocImporter
|
||||||
|
import net.helcel.beans.helper.DEFAULT_TOUCH_RADIUS
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.defaultPreferences
|
||||||
|
import net.helcel.beans.helper.MAX_TOUCH_RADIUS
|
||||||
|
import net.helcel.beans.helper.Settings
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SysTheme(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val prefs = defaultPreferences(context)
|
||||||
|
val themeKey = prefs.getString(stringResource(R.string.key_theme), stringResource(R.string.system))
|
||||||
|
val darkTheme = when (themeKey) {
|
||||||
|
stringResource(R.string.system) -> isSystemInDarkTheme()
|
||||||
|
stringResource(R.string.light) -> false
|
||||||
|
stringResource(R.string.dark) -> true
|
||||||
|
else -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
if(darkTheme) dynamicDarkColorScheme(LocalContext.current ) else dynamicLightColorScheme(LocalContext.current )
|
||||||
|
} else {
|
||||||
|
if(darkTheme) darkColorScheme() else lightColorScheme()
|
||||||
|
}
|
||||||
|
val m2colors = Colors(
|
||||||
|
primary = colorScheme.primary,
|
||||||
|
primaryVariant = colorScheme.primaryContainer,
|
||||||
|
secondary = colorScheme.secondary,
|
||||||
|
background = colorScheme.background,
|
||||||
|
surface = colorScheme.surface,
|
||||||
|
onPrimary = colorScheme.onPrimary,
|
||||||
|
onSecondary = colorScheme.onSecondary,
|
||||||
|
onBackground = colorScheme.onBackground,
|
||||||
|
onSurface = colorScheme.onSurface,
|
||||||
|
secondaryVariant = colorScheme.secondary,
|
||||||
|
error = colorScheme.error,
|
||||||
|
onError = colorScheme.onError,
|
||||||
|
isLight = !darkTheme,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Both, because the app mixes the two: most of it is Material 2, while the
|
||||||
|
// dividers are Material 3 and would otherwise fall back to the default
|
||||||
|
// light scheme.
|
||||||
|
androidx.compose.material3.MaterialTheme(colorScheme = colorScheme) {
|
||||||
|
MaterialTheme(
|
||||||
|
colors = m2colors,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SettingsMainScreen(onExit: ()->Unit = {}) {
|
||||||
|
val nav: NavHostController = rememberNavController()
|
||||||
|
SysTheme {
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.action_settings)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = {
|
||||||
|
if(!nav.popBackStack())
|
||||||
|
onExit()
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
// One host, inside the scaffold. Building it here and again as a
|
||||||
|
// default argument left two of them: settings drawn twice, and
|
||||||
|
// only the inner one ever navigating.
|
||||||
|
Box(modifier = Modifier.padding(innerPadding)) {
|
||||||
|
NavHost(nav, startDestination = "settings") {
|
||||||
|
composable("settings") { SettingsScreen(nav) }
|
||||||
|
composable("about") { AboutScreen() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(navController: NavHostController) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val prefs = defaultPreferences(context)
|
||||||
|
val keyTheme = stringResource(R.string.key_theme)
|
||||||
|
val defaultTheme = stringResource(R.string.system)
|
||||||
|
val keyProjection = stringResource(R.string.key_projection)
|
||||||
|
val keyGroup = stringResource(R.string.key_group)
|
||||||
|
val keyRegional = stringResource(R.string.key_regional)
|
||||||
|
val keyCascadeStats = stringResource(R.string.key_cascade_stats)
|
||||||
|
val keyTouchRadius = stringResource(R.string.key_touch_radius)
|
||||||
|
val offString = stringResource(R.string.off)
|
||||||
|
val onString = stringResource(R.string.on)
|
||||||
|
|
||||||
|
var theme by remember { mutableStateOf(prefs.getString(keyTheme, defaultTheme)!!) }
|
||||||
|
var projection by remember { mutableStateOf(prefs.getString(keyProjection, "default")!!) }
|
||||||
|
var groups by remember { mutableStateOf(prefs.getString(keyGroup, offString)!!) }
|
||||||
|
var regional by remember { mutableStateOf(prefs.getString(keyRegional, offString)!!) }
|
||||||
|
var cascadeStats by remember { mutableStateOf(prefs.getString(keyCascadeStats, offString)!!) }
|
||||||
|
var touchRadius by remember {
|
||||||
|
mutableIntStateOf(prefs.getInt(keyTouchRadius, DEFAULT_TOUCH_RADIUS))
|
||||||
|
}
|
||||||
|
|
||||||
|
var showGroupDialog by remember { mutableStateOf(false) }
|
||||||
|
var showRegionalDialog by remember { mutableStateOf(false) }
|
||||||
|
var showLoad by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
if (showGroupDialog) {
|
||||||
|
EditPlaceColorDialog(
|
||||||
|
deleteMode = true,
|
||||||
|
onDismiss = {
|
||||||
|
val g = Data.selected_group
|
||||||
|
if (g != null) {
|
||||||
|
Data.visits.reassignAllVisitedToGroup(g.key)
|
||||||
|
Data.groups.keepOnly(g.key)
|
||||||
|
Data.saveData()
|
||||||
|
groups = offString
|
||||||
|
prefs.edit { putString(keyGroup, offString) }
|
||||||
|
}
|
||||||
|
showGroupDialog = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showRegionalDialog) {
|
||||||
|
Dialog(
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
RoundedCornerShape(corner = CornerSize(16.dp))
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
text = stringResource(R.string.delete_regions)
|
||||||
|
)
|
||||||
|
Button(onClick = {
|
||||||
|
GeoLocImporter.clearStates()
|
||||||
|
regional = offString
|
||||||
|
prefs.edit { putString(keyRegional, offString) }
|
||||||
|
showRegionalDialog = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = { showRegionalDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLoad) {
|
||||||
|
Dialog(
|
||||||
|
content = {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colors.primary,
|
||||||
|
strokeWidth = 4.dp,
|
||||||
|
modifier = Modifier.size(50.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDismissRequest = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
.background(MaterialTheme.colors.background)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.key_theme),
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
)
|
||||||
|
MultiPreference(
|
||||||
|
arrayOf(
|
||||||
|
stringResource(R.string.system),
|
||||||
|
stringResource(R.string.light),
|
||||||
|
stringResource(R.string.dark)
|
||||||
|
), theme
|
||||||
|
) { newTheme ->
|
||||||
|
theme = newTheme
|
||||||
|
prefs.edit { putString(keyTheme, newTheme) }
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.key_projection),
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
MultiPreference(
|
||||||
|
arrayOf(
|
||||||
|
stringResource(R.string.mercator),
|
||||||
|
stringResource(R.string.azimuthalequidistant),
|
||||||
|
stringResource(R.string.loximuthal),
|
||||||
|
),
|
||||||
|
projection
|
||||||
|
) { newProj ->
|
||||||
|
projection = newProj
|
||||||
|
prefs.edit { putString(keyProjection, newProj) }
|
||||||
|
Settings.refreshProjection()
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
SettingSwitch(
|
||||||
|
label = stringResource(R.string.key_group),
|
||||||
|
subtitle = stringResource(R.string.key_group_desc),
|
||||||
|
isChecked = groups == onString,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
if (!isChecked) {
|
||||||
|
Data.selected_group = null
|
||||||
|
showGroupDialog = true
|
||||||
|
} else {
|
||||||
|
groups = onString
|
||||||
|
prefs.edit { putString(keyGroup, onString) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
SettingSwitch(
|
||||||
|
label = stringResource(R.string.key_regional),
|
||||||
|
subtitle = stringResource(R.string.key_regional_desc),
|
||||||
|
isChecked = regional == onString,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
if (isChecked) {
|
||||||
|
regional = onString
|
||||||
|
prefs.edit { putString(keyRegional, onString) }
|
||||||
|
showLoad = true
|
||||||
|
scope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
GeoLocImporter.importStates(context, true)
|
||||||
|
}
|
||||||
|
showLoad = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showRegionalDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
SettingSwitch(
|
||||||
|
label = stringResource(R.string.pref_cascade_stats),
|
||||||
|
subtitle = stringResource(R.string.pref_cascade_stats_desc),
|
||||||
|
isChecked = cascadeStats == onString,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
cascadeStats = if (isChecked) onString else offString
|
||||||
|
prefs.edit { putString(keyCascadeStats, cascadeStats) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.pref_touch_radius),
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.pref_touch_radius_desc),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Slider(
|
||||||
|
value = touchRadius.toFloat(),
|
||||||
|
onValueChange = { touchRadius = it.roundToInt() },
|
||||||
|
onValueChangeFinished = { prefs.edit { putInt(keyTouchRadius, touchRadius) } },
|
||||||
|
valueRange = 0f..MAX_TOUCH_RADIUS.toFloat(),
|
||||||
|
steps = MAX_TOUCH_RADIUS - 1,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.pref_touch_radius_value, touchRadius),
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.padding(start = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
|
onResult = { uri -> Data.doImport(context, uri) }
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
launcher.launch(arrayOf("*/*"))
|
||||||
|
}, modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_import))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.size(16.dp))
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
Data.doExport(context)
|
||||||
|
}, modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.action_export))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PreferenceButton(stringResource(R.string.about)) {
|
||||||
|
if (navController.currentDestination?.route != "about")
|
||||||
|
navController.navigate("about")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MultiPreference(list: Array<String>, selected: String, onSelected: (String) -> Unit) {
|
||||||
|
Column(Modifier.padding(vertical = 2.dp)) {
|
||||||
|
list.forEach { value ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(42.dp)
|
||||||
|
.clickable { onSelected(value) }) {
|
||||||
|
RadioButton(selected = selected == value, onClick = { onSelected(value) })
|
||||||
|
Text(
|
||||||
|
value, modifier = Modifier.padding(start = 12.dp),
|
||||||
|
style = MaterialTheme.typography.body1,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PreferenceButton(text: String, onClick: () -> Unit) {
|
||||||
|
Button(onClick = onClick, modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)) {
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingSwitch(
|
||||||
|
label: String,
|
||||||
|
subtitle: String? = null,
|
||||||
|
isChecked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!isChecked) }
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text=label,
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
)
|
||||||
|
if (subtitle != null) {
|
||||||
|
Text(
|
||||||
|
text = subtitle,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
androidx.compose.material.Switch(
|
||||||
|
checked = isChecked,
|
||||||
|
onCheckedChange = onCheckedChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,219 @@
|
|||||||
package net.helcel.beans.activity
|
package net.helcel.beans.activity
|
||||||
|
|
||||||
import android.os.Bundle
|
import androidx.compose.foundation.background
|
||||||
import android.view.MenuItem
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import net.helcel.beans.R
|
import net.helcel.beans.R
|
||||||
import net.helcel.beans.activity.adapter.StatsListAdapter
|
import net.helcel.beans.countries.Country
|
||||||
|
import net.helcel.beans.countries.GeoLoc
|
||||||
import net.helcel.beans.countries.GeoLoc.LocType
|
import net.helcel.beans.countries.GeoLoc.LocType
|
||||||
import net.helcel.beans.databinding.ActivityStatBinding
|
import net.helcel.beans.countries.World
|
||||||
|
import net.helcel.beans.helper.AUTO_GROUP
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.Groups
|
||||||
|
import net.helcel.beans.helper.NO_GROUP
|
||||||
import net.helcel.beans.helper.Settings
|
import net.helcel.beans.helper.Settings
|
||||||
import net.helcel.beans.helper.Theme.createActionBar
|
import net.helcel.beans.helper.Theme.getContrastColor
|
||||||
|
|
||||||
private val MODE_LIST = listOf(LocType.WORLD, LocType.COUNTRY, LocType.STATE)
|
private val MODE_LIST = listOf(LocType.WORLD, LocType.COUNTRY, LocType.STATE)
|
||||||
|
|
||||||
class StatsActivity : AppCompatActivity() {
|
@Composable
|
||||||
private lateinit var _binding: ActivityStatBinding
|
fun StatsScreen(
|
||||||
private var activeMode = LocType.WORLD
|
onExit: ()-> Unit
|
||||||
|
) {
|
||||||
|
val modes = if (Settings.isRegional(LocalContext.current)) MODE_LIST else MODE_LIST.take(2)
|
||||||
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
var countMode by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
_binding = ActivityStatBinding.inflate(layoutInflater)
|
|
||||||
setContentView(_binding.root)
|
|
||||||
createActionBar(this, getString(R.string.action_stat))
|
|
||||||
|
|
||||||
_binding.stats.layoutManager =
|
SysTheme {
|
||||||
LinearLayoutManager(this, RecyclerView.VERTICAL, false)
|
Scaffold(
|
||||||
val adapter = StatsListAdapter(_binding.stats, _binding.name)
|
topBar = {
|
||||||
_binding.groupColor.setOnClickListener { adapter.invertCountMode() }
|
TopAppBar(
|
||||||
_binding.stats.adapter = adapter
|
title = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically){
|
||||||
|
Text(text=stringResource(R.string.action_stat), modifier = Modifier.weight(1f))
|
||||||
|
Button(onClick = { countMode = !countMode }) {
|
||||||
|
Text(if (countMode) "Count" else "Area")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onExit) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_binding.pager.adapter = object : FragmentStateAdapter(supportFragmentManager, lifecycle) {
|
)
|
||||||
override fun getItemCount(): Int = if (Settings.isRegional(applicationContext)) 3 else 2
|
},
|
||||||
override fun createFragment(position: Int): Fragment = Fragment()
|
) { padding ->
|
||||||
}
|
Column(Modifier.padding(padding)) {
|
||||||
TabLayoutMediator(_binding.tab, _binding.pager) { tab, position ->
|
TabRow(selectedTabIndex = selectedTab) {
|
||||||
tab.text = MODE_LIST[position].txt
|
modes.forEachIndexed { index, mode ->
|
||||||
}.attach()
|
Tab(
|
||||||
|
selected = selectedTab == index,
|
||||||
|
onClick = { selectedTab = index },
|
||||||
|
text = { Text(mode.txt) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_binding.pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
Row(
|
||||||
override fun onPageSelected(position: Int) {
|
modifier = Modifier
|
||||||
activeMode = MODE_LIST[position]
|
.fillMaxWidth()
|
||||||
adapter.refreshMode(activeMode)
|
.padding(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeMode = modes.getOrNull(selectedTab) ?: LocType.WORLD
|
||||||
|
StatsList(activeMode, countMode)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatsList(activeMode: LocType, countMode: Boolean) {
|
||||||
|
val groups = remember { Data.groups.groupsFlow }
|
||||||
|
val unCat = stringResource(R.string.uncategorized)
|
||||||
|
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(groups.value + listOf(Groups.Group(AUTO_GROUP, unCat))) { group ->
|
||||||
|
StatsRow(group, activeMode, countMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StatsRow(group: Groups.Group, activeMode: LocType, countMode: Boolean) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isCascadeStats = remember { Settings.isCascadeStats(context) }
|
||||||
|
val visits by Data.visits.visitsFlow.collectAsState()
|
||||||
|
|
||||||
|
val continents = remember { World.WWW.children.toList() }
|
||||||
|
val countries = remember {
|
||||||
|
World.WWW.children.flatMap {
|
||||||
|
if (it is Country) listOf(it) else it.children
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
val visitedItems = remember(group, activeMode, isCascadeStats, visits) {
|
||||||
finish()
|
val list = when (activeMode) {
|
||||||
return super.onOptionsItemSelected(item)
|
LocType.WORLD -> continents
|
||||||
|
LocType.COUNTRY -> countries
|
||||||
|
LocType.STATE -> countries.flatMap { it.children }
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExplicitColor(l: GeoLoc): Int? {
|
||||||
|
val c = visits.getOrDefault(l.code, 0)
|
||||||
|
return if (c != NO_GROUP && c != AUTO_GROUP) c else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEffectiveGroup(l: GeoLoc): Int {
|
||||||
|
getExplicitColor(l)?.let { return it }
|
||||||
|
|
||||||
|
if (isCascadeStats) {
|
||||||
|
// Parent -> Child (closest explicit ancestor)
|
||||||
|
val country = countries.find { it.children.contains(l) }
|
||||||
|
val countryColor = country?.let { getExplicitColor(it) }
|
||||||
|
if (countryColor != null) return countryColor
|
||||||
|
|
||||||
|
val continent = continents.find {
|
||||||
|
it.children.contains(l) || (country != null && it.children.contains(country))
|
||||||
|
}
|
||||||
|
val continentColor = continent?.let { getExplicitColor(it) }
|
||||||
|
if (continentColor != null) return continentColor
|
||||||
|
|
||||||
|
val worldColor = getExplicitColor(World.WWW)
|
||||||
|
if (worldColor != null) return worldColor
|
||||||
|
|
||||||
|
// Child -> Parent (most common explicit descendant)
|
||||||
|
val descendants = mutableListOf<GeoLoc>()
|
||||||
|
fun collect(curr: GeoLoc) {
|
||||||
|
curr.children.forEach {
|
||||||
|
descendants.add(it)
|
||||||
|
collect(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collect(l)
|
||||||
|
val descendantColors = descendants.mapNotNull { getExplicitColor(it) }
|
||||||
|
if (descendantColors.isNotEmpty()) {
|
||||||
|
return descendantColors.groupBy { it }.maxByOrNull { it.value.size }?.key ?: AUTO_GROUP
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (l.type == LocType.WORLD || l.type == LocType.COUNTRY) {
|
||||||
|
val descendants = mutableListOf<GeoLoc>()
|
||||||
|
fun collect(curr: GeoLoc) {
|
||||||
|
curr.children.forEach { descendants.add(it); collect(it) }
|
||||||
|
}
|
||||||
|
collect(l)
|
||||||
|
val dc = descendants.mapNotNull { getExplicitColor(it) }
|
||||||
|
if (dc.isNotEmpty()) return dc.groupBy { it }.maxByOrNull { it.value.size }?.key ?: AUTO_GROUP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AUTO_GROUP
|
||||||
|
}
|
||||||
|
|
||||||
|
list.filter { getEffectiveGroup(it) == group.key }
|
||||||
|
}
|
||||||
|
|
||||||
|
val count = visitedItems.size
|
||||||
|
val area = visitedItems.sumOf { it.area.toLong() }
|
||||||
|
|
||||||
|
val displayValue = if (countMode) count.toString() else stringResource(R.string.number_with_unit, area.toInt(), "km²")
|
||||||
|
|
||||||
|
val backgroundColor = group.color.color
|
||||||
|
val textColor = getContrastColor(backgroundColor)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color(backgroundColor))
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text=group.name,
|
||||||
|
modifier= Modifier.weight(1f),
|
||||||
|
color = Color(textColor)
|
||||||
|
)
|
||||||
|
Text(text=displayValue,
|
||||||
|
color = Color(textColor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
package net.helcel.beans.activity.adapter
|
|
||||||
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.checkbox.MaterialCheckBox
|
|
||||||
import net.helcel.beans.activity.fragment.EditPlaceColorFragment
|
|
||||||
import net.helcel.beans.activity.fragment.EditPlaceFragment
|
|
||||||
import net.helcel.beans.countries.GeoLoc
|
|
||||||
import net.helcel.beans.databinding.ItemListGeolocBinding
|
|
||||||
import net.helcel.beans.helper.*
|
|
||||||
import net.helcel.beans.helper.Theme.colorWrapper
|
|
||||||
|
|
||||||
class GeolocListAdapter(
|
|
||||||
private val ctx: EditPlaceFragment, private val l: GeoLoc, private val pager: ViewPagerAdapter,
|
|
||||||
private val parentHolder: FoldingListViewHolder?
|
|
||||||
) : RecyclerView.Adapter<GeolocListAdapter.FoldingListViewHolder>() {
|
|
||||||
|
|
||||||
private val sortedList = l.children.toList().sortedBy { it.fullName }
|
|
||||||
private val holders: MutableSet<FoldingListViewHolder> = mutableSetOf()
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): FoldingListViewHolder {
|
|
||||||
val binding = ItemListGeolocBinding.inflate(
|
|
||||||
LayoutInflater.from(viewGroup.context),
|
|
||||||
viewGroup,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
val holder = FoldingListViewHolder(ctx.requireActivity(), binding, parentHolder, l)
|
|
||||||
holders.add(holder)
|
|
||||||
return holder
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: FoldingListViewHolder, position: Int) {
|
|
||||||
val el = sortedList[position]
|
|
||||||
holder.bind(el)
|
|
||||||
holder.addListeners(el) {
|
|
||||||
if (el.children.isNotEmpty())
|
|
||||||
pager.addFragment(ctx, EditPlaceFragment(el, pager, holder))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return l.children.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshColors(colorDrawable: ColorDrawable) {
|
|
||||||
holders.forEach { it.refreshColor(colorDrawable) }
|
|
||||||
}
|
|
||||||
|
|
||||||
class FoldingListViewHolder(
|
|
||||||
private val ctx: FragmentActivity,
|
|
||||||
private val _binding: ItemListGeolocBinding,
|
|
||||||
private val _parentHolder: FoldingListViewHolder? = null,
|
|
||||||
private val _parentGeoLoc: GeoLoc,
|
|
||||||
) : RecyclerView.ViewHolder(_binding.root), DialogCloser {
|
|
||||||
private lateinit var el: GeoLoc
|
|
||||||
|
|
||||||
private fun bindGroup(el: GeoLoc) {
|
|
||||||
refreshCount(el)
|
|
||||||
_binding.textView.setTypeface(null, Typeface.BOLD)
|
|
||||||
_binding.textView.backgroundTintList = ColorStateList.valueOf(
|
|
||||||
colorWrapper(
|
|
||||||
ctx,
|
|
||||||
android.R.attr.panelColorBackground
|
|
||||||
).color
|
|
||||||
).withAlpha(64)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(el: GeoLoc) {
|
|
||||||
this.el = el
|
|
||||||
_binding.textView.text = el.fullName
|
|
||||||
_binding.textView.backgroundTintList =
|
|
||||||
ColorStateList.valueOf(colorWrapper(ctx, android.R.attr.colorBackground).color)
|
|
||||||
|
|
||||||
if (el.children.isNotEmpty())
|
|
||||||
bindGroup(el)
|
|
||||||
|
|
||||||
refreshCheck(el)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshColor(colorDrawable: ColorDrawable) {
|
|
||||||
if (Data.visits.getVisited(el) !in listOf(NO_GROUP, AUTO_GROUP)) {
|
|
||||||
_binding.checkBox.buttonTintList =
|
|
||||||
ColorStateList.valueOf(colorDrawable.color)
|
|
||||||
refreshCheck(el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addListeners(el: GeoLoc, expandLambda: () -> Boolean) {
|
|
||||||
if (el.children.isNotEmpty()) {
|
|
||||||
_binding.textView.setOnClickListener { expandLambda() }
|
|
||||||
}
|
|
||||||
_binding.checkBox.setOnClickListener {
|
|
||||||
Data.selected_geoloc = el
|
|
||||||
if (Data.groups.size() == 1 && Settings.isSingleGroup(ctx)) {
|
|
||||||
if (_binding.checkBox.isChecked) {
|
|
||||||
// If one has just checked the box (assign unique group)
|
|
||||||
Data.selected_group = Data.groups.getUniqueEntry()
|
|
||||||
onDialogDismiss(false)
|
|
||||||
} else {
|
|
||||||
// If one has just unchecked the box (unassign unique group)
|
|
||||||
Data.selected_group = null
|
|
||||||
onDialogDismiss(true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Data.selected_group = null
|
|
||||||
EditPlaceColorFragment(this).show(
|
|
||||||
ctx.supportFragmentManager,
|
|
||||||
"AddColorDialogFragment"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_parentHolder?.refresh(_parentGeoLoc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogDismiss(clear: Boolean) {
|
|
||||||
if (clear) {
|
|
||||||
Data.visits.setVisited(Data.selected_geoloc, NO_GROUP)
|
|
||||||
Data.saveData()
|
|
||||||
|
|
||||||
if (_parentGeoLoc.children.all { Data.visits.getVisited(it) == NO_GROUP }) {
|
|
||||||
Data.clearing_geoloc = _parentGeoLoc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Data.selected_group != null && Data.selected_geoloc != null) {
|
|
||||||
Data.visits.setVisited(Data.selected_geoloc, Data.selected_group?.key ?: NO_GROUP)
|
|
||||||
Data.saveData()
|
|
||||||
}
|
|
||||||
Data.selected_geoloc?.let { refreshCheck(it) }
|
|
||||||
Data.selected_geoloc = null
|
|
||||||
Data.selected_group = null
|
|
||||||
_parentHolder?.refresh(_parentGeoLoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshCheck(geoLoc: GeoLoc) {
|
|
||||||
_binding.checkBox.checkedState =
|
|
||||||
if (Data.visits.getVisited(geoLoc) !in listOf(NO_GROUP, AUTO_GROUP)) {
|
|
||||||
MaterialCheckBox.STATE_CHECKED
|
|
||||||
} else if (geoLoc.children.isNotEmpty() &&
|
|
||||||
geoLoc.children.all {
|
|
||||||
Data.visits.getVisited(it) !in listOf(NO_GROUP, AUTO_GROUP)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Data.visits.setVisited(geoLoc, AUTO_GROUP)
|
|
||||||
MaterialCheckBox.STATE_CHECKED
|
|
||||||
} else if (geoLoc.children.isEmpty() && Data.visits.getVisited(geoLoc) == AUTO_GROUP) {
|
|
||||||
MaterialCheckBox.STATE_CHECKED
|
|
||||||
} else if (geoLoc.children.any { Data.visits.getVisited(it) != NO_GROUP }) {
|
|
||||||
Data.visits.setVisited(geoLoc, AUTO_GROUP)
|
|
||||||
MaterialCheckBox.STATE_INDETERMINATE
|
|
||||||
} else {
|
|
||||||
Data.visits.setVisited(geoLoc, NO_GROUP)
|
|
||||||
if (Data.clearing_geoloc == geoLoc) {
|
|
||||||
Data.clearing_geoloc = null
|
|
||||||
}
|
|
||||||
MaterialCheckBox.STATE_UNCHECKED
|
|
||||||
}
|
|
||||||
Data.saveData()
|
|
||||||
|
|
||||||
var col = Data.groups.getGroupFromKey(Data.visits.getVisited(geoLoc)).color
|
|
||||||
if (Data.visits.getVisited(geoLoc) == AUTO_GROUP) {
|
|
||||||
col = colorWrapper(ctx, android.R.attr.colorPrimary)
|
|
||||||
} else if (col.color == Color.TRANSPARENT) {
|
|
||||||
col = colorWrapper(ctx, android.R.attr.panelColorBackground)
|
|
||||||
col.alpha = 64
|
|
||||||
}
|
|
||||||
_binding.checkBox.buttonTintList = ColorStateList.valueOf(col.color)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshCount(geoLoc: GeoLoc) {
|
|
||||||
val numerator =
|
|
||||||
geoLoc.children.map { Data.visits.getVisited(it) != NO_GROUP }.count { it }
|
|
||||||
val denominator = geoLoc.children.size
|
|
||||||
_binding.count.text = Settings.getStats(ctx, numerator, denominator)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refresh(geoLoc: GeoLoc) {
|
|
||||||
// Refresh
|
|
||||||
refreshCheck(geoLoc)
|
|
||||||
refreshCount(geoLoc)
|
|
||||||
|
|
||||||
// Recursively refresh parent
|
|
||||||
_parentHolder?.refresh(_parentGeoLoc)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package net.helcel.beans.activity.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import net.helcel.beans.activity.fragment.EditGroupAddFragment
|
|
||||||
import net.helcel.beans.databinding.ItemListGroupBinding
|
|
||||||
import net.helcel.beans.helper.Data
|
|
||||||
import net.helcel.beans.helper.Groups
|
|
||||||
import net.helcel.beans.helper.Theme.getContrastColor
|
|
||||||
|
|
||||||
class GroupListAdapter(
|
|
||||||
private val activity: FragmentActivity,
|
|
||||||
private val selectDialog: DialogFragment,
|
|
||||||
private val delete: Boolean = false
|
|
||||||
) : RecyclerView.Adapter<GroupListAdapter.GroupViewHolder>() {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GroupViewHolder {
|
|
||||||
val binding =
|
|
||||||
ItemListGroupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
return GroupViewHolder(binding, activity, selectDialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: GroupViewHolder, pos: Int) {
|
|
||||||
holder.bind(Data.groups.getGroupFromPos(pos))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return Data.groups.size()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class GroupViewHolder(
|
|
||||||
private val _binding: ItemListGroupBinding,
|
|
||||||
private val activity: FragmentActivity,
|
|
||||||
private val selectDialog: DialogFragment
|
|
||||||
) : RecyclerView.ViewHolder(_binding.root) {
|
|
||||||
private lateinit var dialogFragment: EditGroupAddFragment
|
|
||||||
fun bind(entry: Pair<Int, Groups.Group>) {
|
|
||||||
_binding.groupColor.text = entry.second.name
|
|
||||||
dialogFragment = EditGroupAddFragment(entry.first, {
|
|
||||||
val newEntry = Data.groups.getGroupFromKey(entry.first)
|
|
||||||
_binding.groupColor.text = newEntry.name
|
|
||||||
val newEntryColor = newEntry.color.color
|
|
||||||
val contrastNewEntryColor =
|
|
||||||
getContrastColor(newEntryColor)
|
|
||||||
_binding.groupColor.setBackgroundColor(newEntryColor)
|
|
||||||
_binding.groupColor.setTextColor(contrastNewEntryColor)
|
|
||||||
_binding.name.setTextColor(contrastNewEntryColor)
|
|
||||||
_binding.name.text = "0"
|
|
||||||
}, {
|
|
||||||
notifyItemRemoved(it)
|
|
||||||
})
|
|
||||||
|
|
||||||
val entryColor = entry.second.color.color
|
|
||||||
val contrastEntryColor = getContrastColor(entryColor)
|
|
||||||
_binding.groupColor.setBackgroundColor(entryColor)
|
|
||||||
_binding.groupColor.setTextColor(contrastEntryColor)
|
|
||||||
_binding.name.setTextColor(contrastEntryColor)
|
|
||||||
_binding.name.text = Data.visits.countVisited(entry.first).toString()
|
|
||||||
|
|
||||||
_binding.groupColor.setOnClickListener {
|
|
||||||
Data.selected_group = entry.second
|
|
||||||
selectDialog.dismiss()
|
|
||||||
}
|
|
||||||
if (!delete) {
|
|
||||||
_binding.groupColor.setOnLongClickListener {
|
|
||||||
dialogFragment.show(
|
|
||||||
activity.supportFragmentManager,
|
|
||||||
"AddColorDialogFragment"
|
|
||||||
)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
package net.helcel.beans.activity.adapter
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.textview.MaterialTextView
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.countries.GeoLoc
|
|
||||||
import net.helcel.beans.countries.GeoLoc.LocType
|
|
||||||
import net.helcel.beans.countries.World
|
|
||||||
import net.helcel.beans.databinding.ItemListGroupBinding
|
|
||||||
import net.helcel.beans.helper.AUTO_GROUP
|
|
||||||
import net.helcel.beans.helper.Data
|
|
||||||
import net.helcel.beans.helper.Groups
|
|
||||||
import net.helcel.beans.helper.Settings
|
|
||||||
import net.helcel.beans.helper.Theme.getContrastColor
|
|
||||||
|
|
||||||
class StatsListAdapter(private val stats: RecyclerView, private val total: MaterialTextView) :
|
|
||||||
RecyclerView.Adapter<StatsListAdapter.StatsViewHolder>() {
|
|
||||||
private val unit = "km²"
|
|
||||||
|
|
||||||
private var locMode = LocType.WORLD
|
|
||||||
private lateinit var ctx: Context
|
|
||||||
private var countMode: Boolean = true
|
|
||||||
private var initialSum: Int = 0
|
|
||||||
|
|
||||||
private val wwwTotal: List<GeoLoc> = World.WWW.children.toList()
|
|
||||||
private val countryTotal: List<GeoLoc> = World.WWW.children.flatMap { it.children }
|
|
||||||
private val stateTotal: List<GeoLoc> =
|
|
||||||
World.WWW.children.flatMap { it.children.flatMap { itt -> itt.children } }
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatsViewHolder {
|
|
||||||
ctx = parent.context
|
|
||||||
val binding =
|
|
||||||
ItemListGroupBinding.inflate(LayoutInflater.from(ctx), parent, false)
|
|
||||||
|
|
||||||
return StatsViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: StatsViewHolder, pos: Int) {
|
|
||||||
initialSum += if (pos == itemCount - 1) {
|
|
||||||
holder.bind(
|
|
||||||
Pair(
|
|
||||||
AUTO_GROUP,
|
|
||||||
Groups.Group(AUTO_GROUP, ctx.getString(R.string.uncategorized))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
holder.bind(Data.groups.getGroupFromPos(pos))
|
|
||||||
}
|
|
||||||
val unitNow = if (!countMode) unit else ""
|
|
||||||
total.text = Settings.getStats(ctx, initialSum, getTotal(), unitNow)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return Data.groups.size() + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTotal(): Int {
|
|
||||||
return if (countMode) {
|
|
||||||
when (locMode) {
|
|
||||||
LocType.WORLD -> wwwTotal.size
|
|
||||||
LocType.COUNTRY -> countryTotal.size
|
|
||||||
LocType.STATE -> stateTotal.size
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (locMode) {
|
|
||||||
LocType.WORLD -> wwwTotal.sumOf { it.area }
|
|
||||||
LocType.COUNTRY -> countryTotal.sumOf { it.area }
|
|
||||||
LocType.STATE -> stateTotal.sumOf { it.area }
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshMode(mode: LocType) {
|
|
||||||
val sum = (0 until itemCount).map {
|
|
||||||
val viewHolder = stats.findViewHolderForAdapterPosition(it) as? StatsViewHolder
|
|
||||||
viewHolder?.refresh(mode)
|
|
||||||
}.reduce { acc, i -> acc?.plus((i ?: 0)) }
|
|
||||||
val unitNow = if (!countMode) unit else ""
|
|
||||||
total.text = Settings.getStats(ctx, sum, getTotal(), unitNow)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun invertCountMode() {
|
|
||||||
countMode = !countMode
|
|
||||||
refreshMode(locMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class StatsViewHolder(
|
|
||||||
private val _binding: ItemListGroupBinding
|
|
||||||
) : RecyclerView.ViewHolder(_binding.root) {
|
|
||||||
|
|
||||||
private lateinit var data: Pair<Int, Groups.Group>
|
|
||||||
|
|
||||||
private lateinit var wwwCount: List<GeoLoc>
|
|
||||||
private lateinit var countryCount: List<GeoLoc>
|
|
||||||
private lateinit var stateCount: List<GeoLoc>
|
|
||||||
|
|
||||||
fun bind(entry: Pair<Int, Groups.Group>): Int {
|
|
||||||
data = entry
|
|
||||||
_binding.groupColor.text = entry.second.name
|
|
||||||
|
|
||||||
val entryColor = data.second.color.color
|
|
||||||
val contrastEntryColor = getContrastColor(entryColor)
|
|
||||||
_binding.groupColor.setBackgroundColor(entryColor)
|
|
||||||
_binding.groupColor.setTextColor(contrastEntryColor)
|
|
||||||
_binding.name.setTextColor(contrastEntryColor)
|
|
||||||
|
|
||||||
_binding.groupColor.setOnClickListener { invertCountMode() }
|
|
||||||
compute()
|
|
||||||
return refresh(locMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun compute() {
|
|
||||||
val visited = Data.visits.getVisitedByValue(data.first)
|
|
||||||
wwwCount = World.WWW.children.filter { it.code in visited }
|
|
||||||
countryCount =
|
|
||||||
World.WWW.children.map { it.children.filter { itt -> itt.code in visited } }
|
|
||||||
.flatten()
|
|
||||||
stateCount =
|
|
||||||
World.WWW.children.map { it.children.map { itt -> itt.children.filter { ittt -> ittt.code in visited } } }
|
|
||||||
.flatten().flatten()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh(mode: LocType): Int {
|
|
||||||
locMode = mode
|
|
||||||
return if (countMode) {
|
|
||||||
val count = when (locMode) {
|
|
||||||
LocType.WORLD -> wwwCount.size
|
|
||||||
LocType.COUNTRY -> countryCount.size
|
|
||||||
LocType.STATE -> stateCount.size
|
|
||||||
else -> -1
|
|
||||||
}
|
|
||||||
_binding.name.text = count.toString()
|
|
||||||
count
|
|
||||||
} else {
|
|
||||||
val area = when (locMode) {
|
|
||||||
LocType.WORLD -> wwwCount.sumOf { it.area }
|
|
||||||
LocType.COUNTRY -> countryCount.sumOf { it.area }
|
|
||||||
LocType.STATE -> stateCount.sumOf { it.area }
|
|
||||||
else -> -1
|
|
||||||
}
|
|
||||||
_binding.name.text = ctx.getString(R.string.number_with_unit, area, unit)
|
|
||||||
area
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package net.helcel.beans.activity.adapter
|
|
||||||
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import net.helcel.beans.activity.fragment.EditPlaceFragment
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class ViewPagerAdapter(
|
|
||||||
fragmentManager: FragmentManager,
|
|
||||||
lifecycle: Lifecycle,
|
|
||||||
private val viewPager: ViewPager2
|
|
||||||
) :
|
|
||||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
|
||||||
|
|
||||||
private val fragmentList: MutableList<EditPlaceFragment> = ArrayList()
|
|
||||||
|
|
||||||
fun addFragment(src: EditPlaceFragment?, target: EditPlaceFragment) {
|
|
||||||
val idx = fragmentList.indexOf(src)
|
|
||||||
viewPager.currentItem = max(0, idx)
|
|
||||||
if (src != null && idx >= 0) {
|
|
||||||
fragmentList.subList(idx + 1, fragmentList.size).clear()
|
|
||||||
}
|
|
||||||
fragmentList.add(target)
|
|
||||||
notifyItemRangeChanged(max(0, idx), fragmentList.size)
|
|
||||||
viewPager.currentItem = fragmentList.size - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return fragmentList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
fun backPressed(): Boolean {
|
|
||||||
if (viewPager.currentItem == 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val target = viewPager.currentItem
|
|
||||||
while (fragmentList.size > target) {
|
|
||||||
fragmentList.removeLast()
|
|
||||||
notifyItemRemoved(fragmentList.size)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLabel(pos: Int): String {
|
|
||||||
return fragmentList[pos].loc.fullName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
return fragmentList[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshColors(colorDrawable: ColorDrawable) {
|
|
||||||
fragmentList.forEach{ it.refreshColors(colorDrawable)}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package net.helcel.beans.activity.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import net.helcel.beans.databinding.FragmentAboutBinding
|
|
||||||
|
|
||||||
class AboutFragment : Fragment() {
|
|
||||||
private lateinit var _binding: FragmentAboutBinding
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentAboutBinding.inflate(inflater, container, false)
|
|
||||||
return _binding.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
package net.helcel.beans.activity.fragment
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.graphics.blue
|
|
||||||
import androidx.core.graphics.green
|
|
||||||
import androidx.core.graphics.red
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.databinding.FragmentEditGroupsAddBinding
|
|
||||||
import net.helcel.beans.helper.Data
|
|
||||||
import net.helcel.beans.helper.Groups
|
|
||||||
import net.helcel.beans.helper.Theme.colorToHex6
|
|
||||||
|
|
||||||
|
|
||||||
class EditGroupAddFragment(
|
|
||||||
private val key: Int = 0,
|
|
||||||
val onAddCb: (Int) -> Unit,
|
|
||||||
val onDelCb: (Int) -> Unit,
|
|
||||||
private val deleteEnabled: Boolean = true
|
|
||||||
) : DialogFragment() {
|
|
||||||
|
|
||||||
private lateinit var _binding: FragmentEditGroupsAddBinding
|
|
||||||
private val grp = Data.groups.getGroupFromKey(key)
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
|
||||||
_binding = FragmentEditGroupsAddBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
setupSlider(_binding.colorR, grp.color.color.red / 255F)
|
|
||||||
setupSlider(_binding.colorG, grp.color.color.green / 255F)
|
|
||||||
setupSlider(_binding.colorB, grp.color.color.blue / 255F)
|
|
||||||
setupText(_binding.groupColor, grp)
|
|
||||||
|
|
||||||
_binding.colorView.background = ColorDrawable(grp.color.color)
|
|
||||||
|
|
||||||
|
|
||||||
if (key == 0 || !deleteEnabled) {
|
|
||||||
_binding.btnDelete.visibility = View.INVISIBLE
|
|
||||||
_binding.btnDelete.isEnabled = false
|
|
||||||
}
|
|
||||||
_binding.btnDelete.setOnClickListener {
|
|
||||||
MaterialAlertDialogBuilder(requireActivity())
|
|
||||||
.setMessage(R.string.delete_group)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
val pos = Data.groups.findGroupPos(key)
|
|
||||||
// Remove all countries belonging to that group
|
|
||||||
Data.visits.deleteVisited(key)
|
|
||||||
// Delete the group
|
|
||||||
Data.groups.deleteGroup(key)
|
|
||||||
Data.saveData()
|
|
||||||
onDelCb(pos)
|
|
||||||
dialog?.dismiss()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
_binding.btnOk.setOnClickListener {
|
|
||||||
val name = _binding.groupName.text.toString()
|
|
||||||
val color = _binding.groupColor.text.toString()
|
|
||||||
val key = (if (key != 0) key else Data.groups.genKey())
|
|
||||||
Data.groups.setGroup(key, name, ColorDrawable(Color.parseColor("#$color")))
|
|
||||||
Data.saveData()
|
|
||||||
onAddCb(key)
|
|
||||||
dialog?.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
_binding.btnCancel.setOnClickListener {
|
|
||||||
dialog?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
_binding.groupName.setText(grp.name)
|
|
||||||
builder.setView(_binding.root)
|
|
||||||
return builder.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupText(s: TextInputEditText, grp: Groups.Group?) {
|
|
||||||
s.setText(colorToHex6(ColorDrawable(grp?.color?.color ?: 0)).substring(1))
|
|
||||||
s.addTextChangedListener(
|
|
||||||
EditTextListener(
|
|
||||||
_binding.colorR,
|
|
||||||
_binding.colorG,
|
|
||||||
_binding.colorB,
|
|
||||||
_binding.groupColor,
|
|
||||||
_binding.colorView
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupSlider(s: Slider, v: Float) {
|
|
||||||
s.valueFrom = 0F
|
|
||||||
s.valueTo = 1F
|
|
||||||
s.value = v
|
|
||||||
s.addOnChangeListener(
|
|
||||||
SliderOnChangeListener(
|
|
||||||
_binding.colorR,
|
|
||||||
_binding.colorG,
|
|
||||||
_binding.colorB,
|
|
||||||
_binding.groupColor,
|
|
||||||
_binding.colorView
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class EditTextListener(
|
|
||||||
private val colorEditR: Slider,
|
|
||||||
private val colorEditG: Slider,
|
|
||||||
private val colorEditB: Slider,
|
|
||||||
private val colorEditText: TextInputEditText,
|
|
||||||
private val colorView: View
|
|
||||||
) : TextWatcher {
|
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
||||||
override fun afterTextChanged(s: Editable?) {
|
|
||||||
val col: Color
|
|
||||||
try {
|
|
||||||
col = Color.valueOf(Color.parseColor("#${colorEditText.text}"))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
colorEditR.value = col.red()
|
|
||||||
colorEditG.value = col.green()
|
|
||||||
colorEditB.value = col.blue()
|
|
||||||
colorView.background = ColorDrawable(col.toArgb())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SliderOnChangeListener(
|
|
||||||
private val colorEditR: Slider,
|
|
||||||
private val colorEditG: Slider,
|
|
||||||
private val colorEditB: Slider,
|
|
||||||
private val colorEditText: TextInputEditText,
|
|
||||||
private val colorView: View
|
|
||||||
) : Slider.OnChangeListener {
|
|
||||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
|
||||||
val rgb =
|
|
||||||
ColorDrawable(Color.argb(1F, colorEditR.value, colorEditG.value, colorEditB.value))
|
|
||||||
colorEditText.setText(colorToHex6(rgb).substring(1))
|
|
||||||
colorView.background = rgb
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package net.helcel.beans.activity.fragment
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.activity.adapter.GroupListAdapter
|
|
||||||
import net.helcel.beans.databinding.FragmentEditPlacesColorsBinding
|
|
||||||
import net.helcel.beans.helper.Data
|
|
||||||
import net.helcel.beans.helper.DialogCloser
|
|
||||||
|
|
||||||
|
|
||||||
class EditPlaceColorFragment(private val parent: DialogCloser, private val delete: Boolean = false) :
|
|
||||||
DialogFragment() {
|
|
||||||
|
|
||||||
private lateinit var _binding: FragmentEditPlacesColorsBinding
|
|
||||||
private lateinit var listAdapt: GroupListAdapter
|
|
||||||
private var clear: Boolean = false
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
val ctx = requireContext()
|
|
||||||
val builder = MaterialAlertDialogBuilder(ctx)
|
|
||||||
_binding = FragmentEditPlacesColorsBinding.inflate(layoutInflater)
|
|
||||||
_binding.btnAdd.setOnClickListener {
|
|
||||||
EditGroupAddFragment(0, {
|
|
||||||
listAdapt.notifyItemInserted(Data.groups.findGroupPos(it))
|
|
||||||
}, {}).show(requireActivity().supportFragmentManager, "AddColorDialogFragment")
|
|
||||||
}
|
|
||||||
_binding.btnClear.setOnClickListener {
|
|
||||||
clear = true
|
|
||||||
dialog?.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
val dialog = builder.setView(_binding.root).create()
|
|
||||||
listAdapt = GroupListAdapter(requireActivity(), this, delete)
|
|
||||||
_binding.groupsColor.layoutManager =
|
|
||||||
LinearLayoutManager(ctx, RecyclerView.VERTICAL, false)
|
|
||||||
_binding.groupsColor.adapter = listAdapt
|
|
||||||
|
|
||||||
if (delete) {
|
|
||||||
_binding.btnAdd.visibility = View.GONE
|
|
||||||
_binding.btnClear.text = ctx.getString(R.string.cancel)
|
|
||||||
_binding.warningText.text = ctx.getString(R.string.select_group)
|
|
||||||
} else {
|
|
||||||
_binding.warningText.text = ctx.getString(R.string.edit_group)
|
|
||||||
}
|
|
||||||
|
|
||||||
return dialog
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
parent.onDialogDismiss(clear)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package net.helcel.beans.activity.fragment
|
|
||||||
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import net.helcel.beans.activity.adapter.GeolocListAdapter
|
|
||||||
import net.helcel.beans.activity.adapter.GeolocListAdapter.FoldingListViewHolder
|
|
||||||
import net.helcel.beans.activity.adapter.ViewPagerAdapter
|
|
||||||
import net.helcel.beans.countries.GeoLoc
|
|
||||||
import net.helcel.beans.databinding.FragmentEditPlacesBinding
|
|
||||||
|
|
||||||
class EditPlaceFragment(val loc: GeoLoc, private val pager: ViewPagerAdapter, private val holder: FoldingListViewHolder? = null) : Fragment() {
|
|
||||||
private lateinit var _binding: FragmentEditPlacesBinding
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentEditPlacesBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
_binding.list.setItemViewCacheSize(5)
|
|
||||||
_binding.list.setHasFixedSize(true)
|
|
||||||
_binding.list.layoutManager =
|
|
||||||
LinearLayoutManager(requireContext(), RecyclerView.VERTICAL, false)
|
|
||||||
_binding.list.adapter = GeolocListAdapter(this, loc, pager, holder)
|
|
||||||
return _binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshColors(colorDrawable: ColorDrawable) {
|
|
||||||
(_binding.list.adapter as GeolocListAdapter?)?.refreshColors(colorDrawable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package net.helcel.beans.activity.fragment
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import com.mikepenz.aboutlibraries.LibsBuilder
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.databinding.FragmentLicenseBinding
|
|
||||||
|
|
||||||
class LicenseFragment : Fragment() {
|
|
||||||
private lateinit var _binding: FragmentLicenseBinding
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentLicenseBinding.inflate(inflater, container, false)
|
|
||||||
|
|
||||||
val librariesFragment = LibsBuilder()
|
|
||||||
.withLicenseShown(true)
|
|
||||||
.supportFragment()
|
|
||||||
|
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.license_fragment_view, librariesFragment)
|
|
||||||
.commit()
|
|
||||||
|
|
||||||
return _binding.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
package net.helcel.beans.activity.fragment
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import net.helcel.beans.R
|
|
||||||
import net.helcel.beans.countries.GeoLocImporter
|
|
||||||
import net.helcel.beans.helper.Data
|
|
||||||
import net.helcel.beans.helper.DialogCloser
|
|
||||||
import net.helcel.beans.helper.Settings
|
|
||||||
|
|
||||||
|
|
||||||
class SettingsFragment : PreferenceFragmentCompat(), DialogCloser {
|
|
||||||
private var savedInstanceState: Bundle? = null
|
|
||||||
private var rootKey: String? = null
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
this.savedInstanceState = savedInstanceState
|
|
||||||
this.rootKey = rootKey
|
|
||||||
|
|
||||||
setPreferencesFromResource(R.xml.fragment_settings, rootKey)
|
|
||||||
val ctx = requireContext()
|
|
||||||
|
|
||||||
// Select Light/Dark/System Mode
|
|
||||||
findPreference<Preference>(getString(R.string.key_theme))?.setOnPreferenceChangeListener { _, key ->
|
|
||||||
setTheme(ctx, key as String)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select map projection
|
|
||||||
findPreference<Preference>(getString(R.string.key_projection))?.setOnPreferenceChangeListener { _, key ->
|
|
||||||
Settings.refreshProjection()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle groups
|
|
||||||
findPreference<Preference>(getString(R.string.key_group))?.setOnPreferenceChangeListener { _, key ->
|
|
||||||
if (key as String == ctx.getString(R.string.off)) {
|
|
||||||
val fragment = EditPlaceColorFragment(this, true)
|
|
||||||
fragment.show(
|
|
||||||
this.parentFragmentManager,
|
|
||||||
"AddColorDialogFragment"
|
|
||||||
)
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle regional geolocs
|
|
||||||
findPreference<Preference>(getString(R.string.key_regional))?.setOnPreferenceChangeListener { _, key ->
|
|
||||||
when (key as String) {
|
|
||||||
ctx.getString(R.string.off) -> {
|
|
||||||
MaterialAlertDialogBuilder(requireActivity())
|
|
||||||
.setMessage(R.string.delete_regions)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
GeoLocImporter.clearStates()
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(ctx).edit().putString(
|
|
||||||
ctx.getString(R.string.key_regional),
|
|
||||||
ctx.getString(R.string.off)
|
|
||||||
).apply()
|
|
||||||
refreshPreferences()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
.show()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.getString(R.string.on) -> {
|
|
||||||
GeoLocImporter.importStates(ctx, true)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Open license fragment
|
|
||||||
findPreference<Preference>(getString(R.string.licenses))?.setOnPreferenceClickListener {
|
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_view, LicenseFragment(), getString(R.string.licenses))
|
|
||||||
.commit()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open about fragment
|
|
||||||
findPreference<Preference>(getString(R.string.about))?.setOnPreferenceClickListener {
|
|
||||||
requireActivity().supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.fragment_view, AboutFragment(), getString(R.string.about))
|
|
||||||
.commit()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun setTheme(ctx: Context, key: String?): Boolean {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(
|
|
||||||
when (key) {
|
|
||||||
ctx.getString(R.string.system) -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
ctx.getString(R.string.light) -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
ctx.getString(R.string.dark) -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogDismiss(clear: Boolean) {
|
|
||||||
// When turning groups off, select one group to keep and reassign everything
|
|
||||||
Data.selected_group?.let { selectedGroup ->
|
|
||||||
// Reassign all visited that are not to selectedGroup to selectedGroup
|
|
||||||
Data.visits.reassignAllVisitedToGroup(selectedGroup.key)
|
|
||||||
|
|
||||||
// Delete all groups that are not selectedGroup
|
|
||||||
Data.groups.deleteAllExcept(selectedGroup.key)
|
|
||||||
|
|
||||||
// Save and clear global variables
|
|
||||||
Data.saveData()
|
|
||||||
Data.selected_geoloc = null
|
|
||||||
Data.selected_group = null
|
|
||||||
|
|
||||||
// Actually change preference
|
|
||||||
val ctx = requireContext()
|
|
||||||
val sp = PreferenceManager.getDefaultSharedPreferences(ctx)
|
|
||||||
sp.edit().putString(ctx.getString(R.string.key_group), ctx.getString(R.string.off))
|
|
||||||
.apply()
|
|
||||||
|
|
||||||
// Refresh entire preference fragment to reflect changes
|
|
||||||
refreshPreferences()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refreshPreferences() {
|
|
||||||
preferenceScreen.removeAll()
|
|
||||||
onCreatePreferences(savedInstanceState, rootKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package net.helcel.beans.activity.sub
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import net.helcel.beans.R
|
||||||
|
import net.helcel.beans.BuildConfig
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AboutScreen(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 20.dp).background(MaterialTheme.colors.background),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||||
|
contentDescription = "Logo",
|
||||||
|
modifier = Modifier
|
||||||
|
.size(300.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.APP_NAME,
|
||||||
|
fontSize = 30.sp,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = BuildConfig.VERSION_NAME,
|
||||||
|
fontSize = 25.sp,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.beans_is_foss),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val uri = stringResource(R.string.beans_repo_uri)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.beans_repo,uri),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
uriHandler.openUri(uri)
|
||||||
|
}
|
||||||
|
.padding(vertical = 15.dp, horizontal = 10.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package net.helcel.beans.activity.sub
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Slider
|
||||||
|
import androidx.compose.material.SliderDefaults
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.Theme.colorToHex6
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import net.helcel.beans.R
|
||||||
|
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EditGroupPreview(){
|
||||||
|
EditGroupDialog(0,true,{},{},{})
|
||||||
|
}
|
||||||
|
@Composable
|
||||||
|
fun EditGroupDialog(
|
||||||
|
key: Int = 0,
|
||||||
|
deleteEnabled: Boolean = true,
|
||||||
|
onAddCb: (Int) -> Unit,
|
||||||
|
onDelCb: (Int) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val group by remember { mutableStateOf(Data.groups.getGroupFromKey(key)) }
|
||||||
|
var name by remember { mutableStateOf(group.name) }
|
||||||
|
var colorHex by remember {
|
||||||
|
mutableStateOf(colorToHex6(group.color.color.toDrawable()).substring(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex to Color safely
|
||||||
|
var color = remember {try {
|
||||||
|
Color("#$colorHex".toColorInt())
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Color.Gray
|
||||||
|
}}
|
||||||
|
var r by remember { mutableIntStateOf((color.red *255).toInt()) }
|
||||||
|
var g by remember { mutableIntStateOf((color.green*255).toInt()) }
|
||||||
|
var b by remember { mutableIntStateOf((color.blue*255).toInt()) }
|
||||||
|
|
||||||
|
fun updateHexFromSliders() {
|
||||||
|
val newColor = Color(r, g, b)
|
||||||
|
colorHex = colorToHex6(newColor.toArgb().toDrawable()).substring(1)
|
||||||
|
color = newColor
|
||||||
|
}
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
RoundedCornerShape(corner = CornerSize(16.dp))
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
text = if (key == 0) stringResource(R.string.action_add)
|
||||||
|
else stringResource(R.string.action_edit),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// Group name
|
||||||
|
OutlinedTextField(
|
||||||
|
value = name,
|
||||||
|
onValueChange = { it: String -> name = it },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
placeholder = { Text(stringResource(R.string.name)) },
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedTextColor = MaterialTheme.colors.onBackground,
|
||||||
|
focusedTextColor = MaterialTheme.colors.onBackground,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
|
||||||
|
// Color preview
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp, (96).dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(color),
|
||||||
|
propagateMinConstraints = true,
|
||||||
|
|
||||||
|
content = {}
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
ColorSlider(
|
||||||
|
r.toFloat(),
|
||||||
|
{ r = it.toInt(); updateHexFromSliders() },
|
||||||
|
Color(255, 0, 0)
|
||||||
|
)
|
||||||
|
ColorSlider(
|
||||||
|
g.toFloat(),
|
||||||
|
{ g = it.toInt(); updateHexFromSliders() },
|
||||||
|
Color(0, 255, 0)
|
||||||
|
)
|
||||||
|
ColorSlider(
|
||||||
|
b.toFloat(),
|
||||||
|
{ b = it.toInt(); updateHexFromSliders() },
|
||||||
|
Color(0, 0, 255)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hex input
|
||||||
|
OutlinedTextField(
|
||||||
|
value = colorHex,
|
||||||
|
onValueChange = { n:String->
|
||||||
|
colorHex = n.filter { it.isLetterOrDigit() }
|
||||||
|
},
|
||||||
|
label = { Text(text="Color (hex)", color=MaterialTheme.colors.onBackground) },
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
fontSize = 12.sp
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
unfocusedTextColor =MaterialTheme.colors.onBackground,
|
||||||
|
focusedTextColor = MaterialTheme.colors.onBackground,
|
||||||
|
),
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
Button(onClick = {
|
||||||
|
val newKey = if (key != 0) key else Data.groups.genKey()
|
||||||
|
Data.groups.setGroup(
|
||||||
|
newKey,
|
||||||
|
name,
|
||||||
|
"#$colorHex".toColorInt().toDrawable()
|
||||||
|
)
|
||||||
|
Data.saveData()
|
||||||
|
onAddCb(newKey)
|
||||||
|
onDismiss()
|
||||||
|
}, ) {
|
||||||
|
Text("OK")
|
||||||
|
}
|
||||||
|
if (key != 0 && deleteEnabled) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
val pos = Data.groups.findGroupPos(key)
|
||||||
|
Data.visits.deleteVisited(key)
|
||||||
|
Data.groups.deleteGroup(key)
|
||||||
|
Data.saveData()
|
||||||
|
onDelCb(pos)
|
||||||
|
onDismiss()
|
||||||
|
}) {
|
||||||
|
Text("Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(onClick = { onDismiss() }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColorSlider(v: Float, onChange:(Float)->Unit, c:Color ){
|
||||||
|
Slider(
|
||||||
|
value = v,
|
||||||
|
onValueChange = onChange,
|
||||||
|
valueRange = 0f..255f,
|
||||||
|
steps = 255,
|
||||||
|
modifier = Modifier.height(32.dp),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
thumbColor = c,
|
||||||
|
activeTickColor = c,
|
||||||
|
inactiveTickColor = MaterialTheme.colors.onBackground,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package net.helcel.beans.activity.sub
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import net.helcel.beans.R
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.Groups
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import net.helcel.beans.activity.SysTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditPlaceDialog(delete: Boolean, onDialogDismiss: (Boolean)->Unit){
|
||||||
|
SysTheme {
|
||||||
|
var showEditGroupDialog by remember { mutableStateOf(false) }
|
||||||
|
var showEditPlaceColorDialog by remember { mutableStateOf(true) }
|
||||||
|
var showSelectedKey by remember { mutableIntStateOf(-1) }
|
||||||
|
var showDelete by remember { mutableStateOf(false) }
|
||||||
|
if (showEditGroupDialog)
|
||||||
|
EditGroupDialog(
|
||||||
|
key = showSelectedKey,
|
||||||
|
deleteEnabled = showDelete,
|
||||||
|
onAddCb = { },
|
||||||
|
onDelCb = {
|
||||||
|
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showEditGroupDialog = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (showEditPlaceColorDialog)
|
||||||
|
EditPlaceColorDialog(
|
||||||
|
delete,
|
||||||
|
onAdd = {
|
||||||
|
showSelectedKey = it
|
||||||
|
showDelete = false
|
||||||
|
showEditGroupDialog = true
|
||||||
|
},
|
||||||
|
onDelete = {
|
||||||
|
showSelectedKey = it
|
||||||
|
showDelete = true
|
||||||
|
showEditGroupDialog = true
|
||||||
|
},
|
||||||
|
onClear = {
|
||||||
|
showEditPlaceColorDialog = false
|
||||||
|
onDialogDismiss(true)
|
||||||
|
},
|
||||||
|
onDismiss = {
|
||||||
|
showEditPlaceColorDialog = false
|
||||||
|
onDialogDismiss(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun GroupListPreview() {
|
||||||
|
Data.groups = Groups(0, HashMap())
|
||||||
|
Data.groups.setGroup(0, "Testing", Color.Red.toArgb().toDrawable())
|
||||||
|
Data.groups.setGroup(1, "Testing", Color.Blue.toArgb().toDrawable())
|
||||||
|
EditPlaceColorDialog(false,{},{},{},{})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditPlaceColorDialog(
|
||||||
|
deleteMode: Boolean = false,
|
||||||
|
onAdd: (Int) -> Unit = {},
|
||||||
|
onDelete: (Int) -> Unit= {},
|
||||||
|
onClear: () -> Unit= {},
|
||||||
|
onDismiss: () -> Unit= {},
|
||||||
|
) {
|
||||||
|
val groups by Data.groups.groupsFlow.collectAsState()
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
RoundedCornerShape(corner = CornerSize(16.dp)))
|
||||||
|
.padding(16.dp)
|
||||||
|
,
|
||||||
|
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color=MaterialTheme.colors.onBackground,
|
||||||
|
text = if (deleteMode) stringResource(R.string.select_group)
|
||||||
|
else stringResource(R.string.edit_group)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color=MaterialTheme.colors.onBackground,
|
||||||
|
text = if (deleteMode) stringResource(R.string.select_group_sub)
|
||||||
|
else stringResource(R.string.edit_group_sub)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 300.dp) // cap dialog growth
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
//.weight(1f)
|
||||||
|
) {
|
||||||
|
items(groups, key = { it.key }) { group ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { Data.selected_group = group; onDismiss() },
|
||||||
|
onLongClick = { onDelete(group.key) })
|
||||||
|
.background(
|
||||||
|
Color(88, 88, 88, 88),
|
||||||
|
RoundedCornerShape(corner = CornerSize(16.dp))
|
||||||
|
)
|
||||||
|
.padding(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.background(Color(group.color.color), CircleShape)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(color=MaterialTheme.colors.onBackground,text=group.name)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End) {
|
||||||
|
if (!deleteMode) {
|
||||||
|
Button(onClick = { onAdd(0) }) {
|
||||||
|
Text(stringResource(R.string.action_add))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextButton(onClick = {
|
||||||
|
if (deleteMode) onDismiss() else onClear()
|
||||||
|
}) {
|
||||||
|
Text(
|
||||||
|
text = if (deleteMode) stringResource(R.string.cancel)
|
||||||
|
else stringResource(R.string.action_clear)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package net.helcel.beans.activity.sub
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.CheckboxDefaults
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Tab
|
||||||
|
import androidx.compose.material.TabRow
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TriStateCheckbox
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.state.ToggleableState
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import net.helcel.beans.countries.GeoLoc
|
||||||
|
import net.helcel.beans.countries.Group
|
||||||
|
import net.helcel.beans.countries.World
|
||||||
|
import net.helcel.beans.helper.AUTO_GROUP
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.NO_GROUP
|
||||||
|
import net.helcel.beans.helper.Settings
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun EditPlaceScreenPreview(){
|
||||||
|
EditPlaceScreen(Group.EEE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a place straight away where a single group makes the choice obvious,
|
||||||
|
* as tapping the checkbox does. Returns false when the group still has to be
|
||||||
|
* picked from the colour dialog.
|
||||||
|
*/
|
||||||
|
fun applyDirectVisit(ctx: Context, loc: GeoLoc): Boolean {
|
||||||
|
Data.selected_geoloc = loc
|
||||||
|
Data.selected_group = null
|
||||||
|
if (Data.groups.size() != 1 || !Settings.isSingleGroup(ctx)) return false
|
||||||
|
val current = Data.visits.getVisited(loc)
|
||||||
|
Data.visits.setVisited(
|
||||||
|
loc,
|
||||||
|
if (current == NO_GROUP || current == AUTO_GROUP) Data.groups.getUniqueEntry()!!.key
|
||||||
|
else if (loc.children.any { Data.visits.getVisited(it) != NO_GROUP }) AUTO_GROUP
|
||||||
|
else NO_GROUP,
|
||||||
|
)
|
||||||
|
syncVisited()
|
||||||
|
Data.saveData()
|
||||||
|
Data.selected_geoloc = null
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stores whatever the colour dialog came back with. */
|
||||||
|
fun commitVisitDialog(cleared: Boolean) {
|
||||||
|
if (cleared) {
|
||||||
|
Data.visits.setVisited(Data.selected_geoloc, NO_GROUP)
|
||||||
|
syncVisited()
|
||||||
|
Data.saveData()
|
||||||
|
}
|
||||||
|
if (Data.selected_group != null && Data.selected_geoloc != null) {
|
||||||
|
Data.visits.setVisited(Data.selected_geoloc, Data.selected_group!!.key)
|
||||||
|
syncVisited()
|
||||||
|
Data.saveData()
|
||||||
|
}
|
||||||
|
Data.selected_geoloc = null
|
||||||
|
Data.selected_group = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncVisited(loc: GeoLoc?=World.WWW): Boolean {
|
||||||
|
var changed = false
|
||||||
|
loc?.children?.forEach { tt ->
|
||||||
|
tt.children.forEach {itc->
|
||||||
|
if(Data.visits.getVisited(itc) in listOf(AUTO_GROUP,NO_GROUP)) {
|
||||||
|
val newState = if(itc.children.any { c -> Data.visits.getVisited(c) != NO_GROUP })
|
||||||
|
AUTO_GROUP
|
||||||
|
else
|
||||||
|
NO_GROUP
|
||||||
|
|
||||||
|
if (Data.visits.getVisited(itc) != newState) {
|
||||||
|
Data.visits.setVisited(itc, newState)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(Data.visits.getVisited(tt) in listOf(AUTO_GROUP,NO_GROUP)) {
|
||||||
|
val newState = if(tt.children.any { itc -> Data.visits.getVisited(itc) != NO_GROUP })
|
||||||
|
AUTO_GROUP
|
||||||
|
else
|
||||||
|
NO_GROUP
|
||||||
|
|
||||||
|
if (Data.visits.getVisited(tt) != newState) {
|
||||||
|
Data.visits.setVisited(tt, newState)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sync World from Continents
|
||||||
|
if (loc != null && Data.visits.getVisited(loc) in listOf(AUTO_GROUP, NO_GROUP)) {
|
||||||
|
val newState = if(loc.children.any { Data.visits.getVisited(it) != NO_GROUP })
|
||||||
|
AUTO_GROUP
|
||||||
|
else
|
||||||
|
NO_GROUP
|
||||||
|
if (Data.visits.getVisited(loc) != newState) {
|
||||||
|
Data.visits.setVisited(loc, newState)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EditPlaceScreen(loc: GeoLoc, onExit:()->Unit={}) {
|
||||||
|
val visits by Data.visits.visitsFlow.collectAsState()
|
||||||
|
var showEdit by remember { mutableStateOf(false) }
|
||||||
|
val tabs : SnapshotStateList<GeoLoc> = remember { mutableStateListOf(loc) }
|
||||||
|
val ctx = LocalContext.current
|
||||||
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(tabs.size) {
|
||||||
|
selectedTab = tabs.lastIndex
|
||||||
|
}
|
||||||
|
SideEffect {
|
||||||
|
// visits is used to trigger sync whenever data changes
|
||||||
|
if (visits.isNotEmpty() || true) {
|
||||||
|
if (syncVisited()) {
|
||||||
|
Data.saveData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackHandler {
|
||||||
|
if (tabs.size > 1) tabs.removeAt(tabs.lastIndex)
|
||||||
|
else onExit()
|
||||||
|
}
|
||||||
|
if(showEdit)
|
||||||
|
EditPlaceDialog(false) {
|
||||||
|
showEdit = false
|
||||||
|
commitVisitDialog(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
val currentTab = tabs.getOrNull(selectedTab) ?: return@Column
|
||||||
|
TabRow(
|
||||||
|
selectedTabIndex = min(tabs.lastIndex, selectedTab),
|
||||||
|
) {
|
||||||
|
tabs.forEachIndexed { index, tab ->
|
||||||
|
Tab(
|
||||||
|
selected = selectedTab == index,
|
||||||
|
onClick = {
|
||||||
|
while (tabs.size > index + 1)
|
||||||
|
tabs.removeAt(tabs.lastIndex)
|
||||||
|
},
|
||||||
|
text = { Text(tab.fullName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
items(currentTab.children.toList(), key= {it.code}) { loc ->
|
||||||
|
|
||||||
|
GeoLocRow(loc, {
|
||||||
|
if (loc.children.isNotEmpty()){
|
||||||
|
tabs.add(loc)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
if (!applyDirectVisit(ctx, loc)) showEdit = true
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GeoLocRow(
|
||||||
|
loc: GeoLoc,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onToggle: () -> Unit,
|
||||||
|
) {
|
||||||
|
val visits by Data.visits.visitsFlow.collectAsState()
|
||||||
|
val checked by remember(visits, loc) {
|
||||||
|
derivedStateOf {
|
||||||
|
when (visits.getOrElse(loc.code) { NO_GROUP }) {
|
||||||
|
NO_GROUP -> ToggleableState.Off
|
||||||
|
AUTO_GROUP -> ToggleableState.Indeterminate
|
||||||
|
else -> ToggleableState.On
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val color = if (Data.visits.getVisited(loc) !in listOf(NO_GROUP, AUTO_GROUP))
|
||||||
|
Color(Data.groups.getGroupFromKey(Data.visits.getVisited(loc)).color.color)
|
||||||
|
else MaterialTheme.colors.onBackground
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(50.dp)
|
||||||
|
.clickable(onClick = onClick) // whole row clickable
|
||||||
|
.padding(horizontal = 20.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = loc.fullName,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "",//loc.children.size.toString(),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
modifier = Modifier.padding(end = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
TriStateCheckbox(
|
||||||
|
state = checked,
|
||||||
|
onClick = onToggle,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = color,
|
||||||
|
),
|
||||||
|
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier
|
||||||
|
.height(2.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colors.onBackground))
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
package net.helcel.beans.activity.sub
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import net.helcel.beans.R
|
||||||
|
import net.helcel.beans.activity.SysTheme
|
||||||
|
import net.helcel.beans.countries.GeoLoc
|
||||||
|
import net.helcel.beans.countries.GeoLocTree
|
||||||
|
import net.helcel.beans.helper.AUTO_GROUP
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.NO_GROUP
|
||||||
|
|
||||||
|
private class PickRow(val loc: GeoLoc, val depth: Int)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns the shapes a tap landed near into an indented list.
|
||||||
|
*
|
||||||
|
* Each candidate brings its continent and country along, so a tap that is
|
||||||
|
* ambiguous between two regions can still be answered with "the whole country"
|
||||||
|
* or "the whole continent" instead of only the regions themselves.
|
||||||
|
*/
|
||||||
|
private fun rowsFor(candidates: List<GeoLoc>): List<PickRow> {
|
||||||
|
val rows = LinkedHashMap<String, PickRow>()
|
||||||
|
candidates.forEach { candidate ->
|
||||||
|
GeoLocTree.chain(candidate).forEachIndexed { depth, loc ->
|
||||||
|
rows.getOrPut(loc.code) { PickRow(loc, depth) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.values.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MapPickDialog(
|
||||||
|
candidates: List<GeoLoc>,
|
||||||
|
onPick: (GeoLoc) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
val visits by Data.visits.visitsFlow.collectAsState()
|
||||||
|
val rows = remember(candidates) { rowsFor(candidates) }
|
||||||
|
|
||||||
|
SysTheme {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
content = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colors.background,
|
||||||
|
RoundedCornerShape(corner = CornerSize(16.dp)),
|
||||||
|
)
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
text = stringResource(R.string.select_place),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
text = stringResource(R.string.select_place_sub),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 360.dp),
|
||||||
|
) {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
items(rows, key = { it.loc.code }) { row ->
|
||||||
|
val group = visits.getOrElse(row.loc.code) { NO_GROUP }
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onPick(row.loc) }
|
||||||
|
.background(
|
||||||
|
Color(88, 88, 88, 88),
|
||||||
|
RoundedCornerShape(corner = CornerSize(16.dp)),
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
start = 8.dp + 16.dp * row.depth,
|
||||||
|
top = 8.dp,
|
||||||
|
end = 8.dp,
|
||||||
|
bottom = 8.dp,
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.background(
|
||||||
|
if (group == NO_GROUP || group == AUTO_GROUP) {
|
||||||
|
MaterialTheme.colors.onBackground
|
||||||
|
} else {
|
||||||
|
Color(
|
||||||
|
Data.groups.getGroupFromKey(group).color.color
|
||||||
|
)
|
||||||
|
},
|
||||||
|
CircleShape,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
text = row.loc.fullName,
|
||||||
|
// Long names wrap inside the row rather
|
||||||
|
// than running off the end of it.
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ enum class Country(
|
|||||||
override val area: Int
|
override val area: Int
|
||||||
) : GeoLoc {
|
) : GeoLoc {
|
||||||
ATA("Antarctica", 14000000),
|
ATA("Antarctica", 14000000),
|
||||||
HKG("Hong Kong", 1104),
|
|
||||||
MAC("Macao", 32),
|
// HKG("Hong Kong", 1104),
|
||||||
ANT("Netherlands Antilles", 800),
|
// MAC("Macao", 32),
|
||||||
|
// ANT("Netherlands Antilles", 800),
|
||||||
AFG("Afghanistan", 645487),
|
AFG("Afghanistan", 645487),
|
||||||
XAD("Akrotiri and Dhekelia", 234),
|
XAD("Akrotiri and Dhekelia", 234),
|
||||||
ALA("Åland", 1483),
|
ALA("Åland", 1483),
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ interface GeoLoc {
|
|||||||
|
|
||||||
val type: LocType
|
val type: LocType
|
||||||
val children: Set<GeoLoc>
|
val children: Set<GeoLoc>
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ object GeoLocImporter {
|
|||||||
|
|
||||||
fun importStates(ctx: Context, force: Boolean = false) {
|
fun importStates(ctx: Context, force: Boolean = false) {
|
||||||
if (!Settings.isRegional(ctx) and !force) {
|
if (!Settings.isRegional(ctx) and !force) {
|
||||||
|
GeoLocTree.rebuild()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val fs = BufferedReader(InputStreamReader(ctx.assets.open("geoloc_state.txt")))
|
val fs = BufferedReader(InputStreamReader(ctx.assets.open("geoloc_state.txt")))
|
||||||
@@ -26,6 +27,7 @@ object GeoLocImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GeoLocTree.rebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearStates() {
|
fun clearStates() {
|
||||||
@@ -39,6 +41,7 @@ object GeoLocImporter {
|
|||||||
}
|
}
|
||||||
country.children.clear()
|
country.children.clear()
|
||||||
}
|
}
|
||||||
|
GeoLocTree.rebuild()
|
||||||
Data.saveData()
|
Data.saveData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package net.helcel.beans.countries
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code to place lookup, plus the chain of parents above a place.
|
||||||
|
*
|
||||||
|
* Regions only exist once [GeoLocImporter] has read them, so this is rebuilt
|
||||||
|
* whenever that set changes rather than derived once and cached forever.
|
||||||
|
*/
|
||||||
|
object GeoLocTree {
|
||||||
|
|
||||||
|
private var index: Map<String, GeoLoc> = emptyMap()
|
||||||
|
private var parents: Map<String, GeoLoc> = emptyMap()
|
||||||
|
|
||||||
|
fun rebuild() {
|
||||||
|
val byCode = HashMap<String, GeoLoc>()
|
||||||
|
val parent = HashMap<String, GeoLoc>()
|
||||||
|
byCode[World.WWW.code] = World.WWW
|
||||||
|
World.WWW.children.forEach { child ->
|
||||||
|
byCode[child.code] = child
|
||||||
|
parent[child.code] = World.WWW
|
||||||
|
child.children.forEach { country ->
|
||||||
|
byCode[country.code] = country
|
||||||
|
parent[country.code] = child
|
||||||
|
country.children.forEach { state ->
|
||||||
|
byCode[state.code] = state
|
||||||
|
parent[state.code] = country
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index = byCode
|
||||||
|
parents = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
fun find(code: String): GeoLoc? {
|
||||||
|
if (index.isEmpty()) rebuild()
|
||||||
|
return index[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** [loc] and everything above it, world first, so it reads as a path. */
|
||||||
|
fun chain(loc: GeoLoc): List<GeoLoc> {
|
||||||
|
if (index.isEmpty()) rebuild()
|
||||||
|
val path = ArrayList<GeoLoc>(4)
|
||||||
|
var current: GeoLoc? = loc
|
||||||
|
while (current != null && current != World.WWW) {
|
||||||
|
path.add(current)
|
||||||
|
current = parents[current.code]
|
||||||
|
}
|
||||||
|
return path.reversed()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,11 @@ package net.helcel.beans.countries
|
|||||||
|
|
||||||
import net.helcel.beans.countries.Country.*
|
import net.helcel.beans.countries.Country.*
|
||||||
|
|
||||||
|
@Suppress("RedundantSuppression", "SpellCheckingInspection", "unused")
|
||||||
enum class Group(override val fullName: String, override val children: Set<GeoLoc>) : GeoLoc {
|
enum class Group(override val fullName: String, override val children: Set<GeoLoc>) : GeoLoc {
|
||||||
|
|
||||||
EEE(
|
EEE(
|
||||||
"Europe", setOf(
|
"Europe", setOf(
|
||||||
XAD,
|
|
||||||
ALA,// Åland Islands: an autonomous region of Finland, but not a member of the EU or UN
|
ALA,// Åland Islands: an autonomous region of Finland, but not a member of the EU or UN
|
||||||
ALB,
|
ALB,
|
||||||
AND,
|
AND,
|
||||||
@@ -16,7 +16,6 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
BIH,
|
BIH,
|
||||||
BGR,
|
BGR,
|
||||||
HRV,
|
HRV,
|
||||||
CYP,
|
|
||||||
CZE,
|
CZE,
|
||||||
DNK,
|
DNK,
|
||||||
EST,
|
EST,
|
||||||
@@ -26,6 +25,7 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
DEU,
|
DEU,
|
||||||
GIB, // Gibraltar: a British overseas territory located at the southern tip of the Iberian Peninsula
|
GIB, // Gibraltar: a British overseas territory located at the southern tip of the Iberian Peninsula
|
||||||
GRC,
|
GRC,
|
||||||
|
GRL,
|
||||||
GGY, // Guernsey: a British Crown dependency in the English Channel
|
GGY, // Guernsey: a British Crown dependency in the English Channel
|
||||||
HUN,
|
HUN,
|
||||||
ISL,
|
ISL,
|
||||||
@@ -33,7 +33,6 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
IMN, // Isle of Man: a British Crown dependency located in the Irish Sea
|
IMN, // Isle of Man: a British Crown dependency located in the Irish Sea
|
||||||
ITA,
|
ITA,
|
||||||
JEY, // Jersey: a British Crown dependency located in the English Channel
|
JEY, // Jersey: a British Crown dependency located in the English Channel
|
||||||
KAZ,
|
|
||||||
XKO,
|
XKO,
|
||||||
LVA,
|
LVA,
|
||||||
LIE,
|
LIE,
|
||||||
@@ -65,6 +64,7 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
),
|
),
|
||||||
ABB(
|
ABB(
|
||||||
"Asia", setOf(
|
"Asia", setOf(
|
||||||
|
XAD,
|
||||||
AFG,
|
AFG,
|
||||||
ARM,
|
ARM,
|
||||||
AZE,
|
AZE,
|
||||||
@@ -77,8 +77,9 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
CCK, // Cocos (Keeling) Islands: an Australian external territory in the Indian Ocean
|
CCK, // Cocos (Keeling) Islands: an Australian external territory in the Indian Ocean
|
||||||
CHN,
|
CHN,
|
||||||
CXR, // Christmas Island: an Australian external territory in the Indian Ocean
|
CXR, // Christmas Island: an Australian external territory in the Indian Ocean
|
||||||
|
CYP,
|
||||||
GEO,
|
GEO,
|
||||||
HKG,
|
//HKG,
|
||||||
IND,
|
IND,
|
||||||
IDN,
|
IDN,
|
||||||
IRN,
|
IRN,
|
||||||
@@ -86,11 +87,12 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
ISR,
|
ISR,
|
||||||
JPN,
|
JPN,
|
||||||
JOR,
|
JOR,
|
||||||
|
KAZ,
|
||||||
KWT,
|
KWT,
|
||||||
KGZ,
|
KGZ,
|
||||||
LAO,
|
LAO,
|
||||||
LBN,
|
LBN,
|
||||||
MAC,
|
//MAC,
|
||||||
MYS,
|
MYS,
|
||||||
MDV,
|
MDV,
|
||||||
MNG,
|
MNG,
|
||||||
@@ -127,6 +129,7 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
BDI,
|
BDI,
|
||||||
BEN,
|
BEN,
|
||||||
BWA,
|
BWA,
|
||||||
|
BVT, // Bouvet Island: an uninhabited territory of Norway in the South Atlantic
|
||||||
BFA,
|
BFA,
|
||||||
BDI,
|
BDI,
|
||||||
CPV,
|
CPV,
|
||||||
@@ -148,6 +151,7 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
GHA,
|
GHA,
|
||||||
GIN,
|
GIN,
|
||||||
GNB,
|
GNB,
|
||||||
|
HMD, // Heard Island and McDonald Islands: an uninhabited Australian external territory in the southern Indian Ocean
|
||||||
KEN,
|
KEN,
|
||||||
LSO,
|
LSO,
|
||||||
LBR,
|
LBR,
|
||||||
@@ -206,7 +210,6 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
DMA,
|
DMA,
|
||||||
DOM,
|
DOM,
|
||||||
SLV,
|
SLV,
|
||||||
GRL,
|
|
||||||
GRD,
|
GRD,
|
||||||
GLP,
|
GLP,
|
||||||
GTM,
|
GTM,
|
||||||
@@ -216,7 +219,7 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
MTQ,
|
MTQ,
|
||||||
MEX,
|
MEX,
|
||||||
MSR,
|
MSR,
|
||||||
ANT,
|
//ANT,
|
||||||
CUW,
|
CUW,
|
||||||
NIC,
|
NIC,
|
||||||
PAN,
|
PAN,
|
||||||
@@ -248,6 +251,7 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
GUY,
|
GUY,
|
||||||
PRY,
|
PRY,
|
||||||
PER,
|
PER,
|
||||||
|
SGS, // South Georgia and the South Sandwich Islands: a British overseas territory in the southern Atlantic Ocean
|
||||||
SUR,
|
SUR,
|
||||||
URY,
|
URY,
|
||||||
VEN,
|
VEN,
|
||||||
@@ -284,11 +288,8 @@ enum class Group(override val fullName: String, override val children: Set<GeoLo
|
|||||||
),
|
),
|
||||||
|
|
||||||
XXX(
|
XXX(
|
||||||
"Others", setOf(
|
"Other", setOf(
|
||||||
ATA, // Antarctica: not in any other region
|
|
||||||
BVT, // Bouvet Island: an uninhabited territory of Norway in the South Atlantic
|
|
||||||
HMD, // Heard Island and McDonald Islands: an uninhabited Australian external territory in the southern Indian Ocean
|
|
||||||
SGS, // South Georgia and the South Sandwich Islands: a British overseas territory in the southern Atlantic Ocean
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ enum class World(override val fullName: String, override val children: Set<GeoLo
|
|||||||
|
|
||||||
WWW(
|
WWW(
|
||||||
"World", setOf(
|
"World", setOf(
|
||||||
EEE, ABB, FFF, NNN, SRR, UUU, XXX
|
EEE, ABB, FFF, NNN, SRR, UUU, Country.ATA,
|
||||||
)
|
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
override val area = children.fold(0) { acc, i ->
|
override val area = children.fold(0) { acc, i ->
|
||||||
acc + i.area
|
acc + i.area
|
||||||
}
|
}
|
||||||
|
|
||||||
override val type = GeoLoc.LocType.WORLD
|
override val type = GeoLoc.LocType.WORLD
|
||||||
override val code = this.name
|
override val code = this.name
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
package net.helcel.beans.helper
|
package net.helcel.beans.helper
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import net.helcel.beans.R
|
import net.helcel.beans.R
|
||||||
import net.helcel.beans.countries.GeoLoc
|
import net.helcel.beans.countries.GeoLoc
|
||||||
import java.util.HashMap
|
import java.util.HashMap
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import android.content.Intent
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
object Data {
|
object Data {
|
||||||
var visits : Visits = Visits(0, HashMap())
|
var visits : Visits = Visits(0, HashMap())
|
||||||
@@ -15,7 +22,6 @@ object Data {
|
|||||||
|
|
||||||
var selected_group : Groups.Group? = null
|
var selected_group : Groups.Group? = null
|
||||||
var selected_geoloc: GeoLoc? = null
|
var selected_geoloc: GeoLoc? = null
|
||||||
var clearing_geoloc: GeoLoc? = null
|
|
||||||
|
|
||||||
private val groupsSerial = Groups.GroupsSerializer()
|
private val groupsSerial = Groups.GroupsSerializer()
|
||||||
private val visitsSerial = Visits.VisitsSerializer()
|
private val visitsSerial = Visits.VisitsSerializer()
|
||||||
@@ -23,7 +29,7 @@ object Data {
|
|||||||
private lateinit var sharedPreferences: SharedPreferences
|
private lateinit var sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
fun loadData(ctx: Context, id:Int) {
|
fun loadData(ctx: Context, id:Int) {
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
sharedPreferences = defaultPreferences(ctx)
|
||||||
|
|
||||||
val groupsString = sharedPreferences.getString("groups_$id",null)
|
val groupsString = sharedPreferences.getString("groups_$id",null)
|
||||||
val visitsString = sharedPreferences.getString("visits_$id",null)
|
val visitsString = sharedPreferences.getString("visits_$id",null)
|
||||||
@@ -33,17 +39,82 @@ object Data {
|
|||||||
|
|
||||||
// Add default group "Visited" with app's color if there is no group already
|
// Add default group "Visited" with app's color if there is no group already
|
||||||
if (groups.size() == 0) {
|
if (groups.size() == 0) {
|
||||||
groups.setGroup(DEFAULT_GROUP, "Visited", ColorDrawable(ContextCompat.getColor(ctx, R.color.blue)))
|
groups.setGroup(DEFAULT_GROUP, "Visited",
|
||||||
saveData()
|
ContextCompat.getColor(ctx, R.color.blue).toDrawable())
|
||||||
}
|
}
|
||||||
|
saveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveData() {
|
fun saveData() {
|
||||||
if(groups.id != visits.id) return
|
if(groups.id != visits.id) return
|
||||||
val id = groups.id
|
val id = groups.id
|
||||||
val editor = sharedPreferences.edit()
|
sharedPreferences.edit(commit=true) {
|
||||||
editor.putString("groups_$id", groupsSerial.writeTo(groups))
|
putString("groups_$id", groupsSerial.writeTo(groups))
|
||||||
editor.putString("visits_$id", visitsSerial.writeTo(visits))
|
putString("visits_$id", visitsSerial.writeTo(visits))
|
||||||
editor.apply()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportData(ctx: Context, filepath: Uri){
|
||||||
|
val groupsJson = groupsSerial.writeTo(groups)
|
||||||
|
val visitsJson = visitsSerial.writeTo(visits)
|
||||||
|
val outputStream = ctx.contentResolver.openOutputStream(filepath)
|
||||||
|
outputStream?.write(
|
||||||
|
buildString {
|
||||||
|
append(groupsJson)
|
||||||
|
append("\n---\n") // optional separator
|
||||||
|
append(visitsJson)
|
||||||
|
}.toByteArray())
|
||||||
|
outputStream?.flush()
|
||||||
|
outputStream?.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun importData(ctx: Context, filePath: Uri) {
|
||||||
|
val inputStream = ctx.contentResolver.openInputStream(filePath)
|
||||||
|
val data = inputStream?.bufferedReader().use { it?.readText() }
|
||||||
|
if(data==null) return
|
||||||
|
val lines = data.split("\n---\n")
|
||||||
|
val groupsJson = lines[0]
|
||||||
|
val visitsJson = lines[1]
|
||||||
|
|
||||||
|
groups = if(groupsJson.isNotEmpty()) groupsSerial.readFrom(groupsJson.byteInputStream()) else groupsSerial.defaultValue
|
||||||
|
visits = if(visitsJson.isNotEmpty()) visitsSerial.readFrom(visitsJson.byteInputStream()) else visitsSerial.defaultValue
|
||||||
|
|
||||||
|
// Add default group "Visited" with app's color if there is no group already
|
||||||
|
if (groups.size() == 0) {
|
||||||
|
groups.setGroup(DEFAULT_GROUP, "Visited",
|
||||||
|
ContextCompat.getColor(ctx, R.color.blue).toDrawable())
|
||||||
|
}
|
||||||
|
saveData()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doImport(ctx: Context, file: Uri?){
|
||||||
|
if(file!=null) {
|
||||||
|
importData(ctx, file)
|
||||||
|
val intent = ctx.packageManager
|
||||||
|
.getLaunchIntentForPackage(ctx.packageName)?.apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
ctx.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun doExport(ctx: Context){
|
||||||
|
val fileName = "beans_backup.json"
|
||||||
|
val resolver = ctx.contentResolver
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(MediaStore.Downloads.DISPLAY_NAME, fileName) // "backup.json"
|
||||||
|
put(MediaStore.Downloads.MIME_TYPE, "text/*")
|
||||||
|
put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
}
|
||||||
|
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
|
||||||
|
} else {
|
||||||
|
val downloadsDir =
|
||||||
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
val file = File(downloadsDir, fileName)
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
if(uri!=null) exportData(ctx, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package net.helcel.beans.helper
|
|
||||||
|
|
||||||
interface DialogCloser {
|
|
||||||
fun onDialogDismiss(clear: Boolean)
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,17 @@ package net.helcel.beans.helper
|
|||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Serializer
|
import kotlinx.serialization.Serializer
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import net.helcel.beans.R
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import kotlin.coroutines.coroutineContext
|
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
|
||||||
private const val randSeed = 0
|
private const val randSeed = 0
|
||||||
@@ -20,52 +22,69 @@ const val NO_GROUP = 0
|
|||||||
const val DEFAULT_GROUP = 1
|
const val DEFAULT_GROUP = 1
|
||||||
const val AUTO_GROUP = -1
|
const val AUTO_GROUP = -1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
class Groups(val id: Int, private val grps: HashMap<Int, Group>) {
|
class Groups(val id: Int, @SerialName("grps") private val groups: HashMap<Int, Group>) {
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
private val _groupsFlow = MutableStateFlow<List<Group>>(groups.values.toList())
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
val groupsFlow: StateFlow<List<Group>> = _groupsFlow.asStateFlow()
|
||||||
|
|
||||||
fun setGroup(key: Int, name: String, col: ColorDrawable) {
|
fun setGroup(key: Int, name: String, col: ColorDrawable) {
|
||||||
grps[key] = Group(key, name, col)
|
val old = groups[key]
|
||||||
|
if (old != null && old.name == name && old.color.color == col.color) return
|
||||||
|
groups[key] = Group(key, name, col)
|
||||||
|
updateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteGroup(key: Int) {
|
fun deleteGroup(key: Int) {
|
||||||
grps.remove(key)
|
groups.remove(key)
|
||||||
|
updateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteAllExcept(grp: Int) {
|
fun keepOnly(key: Int) {
|
||||||
val keysToDelete = grps.keys.filter { it != grp }
|
val keep = groups[key] ?: return
|
||||||
keysToDelete.forEach { grps.remove(it) }
|
groups.clear()
|
||||||
|
groups[key] = keep
|
||||||
|
updateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFlow() {
|
||||||
|
_groupsFlow.value = groups.values.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGroupFromKey(key: Int): Group {
|
fun getGroupFromKey(key: Int): Group {
|
||||||
return grps.getOrDefault(key, EmptyGroup())
|
return groups.getOrDefault(key, EmptyGroup())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun genKey(): Int {
|
fun genKey(): Int {
|
||||||
val key = rnd.nextInt()
|
val key = rnd.nextInt()
|
||||||
if (grps.containsKey(key) || key in listOf(NO_GROUP, DEFAULT_GROUP, AUTO_GROUP)) return genKey()
|
if (groups.containsKey(key) || key in listOf(NO_GROUP, DEFAULT_GROUP, AUTO_GROUP)) return genKey()
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
fun size(): Int {
|
fun size(): Int {
|
||||||
return grps.size
|
return groups.size
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getUniqueEntry(): Group? {
|
fun getUniqueEntry(): Group? {
|
||||||
assert(size() == 1)
|
assert(size() == 1)
|
||||||
return if (grps.size == 1) {
|
return if (groups.size == 1) {
|
||||||
grps[grps.keys.first()]
|
groups[groups.keys.first()]
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getGroupFromPos(pos: Int): Pair<Int, Group> {
|
fun getGroupFromPos(pos: Int): Pair<Int, Group> {
|
||||||
val key = grps.keys.toList()[pos]
|
if(groups.keys.isEmpty()) return Pair(NO_GROUP,Group(NO_GROUP,"-"))
|
||||||
|
val key = groups.keys.toList()[pos]
|
||||||
return Pair(key, getGroupFromKey(key))
|
return Pair(key, getGroupFromKey(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findGroupPos(key: Int): Int {
|
fun findGroupPos(key: Int): Int {
|
||||||
return grps.keys.toList().indexOf(key)
|
return groups.keys.toList().indexOf(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmptyGroup : Group(0, "")
|
class EmptyGroup : Group(0, "")
|
||||||
@@ -74,9 +93,7 @@ class Groups(val id: Int, private val grps: HashMap<Int, Group>) {
|
|||||||
open class Group(
|
open class Group(
|
||||||
val key: Int,
|
val key: Int,
|
||||||
val name: String,
|
val name: String,
|
||||||
@Serializable(with = Theme.ColorDrawableSerializer::class) val color: ColorDrawable = ColorDrawable(
|
@Serializable(with = Theme.ColorDrawableSerializer::class) val color: ColorDrawable = Color.GRAY.toDrawable()
|
||||||
Color.GRAY
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package net.helcel.beans.helper
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preference file opened directly, under the name and mode androidx's
|
||||||
|
* PreferenceManager used. Settings written by earlier versions still load, and
|
||||||
|
* the app no longer pulls in the preference UI framework for one call.
|
||||||
|
*/
|
||||||
|
fun defaultPreferences(ctx: Context): SharedPreferences =
|
||||||
|
ctx.getSharedPreferences(ctx.packageName + "_preferences", Context.MODE_PRIVATE)
|
||||||
@@ -2,21 +2,26 @@ package net.helcel.beans.helper
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import net.helcel.beans.R
|
import net.helcel.beans.R
|
||||||
import net.helcel.beans.activity.MainActivity
|
import net.helcel.beans.activity.MainScreen
|
||||||
import net.helcel.beans.activity.fragment.SettingsFragment
|
|
||||||
|
/**
|
||||||
|
* Default distance, in dp, around a tap that still counts as hitting a place.
|
||||||
|
* Kept small because the radius is measured on screen: zoomed out to the whole
|
||||||
|
* world, even a few dp reach across several countries.
|
||||||
|
*/
|
||||||
|
const val DEFAULT_TOUCH_RADIUS = 4
|
||||||
|
|
||||||
|
/** Largest tap radius the setting offers. */
|
||||||
|
const val MAX_TOUCH_RADIUS = 48
|
||||||
|
|
||||||
object Settings {
|
object Settings {
|
||||||
|
|
||||||
private lateinit var sp: SharedPreferences
|
private lateinit var sp: SharedPreferences
|
||||||
private lateinit var mainActivity: MainActivity
|
private lateinit var mainActivity: MainScreen
|
||||||
fun start(ctx: MainActivity) {
|
fun start(ctx: MainScreen) {
|
||||||
mainActivity = ctx
|
mainActivity = ctx
|
||||||
sp = PreferenceManager.getDefaultSharedPreferences(ctx)
|
sp = defaultPreferences(ctx)
|
||||||
SettingsFragment.setTheme(
|
|
||||||
ctx, sp.getString(ctx.getString(R.string.key_theme), ctx.getString(R.string.system))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isSingleGroup(ctx: Context): Boolean {
|
fun isSingleGroup(ctx: Context): Boolean {
|
||||||
@@ -33,6 +38,21 @@ object Settings {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far around a tap, in dp, the map looks for other places. Anything
|
||||||
|
* within it is offered as a candidate instead of painting straight away.
|
||||||
|
*/
|
||||||
|
fun getTouchRadius(ctx: Context): Float {
|
||||||
|
return sp.getInt(ctx.getString(R.string.key_touch_radius), DEFAULT_TOUCH_RADIUS).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isCascadeStats(ctx: Context): Boolean {
|
||||||
|
return getBooleanValue(
|
||||||
|
ctx,
|
||||||
|
sp.getString(ctx.getString(R.string.key_cascade_stats), ctx.getString(R.string.off))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun getStatPref(ctx: Context): String? {
|
fun getStatPref(ctx: Context): String? {
|
||||||
return sp.getString(
|
return sp.getString(
|
||||||
ctx.getString(R.string.key_stats),
|
ctx.getString(R.string.key_stats),
|
||||||
@@ -41,7 +61,7 @@ object Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun refreshProjection(): Boolean {
|
fun refreshProjection(): Boolean {
|
||||||
mainActivity.refreshProjection()
|
(mainActivity).refreshProjection()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,16 @@
|
|||||||
package net.helcel.beans.helper
|
package net.helcel.beans.helper
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.util.TypedValue
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
|
||||||
object Theme {
|
object Theme {
|
||||||
fun colorWrapper(ctx: Context, res: Int): ColorDrawable {
|
|
||||||
val colorPrimaryTyped = TypedValue()
|
|
||||||
ctx.theme.resolveAttribute(res, colorPrimaryTyped, true)
|
|
||||||
return ColorDrawable(colorPrimaryTyped.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun colorToHex6(c: ColorDrawable): String {
|
fun colorToHex6(c: ColorDrawable): String {
|
||||||
return '#' + colorToHex8(c).substring(3)
|
return '#' + colorToHex8(c).substring(3)
|
||||||
@@ -28,11 +21,6 @@ object Theme {
|
|||||||
return '#' + c.color.toHexString()
|
return '#' + c.color.toHexString()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createActionBar(ctx: AppCompatActivity, title: String) {
|
|
||||||
ctx.supportActionBar?.title = title
|
|
||||||
ctx.supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getContrastColor(color: Int): Int {
|
fun getContrastColor(color: Int): Int {
|
||||||
val whiteContrast = ColorUtils.calculateContrast(Color.WHITE, color)
|
val whiteContrast = ColorUtils.calculateContrast(Color.WHITE, color)
|
||||||
val blackContrast = ColorUtils.calculateContrast(Color.BLACK, color)
|
val blackContrast = ColorUtils.calculateContrast(Color.BLACK, color)
|
||||||
@@ -43,7 +31,7 @@ object Theme {
|
|||||||
override val descriptor = PrimitiveSerialDescriptor("ColorDrawable", PrimitiveKind.INT)
|
override val descriptor = PrimitiveSerialDescriptor("ColorDrawable", PrimitiveKind.INT)
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): ColorDrawable {
|
override fun deserialize(decoder: Decoder): ColorDrawable {
|
||||||
return ColorDrawable(decoder.decodeInt())
|
return decoder.decodeInt().toDrawable()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: ColorDrawable) {
|
override fun serialize(encoder: Encoder, value: ColorDrawable) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package net.helcel.beans.helper
|
package net.helcel.beans.helper
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.Serializer
|
import kotlinx.serialization.Serializer
|
||||||
@@ -11,19 +13,31 @@ import java.io.InputStream
|
|||||||
@Serializable
|
@Serializable
|
||||||
class Visits(val id: Int, private val locs: HashMap<String, Int>) {
|
class Visits(val id: Int, private val locs: HashMap<String, Int>) {
|
||||||
|
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
private val _visitsFlow = MutableStateFlow<Map<String,Int>>(locs.toMutableMap())
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
val visitsFlow: StateFlow<Map<String,Int>> = _visitsFlow
|
||||||
|
|
||||||
fun setVisited(key: GeoLoc?, b: Int) {
|
fun setVisited(key: GeoLoc?, b: Int) {
|
||||||
if (key == null)
|
if (key == null || locs[key.code] == b)
|
||||||
return
|
return
|
||||||
locs[key.code] = b
|
locs[key.code] = b
|
||||||
|
updateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateFlow() {
|
||||||
|
_visitsFlow.value = locs.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteVisited(key: Int) {
|
fun deleteVisited(key: Int) {
|
||||||
val keysToDelete = locs
|
val keysToDelete = locs
|
||||||
.filter { it.value == key }
|
.filter { it.value == key }
|
||||||
.map { it.key }
|
.map { it.key }
|
||||||
|
if (keysToDelete.isEmpty()) return
|
||||||
keysToDelete.forEach {
|
keysToDelete.forEach {
|
||||||
locs.remove(it)
|
locs.remove(it)
|
||||||
}
|
}
|
||||||
|
updateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getVisited(key: GeoLoc): Int {
|
fun getVisited(key: GeoLoc): Int {
|
||||||
@@ -47,11 +61,18 @@ class Visits(val id: Int, private val locs: HashMap<String, Int>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun reassignAllVisitedToGroup(group: Int) {
|
fun reassignAllVisitedToGroup(group: Int) {
|
||||||
|
var changed = false
|
||||||
val keys = locs.filter { (_, grp) ->
|
val keys = locs.filter { (_, grp) ->
|
||||||
grp !in listOf(NO_GROUP, AUTO_GROUP)
|
grp !in listOf(NO_GROUP, AUTO_GROUP)
|
||||||
}.keys
|
}.keys
|
||||||
keys.forEach {
|
keys.forEach {
|
||||||
locs[it] = group
|
if (locs[it] != group) {
|
||||||
|
locs[it] = group
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
updateFlow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package net.helcel.beans.map
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import net.helcel.beans.R
|
||||||
|
import net.helcel.beans.helper.defaultPreferences
|
||||||
|
|
||||||
|
/** Picks the map asset matching the projection the user chose. */
|
||||||
|
object MapAssets {
|
||||||
|
|
||||||
|
fun assetFor(ctx: Context): String {
|
||||||
|
val preferences = defaultPreferences(ctx)
|
||||||
|
return when (
|
||||||
|
preferences.getString(
|
||||||
|
ctx.getString(R.string.key_projection),
|
||||||
|
ctx.getString(R.string.mercator),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ctx.getString(R.string.azimuthalequidistant) -> "aeqd01.bmap"
|
||||||
|
ctx.getString(R.string.loximuthal) -> "loxim01.bmap"
|
||||||
|
else -> "webmercator01.bmap"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package net.helcel.beans.map
|
||||||
|
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.RectF
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
private const val VERSION = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the packed map assets produced by `packmap.js`.
|
||||||
|
*
|
||||||
|
* The maps used to ship as SVG, which spends around four megabytes per
|
||||||
|
* projection spelling coordinates out in decimal ASCII inside XML. The packed
|
||||||
|
* form says the same thing in a fraction of that, and reading it is a straight
|
||||||
|
* scan over a byte array instead of an XML parse with a float scanner on top.
|
||||||
|
*
|
||||||
|
* The format is documented in `packmap.js`; the two have to agree.
|
||||||
|
*/
|
||||||
|
object MapReader {
|
||||||
|
|
||||||
|
fun read(input: InputStream): MapWorld {
|
||||||
|
val reader = ByteReader(input.readBytes())
|
||||||
|
reader.readHeader()
|
||||||
|
val width = reader.unsigned() * reader.scale
|
||||||
|
val height = reader.unsigned() * reader.scale
|
||||||
|
val count = reader.unsigned()
|
||||||
|
val shapes = ArrayList<MapShape>(count)
|
||||||
|
repeat(count) { shapes.add(reader.shape()) }
|
||||||
|
return MapWorld(width, height, shapes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ByteReader(private val data: ByteArray) {
|
||||||
|
|
||||||
|
private var pos = 0
|
||||||
|
|
||||||
|
/** One grid step in user units, read from the header. */
|
||||||
|
var scale = 1f
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun readHeader() {
|
||||||
|
require(
|
||||||
|
data.size > 5 &&
|
||||||
|
data[0] == 'B'.code.toByte() && data[1] == 'M'.code.toByte() &&
|
||||||
|
data[2] == 'A'.code.toByte() && data[3] == 'P'.code.toByte()
|
||||||
|
) { "not a packed map" }
|
||||||
|
require(data[4].toInt() == VERSION) { "unsupported packed map version ${data[4]}" }
|
||||||
|
pos = 5
|
||||||
|
scale = 1f / unsigned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unsigned(): Int {
|
||||||
|
var result = 0
|
||||||
|
var shift = 0
|
||||||
|
while (true) {
|
||||||
|
val byte = data[pos++].toInt()
|
||||||
|
result = result or ((byte and 0x7F) shl shift)
|
||||||
|
if (byte and 0x80 == 0) return result
|
||||||
|
shift += 7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zigzag, because deltas run both ways and are almost always tiny. */
|
||||||
|
fun signed(): Int {
|
||||||
|
val value = unsigned()
|
||||||
|
return if (value and 1 == 1) -((value ushr 1) + 1) else value ushr 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flag(): Boolean = data[pos++].toInt() != 0
|
||||||
|
|
||||||
|
fun text(): String {
|
||||||
|
val length = unsigned()
|
||||||
|
val value = String(data, pos, length, Charsets.UTF_8)
|
||||||
|
pos += length
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shape(): MapShape {
|
||||||
|
val code = text()
|
||||||
|
val isState = flag()
|
||||||
|
val ringCount = unsigned()
|
||||||
|
|
||||||
|
val rings = ArrayList<FloatArray>(ringCount)
|
||||||
|
var nonZero: Path? = null
|
||||||
|
var evenOdd: Path? = null
|
||||||
|
var minX = Float.MAX_VALUE
|
||||||
|
var minY = Float.MAX_VALUE
|
||||||
|
var maxX = -Float.MAX_VALUE
|
||||||
|
var maxY = -Float.MAX_VALUE
|
||||||
|
|
||||||
|
repeat(ringCount) {
|
||||||
|
val points = unsigned()
|
||||||
|
val useEvenOdd = flag()
|
||||||
|
val ring = FloatArray(points * 2)
|
||||||
|
// Each point is a delta from the one before it, the first from the
|
||||||
|
// origin, so the whole ring decodes in one pass.
|
||||||
|
var x = 0
|
||||||
|
var y = 0
|
||||||
|
for (i in 0 until points) {
|
||||||
|
x += signed()
|
||||||
|
y += signed()
|
||||||
|
val fx = x * scale
|
||||||
|
val fy = y * scale
|
||||||
|
ring[i * 2] = fx
|
||||||
|
ring[i * 2 + 1] = fy
|
||||||
|
if (fx < minX) minX = fx
|
||||||
|
if (fx > maxX) maxX = fx
|
||||||
|
if (fy < minY) minY = fy
|
||||||
|
if (fy > maxY) maxY = fy
|
||||||
|
}
|
||||||
|
rings.add(ring)
|
||||||
|
|
||||||
|
val path = if (useEvenOdd) {
|
||||||
|
evenOdd ?: Path().also { it.fillType = Path.FillType.EVEN_ODD; evenOdd = it }
|
||||||
|
} else {
|
||||||
|
nonZero ?: Path().also { nonZero = it }
|
||||||
|
}
|
||||||
|
path.moveTo(ring[0], ring[1])
|
||||||
|
var i = 2
|
||||||
|
while (i < ring.size) {
|
||||||
|
path.lineTo(ring[i], ring[i + 1])
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
path.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapShape(
|
||||||
|
code, isState, rings,
|
||||||
|
RectF(minX, minY, maxX, maxY),
|
||||||
|
nonZero, evenOdd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package net.helcel.beans.map
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.RectF
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/** Draws a [MapWorld] onto a canvas through a pan/zoom transform. */
|
||||||
|
class MapRenderer(private val map: MapWorld) {
|
||||||
|
|
||||||
|
private val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
private val stroke = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeCap = Paint.Cap.ROUND
|
||||||
|
strokeJoin = Paint.Join.ROUND
|
||||||
|
}
|
||||||
|
private val inverse = Matrix()
|
||||||
|
private val values = FloatArray(9)
|
||||||
|
private val visible = RectF()
|
||||||
|
|
||||||
|
/** False when the border is too faint to be worth a draw call at all. */
|
||||||
|
private var strokeVisible = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paints the map into [canvas], mapping user units through [transform].
|
||||||
|
* Shapes outside the canvas are skipped, which is what makes drawing cheap
|
||||||
|
* once the map is zoomed in.
|
||||||
|
*/
|
||||||
|
fun draw(canvas: Canvas, transform: Matrix, style: MapStyle) {
|
||||||
|
// SRC rather than blended: the bitmap is reused between renders.
|
||||||
|
canvas.drawColor(style.background, PorterDuff.Mode.SRC)
|
||||||
|
if (!transform.invert(inverse)) return
|
||||||
|
visible.set(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat())
|
||||||
|
inverse.mapRect(visible)
|
||||||
|
|
||||||
|
// Widths are given in device pixels, so undo the zoom the canvas is
|
||||||
|
// about to apply to them, then let them grow slowly with it.
|
||||||
|
transform.getValues(values)
|
||||||
|
val scale = values[Matrix.MSCALE_X]
|
||||||
|
val perPixel = 1f / scale
|
||||||
|
val fitted = min(canvas.width / map.width, canvas.height / map.height)
|
||||||
|
val zoom = (scale / fitted).coerceAtLeast(1f)
|
||||||
|
val regionPx = strokeWidthAt(zoom, REGION_STROKE_STOPS)
|
||||||
|
val countryPx = strokeWidthAt(zoom, COUNTRY_STROKE_STOPS)
|
||||||
|
|
||||||
|
canvas.save()
|
||||||
|
canvas.concat(transform)
|
||||||
|
setStroke(style.background, if (style.regional) regionPx else countryPx, perPixel)
|
||||||
|
for ((code, color) in style.fills) {
|
||||||
|
val shape = map.byCode[code] ?: continue
|
||||||
|
if (!RectF.intersects(shape.bounds, visible)) continue
|
||||||
|
fill.color = color
|
||||||
|
drawShape(canvas, shape.nonZero, true)
|
||||||
|
drawShape(canvas, shape.evenOdd, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With regions on, the country outlines come back on top as the thicker
|
||||||
|
// borders between them.
|
||||||
|
if (style.regional) {
|
||||||
|
setStroke(style.background, countryPx, perPixel)
|
||||||
|
for (shape in map.countries) {
|
||||||
|
if (!RectF.intersects(shape.bounds, visible)) continue
|
||||||
|
drawShape(canvas, shape.nonZero, false)
|
||||||
|
drawShape(canvas, shape.evenOdd, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a border width given in device pixels.
|
||||||
|
*
|
||||||
|
* Anything thinner than a pixel cannot be drawn as such: the rasteriser
|
||||||
|
* turns it into a one pixel hairline at full strength, which is why every
|
||||||
|
* border below that width used to come out the same weight no matter how
|
||||||
|
* far out the map was. Below a pixel the width is carried as opacity
|
||||||
|
* instead, so a quarter-pixel border reads as a quarter as dark.
|
||||||
|
*/
|
||||||
|
private fun setStroke(color: Int, devicePx: Float, perPixel: Float) {
|
||||||
|
strokeVisible = devicePx > 0.004f
|
||||||
|
if (!strokeVisible) return
|
||||||
|
stroke.color = color
|
||||||
|
if (devicePx < 1f) {
|
||||||
|
stroke.strokeWidth = perPixel
|
||||||
|
stroke.alpha = (devicePx * 255f).roundToInt().coerceIn(0, 255)
|
||||||
|
} else {
|
||||||
|
stroke.strokeWidth = perPixel * devicePx
|
||||||
|
stroke.alpha = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawShape(canvas: Canvas, path: Path?, filled: Boolean) {
|
||||||
|
if (path == null) return
|
||||||
|
if (filled) canvas.drawPath(path, fill)
|
||||||
|
if (strokeVisible) canvas.drawPath(path, stroke)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package net.helcel.beans.map
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import net.helcel.beans.countries.GeoLoc
|
||||||
|
import net.helcel.beans.countries.World
|
||||||
|
import net.helcel.beans.helper.AUTO_GROUP
|
||||||
|
import net.helcel.beans.helper.Data
|
||||||
|
import net.helcel.beans.helper.NO_GROUP
|
||||||
|
import net.helcel.beans.helper.Settings
|
||||||
|
import kotlin.math.ln
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Border width in device pixels at each zoom, interpolated in between.
|
||||||
|
*
|
||||||
|
* Zoom counts how far in the map is from its fitted size, so 1 is the whole
|
||||||
|
* world on screen and 64 is as far in as it goes. The stops step by fours and
|
||||||
|
* are interpolated on a log axis, so the weight changes evenly as you pinch
|
||||||
|
* rather than in a rush at one end.
|
||||||
|
*
|
||||||
|
* Region borders start at nothing on purpose: a sub-national border says very
|
||||||
|
* little with the whole world in view, and drawing three and a half thousand of
|
||||||
|
* them only muddies the land.
|
||||||
|
*
|
||||||
|
* zoom 1x 4x 16x 64x
|
||||||
|
* region 0.0 0.5 1.0 2.0
|
||||||
|
* country 1.0 1.5 3.0 6.0
|
||||||
|
*/
|
||||||
|
val STROKE_ZOOM_STOPS = floatArrayOf(1f, 4f, 16f, 64f)
|
||||||
|
|
||||||
|
val REGION_STROKE_STOPS = floatArrayOf(0f, 0.5f, 1f, 2f)
|
||||||
|
|
||||||
|
val COUNTRY_STROKE_STOPS = floatArrayOf(1f, 1.5f, 3f, 6f)
|
||||||
|
|
||||||
|
/** The width for [zoom], straight-line between the stops on a log axis. */
|
||||||
|
fun strokeWidthAt(zoom: Float, widths: FloatArray): Float {
|
||||||
|
if (zoom <= STROKE_ZOOM_STOPS.first()) return widths.first()
|
||||||
|
for (i in 1 until STROKE_ZOOM_STOPS.size) {
|
||||||
|
if (zoom <= STROKE_ZOOM_STOPS[i]) {
|
||||||
|
val low = STROKE_ZOOM_STOPS[i - 1]
|
||||||
|
val t = ln(zoom / low) / ln(STROKE_ZOOM_STOPS[i] / low)
|
||||||
|
return widths[i - 1] + t * (widths[i] - widths[i - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return widths.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The colour every shape is drawn with, resolved once per render.
|
||||||
|
*
|
||||||
|
* This replaces the stylesheet the map used to be rendered through. A place
|
||||||
|
* inherits the colour of its parent, so painting a whole continent still shows
|
||||||
|
* up on each country inside it, and [fills] ends up holding exactly the shapes
|
||||||
|
* that get drawn — which is also exactly what a tap may land on.
|
||||||
|
*/
|
||||||
|
class MapStyle(
|
||||||
|
val regional: Boolean,
|
||||||
|
val land: Int,
|
||||||
|
val background: Int,
|
||||||
|
val fills: Map<String, Int>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun build(ctx: Context, land: Int, background: Int): MapStyle {
|
||||||
|
val regional = Settings.isRegional(ctx)
|
||||||
|
val fills = HashMap<String, Int>()
|
||||||
|
World.WWW.children.forEach { child ->
|
||||||
|
if (child.type == GeoLoc.LocType.COUNTRY) {
|
||||||
|
// A country hanging straight off the world, such as Antarctica.
|
||||||
|
addCountry(fills, child, colorOf(World.WWW, regional), regional, land)
|
||||||
|
} else {
|
||||||
|
val continent = colorOf(child, regional) ?: colorOf(World.WWW, regional)
|
||||||
|
child.children.forEach { addCountry(fills, it, continent, regional, land) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MapStyle(regional, land, background, fills)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addCountry(
|
||||||
|
fills: HashMap<String, Int>,
|
||||||
|
country: GeoLoc,
|
||||||
|
inherited: Int?,
|
||||||
|
regional: Boolean,
|
||||||
|
land: Int,
|
||||||
|
) {
|
||||||
|
val color = colorOf(country, regional) ?: inherited
|
||||||
|
// Regions are only drawn for countries that actually have some; the
|
||||||
|
// rest keep their outline filled so the map stays complete.
|
||||||
|
if (regional && country.children.isNotEmpty()) {
|
||||||
|
country.children.forEach { state ->
|
||||||
|
fills[state.code] = colorOf(state, regional) ?: color ?: land
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fills[country.code] = color ?: land
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The group colour assigned to [loc], or `null` when it has none of its own. */
|
||||||
|
private fun colorOf(loc: GeoLoc, regional: Boolean): Int? {
|
||||||
|
val key = Data.visits.getVisited(loc)
|
||||||
|
val group = Data.groups.getGroupFromKey(key)
|
||||||
|
if (group.key != NO_GROUP) return group.color.color
|
||||||
|
// A place marked automatically stands in for its children. That only
|
||||||
|
// needs a colour of its own where the children are not drawn, and
|
||||||
|
// never on a continent, which would swallow the whole map.
|
||||||
|
val isParent = loc.type == GeoLoc.LocType.GROUP ||
|
||||||
|
loc.type == GeoLoc.LocType.CUSTOM_GROUP ||
|
||||||
|
loc.type == GeoLoc.LocType.WORLD
|
||||||
|
if (key == AUTO_GROUP && !regional && !isParent) {
|
||||||
|
return Data.groups.getGroupFromPos(0).second.color.color
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
package net.helcel.beans.map
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.HandlerThread
|
||||||
|
import android.os.SystemClock
|
||||||
|
import android.view.GestureDetector
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.ScaleGestureDetector
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.OverScroller
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/** A shape a tap landed on or near, with how far away it was in user units. */
|
||||||
|
class MapPick(val code: String, val distance: Float)
|
||||||
|
|
||||||
|
/** How far past the fitted map a pinch may zoom in. */
|
||||||
|
private const val MAX_ZOOM = 64f
|
||||||
|
|
||||||
|
/** How much a double tap zooms in by. */
|
||||||
|
private const val DOUBLE_TAP_ZOOM = 3f
|
||||||
|
|
||||||
|
/** Time a gesture has to settle before the map is redrawn sharp again. */
|
||||||
|
private const val RENDER_DELAY_MS = 80L
|
||||||
|
|
||||||
|
/** How long the map may stay stretched while a gesture keeps going. */
|
||||||
|
private const val MAX_STALE_MS = 500L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most candidates a single tap will offer. A wide radius over a crowded part of
|
||||||
|
* the map can reach dozens of places, which is more of a list than anyone wants
|
||||||
|
* to read; the nearest few are what the tap was plausibly aiming at.
|
||||||
|
*/
|
||||||
|
private const val MAX_CANDIDATES = 16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The map itself: pan, pinch and tap over a [MapWorld].
|
||||||
|
*
|
||||||
|
* Drawing every country as vectors costs far too much to do on each frame, so a
|
||||||
|
* render lands in an off-screen bitmap on a worker thread and gestures just move
|
||||||
|
* that bitmap around. Once the gesture settles the map is drawn again at the new
|
||||||
|
* zoom, which is what keeps it sharp all the way in.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class MapView(context: Context) : View(context) {
|
||||||
|
|
||||||
|
var world: MapWorld? = null
|
||||||
|
set(value) {
|
||||||
|
if (field === value) return
|
||||||
|
field = value
|
||||||
|
renderer = value?.let(::MapRenderer)
|
||||||
|
fitToView()
|
||||||
|
requestRender(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var style: MapStyle? = null
|
||||||
|
set(value) {
|
||||||
|
if (field === value) return
|
||||||
|
field = value
|
||||||
|
requestRender(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How far from a tap, in dp, other shapes still count as candidates. */
|
||||||
|
var touchRadiusDp: Float = 4f
|
||||||
|
|
||||||
|
/** Called on a tap with every nearby shape, nearest first. */
|
||||||
|
var onPick: ((List<MapPick>) -> Unit)? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var renderer: MapRenderer? = null
|
||||||
|
|
||||||
|
private val scroller = OverScroller(context)
|
||||||
|
|
||||||
|
private val transform = Matrix()
|
||||||
|
private val inverse = Matrix()
|
||||||
|
private val values = FloatArray(9)
|
||||||
|
private val point = FloatArray(2)
|
||||||
|
private var minScale = 1f
|
||||||
|
private var maxScale = 1f
|
||||||
|
|
||||||
|
private val blitPaint = Paint(Paint.FILTER_BITMAP_FLAG)
|
||||||
|
private val blit = Matrix()
|
||||||
|
|
||||||
|
private val lock = Any()
|
||||||
|
private var front: Bitmap? = null
|
||||||
|
private var back: Bitmap? = null
|
||||||
|
private val frontMatrix = Matrix()
|
||||||
|
private val frontValues = FloatArray(9)
|
||||||
|
private val pendingMatrix = Matrix()
|
||||||
|
private var pendingWidth = 0
|
||||||
|
private var pendingHeight = 0
|
||||||
|
@Volatile private var lastRenderAt = 0L
|
||||||
|
|
||||||
|
private val renderThread = HandlerThread("map-render").apply { start() }
|
||||||
|
private val renderHandler = Handler(renderThread.looper)
|
||||||
|
private val renderTask = Runnable { render() }
|
||||||
|
|
||||||
|
private val scaleDetector = ScaleGestureDetector(
|
||||||
|
context,
|
||||||
|
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
|
val scale = currentScale()
|
||||||
|
val target = (scale * detector.scaleFactor).coerceIn(minScale, maxScale)
|
||||||
|
transform.postScale(target / scale, target / scale, detector.focusX, detector.focusY)
|
||||||
|
clamp()
|
||||||
|
invalidate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
private val gestureDetector = GestureDetector(
|
||||||
|
context,
|
||||||
|
object : GestureDetector.SimpleOnGestureListener() {
|
||||||
|
override fun onDown(e: MotionEvent): Boolean {
|
||||||
|
scroller.forceFinished(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFling(
|
||||||
|
e1: MotionEvent?,
|
||||||
|
e2: MotionEvent,
|
||||||
|
velocityX: Float,
|
||||||
|
velocityY: Float,
|
||||||
|
): Boolean {
|
||||||
|
val map = world ?: return false
|
||||||
|
transform.getValues(values)
|
||||||
|
val scale = values[Matrix.MSCALE_X]
|
||||||
|
val x = values[Matrix.MTRANS_X].toInt()
|
||||||
|
val y = values[Matrix.MTRANS_Y].toInt()
|
||||||
|
val w = map.width * scale
|
||||||
|
val h = map.height * scale
|
||||||
|
scroller.forceFinished(true)
|
||||||
|
scroller.fling(
|
||||||
|
x, y, velocityX.toInt(), velocityY.toInt(),
|
||||||
|
if (w > width) (width - w).toInt() else x, if (w > width) 0 else x,
|
||||||
|
if (h > height) (height - h).toInt() else y, if (h > height) 0 else y,
|
||||||
|
)
|
||||||
|
postInvalidateOnAnimation()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScroll(
|
||||||
|
e1: MotionEvent?,
|
||||||
|
e2: MotionEvent,
|
||||||
|
distanceX: Float,
|
||||||
|
distanceY: Float,
|
||||||
|
): Boolean {
|
||||||
|
transform.postTranslate(-distanceX, -distanceY)
|
||||||
|
clamp()
|
||||||
|
invalidate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||||
|
pick(e.x, e.y)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDoubleTap(e: MotionEvent): Boolean {
|
||||||
|
val scale = currentScale()
|
||||||
|
val target = if (scale >= maxScale * 0.99f) {
|
||||||
|
minScale
|
||||||
|
} else {
|
||||||
|
min(scale * DOUBLE_TAP_ZOOM, maxScale)
|
||||||
|
}
|
||||||
|
transform.postScale(target / scale, target / scale, e.x, e.y)
|
||||||
|
clamp()
|
||||||
|
invalidate()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
scaleDetector.onTouchEvent(event)
|
||||||
|
gestureDetector.onTouchEvent(event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun computeScroll() {
|
||||||
|
if (!scroller.computeScrollOffset()) return
|
||||||
|
transform.getValues(values)
|
||||||
|
values[Matrix.MTRANS_X] = scroller.currX.toFloat()
|
||||||
|
values[Matrix.MTRANS_Y] = scroller.currY.toFloat()
|
||||||
|
transform.setValues(values)
|
||||||
|
postInvalidateOnAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
fitToView()
|
||||||
|
requestRender(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
val current = style
|
||||||
|
canvas.drawColor(current?.background ?: 0)
|
||||||
|
|
||||||
|
var stale = true
|
||||||
|
synchronized(lock) {
|
||||||
|
val bitmap = front
|
||||||
|
if (bitmap != null && !bitmap.isRecycled && frontMatrix.invert(blit)) {
|
||||||
|
blit.postConcat(transform)
|
||||||
|
canvas.drawBitmap(bitmap, blit, blitPaint)
|
||||||
|
transform.getValues(values)
|
||||||
|
stale = !values.contentEquals(frontValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scheduling only ever happens from here or from a setter, both on the
|
||||||
|
// main thread, so a render never starts while its bitmap is on screen.
|
||||||
|
if (stale) {
|
||||||
|
val overdue = SystemClock.uptimeMillis() - lastRenderAt > MAX_STALE_MS
|
||||||
|
requestRender(if (overdue) 0L else RENDER_DELAY_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow()
|
||||||
|
scroller.forceFinished(true)
|
||||||
|
renderHandler.removeCallbacks(renderTask)
|
||||||
|
renderThread.quitSafely()
|
||||||
|
// Dropped rather than recycled: a render may still be part way through
|
||||||
|
// one of them, and the collector is happy to take them from here.
|
||||||
|
synchronized(lock) {
|
||||||
|
front = null
|
||||||
|
back = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Centres the map in the view and works out how far it may be zoomed. */
|
||||||
|
private fun fitToView() {
|
||||||
|
val map = world ?: return
|
||||||
|
if (width == 0 || height == 0 || map.width <= 0f || map.height <= 0f) return
|
||||||
|
val scale = min(width / map.width, height / map.height)
|
||||||
|
minScale = scale
|
||||||
|
maxScale = scale * MAX_ZOOM
|
||||||
|
transform.setScale(scale, scale)
|
||||||
|
transform.postTranslate(
|
||||||
|
(width - map.width * scale) / 2f,
|
||||||
|
(height - map.height * scale) / 2f,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun currentScale(): Float {
|
||||||
|
transform.getValues(values)
|
||||||
|
return values[Matrix.MSCALE_X]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keeps the map from being dragged away from the viewport. */
|
||||||
|
private fun clamp() {
|
||||||
|
val map = world ?: return
|
||||||
|
transform.getValues(values)
|
||||||
|
val scale = values[Matrix.MSCALE_X]
|
||||||
|
val w = map.width * scale
|
||||||
|
val h = map.height * scale
|
||||||
|
values[Matrix.MTRANS_X] = if (w <= width) {
|
||||||
|
(width - w) / 2f
|
||||||
|
} else {
|
||||||
|
values[Matrix.MTRANS_X].coerceIn(width - w, 0f)
|
||||||
|
}
|
||||||
|
values[Matrix.MTRANS_Y] = if (h <= height) {
|
||||||
|
(height - h) / 2f
|
||||||
|
} else {
|
||||||
|
values[Matrix.MTRANS_Y].coerceIn(height - h, 0f)
|
||||||
|
}
|
||||||
|
transform.setValues(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestRender(delay: Long) {
|
||||||
|
if (width == 0 || height == 0 || renderer == null || style == null) return
|
||||||
|
synchronized(lock) {
|
||||||
|
pendingMatrix.set(transform)
|
||||||
|
pendingWidth = width
|
||||||
|
pendingHeight = height
|
||||||
|
}
|
||||||
|
renderHandler.removeCallbacks(renderTask)
|
||||||
|
renderHandler.postDelayed(renderTask, delay)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun render() {
|
||||||
|
val matrix = Matrix()
|
||||||
|
val w: Int
|
||||||
|
val h: Int
|
||||||
|
synchronized(lock) {
|
||||||
|
matrix.set(pendingMatrix)
|
||||||
|
w = pendingWidth
|
||||||
|
h = pendingHeight
|
||||||
|
}
|
||||||
|
if (w <= 0 || h <= 0) return
|
||||||
|
val current = renderer ?: return
|
||||||
|
val currentStyle = style ?: return
|
||||||
|
|
||||||
|
var target = back
|
||||||
|
if (target == null || target.width != w || target.height != h) {
|
||||||
|
target?.recycle()
|
||||||
|
target = createBitmap(w, h)
|
||||||
|
back = target
|
||||||
|
}
|
||||||
|
current.draw(Canvas(target), matrix, currentStyle)
|
||||||
|
|
||||||
|
synchronized(lock) {
|
||||||
|
back = front
|
||||||
|
front = target
|
||||||
|
frontMatrix.set(matrix)
|
||||||
|
matrix.getValues(frontValues)
|
||||||
|
}
|
||||||
|
lastRenderAt = SystemClock.uptimeMillis()
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reports every shape within the tap radius of ([x], [y]), nearest first. */
|
||||||
|
private fun pick(x: Float, y: Float) {
|
||||||
|
val map = world ?: return
|
||||||
|
val current = style ?: return
|
||||||
|
val callback = onPick ?: return
|
||||||
|
if (!transform.invert(inverse)) return
|
||||||
|
|
||||||
|
point[0] = x
|
||||||
|
point[1] = y
|
||||||
|
inverse.mapPoints(point)
|
||||||
|
val radius = touchRadiusDp * resources.displayMetrics.density / currentScale()
|
||||||
|
|
||||||
|
val picks = ArrayList<MapPick>()
|
||||||
|
for (code in current.fills.keys) {
|
||||||
|
val shape = map.byCode[code] ?: continue
|
||||||
|
val bounds = shape.bounds
|
||||||
|
if (point[0] < bounds.left - radius || point[0] > bounds.right + radius) continue
|
||||||
|
if (point[1] < bounds.top - radius || point[1] > bounds.bottom + radius) continue
|
||||||
|
val distance = shape.distanceTo(point[0], point[1])
|
||||||
|
if (distance <= radius) picks.add(MapPick(code, distance))
|
||||||
|
}
|
||||||
|
if (picks.isEmpty()) return
|
||||||
|
picks.sortBy { it.distance }
|
||||||
|
callback(if (picks.size > MAX_CANDIDATES) picks.subList(0, MAX_CANDIDATES) else picks)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package net.helcel.beans.map
|
||||||
|
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.RectF
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One named piece of map geometry: a country outline or a single region.
|
||||||
|
*
|
||||||
|
* The geometry is kept twice on purpose. [nonZero] and [evenOdd] are what the
|
||||||
|
* renderer hands to the canvas, while [rings] keeps the raw points because
|
||||||
|
* hit testing needs the distance to an edge, which a [Path] cannot give us.
|
||||||
|
*/
|
||||||
|
class MapShape(
|
||||||
|
val code: String,
|
||||||
|
val isState: Boolean,
|
||||||
|
val rings: List<FloatArray>,
|
||||||
|
val bounds: RectF,
|
||||||
|
val nonZero: Path?,
|
||||||
|
val evenOdd: Path?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** True when ([x], [y]) falls inside the shape. Holes and lakes count as outside. */
|
||||||
|
fun contains(x: Float, y: Float): Boolean {
|
||||||
|
if (x < bounds.left || x > bounds.right || y < bounds.top || y > bounds.bottom) return false
|
||||||
|
// Even-odd crossing count over every ring at once: rings never overlap, so
|
||||||
|
// an island still reads as inside and a hole still reads as outside.
|
||||||
|
var inside = false
|
||||||
|
for (ring in rings) {
|
||||||
|
var j = ring.size - 2
|
||||||
|
var i = 0
|
||||||
|
while (i < ring.size) {
|
||||||
|
val yi = ring[i + 1]
|
||||||
|
val yj = ring[j + 1]
|
||||||
|
if ((yi > y) != (yj > y)) {
|
||||||
|
val xi = ring[i]
|
||||||
|
val xj = ring[j]
|
||||||
|
if (x < xi + (y - yi) * (xj - xi) / (yj - yi)) inside = !inside
|
||||||
|
}
|
||||||
|
j = i
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return inside
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Distance from ([x], [y]) to the shape, `0` when the point is inside it. */
|
||||||
|
fun distanceTo(x: Float, y: Float): Float {
|
||||||
|
if (contains(x, y)) return 0f
|
||||||
|
var best = Float.MAX_VALUE
|
||||||
|
for (ring in rings) {
|
||||||
|
var j = ring.size - 2
|
||||||
|
var i = 0
|
||||||
|
while (i < ring.size) {
|
||||||
|
val d = segmentDistanceSq(x, y, ring[j], ring[j + 1], ring[i], ring[i + 1])
|
||||||
|
if (d < best) best = d
|
||||||
|
j = i
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (best == Float.MAX_VALUE) Float.MAX_VALUE else sqrt(best)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun segmentDistanceSq(
|
||||||
|
px: Float, py: Float,
|
||||||
|
ax: Float, ay: Float,
|
||||||
|
bx: Float, by: Float,
|
||||||
|
): Float {
|
||||||
|
val dx = bx - ax
|
||||||
|
val dy = by - ay
|
||||||
|
val len = dx * dx + dy * dy
|
||||||
|
val t = if (len <= 0f) 0f else (((px - ax) * dx + (py - ay) * dy) / len).coerceIn(0f, 1f)
|
||||||
|
val cx = px - (ax + t * dx)
|
||||||
|
val cy = py - (ay + t * dy)
|
||||||
|
return cx * cx + cy * cy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The whole map, in SVG user units, as a flat set of named shapes. */
|
||||||
|
class MapWorld(val width: Float, val height: Float, val shapes: List<MapShape>) {
|
||||||
|
|
||||||
|
val byCode: Map<String, MapShape> = shapes.associateBy { it.code }
|
||||||
|
|
||||||
|
/** Country outlines, keyed by ISO code. Drawn as borders when regions are on. */
|
||||||
|
val countries: List<MapShape> = shapes.filter { !it.isState }
|
||||||
|
}
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package net.helcel.beans.svg
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import net.helcel.beans.countries.World
|
|
||||||
import net.helcel.beans.helper.AUTO_GROUP
|
|
||||||
import net.helcel.beans.helper.Data.groups
|
|
||||||
import net.helcel.beans.helper.Data.visits
|
|
||||||
import net.helcel.beans.helper.NO_GROUP
|
|
||||||
import net.helcel.beans.helper.Settings
|
|
||||||
import net.helcel.beans.helper.Theme.colorToHex6
|
|
||||||
import net.helcel.beans.helper.Theme.colorWrapper
|
|
||||||
|
|
||||||
class CSSWrapper(private val ctx: Context) {
|
|
||||||
|
|
||||||
private val colorForeground: String =
|
|
||||||
colorToHex6(colorWrapper(ctx, android.R.attr.panelColorBackground))
|
|
||||||
private val colorBackground: String =
|
|
||||||
colorToHex6(colorWrapper(ctx, android.R.attr.colorBackground))
|
|
||||||
|
|
||||||
private val continents: String = World.WWW.children.joinToString(",") { "#${it.code}2" }
|
|
||||||
private val countries: String = World.WWW.children.joinToString(",") { itt ->
|
|
||||||
itt.children.joinToString(",") { "#${it.code}2" }
|
|
||||||
}
|
|
||||||
private val regional: String = World.WWW.children.joinToString(",") { itt ->
|
|
||||||
itt.children.joinToString(",") { "#${it.code}1" }
|
|
||||||
}
|
|
||||||
private val countryOnlyCSS: String =
|
|
||||||
"svg{fill:$colorForeground;stroke:$colorBackground;stroke-width:0.1;}" +
|
|
||||||
"${regional}{display:none;}"
|
|
||||||
private val countryRegionalCSS: String =
|
|
||||||
"svg{fill:$colorForeground;stroke:$colorBackground;stroke-width:0.01;}" +
|
|
||||||
"$continents,$countries{fill:none;stroke:$colorBackground;stroke-width:0.1;}"
|
|
||||||
private var customCSS: String = ""
|
|
||||||
|
|
||||||
init {
|
|
||||||
refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun refresh() {
|
|
||||||
val id = if (Settings.isRegional(ctx)) "1" else "2"
|
|
||||||
customCSS = visits.getVisitedByValue().map { (k, v) ->
|
|
||||||
(if (groups.getGroupFromKey(k).key != NO_GROUP) {
|
|
||||||
v
|
|
||||||
} else if (!Settings.isRegional(ctx) && k == AUTO_GROUP) {
|
|
||||||
v.filter { it !in World.WWW.children.map { it1 -> it1.code } }
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}).takeIf { it.isNotEmpty() }
|
|
||||||
?.joinToString(",") { "#${it}$id,#${it}" } + "{fill:${
|
|
||||||
colorToHex6(
|
|
||||||
if (k == AUTO_GROUP)
|
|
||||||
colorWrapper(ctx, android.R.attr.colorPrimary)
|
|
||||||
else groups.getGroupFromKey(k).color
|
|
||||||
)
|
|
||||||
};}"
|
|
||||||
}.joinToString("")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun get(): String {
|
|
||||||
refresh()
|
|
||||||
return if (Settings.isRegional(ctx)) {
|
|
||||||
countryRegionalCSS + customCSS
|
|
||||||
} else {
|
|
||||||
countryOnlyCSS + customCSS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package net.helcel.beans.svg
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.caverock.androidsvg.SVG
|
|
||||||
import net.helcel.beans.R
|
|
||||||
|
|
||||||
class SVGWrapper(ctx: Context) {
|
|
||||||
|
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
|
||||||
private val svgFile = when (sharedPreferences.getString(
|
|
||||||
ctx.getString(R.string.key_projection),
|
|
||||||
ctx.getString(R.string.mercator)
|
|
||||||
)) {
|
|
||||||
ctx.getString(R.string.azimuthalequidistant) -> "aeqd01.svg"
|
|
||||||
ctx.getString(R.string.loximuthal) -> "loxim01.svg"
|
|
||||||
ctx.getString(R.string.mercator) -> "webmercator01.svg"
|
|
||||||
else -> "webmercator01.svg"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var svg: SVG? = SVG.getFromAsset(ctx.assets, svgFile)
|
|
||||||
|
|
||||||
fun get(): SVG? {
|
|
||||||
return svg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:viewportHeight="960">
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnBackground"
|
|
||||||
android:pathData="M80,160Q80,127 103.5,103.5Q127,80 160,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L240,720L80,880ZM206,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L160,160Q160,160 160,160Q160,160 160,160L160,685L206,640ZM160,640L160,640L160,160Q160,160 160,160Q160,160 160,160L160,160Q160,160 160,160Q160,160 160,160L160,640Q160,640 160,640Q160,640 160,640L160,640Z" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorPrimary"
|
|
||||||
android:pathData="M480,280Q497,280 508.5,268.5Q520,257 520,240Q520,223 508.5,211.5Q497,200 480,200Q463,200 451.5,211.5Q440,223 440,240Q440,257 451.5,268.5Q463,280 480,280ZM440,600L520,600L520,360L440,360L440,600ZM80,880L80,160" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnBackground"
|
|
||||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
|
||||||
|
|
||||||
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:width="24dp"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/white"
|
|
||||||
android:pathData="M346,820L100,574Q90,564 85,552Q80,540 80,527Q80,514 85,502Q90,490 100,480L330,251L224,145L286,80L686,480Q696,490 700.5,502Q705,514 705,527Q705,540 700.5,552Q696,564 686,574L440,820Q430,830 418,835Q406,840 393,840Q380,840 368,835Q356,830 346,820ZM393,314L179,528Q179,528 179,528Q179,528 179,528L607,528Q607,528 607,528Q607,528 607,528L393,314ZM792,840Q756,840 731,814.5Q706,789 706,752Q706,725 719.5,701Q733,677 750,654L792,600L836,654Q852,677 866,701Q880,725 880,752Q880,789 854,814.5Q828,840 792,840Z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="72dp"
|
|
||||||
android:height="72dp"
|
|
||||||
android:viewportWidth="72"
|
|
||||||
android:viewportHeight="72">
|
|
||||||
<path
|
|
||||||
android:pathData="M31,16l0,-4l10,0l0,4"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="4"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="?attr/colorOnBackground"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M51,25v31c0,2.209 -1.791,4 -4,4H25c-2.209,0 -4,-1.791 -4,-4V25"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="4"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="?attr/colorOnBackground"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M17,16h38v4h-38z"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="4"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="?attr/colorOnBackground"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M41,28.25L41,55"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="4"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="?attr/colorOnBackground"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
<path
|
|
||||||
android:pathData="M31,28.25L31,55"
|
|
||||||
android:strokeLineJoin="round"
|
|
||||||
android:strokeWidth="4"
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:strokeColor="?attr/colorOnBackground"
|
|
||||||
android:strokeLineCap="round"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:width="24dp"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<path
|
|
||||||
android:fillColor="@color/white"
|
|
||||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:width="24dp"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android" >
|
|
||||||
|
|
||||||
<path android:fillColor="?attr/colorOnBackground" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="?attr/colorPrimary" android:pathData="M8,14m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
|
|
||||||
|
|
||||||
<path android:fillColor="?attr/colorPrimary" android:pathData="M12,8m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
|
|
||||||
|
|
||||||
<path android:fillColor="?attr/colorPrimary" android:pathData="M16,14m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="128"
|
||||||
|
android:viewportHeight="128">
|
||||||
|
<group android:scaleX="0.8918919"
|
||||||
|
android:scaleY="0.8918919"
|
||||||
|
android:translateX="6.918919"
|
||||||
|
android:translateY="6.918919">
|
||||||
|
<group
|
||||||
|
android:scaleX="1.25"
|
||||||
|
android:scaleY="1.25"
|
||||||
|
android:translateX="18"
|
||||||
|
android:translateY="18">
|
||||||
|
<path
|
||||||
|
android:pathData="M56.803,45.75c0.599,-0.299 1.157,-0.672 1.663,-1.11 0.804,-0.698 1.463,-1.546 1.94,-2.497 0.599,-1.303 0.883,-2.728 0.829,-4.161 0.034,-1.498 -0.154,-2.994 -0.558,-4.437 -0.451,-1.645 -1.104,-3.229 -1.944,-4.714 -0.967,-2.141 -2.28,-4.108 -3.887,-5.822 -1.242,-1.326 -2.757,-2.366 -4.44,-3.048 -0.877,-0.372 -1.821,-0.56 -2.774,-0.553 -1.402,0.074 -2.729,0.697 -3.882,1.389 -1.285,0.81 -2.244,1.735 -2.771,3.053 -0.739,2.362 0.827,4.821 1.113,5.269 0.003,0.005 0.187,0.281 0.555,0.832 1.512,2.264 1.589,2.358 1.666,2.495 0.646,1.221 1.114,2.528 1.389,3.882 1.043,4.001 1.565,6.001 2.223,6.932 1.208,1.71 3.455,3.414 5.826,3.325 1.059,-0.088 2.093,-0.371 3.05,-0.835Z"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="@color/white"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M22,41c-1.54,0.554 -2.83,1.642 -3.636,3.066 -0.714,1.365 -0.957,2.928 -0.693,4.445 0.178,1.247 0.632,2.439 1.329,3.488 2.032,3.228 5.383,4.423 7,5 1.613,0.561 3.295,0.897 5,1 2.387,0.205 6.923,0.535 11,-2 1.287,-0.665 2.335,-1.713 3,-3 0.661,-1.601 0.661,-3.399 0,-5 -0.389,-1.156 -1.121,-2.165 -2.099,-2.894 -0.919,-0.599 -1.974,-0.956 -3.069,-1.039 -2.057,-0.313 -2.756,0.139 -5.014,0.057 -1.271,-0.019 -2.534,-0.214 -3.753,-0.577 -3.036,-0.95 -3.33,-2.457 -5.889,-2.829 -1.066,-0.158 -2.155,-0.061 -3.177,0.282Z"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="@color/white"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M12.7,26.975c1.758,-3.604 5.311,-3.191 9.58,-7.091 2.701,-2.469 2.858,-4.078 5.613,-5.202 1.407,-0.644 2.984,-0.819 4.498,-0.5 2.053,0.475 3.761,1.891 4.61,3.819 1.116,2.742 -0.314,5.437 -1.398,7.482 -0.951,1.689 -2.167,3.215 -3.602,4.518 -1.479,1.552 -3.161,2.898 -5,4 -2.629,1.541 -6.332,3.711 -9.989,2.43 -1.481,-0.548 -2.768,-1.521 -3.699,-2.796 -1.496,-1.906 -1.735,-4.512 -0.612,-6.659Z"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="@color/white"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24.0"
|
|
||||||
android:viewportHeight="24.0">
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnBackground"
|
|
||||||
android:pathData="M22,7h-9v2h9V7zM22,15h-9v2h9V15z"/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorPrimary"
|
|
||||||
android:pathData="M5.54,11L2,7.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,11z"/>
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorPrimary"
|
|
||||||
android:pathData="M5.54,19L2,15.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,19z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:width="24dp"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android" >
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnBackground"
|
|
||||||
android:pathData="M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48L3,20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48L21,3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM10,5.47l4,1.4v11.66l-4,-1.4L10,5.47zM5,6.46l3,-1.01v11.7l-3,1.16L5,6.46zM19,17.54l-3,1.01L16,6.86l3,-1.16v11.84z" />
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorPrimary"
|
|
||||||
android:pathData="M15,18.89l-6,-2.11L9,5.11l6,2.11v11.67z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:width="24dp"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android" >
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnBackground"
|
|
||||||
android:pathData="M12,22C6.49,22 2,17.51 2,12S6.49,2 12,2s10,4.04 10,9c0,3.31 -2.69,6 -6,6h-1.77c-0.28,0 -0.5,0.22 -0.5,0.5c0,0.12 0.05,0.23 0.13,0.33c0.41,0.47 0.64,1.06 0.64,1.67C14.5,20.88 13.38,22 12,22zM12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8c0.28,0 0.5,-0.22 0.5,-0.5c0,-0.16 -0.08,-0.28 -0.14,-0.35c-0.41,-0.46 -0.63,-1.05 -0.63,-1.65c0,-1.38 1.12,-2.5 2.5,-2.5H16c2.21,0 4,-1.79 4,-4C20,7.14 16.41,4 12,4z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorPrimary"
|
|
||||||
android:pathData="M17.5,13c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5C19,12.33 18.33,13 17.5,13zM14.5,9C13.67,9 13,8.33 13,7.5C13,6.67 13.67,6 14.5,6S16,6.67 16,7.5C16,8.33 15.33,9 14.5,9zM5,11.5C5,10.67 5.67,10 6.5,10S8,10.67 8,11.5C8,12.33 7.33,13 6.5,13S5,12.33 5,11.5zM11,7.5C11,8.33 10.33,9 9.5,9S8,8.33 8,7.5C8,6.67 8.67,6 9.5,6S11,6.67 11,7.5z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="960"
|
|
||||||
android:viewportWidth="960"
|
|
||||||
android:width="24dp"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android" >
|
|
||||||
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorOnBackground"
|
|
||||||
android:pathData="M105,727L40,680L240,360L360,500L520,240L629,403Q606,404 585.5,408.5Q565,413 545,421L523,388L371,635L250,494L105,727ZM732,423Q713,415 692.5,410Q672,405 650,404L855,80L920,127L732,423Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="?attr/colorPrimary"
|
|
||||||
android:pathData="M863,920L738,795Q718,809 693.5,816Q669,823 643,823Q568,823 515.5,770.5Q463,718 463,643Q463,568 515.5,515.5Q568,463 643,463Q718,463 770.5,515.5Q823,568 823,643Q823,669 816,693.5Q809,718 795,739L920,863L863,920ZM643,743Q685,743 714,714Q743,685 743,643Q743,601 714,572Q685,543 643,543Q601,543 572,572Q543,601 543,643Q543,685 572,714Q601,743 643,743Z" />
|
|
||||||
</vector>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<vector
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:width="24dp"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android" >
|
|
||||||
|
|
||||||
<path android:fillColor="?attr/colorOnBackground" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
|
||||||
|
|
||||||
<path android:fillColor="?attr/colorPrimary" android:pathData="M12,10h-2v2H9v-2H7V9h2V7h1v2h2v1z"/>
|
|
||||||
|
|
||||||
</vector>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:theme="@style/Theme.Beans"
|
|
||||||
tools:context=".activity.EditActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
|
||||||
android:id="@+id/tab"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
|
||||||
android:id="@+id/pager"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:theme="@style/Theme.Beans"
|
|
||||||
tools:context=".activity.MainActivity">
|
|
||||||
|
|
||||||
<com.github.chrisbanes.photoview.PhotoView
|
|
||||||
android:id="@+id/photo_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:theme="@style/Theme.Beans"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".activity.SettingsActivity">
|
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
|
||||||
android:id="@+id/fragment_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:theme="@style/Theme.Beans"
|
|
||||||
tools:context=".activity.StatsActivity">
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
|
||||||
android:id="@+id/tab"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
|
||||||
android:id="@+id/pager"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingBottom="10dp">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/group_color"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:layout_marginBottom="2dp"
|
|
||||||
android:paddingStart="56dp"
|
|
||||||
android:text="@string/total"
|
|
||||||
android:textAlignment="textStart"
|
|
||||||
android:textColor="?attr/colorOnPrimary"
|
|
||||||
app:cornerRadius="0dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:ignore="RtlSymmetry" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:gravity="start|center_vertical"
|
|
||||||
android:paddingStart="20dp"
|
|
||||||
android:paddingEnd="52dp"
|
|
||||||
android:text=""
|
|
||||||
android:textColor="?attr/colorOnPrimary"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/group_color"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/group_color"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/group_color" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/stats"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".activity.fragment.AboutFragment">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:layout_width="300dp"
|
|
||||||
android:layout_height="300dp"
|
|
||||||
android:layout_marginTop="20dp"
|
|
||||||
android:contentDescription="@string/logo"
|
|
||||||
android:src="@drawable/ic_launcher_foreground" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
android:layout_marginTop="15dp"
|
|
||||||
android:layout_marginEnd="10dp"
|
|
||||||
android:layout_marginBottom="10dp"
|
|
||||||
android:text="@string/app_name"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="30sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
android:layout_marginTop="0dp"
|
|
||||||
android:layout_marginEnd="10dp"
|
|
||||||
android:layout_marginBottom="15dp"
|
|
||||||
android:text="@string/app_version"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="25sp" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
android:layout_marginTop="15dp"
|
|
||||||
android:layout_marginEnd="10dp"
|
|
||||||
android:layout_marginBottom="15dp"
|
|
||||||
android:text="@string/beans_is_foss"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
android:layout_marginTop="15dp"
|
|
||||||
android:layout_marginEnd="10dp"
|
|
||||||
android:layout_marginBottom="15dp"
|
|
||||||
android:autoLink="web"
|
|
||||||
android:text="@string/beans_repo"
|
|
||||||
android:textAlignment="center" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
tools:context=".activity.fragment.EditGroupAddFragment">
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/group_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:autofillHints=""
|
|
||||||
android:hint="@string/name"
|
|
||||||
android:inputType="text" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/colorView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="@color/black"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/colorR"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
|
||||||
android:id="@+id/colorR"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/colorG"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/colorView"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:thumbColor="@color/red"
|
|
||||||
app:trackColorActive="@color/red" />
|
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
|
||||||
android:id="@+id/colorG"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:foregroundTint="#FF0000"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/colorB"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/colorView"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/colorR"
|
|
||||||
app:thumbColor="@color/green"
|
|
||||||
app:trackColorActive="@color/green" />
|
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
|
||||||
android:id="@+id/colorB"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/colorView"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/colorG"
|
|
||||||
app:thumbColor="@color/blue"
|
|
||||||
app:trackColorActive="@color/blue" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:labelFor="@id/group_color"
|
|
||||||
android:text="@string/hashtag" />
|
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
|
||||||
android:id="@+id/group_color"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:autofillHints=""
|
|
||||||
android:hint="@string/color_rrggbb"
|
|
||||||
android:inputType="text"
|
|
||||||
android:maxLength="6" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_marginTop="10dp">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnDelete"
|
|
||||||
android:layout_width="52dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingLeft="6dp"
|
|
||||||
android:paddingRight="6dp"
|
|
||||||
android:tooltipText="@string/delete"
|
|
||||||
app:icon="@drawable/delete"
|
|
||||||
app:iconGravity="textStart"
|
|
||||||
app:iconPadding="0dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnCancel"
|
|
||||||
android:layout_width="80dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="6dp"
|
|
||||||
android:paddingLeft="6dp"
|
|
||||||
android:paddingRight="6dp"
|
|
||||||
android:text="@string/cancel"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@+id/btnOk"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnOk"
|
|
||||||
android:layout_width="52dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingLeft="6dp"
|
|
||||||
android:paddingRight="6dp"
|
|
||||||
android:text="@string/ok"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".activity.fragment.EditPlaceFragment">
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:scrollbars="vertical"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:nestedScrollingEnabled="false"
|
|
||||||
android:scrollbars="vertical" />
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/warning_text"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="10dp" />
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="4dp">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/groups_color"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="10dp">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnAdd"
|
|
||||||
android:layout_width="64dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingLeft="6dp"
|
|
||||||
android:paddingRight="6dp"
|
|
||||||
android:text="@string/add"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/btnClear"
|
|
||||||
android:layout_width="64dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingLeft="6dp"
|
|
||||||
android:paddingRight="6dp"
|
|
||||||
android:text="@string/clear"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
tools:context=".activity.fragment.LicenseFragment">
|
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
|
||||||
android:id="@+id/license_fragment_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/textView"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:gravity="start|center_vertical"
|
|
||||||
android:insetTop="4dp"
|
|
||||||
android:insetBottom="4dp"
|
|
||||||
android:paddingStart="20dp"
|
|
||||||
android:paddingEnd="20dp"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
android:textAppearance="?attr/textAppearanceBody2"
|
|
||||||
android:textColor="?attr/colorOnBackground"
|
|
||||||
|
|
||||||
app:cornerRadius="4dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/checkBox"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/checkBox"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/count"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:gravity="start|center_vertical"
|
|
||||||
android:paddingStart="20dp"
|
|
||||||
android:paddingEnd="20dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/checkBox"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/checkBox"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.checkbox.MaterialCheckBox
|
|
||||||
android:id="@+id/checkBox"
|
|
||||||
android:layout_width="50dp"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
app:checkedState="indeterminate"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/textView"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="1"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/textView"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/textView"
|
|
||||||
app:layout_constraintVertical_bias="0.5" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/group_color"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="32dp"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:layout_marginEnd="32dp"
|
|
||||||
android:layout_marginBottom="2dp"
|
|
||||||
android:textAlignment="textStart"
|
|
||||||
android:textColor="?attr/colorOnPrimary"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
|
||||||
android:id="@+id/name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="50dp"
|
|
||||||
android:gravity="start|center_vertical"
|
|
||||||
android:paddingStart="20dp"
|
|
||||||
android:paddingEnd="20dp"
|
|
||||||
android:textColor="?attr/colorOnPrimary"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/group_color"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/group_color"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/group_color" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:context="net.helcel.beans.activity.EditActivity" >
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_color"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
android:icon="@drawable/color"
|
|
||||||
android:title="@string/action_color"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:context="net.helcel.beans.activity.MainActivity" >
|
|
||||||
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_edit"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
android:icon="@drawable/edit"
|
|
||||||
android:title="@string/action_edit"
|
|
||||||
app:showAsAction="ifRoom" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_stats"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
android:title="@string/action_stat"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/action_settings"
|
|
||||||
android:orderInCategory="100"
|
|
||||||
android:title="@string/action_settings"
|
|
||||||
app:showAsAction="never" />
|
|
||||||
|
|
||||||
</menu>
|
|
||||||
+1
@@ -2,4 +2,5 @@
|
|||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||||
|
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Beans" parent="android:Theme.Material.NoActionBar">
|
||||||
|
<!-- Status bar color in dark mode -->
|
||||||
|
<item name="android:statusBarColor">@color/blue</item>
|
||||||
|
<!-- Window background in dark mode -->
|
||||||
|
<item name="android:windowBackground">@color/darkgray</item>
|
||||||
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.Beans" parent="android:Theme.Material.Light.NoActionBar">
|
||||||
|
<item name="android:statusBarColor">@color/blue</item>
|
||||||
|
<item name="android:windowBackground">@color/blue</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string-array name="entries_theme">
|
|
||||||
<item>@string/system</item>
|
|
||||||
<item>@string/light</item>
|
|
||||||
<item>@string/dark</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="entries_stats">
|
|
||||||
<item>@string/counters</item>
|
|
||||||
<item>@string/percentages</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="entries_onoff">
|
|
||||||
<item>@string/on</item>
|
|
||||||
<item>@string/off</item>
|
|
||||||
</string-array>
|
|
||||||
|
|
||||||
<string-array name="map_projection">
|
|
||||||
<item>@string/azimuthalequidistant</item>
|
|
||||||
<item>@string/loximuthal</item>
|
|
||||||
<item>@string/mercator</item>
|
|
||||||
|
|
||||||
</string-array>
|
|
||||||
</resources>
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Beans</string>
|
|
||||||
<string name="app_version">1.0</string>
|
|
||||||
<string name="action_settings">Settings</string>
|
<string name="action_settings">Settings</string>
|
||||||
<string name="action_stat">Stats</string>
|
<string name="action_stat">Stats</string>
|
||||||
<string name="action_edit">Edit</string>
|
<string name="action_edit">Edit</string>
|
||||||
|
<string name="action_add">Add</string>
|
||||||
|
<string name="action_clear">Clear</string>
|
||||||
<string name="action_color">Color</string>
|
<string name="action_color">Color</string>
|
||||||
<string name="key_theme">App theme</string>
|
<string name="key_theme">App theme</string>
|
||||||
<string name="system">System</string>
|
<string name="system">System</string>
|
||||||
@@ -13,21 +13,39 @@
|
|||||||
<string name="key_stats">Statistics</string>
|
<string name="key_stats">Statistics</string>
|
||||||
<string name="counters">Prefer counters (#)</string>
|
<string name="counters">Prefer counters (#)</string>
|
||||||
<string name="percentages">Prefer percentages (%)</string>
|
<string name="percentages">Prefer percentages (%)</string>
|
||||||
<string name="licenses">Licenses</string>
|
|
||||||
<string name="key_group">Groups</string>
|
<string name="key_group">Groups</string>
|
||||||
<string name="key_regional">Regional</string>
|
<string name="key_regional">Regional mode</string>
|
||||||
|
<string name="key_regional_desc">Enable tracking of states and provinces</string>
|
||||||
|
<string name="key_cascade_stats">key_cascade_stats</string>
|
||||||
|
<string name="pref_cascade_stats">Cascade statistics</string>
|
||||||
|
<string name="pref_cascade_stats_desc">Include parents and children in the counts</string>
|
||||||
|
<string name="key_group_desc">Enable multiple colors</string>
|
||||||
|
<string name="key_touch_radius">key_touch_radius</string>
|
||||||
|
<string name="pref_touch_radius">Tap radius</string>
|
||||||
|
<string name="pref_touch_radius_desc">How far around a tap on the map other places should be considered</string>
|
||||||
|
<string name="pref_touch_radius_value">%1$d dp</string>
|
||||||
<string name="about">About</string>
|
<string name="about">About</string>
|
||||||
|
<string name="pref_category_appearance">Appearance</string>
|
||||||
|
<string name="pref_category_features">Features</string>
|
||||||
|
<string name="pref_category_data">Data</string>
|
||||||
|
<string name="action_import">Import</string>
|
||||||
|
<string name="action_export">Export</string>
|
||||||
|
<string name="import_desc">Load data from a previously exported file.</string>
|
||||||
|
<string name="export_desc">Save your current data to a file for backup or transfer.</string>
|
||||||
|
<string name="key_projection_desc">Select how the map is projected onto a 2D surface.</string>
|
||||||
<string name="beans_is_foss">Beans is free and open source software, licensed under the GNU General Public License (version 3 or later)</string>
|
<string name="beans_is_foss">Beans is free and open source software, licensed under the GNU General Public License (version 3 or later)</string>
|
||||||
<string name="beans_repo">Project repository: https://github.com/helcel-net/beans\n Feel free to report issues or contribute to the project.</string>
|
<string name="beans_repo_uri">https://github.com/helcel-net/beans</string>
|
||||||
<string name="foss_licenses">Free and open source dependencies and licenses</string>
|
<string name="beans_repo">Project repository: %1$s\n Feel free to report issues or contribute.</string>
|
||||||
<string name="about_beans">About the Beans application</string>
|
<string name="about_beans">About the Beans application</string>
|
||||||
<string name="edit_group">Select the group to assign. Long press on a group to edit its name and color.</string>
|
<string name="edit_group">Select the group to assign.</string>
|
||||||
|
<string name="edit_group_sub">Long press on a group to edit its name and color.</string>
|
||||||
|
<string name="select_group">Select the group to keep.</string>
|
||||||
|
<string name="select_group_sub">All others will be deleted and its mappings reassigned to the group you choose here.</string>
|
||||||
<string name="delete_group">Are your sure you want to delete this group and remove all its country mappings?</string>
|
<string name="delete_group">Are your sure you want to delete this group and remove all its country mappings?</string>
|
||||||
<string name="select_group">Select one group you want to keep. All others will be deleted and its mappings reassigned to the group you choose here.</string>
|
<string name="select_place">Select a place</string>
|
||||||
|
<string name="select_place_sub">Tap the place to color, or a larger one that contains it.</string>
|
||||||
<string name="delete_regions">Are you sure you want to disable regions and reassign all regional mappings to the corresponding countries?</string>
|
<string name="delete_regions">Are you sure you want to disable regions and reassign all regional mappings to the corresponding countries?</string>
|
||||||
<string name="add">Add</string>
|
|
||||||
<string name="clear">Clear</string>
|
|
||||||
<string name="logo">Logo</string>
|
|
||||||
<string name="name">Name</string>
|
<string name="name">Name</string>
|
||||||
<string name="rate">%1$d/%2$d</string>
|
<string name="rate">%1$d/%2$d</string>
|
||||||
<string name="rate_with_unit">%1$d / %2$d %3$s</string>
|
<string name="rate_with_unit">%1$d / %2$d %3$s</string>
|
||||||
@@ -39,9 +57,10 @@
|
|||||||
<string name="off">Off</string>
|
<string name="off">Off</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
<string name="ok">Ok</string>
|
<string name="ok">OK</string>
|
||||||
<string name="total">Total</string>
|
<string name="total">Total</string>
|
||||||
<string name="uncategorized">Uncategorized</string>
|
<string name="uncategorized">Uncategorized</string>
|
||||||
|
|
||||||
<string name="azimuthalequidistant">Azimuthal Equidistant</string>
|
<string name="azimuthalequidistant">Azimuthal Equidistant</string>
|
||||||
<string name="mercator">Mercator</string>
|
<string name="mercator">Mercator</string>
|
||||||
<string name="loximuthal">Loximuthal</string>
|
<string name="loximuthal">Loximuthal</string>
|
||||||
|
|||||||
@@ -1,32 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<style name="Theme.Beans" parent="Theme.Material3.DayNight">
|
<style name="Theme.Beans" parent="android:Theme.Material.Light.NoActionBar">
|
||||||
<item name="colorPrimary">@color/blue</item>
|
<!-- Status bar color -->
|
||||||
<item name="background">@color/darkgray</item>
|
<item name="android:statusBarColor">@color/blue</item>
|
||||||
<item name="android:colorPrimary">?attr/colorPrimary</item>
|
<!-- Window background - shows through statusBarsPadding() area -->
|
||||||
<item name="android:panelColorBackground">@color/lightgray</item>
|
<item name="android:windowBackground">@color/blue</item>
|
||||||
<item name="android:statusBarColor">?attr/colorPrimary</item>
|
<item name="android:windowLightStatusBar">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||||
|
|
||||||
<item name="checkboxStyle">@style/Theme.Beans.CheckBox</item>
|
|
||||||
<item name="actionBarStyle">@style/Theme.Beans.ActionBar</item>
|
|
||||||
<item name="android:actionOverflowButtonStyle">@style/Theme.Beans.ActionBar.ButtonOverflow</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.Beans.CheckBox" parent="Widget.Material3.CompoundButton.CheckBox">
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.Beans.ActionBar" parent="Widget.Material3.ActionBar.Solid">
|
|
||||||
<item name="background">?attr/colorPrimary</item>
|
|
||||||
<item name="titleTextStyle">@style/Theme.Beans.ActionBar.Text</item>
|
|
||||||
<item name="android:tint">@color/white</item>
|
|
||||||
<item name="actionMenuTextColor">@color/white</item>
|
|
||||||
<item name="homeAsUpIndicator">@drawable/back</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.Beans.ActionBar.Text" parent="TextAppearance.Material3.ActionBar.Title">
|
|
||||||
<item name="android:textColor">@color/white</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.Beans.ActionBar.ButtonOverflow" parent="Widget.Material3.Search.ActionButton.Overflow">
|
|
||||||
<item name="android:tint">@color/white</item>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:theme="@style/Theme.Beans">
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:defaultValue="@string/system"
|
|
||||||
app:enabled="true"
|
|
||||||
app:entries="@array/entries_theme"
|
|
||||||
app:entryValues="@array/entries_theme"
|
|
||||||
app:icon="@drawable/palette"
|
|
||||||
app:key="@string/key_theme"
|
|
||||||
app:title="@string/key_theme"
|
|
||||||
app:useSimpleSummaryProvider="true" />
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:defaultValue="@string/mercator"
|
|
||||||
app:enabled="true"
|
|
||||||
app:entries="@array/map_projection"
|
|
||||||
app:entryValues="@array/map_projection"
|
|
||||||
app:icon="@drawable/map"
|
|
||||||
app:key="@string/key_projection"
|
|
||||||
app:title="@string/key_projection"
|
|
||||||
app:useSimpleSummaryProvider="true" />
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:defaultValue="@string/counters"
|
|
||||||
app:enabled="true"
|
|
||||||
app:entries="@array/entries_stats"
|
|
||||||
app:entryValues="@array/entries_stats"
|
|
||||||
app:icon="@drawable/stats"
|
|
||||||
app:key="@string/key_stats"
|
|
||||||
app:title="@string/key_stats"
|
|
||||||
app:useSimpleSummaryProvider="true" />
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:defaultValue="@string/off"
|
|
||||||
app:enabled="true"
|
|
||||||
app:allowDividerAbove="true"
|
|
||||||
app:entries="@array/entries_onoff"
|
|
||||||
app:entryValues="@array/entries_onoff"
|
|
||||||
app:icon="@drawable/group"
|
|
||||||
app:key="@string/key_group"
|
|
||||||
app:title="@string/key_group"
|
|
||||||
app:useSimpleSummaryProvider="true" />
|
|
||||||
|
|
||||||
<ListPreference
|
|
||||||
app:defaultValue="@string/off"
|
|
||||||
app:enabled="true"
|
|
||||||
app:entries="@array/entries_onoff"
|
|
||||||
app:entryValues="@array/entries_onoff"
|
|
||||||
app:icon="@drawable/zoomin"
|
|
||||||
app:key="@string/key_regional"
|
|
||||||
app:title="@string/key_regional"
|
|
||||||
app:useSimpleSummaryProvider="true" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:summary="@string/foss_licenses"
|
|
||||||
app:enabled="true"
|
|
||||||
app:allowDividerAbove="true"
|
|
||||||
app:icon="@drawable/licenses"
|
|
||||||
app:key="@string/licenses"
|
|
||||||
app:title="@string/licenses" />
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:summary="@string/about_beans"
|
|
||||||
app:enabled="true"
|
|
||||||
app:icon="@drawable/about"
|
|
||||||
app:key="@string/about"
|
|
||||||
app:title="@string/about" />
|
|
||||||
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.3.1' apply false
|
id 'com.android.application' version '9.4.0' apply false
|
||||||
id 'com.android.library' version '8.3.1' apply false
|
id 'com.android.library' version '9.3.2' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
id 'org.jetbrains.kotlin.android' version '2.4.10' apply false
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"uniqueId": "org.gadm:data",
|
||||||
|
"developers": [
|
||||||
|
{
|
||||||
|
"organisationUrl": "https://gadm.org/index.html",
|
||||||
|
"name": "GADM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactVersion": "4.1",
|
||||||
|
"description": "GADM provides maps and spatial data for all countries and their sub-divisions.",
|
||||||
|
"scm": {
|
||||||
|
"connection": "scm:git@github.com:mikepenz/MaterialDrawer.git",
|
||||||
|
"url": "https://github.com/mikepenz/MaterialDrawer",
|
||||||
|
"developerConnection": "scm:git@github.com:mikepenz/MaterialDrawer.git"
|
||||||
|
},
|
||||||
|
"name": "GADM maps and data",
|
||||||
|
"website": "https://gadm.org/index.html",
|
||||||
|
"licenses": [
|
||||||
|
"0151ac7b561a385c536ad4c94532e60b"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"content": "<b>The data are freely available for academic use and other non-commercial use. Redistribution or commercial use is not allowed without prior permission.</b>\n\nUsing the data to create maps for publishing of academic research articles is allowed. Thus you can use the maps you made with GADM data for figures in articles published by PLoS, Springer Nature, Elsevier, MDPI, etc. You are allowed (but not required) to publish these articles (and the maps they contain) under an open license such as CC-BY as is the case with PLoS journals and may be the case with other open access articles. <b>Data for the following countries is covered by a a different license</b> <b>Austria</b>: Creative Commons Attribution-ShareAlike 2.0 (source: Government of Ausria)",
|
||||||
|
"hash": "0151ac7b561a385c536ad4c94532e60b",
|
||||||
|
"url": "https://gadm.org/license.html",
|
||||||
|
"name": "GADM license"
|
||||||
|
}
|
||||||
+9
-10
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/node
|
#!/bin/node
|
||||||
|
|
||||||
import {readFileSync, existsSync} from 'fs'
|
import {readFileSync,existsSync} from 'fs'
|
||||||
import area from '@turf/area'
|
import area from '@turf/area'
|
||||||
import * as turf from '@turf/turf'
|
import * as turf from '@turf/turf'
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ const countries =
|
|||||||
|
|
||||||
|
|
||||||
const groups = {
|
const groups = {
|
||||||
"EEE":["ALB","AND","AUT","BLR","BEL","BIH","BGR","HRV","CYP","CZE","DNK","EST","FIN","FRA","DEU","GRC","HUN","ISL","IRL","ITA","KAZ","XKO","LVA","LIE","LTU","LUX","MLT","MDA","MCO","MNE","NLD","MKD","NOR","POL","PRT","ROU","RUS","SMR","SRB","SVK","SVN","ESP","SWE","CHE","UKR","GBR","VAT","XAD"],
|
"EEE":["ALA","ALB","AND","AUT","BLR","BEL","BIH","BGR","HRV","CYP","CZE","DNK","EST","FIN","FRA","DEU","GIB","GGY","GRC","HUN","ISL","IRL","IMN","JEY","ITA","KAZ","XKO","LVA","LIE","LTU","LUX","MLT","MDA","MCO","MNE","NLD","MKD","NOR","SJM","POL","PRT","ROU","RUS","SMR","SRB","SVK","SVN","ESP","SWE","CHE","UKR","GBR","VAT","XAD"],
|
||||||
"ABB":["AFG","ARM","AZE","BHR","BGD","BTN","BRN","KHM","CHN","GEO","HKG","IND","IDN","IRN","IRQ","ISR","JPN","JOR","KWT","KGZ","LAO","LBN","MAC","MYS","MDV","MNG","MMR","NPL","PRK","OMN","PAK","PSE","PHL","QAT","SAU","SGP","KOR","LKA","SYR","TWN","TJK","THA","TLS","TUR","TKM","ARE","UZB","VNM","YEM","ZNC"],
|
"ABB":["AFG","ARM","AZE","BHR","BGD","BTN","BRN","KHM","CHN","GEO","HKG","IND","IDN","IRN","IRQ","ISR","JPN","JOR","KWT","KGZ","LAO","LBN","MAC","MYS","MDV","MNG","MMR","NPL","PRK","OMN","PAK","PSE","PHL","QAT","SAU","SGP","KOR","LKA","SYR","TWN","TJK","THA","TLS","TUR","TKM","ARE","UZB","VNM","YEM","ZNC"],
|
||||||
"FFF":["DZA","AGO","BDI","BEN","BWA","BFA","BDI","CPV","CMR","CAF","TCD","COM","COG","COD","CIV","DJI","EGY","GNQ","ERI","SWZ","ETH","GAB","GMB","GHA","GIN","GNB","KEN","LSO","LBR","LBY","MDG","MWI","MLI","MRT","MUS","MYT","MAR","MOZ","NAM","NER","NGA","COD","REU","RWA","STP","SEN","SYC","SLE","SOM","ZAF","SSD","SHN","SDN","TZA","TGO","TUN","UGA","COD","ZMB","ZWE","ESH"],
|
"FFF":["DZA","AGO","BDI","BEN","BWA","BFA","BDI","CPV","CMR","CAF","TCD","COM","COG","COD","CIV","DJI","EGY","GNQ","ERI","SWZ","ETH","GAB","GMB","GHA","GIN","GNB","KEN","LSO","LBR","LBY","MDG","MWI","MLI","MRT","MUS","MYT","MAR","MOZ","NAM","NER","NGA","COD","REU","RWA","STP","SEN","SYC","SLE","SOM","ZAF","SSD","SHN","SDN","TZA","TGO","TUN","UGA","COD","ZMB","ZWE","ESH"],
|
||||||
"NNN":["ABW","AIA","ATG","BHS","BRB","BLZ","BMU","VGB","CAN","CYM","CRI","CUB","CUW","DMA","DOM","SLV","GRL","GRD","GLP","GTM","HTI","HND","JAM","MTQ","MEX","MSR","ANT","CUW","NIC","PAN","PRI","KNA","LCA","MAF","SPM","VCT","TTO","TCA","USA","XCL"],
|
"NNN":["ABW","AIA","ATG","BHS","BRB","BLZ","BMU","VGB","CAN","CYM","CRI","CUB","CUW","DMA","DOM","SLV","GRL","GRD","GLP","GTM","HTI","HND","JAM","MTQ","MEX","MSR","ANT","CUW","NIC","PAN","PRI","KNA","LCA","MAF","SPM","VCT","TTO","TCA","USA","XCL"],
|
||||||
@@ -32,7 +32,6 @@ const groups = {
|
|||||||
"UUU":["ASM","AUS","COK","FJI","PYF","GUM","KIR","MHL","FSM","NRU","NCL","NZL","NIU","NFK","MNP","PLW","PNG","PCN","SLB","TKL","TON","TUV","VUT","WLF"],
|
"UUU":["ASM","AUS","COK","FJI","PYF","GUM","KIR","MHL","FSM","NRU","NCL","NZL","NIU","NFK","MNP","PLW","PNG","PCN","SLB","TKL","TON","TUV","VUT","WLF"],
|
||||||
"XXX":[
|
"XXX":[
|
||||||
"ATA", // Antarctica: not in any other region
|
"ATA", // Antarctica: not in any other region
|
||||||
"ALA",// Åland Islands: an autonomous region of Finland, but not a member of the EU or UN
|
|
||||||
"BES",// Bonaire, Sint Eustatius and Saba: special municipalities of the Netherlands in the Caribbean
|
"BES",// Bonaire, Sint Eustatius and Saba: special municipalities of the Netherlands in the Caribbean
|
||||||
"BVT",// Bouvet Island: an uninhabited territory of Norway in the South Atlantic
|
"BVT",// Bouvet Island: an uninhabited territory of Norway in the South Atlantic
|
||||||
"IOT",// British Indian Ocean Territory: a British overseas territory in the Indian Ocean
|
"IOT",// British Indian Ocean Territory: a British overseas territory in the Indian Ocean
|
||||||
@@ -40,16 +39,11 @@ const groups = {
|
|||||||
"CCK",// Cocos (Keeling) Islands: an Australian external territory in the Indian Ocean
|
"CCK",// Cocos (Keeling) Islands: an Australian external territory in the Indian Ocean
|
||||||
"FRO",// Faroe Islands: an autonomous region of Denmark
|
"FRO",// Faroe Islands: an autonomous region of Denmark
|
||||||
"ATF",// French Southern and Antarctic Lands: a territory of France located in the southern Indian Ocean
|
"ATF",// French Southern and Antarctic Lands: a territory of France located in the southern Indian Ocean
|
||||||
"GIB",// Gibraltar: a British overseas territory located at the southern tip of the Iberian Peninsula
|
|
||||||
"GGY",// Guernsey: a British Crown dependency in the English Channel
|
|
||||||
"HMD",// Heard Island and McDonald Islands: an uninhabited Australian external territory in the southern Indian Ocean
|
"HMD",// Heard Island and McDonald Islands: an uninhabited Australian external territory in the southern Indian Ocean
|
||||||
"IMN",// Isle of Man: a British Crown dependency located in the Irish Sea
|
|
||||||
"JEY",// Jersey: a British Crown dependency located in the English Channel
|
|
||||||
"BLM",// Saint Barthélemy: an overseas collectivity of France in the Caribbean
|
"BLM",// Saint Barthélemy: an overseas collectivity of France in the Caribbean
|
||||||
"WSM",// Samoa: an independent island nation in the South Pacific
|
"WSM",// Samoa: an independent island nation in the South Pacific
|
||||||
"SXM",// Sint Maarten: a constituent country of the Kingdom of the Netherlands in the Caribbean
|
"SXM",// Sint Maarten: a constituent country of the Kingdom of the Netherlands in the Caribbean
|
||||||
"SGS",// South Georgia and the South Sandwich Islands: a British overseas territory in the southern Atlantic Ocean
|
"SGS",// South Georgia and the South Sandwich Islands: a British overseas territory in the southern Atlantic Ocean
|
||||||
"SJM",// Svalbard and Jan Mayen: an archipelago administered by Norway
|
|
||||||
"UMI",// United States Minor Outlying Islands: a collection of nine insular areas of the United States
|
"UMI",// United States Minor Outlying Islands: a collection of nine insular areas of the United States
|
||||||
"VIR",// United States Virgin Islands: an unincorporated territory of the United States in the Caribbean
|
"VIR",// United States Virgin Islands: an unincorporated territory of the United States in the Caribbean
|
||||||
]
|
]
|
||||||
@@ -63,8 +57,8 @@ const formatStr = (str)=> str.replace(/(?<!\b\w\u00E0-\u00FC)\B[A-Z\u00C0-\u00DC
|
|||||||
return ', ';
|
return ', ';
|
||||||
} else {
|
} else {
|
||||||
return ' ' + match;
|
return ' ' + match;
|
||||||
}}).replace("ofthe "," of the ").replace("dela ", " de la ").replace("delos ", " de los ").replace("áD","á D")
|
}}).replace("ofthe "," of the ").replace("dela ", " de la ").replace("delos ", " de los ").replace("áD","á D").replace("eÁ","e Á")
|
||||||
.replace("Côted'","Côte d'").replace("leof ","le of ").replace("dde ","d de ").replace("iode ","io de ").replace("àde ","à de ")
|
.replace("ed'","e d'").replace("leof ","le of ").replace("dde ","d de ").replace("iode ","io de ").replace("àde ","à de ")
|
||||||
.replace("yof ","y of ").replace("Andrésy ","Andrés y")
|
.replace("yof ","y of ").replace("Andrésy ","Andrés y")
|
||||||
.replace("aand ","a and ").replace("iand ", "i and ").replace("tsand ","ts and ").replace("onand ","on and ").replace("reand ", "re and ")
|
.replace("aand ","a and ").replace("iand ", "i and ").replace("tsand ","ts and ").replace("onand ","on and ").replace("reand ", "re and ")
|
||||||
.replace("odel ","o del ").replace("adel ", "a del ").replace("ndel ","n del ").replace("zdel ","z del ").replace("falde ", "fal de ").replace("casdel ","cas del ")
|
.replace("odel ","o del ").replace("adel ", "a del ").replace("ndel ","n del ").replace("zdel ","z del ").replace("falde ", "fal de ").replace("casdel ","cas del ")
|
||||||
@@ -76,6 +70,11 @@ const formatStr = (str)=> str.replace(/(?<!\b\w\u00E0-\u00FC)\B[A-Z\u00C0-\u00DC
|
|||||||
.replace("Valledel ","Valle del ").replace("Valde ","Val de ").replace("Îlesdu ","Îles du ")
|
.replace("Valledel ","Valle del ").replace("Valde ","Val de ").replace("Îlesdu ","Îles du ")
|
||||||
.replace("sÉ","s É").replace("áO","á O").replace("N C Tof ","NCT of ").replace("N A","NA")
|
.replace("sÉ","s É").replace("áO","á O").replace("N C Tof ","NCT of ").replace("N A","NA")
|
||||||
.replace("Nortede ", "Norte de ")
|
.replace("Nortede ", "Norte de ")
|
||||||
|
.replace("Pinardel ", "Pinar del ")
|
||||||
|
.replace("Greeceand", "Greece and the Ionian")
|
||||||
|
.replace("Vientiane", "Vientiane Province")
|
||||||
|
.replace("Vientiane Province[prefecture]", "Vientiane Prefecture")
|
||||||
|
.replace("Valduz", "Vaduz")
|
||||||
.trim()
|
.trim()
|
||||||
|
|
||||||
const parse0 = (country) => {
|
const parse0 = (country) => {
|
||||||
|
|||||||
+48
-43
@@ -1,17 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
LOCAL_SVG_PATH="app/src/main/assets/"
|
|
||||||
|
|
||||||
GADM_VERSION="4.1"
|
GADM_VERSION="4.1"
|
||||||
GADM_BASEPATH="https://geodata.ucdavis.edu/gadm"
|
GADM_BASEPATH="https://geodata.ucdavis.edu/gadm"
|
||||||
|
|
||||||
mapshaper="./node_modules/mapshaper/bin/mapshaper"
|
mapshaper="./node_modules/mapshaper/bin/mapshaper"
|
||||||
ATA_URL="https://media.githubusercontent.com/media/wmgeolab/geoBoundaries/905b0baf5f4fb3b9ccf45293647dcacdb2b799d4/releaseData/gbOpen/ATA/ADM0/geoBoundaries-ATA-ADM0_simplified.geojson"
|
ATA_URL="https://media.githubusercontent.com/media/wmgeolab/geoBoundaries/905b0baf5f4fb3b9ccf45293647dcacdb2b799d4/releaseData/gbOpen/ATA/ADM0/geoBoundaries-ATA-ADM0_simplified.geojson"
|
||||||
|
|
||||||
|
# Caspian Sea: "XCA"
|
||||||
countries=(
|
countries=(
|
||||||
"AFG" "XAD" "ALA" "ALB" "DZA" "ASM" "AND" "AGO" "AIA" "ATG" "ARG" "ARM" "ABW" "AUS" "AUT" "AZE"
|
"AFG" "XAD" "ALA" "ALB" "DZA" "ASM" "AND" "AGO" "AIA" "ATG" "ARG" "ARM" "ABW" "AUS" "AUT" "AZE"
|
||||||
"BHS" "BHR" "BGD" "BRB" "BLR" "BEL" "BLZ" "BEN" "BMU" "BTN" "BOL" "BES" "BIH" "BWA" "BVT" "BRA" "IOT" "VGB" "BRN" "BGR" "BFA" "BDI" "KHM"
|
"BHS" "BHR" "BGD" "BRB" "BLR" "BEL" "BLZ" "BEN" "BMU" "BTN" "BOL" "BES" "BIH" "BWA" "BVT" "BRA" "IOT" "VGB" "BRN" "BGR" "BFA" "BDI" "KHM"
|
||||||
"CMR" "CAN" "CPV" "XCA" "CYM" "CAF" "TCD" "CHL" "CHN" "CXR" "XCL" "CCK" "COL" "COM" "COK" "CRI" "CIV" "HRV" "CUB" "CUW" "CYP" "CZE" "COD"
|
"CMR" "CAN" "CPV" "CYM" "CAF" "TCD" "CHL" "CHN" "CXR" "XCL" "CCK" "COL" "COM" "COK" "CRI" "CIV" "HRV" "CUB" "CUW" "CYP" "CZE" "COD"
|
||||||
"DNK" "DJI" "DMA" "DOM" "ECU" "EGY" "SLV" "GNQ" "ERI" "EST" "ETH" "FLK" "FRO" "FJI" "FIN" "FRA" "GUF" "PYF" "ATF"
|
"DNK" "DJI" "DMA" "DOM" "ECU" "EGY" "SLV" "GNQ" "ERI" "EST" "ETH" "FLK" "FRO" "FJI" "FIN" "FRA" "GUF" "PYF" "ATF"
|
||||||
"GAB" "GMB" "GEO" "DEU" "GHA" "GIB" "GRC" "GRL" "GRD" "GLP" "GUM" "GTM" "GGY" "GIN" "GNB" "GUY" "HTI" "HMD" "HND" "HUN"
|
"GAB" "GMB" "GEO" "DEU" "GHA" "GIB" "GRC" "GRL" "GRD" "GLP" "GUM" "GTM" "GGY" "GIN" "GNB" "GUY" "HTI" "HMD" "HND" "HUN"
|
||||||
"ISL" "IND" "IDN" "IRN" "IRQ" "IRL" "IMN" "ISR" "ITA" "JAM" "JPN" "JEY" "JOR" "KAZ" "KEN" "KIR" "XKO" "KWT" "KGZ"
|
"ISL" "IND" "IDN" "IRN" "IRQ" "IRL" "IMN" "ISR" "ITA" "JAM" "JPN" "JEY" "JOR" "KAZ" "KEN" "KIR" "XKO" "KWT" "KGZ"
|
||||||
@@ -79,44 +78,47 @@ download_1() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
toSVG_0() {
|
generate_map() {
|
||||||
local input_files=("ATA")
|
local OUT_FILE="$1" # First argument: Output destination path
|
||||||
|
local PROJ_ARGS="$2" # Second argument: Projection parameters
|
||||||
|
shift 2 # Remove the first two arguments, leaving only the files
|
||||||
|
local FILES_TO_RUN=("$@") # Capture all remaining arguments as the file array
|
||||||
|
|
||||||
for country in "${countries[@]}"
|
echo "Generating: $OUT_FILE using projection [$PROJ_ARGS]"
|
||||||
do
|
echo "Processing ${#FILES_TO_RUN[@]} files..."
|
||||||
input_file="./temp/0/${country}.json"
|
|
||||||
if [ -f "$input_file" ]; then
|
|
||||||
input_files+=("$input_file")
|
|
||||||
else
|
|
||||||
echo "Input file $input_file not found."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
|
# Each feature is tagged with the place it belongs to and whether it is a
|
||||||
|
# country outline or one of its regions. packmap.js groups on that; nothing
|
||||||
|
# downstream needs the features split into layers.
|
||||||
|
local JS_PIPELINE_LOGIC="
|
||||||
|
const conflicts = {
|
||||||
|
'Z01': 'IND', 'Z04': 'IND', 'Z05': 'IND', 'Z07': 'IND', 'Z09': 'IND',
|
||||||
|
'Z02': 'CHN', 'Z03': 'CHN','Z08': 'CHN',
|
||||||
|
'Z06': 'PAK'
|
||||||
|
};
|
||||||
|
let rawCode = GID_0 || 'UNK';
|
||||||
|
let cCode = conflicts[rawCode] ? conflicts[rawCode] : rawCode;
|
||||||
|
// Antarctica's boundary file carries no country code at either level.
|
||||||
|
if (cCode === 'UNK') cCode = 'ATA';
|
||||||
|
let isGidMissing = (!GID || GID === 'undefined' || GID === 'null' || GID === '');
|
||||||
|
CODE = isGidMissing ? cCode : GID;
|
||||||
|
LEVEL = isGidMissing ? 0 : 1;
|
||||||
|
"
|
||||||
|
|
||||||
|
"$mapshaper" -i "${FILES_TO_RUN[@]}" combine-files \
|
||||||
|
-snap \
|
||||||
|
-merge-layers force\
|
||||||
|
-proj $PROJ_ARGS densify \
|
||||||
|
-simplify 10% weighted keep-shapes \
|
||||||
|
-filter-islands min-area=0 \
|
||||||
|
-sort 'this.area' ascending \
|
||||||
|
-each "$JS_PIPELINE_LOGIC" \
|
||||||
|
-o "$OUT_FILE" \
|
||||||
|
format=geojson
|
||||||
|
|
||||||
"$mapshaper" -i combine-files ${input_files[@]} -proj webmercator -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/mercator0.svg svg-data=GID_0,COUNTRY id-field=GID_0
|
|
||||||
"$mapshaper" -i combine-files ${input_files[@]} -proj aeqd +lat_0=90 -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/aeqd0.svg svg-data=GID_0,COUNTRY id-field=GID_0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toSVG_1() {
|
build_maps() {
|
||||||
input_files=("ATA")
|
|
||||||
|
|
||||||
for country in "${countries[@]}"
|
|
||||||
do
|
|
||||||
input_file="./temp/1/${country}.json"
|
|
||||||
# input_file="./temp/1/gadm41_${country}_1.json"
|
|
||||||
if [ -f "$input_file" ]; then
|
|
||||||
input_files+=("$input_file")
|
|
||||||
else
|
|
||||||
echo "Input file $input_file not found."
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
"$mapshaper" -i combine-files ${input_files[@]} -proj webmercator -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/mercator1.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
|
||||||
"$mapshaper" -i combine-files ${input_files[@]} -proj aeqd +lat_0=90 -simplify 0.005 weighted keep-shapes resolution=1200x1200 -o ./app/src/main/assets/aeqd1.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
toSVG_01() {
|
|
||||||
input_files=()
|
input_files=()
|
||||||
|
|
||||||
input_files+=("./temp/1/ATA.json")
|
input_files+=("./temp/1/ATA.json")
|
||||||
@@ -136,9 +138,12 @@ toSVG_01() {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
"$mapshaper" -i combine-files ${input_files[@]} snap -proj loxim densify -simplify 0.001 weighted keep-shapes -o ./app/src/main/assets/loxim01.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
generate_map "./mapdata/loxim01.geojson" "loxim" "${input_files[@]}"
|
||||||
"$mapshaper" -i combine-files ${input_files[@]} snap -proj webmercator densify -simplify 0.001 weighted keep-shapes -o ./app/src/main/assets/webmercator01.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
generate_map "./mapdata/webmercator01.geojson" "webmercator" "${input_files[@]}"
|
||||||
"$mapshaper" -i combine-files ${input_files[@]} snap -proj aeqd +lat_0=90 densify -simplify 0.001 weighted keep-shapes -o ./app/src/main/assets/aeqd01.svg svg-data=GID_0,COUNTRY,GID,NAME id-field=GID
|
generate_map "./mapdata/aeqd01.geojson" "aeqd +lat_0=90" "${input_files[@]}"
|
||||||
|
|
||||||
|
# The app reads the packed form; the GeoJSON above is only an intermediate.
|
||||||
|
node packmap.js ./mapdata/loxim01.geojson ./mapdata/webmercator01.geojson ./mapdata/aeqd01.geojson
|
||||||
}
|
}
|
||||||
|
|
||||||
do_1() {
|
do_1() {
|
||||||
@@ -153,10 +158,10 @@ do_0() {
|
|||||||
do
|
do
|
||||||
download_0 "$country"
|
download_0 "$country"
|
||||||
done
|
done
|
||||||
wget -q -O "./temp/1/ATA.json" "$ATA_URL"
|
wget -q -O "./temp/0/ATA.json" "$ATA_URL"
|
||||||
}
|
}
|
||||||
# do_0
|
# do_0
|
||||||
# do_1
|
# do_1
|
||||||
# toSVG_0
|
build_maps
|
||||||
# toSVG_1
|
|
||||||
toSVG_01
|
#CUW, CCK, XCL, CXR, IOT, BVT, ABW, FLK, GIB, HMD, KIR, SXM, MDV, MCO, NIU, NFK, PCN, MAF, SGS, VAT
|
||||||
+4
-19
@@ -1,24 +1,9 @@
|
|||||||
# Project-wide Gradle settings.
|
# Project-wide Gradle settings.
|
||||||
# IDE (e.g. Android Studio) users:
|
|
||||||
# Gradle settings configured through the IDE *will override*
|
|
||||||
# any settings specified in this file.
|
|
||||||
# For more details on how to configure your build environment visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
# When configured, Gradle will run in incubating parallel mode.
|
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
|
||||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
|
||||||
# org.gradle.parallel=true
|
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
|
||||||
# Android operating system, and which are packaged with your app's APK
|
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=false
|
android.enableJetifier=false
|
||||||
# Kotlin code style for this project: "official" or "obsolete":
|
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
android.nonTransitiveRClass=true
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
android.uniquePackageNames=false
|
||||||
# thereby reducing the size of the R class for that library
|
android.dependency.useConstraints=false
|
||||||
android.nonTransitiveRClass=true
|
android.r8.strictFullModeForKeepRules=false
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
|
retries=0
|
||||||
|
retryBackOffMs=500
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
#
|
#
|
||||||
# Copyright © 2015-2021 the original authors.
|
# Copyright © 2015 the original authors.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
@@ -15,10 +15,12 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
# gradlew start up script for POSIX generated by Gradle.
|
||||||
#
|
#
|
||||||
# Important for running:
|
# Important for running:
|
||||||
#
|
#
|
||||||
@@ -27,7 +29,7 @@
|
|||||||
# bash, then to run this script, type that shell name before the whole
|
# bash, then to run this script, type that shell name before the whole
|
||||||
# command line, like:
|
# command line, like:
|
||||||
#
|
#
|
||||||
# ksh Gradle
|
# ksh gradlew
|
||||||
#
|
#
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
# requires all of these POSIX shell features:
|
# requires all of these POSIX shell features:
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -84,7 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -112,7 +114,6 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -170,7 +171,6 @@ fi
|
|||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if "$cygwin" || "$msys" ; then
|
if "$cygwin" || "$msys" ; then
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
@@ -203,15 +203,14 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
Vendored
+14
-24
@@ -13,16 +13,18 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@rem Gradle startup script for Windows
|
@rem gradlew startup script for Windows
|
||||||
@rem
|
@rem
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
setlocal EnableExtensions
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
@@ -49,7 +51,7 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
:findJavaFromJavaHome
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
@@ -63,30 +65,18 @@ echo. 1>&2
|
|||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation. 1>&2
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
"%COMSPEC%" /c exit 1
|
||||||
|
|
||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute gradlew
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||||
|
@rem which allows us to clear the local environment before executing the java command
|
||||||
|
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||||
|
|
||||||
:end
|
:exitWithErrorLevel
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
|
|||||||
-325
@@ -1,325 +0,0 @@
|
|||||||
var continents = {
|
|
||||||
XAD:"EEE",
|
|
||||||
ALA:"EEE",
|
|
||||||
ALB:"EEE",
|
|
||||||
AND:"EEE",
|
|
||||||
AUT:"EEE",
|
|
||||||
BLR:"EEE",
|
|
||||||
BEL:"EEE",
|
|
||||||
BIH:"EEE",
|
|
||||||
BGR:"EEE",
|
|
||||||
HRV:"EEE",
|
|
||||||
CYP:"EEE",
|
|
||||||
CZE:"EEE",
|
|
||||||
DNK:"EEE",
|
|
||||||
EST:"EEE",
|
|
||||||
FRO:"EEE",
|
|
||||||
FIN:"EEE",
|
|
||||||
FRA:"EEE",
|
|
||||||
DEU:"EEE",
|
|
||||||
GIB:"EEE",
|
|
||||||
GRC:"EEE",
|
|
||||||
GGY:"EEE",
|
|
||||||
HUN:"EEE",
|
|
||||||
ISL:"EEE",
|
|
||||||
IRL:"EEE",
|
|
||||||
IMN:"EEE",
|
|
||||||
ITA:"EEE",
|
|
||||||
JEY:"EEE",
|
|
||||||
KAZ:"EEE",
|
|
||||||
XKO:"EEE",
|
|
||||||
LVA:"EEE",
|
|
||||||
LIE:"EEE",
|
|
||||||
LTU:"EEE",
|
|
||||||
LUX:"EEE",
|
|
||||||
MLT:"EEE",
|
|
||||||
MDA:"EEE",
|
|
||||||
MCO:"EEE",
|
|
||||||
MNE:"EEE",
|
|
||||||
NLD:"EEE",
|
|
||||||
MKD:"EEE",
|
|
||||||
NOR:"EEE",
|
|
||||||
POL:"EEE",
|
|
||||||
PRT:"EEE",
|
|
||||||
ROU:"EEE",
|
|
||||||
RUS:"EEE",
|
|
||||||
SMR:"EEE",
|
|
||||||
SRB:"EEE",
|
|
||||||
SVK:"EEE",
|
|
||||||
SVN:"EEE",
|
|
||||||
ESP:"EEE",
|
|
||||||
SJM:"EEE",
|
|
||||||
SWE:"EEE",
|
|
||||||
CHE:"EEE",
|
|
||||||
UKR:"EEE",
|
|
||||||
GBR:"EEE",
|
|
||||||
VAT:"EEE",
|
|
||||||
|
|
||||||
AFG:"ABB",
|
|
||||||
ARM:"ABB",
|
|
||||||
AZE:"ABB",
|
|
||||||
BHR:"ABB",
|
|
||||||
BGD:"ABB",
|
|
||||||
BTN:"ABB",
|
|
||||||
IOT:"ABB",
|
|
||||||
BRN:"ABB",
|
|
||||||
KHM:"ABB",
|
|
||||||
CCK:"ABB",
|
|
||||||
CHN:"ABB",
|
|
||||||
CXR:"ABB",
|
|
||||||
GEO:"ABB",
|
|
||||||
HKG:"ABB",
|
|
||||||
IND:"ABB",
|
|
||||||
IDN:"ABB",
|
|
||||||
IRN:"ABB",
|
|
||||||
IRQ:"ABB",
|
|
||||||
ISR:"ABB",
|
|
||||||
JPN:"ABB",
|
|
||||||
JOR:"ABB",
|
|
||||||
KWT:"ABB",
|
|
||||||
KGZ:"ABB",
|
|
||||||
LAO:"ABB",
|
|
||||||
LBN:"ABB",
|
|
||||||
MAC:"ABB",
|
|
||||||
MYS:"ABB",
|
|
||||||
MDV:"ABB",
|
|
||||||
MNG:"ABB",
|
|
||||||
MMR:"ABB",
|
|
||||||
NPL:"ABB",
|
|
||||||
PRK:"ABB",
|
|
||||||
OMN:"ABB",
|
|
||||||
PAK:"ABB",
|
|
||||||
PSE:"ABB",
|
|
||||||
PHL:"ABB",
|
|
||||||
QAT:"ABB",
|
|
||||||
SAU:"ABB",
|
|
||||||
SGP:"ABB",
|
|
||||||
KOR:"ABB",
|
|
||||||
LKA:"ABB",
|
|
||||||
SYR:"ABB",
|
|
||||||
TWN:"ABB",
|
|
||||||
TJK:"ABB",
|
|
||||||
THA:"ABB",
|
|
||||||
TLS:"ABB",
|
|
||||||
TUR:"ABB",
|
|
||||||
TKM:"ABB",
|
|
||||||
ARE:"ABB",
|
|
||||||
UZB:"ABB",
|
|
||||||
VNM:"ABB",
|
|
||||||
YEM:"ABB",
|
|
||||||
ZNC:"ABB",
|
|
||||||
|
|
||||||
DZA:"FFF",
|
|
||||||
AGO:"FFF",
|
|
||||||
BDI:"FFF",
|
|
||||||
BEN:"FFF",
|
|
||||||
BWA:"FFF",
|
|
||||||
BFA:"FFF",
|
|
||||||
BDI:"FFF",
|
|
||||||
CPV:"FFF",
|
|
||||||
CMR:"FFF",
|
|
||||||
CAF:"FFF",
|
|
||||||
TCD:"FFF",
|
|
||||||
COM:"FFF",
|
|
||||||
COG:"FFF",
|
|
||||||
COD:"FFF",
|
|
||||||
CIV:"FFF",
|
|
||||||
DJI:"FFF",
|
|
||||||
EGY:"FFF",
|
|
||||||
GNQ:"FFF",
|
|
||||||
ERI:"FFF",
|
|
||||||
ETH:"FFF",
|
|
||||||
ATF:"FFF",
|
|
||||||
GAB:"FFF",
|
|
||||||
GMB:"FFF",
|
|
||||||
GHA:"FFF",
|
|
||||||
GIN:"FFF",
|
|
||||||
GNB:"FFF",
|
|
||||||
KEN:"FFF",
|
|
||||||
LSO:"FFF",
|
|
||||||
LBR:"FFF",
|
|
||||||
LBY:"FFF",
|
|
||||||
MDG:"FFF",
|
|
||||||
MWI:"FFF",
|
|
||||||
MLI:"FFF",
|
|
||||||
MRT:"FFF",
|
|
||||||
MUS:"FFF",
|
|
||||||
MYT:"FFF",
|
|
||||||
MAR:"FFF",
|
|
||||||
MOZ:"FFF",
|
|
||||||
NAM:"FFF",
|
|
||||||
NER:"FFF",
|
|
||||||
NGA:"FFF",
|
|
||||||
COD:"FFF",
|
|
||||||
REU:"FFF",
|
|
||||||
RWA:"FFF",
|
|
||||||
STP:"FFF",
|
|
||||||
SEN:"FFF",
|
|
||||||
SYC:"FFF",
|
|
||||||
SLE:"FFF",
|
|
||||||
SOM:"FFF",
|
|
||||||
ZAF:"FFF",
|
|
||||||
SSD:"FFF",
|
|
||||||
SHN:"FFF",
|
|
||||||
SDN:"FFF",
|
|
||||||
SWZ:"FFF",
|
|
||||||
TZA:"FFF",
|
|
||||||
TGO:"FFF",
|
|
||||||
TUN:"FFF",
|
|
||||||
UGA:"FFF",
|
|
||||||
COD:"FFF",
|
|
||||||
ZMB:"FFF",
|
|
||||||
ZWE:"FFF",
|
|
||||||
ESH:"FFF",
|
|
||||||
|
|
||||||
ABW:"NNN",
|
|
||||||
AIA:"NNN",
|
|
||||||
ATG:"NNN",
|
|
||||||
BHS:"NNN",
|
|
||||||
BRB:"NNN",
|
|
||||||
BLZ:"NNN",
|
|
||||||
BMU:"NNN",
|
|
||||||
BES:"NNN",
|
|
||||||
VGB:"NNN",
|
|
||||||
CAN:"NNN",
|
|
||||||
CYM:"NNN",
|
|
||||||
XCL:"NNN",
|
|
||||||
CRI:"NNN",
|
|
||||||
CUB:"NNN",
|
|
||||||
CUW:"NNN",
|
|
||||||
DMA:"NNN",
|
|
||||||
DOM:"NNN",
|
|
||||||
SLV:"NNN",
|
|
||||||
GRL:"NNN",
|
|
||||||
GRD:"NNN",
|
|
||||||
GLP:"NNN",
|
|
||||||
GTM:"NNN",
|
|
||||||
HTI:"NNN",
|
|
||||||
HND:"NNN",
|
|
||||||
JAM:"NNN",
|
|
||||||
MTQ:"NNN",
|
|
||||||
MEX:"NNN",
|
|
||||||
MSR:"NNN",
|
|
||||||
ANT:"NNN",
|
|
||||||
CUW:"NNN",
|
|
||||||
NIC:"NNN",
|
|
||||||
PAN:"NNN",
|
|
||||||
PRI:"NNN",
|
|
||||||
BLM:"NNN",
|
|
||||||
KNA:"NNN",
|
|
||||||
LCA:"NNN",
|
|
||||||
MAF:"NNN",
|
|
||||||
SPM:"NNN",
|
|
||||||
VCT:"NNN",
|
|
||||||
SXM:"NNN",
|
|
||||||
TTO:"NNN",
|
|
||||||
TCA:"NNN",
|
|
||||||
USA:"NNN",
|
|
||||||
UMI:"NNN",
|
|
||||||
VIR:"NNN",
|
|
||||||
|
|
||||||
ARG:"SRR",
|
|
||||||
BOL:"SRR",
|
|
||||||
BRA:"SRR",
|
|
||||||
CHL:"SRR",
|
|
||||||
COL:"SRR",
|
|
||||||
ECU:"SRR",
|
|
||||||
FLK:"SRR",
|
|
||||||
GUF:"SRR",
|
|
||||||
GUY:"SRR",
|
|
||||||
PRY:"SRR",
|
|
||||||
PER:"SRR",
|
|
||||||
SUR:"SRR",
|
|
||||||
URY:"SRR",
|
|
||||||
VEN:"SRR",
|
|
||||||
|
|
||||||
ASM:"UUU",
|
|
||||||
AUS:"UUU",
|
|
||||||
COK:"UUU",
|
|
||||||
FJI:"UUU",
|
|
||||||
PYF:"UUU",
|
|
||||||
GUM:"UUU",
|
|
||||||
KIR:"UUU",
|
|
||||||
MHL:"UUU",
|
|
||||||
FSM:"UUU",
|
|
||||||
NRU:"UUU",
|
|
||||||
NCL:"UUU",
|
|
||||||
NZL:"UUU",
|
|
||||||
NIU:"UUU",
|
|
||||||
NFK:"UUU",
|
|
||||||
MNP:"UUU",
|
|
||||||
PLW:"UUU",
|
|
||||||
PNG:"UUU",
|
|
||||||
PCN:"UUU",
|
|
||||||
WSM:"UUU",
|
|
||||||
SLB:"UUU",
|
|
||||||
TKL:"UUU",
|
|
||||||
TON:"UUU",
|
|
||||||
TUV:"UUU",
|
|
||||||
VUT:"UUU",
|
|
||||||
WLF:"UUU",
|
|
||||||
|
|
||||||
ATA:"XXX",
|
|
||||||
BVT:"XXX",
|
|
||||||
HMD:"XXX",
|
|
||||||
SGS:"XXX",
|
|
||||||
}
|
|
||||||
|
|
||||||
import fs from "fs"
|
|
||||||
import {JSDOM} from "jsdom"
|
|
||||||
|
|
||||||
function groupByContinent(svgFilePath) {
|
|
||||||
// Read the SVG file
|
|
||||||
fs.readFile(svgFilePath, 'utf-8', (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error reading SVG file:', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a virtual DOM with JSDOM
|
|
||||||
const dom = new JSDOM(data);
|
|
||||||
const document = dom.window.document;
|
|
||||||
|
|
||||||
// Select the root <svg> element
|
|
||||||
const svgElement = document.querySelector('svg');
|
|
||||||
|
|
||||||
// Group <g> elements representing countries by continent
|
|
||||||
const world = {}; // Renamed continents object to 'world'
|
|
||||||
svgElement.querySelectorAll('g').forEach(countryGroup => {
|
|
||||||
const countryId = countryGroup.getAttribute('id');
|
|
||||||
if (countryId!= null){
|
|
||||||
const continentId = continents[countryId.replace(/\d/g, "")] || "XXX"; // Remove numbers from country ID to get continent ID
|
|
||||||
if (!world[continentId]) {
|
|
||||||
world[continentId] = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
||||||
world[continentId].setAttribute('id', continentId);
|
|
||||||
}
|
|
||||||
world[continentId].innerHTML += countryGroup.outerHTML;
|
|
||||||
}else{
|
|
||||||
|
|
||||||
console.log(countryId,countryGroup.outerHTML)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create new <g> elements for each continent
|
|
||||||
svgElement.innerHTML = '';
|
|
||||||
for (const continentId in world) {
|
|
||||||
svgElement.appendChild(world[continentId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output the modified SVG
|
|
||||||
fs.writeFile(svgFilePath, svgElement.outerHTML, 'utf-8', err => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error writing to SVG file:', err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('SVG file updated successfully.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Example usage:
|
|
||||||
const svgFileBasePath = "./app/src/main/assets/" ;
|
|
||||||
groupByContinent(svgFileBasePath+ "webmercator01.svg");
|
|
||||||
groupByContinent(svgFileBasePath+ "aeqd01.svg");
|
|
||||||
groupByContinent(svgFileBasePath+ "loxim01.svg");
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user