Compare commits
631 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e8f20b26 | ||
|
|
4531a6b05f | ||
|
|
2ef70d0da0 | ||
|
|
4fe769cbbf | ||
|
|
de068a760e | ||
|
|
a183ff803d | ||
|
|
e84c381423 | ||
|
|
85a7eeeeea | ||
|
|
0d9fb97bbb | ||
|
|
9ed3631c30 | ||
|
|
0a68df6492 | ||
|
|
f3f47d9407 | ||
|
|
b6ff956637 | ||
|
|
aa99c18a1b | ||
|
|
1be5ecac6b | ||
|
|
c8eff885b5 | ||
|
|
e0eb187442 | ||
|
|
0fbce15e41 | ||
|
|
f11dce968e | ||
|
|
df341c777e | ||
|
|
bed6d7d6ab | ||
|
|
97eb85e97c | ||
|
|
4448f61430 | ||
|
|
9beb4ded2e | ||
|
|
fc3a0fa178 | ||
|
|
03e71b3000 | ||
|
|
8503c64f04 | ||
|
|
0dd25faced | ||
|
|
00cf429bd9 | ||
|
|
628d42703f | ||
|
|
f271e61ea2 | ||
|
|
2e11a4907a | ||
|
|
0e19d6c9b2 | ||
|
|
850ac3ea83 | ||
|
|
c3c5761ffa | ||
|
|
0ff91d76b1 | ||
|
|
cd4be5898b | ||
|
|
d80de6fde7 | ||
|
|
193db578f0 | ||
|
|
3abd015505 | ||
|
|
84c536a597 | ||
|
|
480bbd3628 | ||
|
|
b708437a16 | ||
|
|
fcbe107fe7 | ||
|
|
bf3e7d7117 | ||
|
|
f78c66a9f4 | ||
|
|
7e52a2e296 | ||
|
|
4625bb5806 | ||
|
|
d626cc09d5 | ||
|
|
57c4e249cf | ||
|
|
016ce3ff42 | ||
|
|
383baa900c | ||
|
|
551b4cae80 | ||
|
|
0c13ad6869 | ||
|
|
8b2e388a81 | ||
|
|
c34b0f6f0f | ||
|
|
f6f0ed40c1 | ||
|
|
b82ef8695c | ||
|
|
0f4e1a8e0d | ||
|
|
20ddf04614 | ||
|
|
7befa24aff | ||
|
|
93d68d3867 | ||
|
|
9037b41b49 | ||
|
|
02751233f8 | ||
|
|
a57b1d5614 | ||
|
|
adf18341d0 | ||
|
|
bdd2bc8645 | ||
|
|
338b789e62 | ||
|
|
98fda1a53f | ||
|
|
e7debfec46 | ||
|
|
62d0de3ef6 | ||
|
|
ef0f71310b | ||
|
|
052990c4ef | ||
|
|
077d9b976c | ||
|
|
78ba11ca5f | ||
|
|
b690d01243 | ||
|
|
458530e80c | ||
|
|
ddbfd0a201 | ||
|
|
6c13a624a9 | ||
|
|
70452ba7a6 | ||
|
|
14c64299ec | ||
|
|
c2626cdee4 | ||
|
|
52a945d0d9 | ||
|
|
29aefa4197 | ||
|
|
cfe6a814d4 | ||
|
|
9ef7852bab | ||
|
|
0a1e0a2dcf | ||
|
|
5b9a83cbcc | ||
|
|
fc61522955 | ||
|
|
35ee438376 | ||
|
|
8cc89101e7 | ||
|
|
2150d086e0 | ||
|
|
a9a07ddcfa | ||
|
|
32d49833d8 | ||
|
|
2a92d287af | ||
|
|
975b98e4dc | ||
|
|
237d5accc5 | ||
|
|
760194bde8 | ||
|
|
ff0df0d9cc | ||
|
|
dd60a1fdfb | ||
|
|
5a19fb8336 | ||
|
|
51851addc1 | ||
|
|
00c8078642 | ||
|
|
ca54fb6eb0 | ||
|
|
1107cf1a9c | ||
|
|
1dea88a135 | ||
|
|
6fae9e9a30 | ||
|
|
a1c6d87c54 | ||
|
|
80b7293879 | ||
|
|
2f57ee4c83 | ||
|
|
3f8aa744e7 | ||
|
|
fb11149b78 | ||
|
|
8b41c706b6 | ||
|
|
5a61fcf6ee | ||
|
|
c7b3ae7ed1 | ||
|
|
4aea7d08ce | ||
|
|
2f16838e1e | ||
|
|
619730e2ab | ||
|
|
c8aa26e2d9 | ||
|
|
8703fde9b1 | ||
|
|
3051d800bd | ||
|
|
521f3ad809 | ||
|
|
730a3baedc | ||
|
|
26c5e07f04 | ||
|
|
3feae80359 | ||
|
|
24aedfc400 | ||
|
|
aa6cc80172 | ||
|
|
74ed9e9e42 | ||
|
|
2b7b86da96 | ||
|
|
9f9a4c81b3 | ||
|
|
d567b30f4b | ||
|
|
6d7c4ce0ab | ||
|
|
e062b8f9e9 | ||
|
|
08403b7a4e | ||
|
|
c6ed5d35e7 | ||
|
|
dba3460b60 | ||
|
|
f07f624fcf | ||
|
|
48ff2f328f | ||
|
|
9ae2423a40 | ||
|
|
2bc3c78c75 | ||
|
|
18e9fe75fb | ||
|
|
880a741a44 | ||
|
|
2c6ddcc64b | ||
|
|
8f2e757b77 | ||
|
|
ff177955b3 | ||
|
|
8bb8066a98 | ||
|
|
2747ddbf65 | ||
|
|
b939e9424d | ||
|
|
fb9dea5d1e | ||
|
|
da4d5d711b | ||
|
|
331cbec5f1 | ||
|
|
7f02284285 | ||
|
|
ac2c3a6d97 | ||
|
|
c3bc80fec6 | ||
|
|
09779a0710 | ||
|
|
e82c6ef866 | ||
|
|
861ae9be64 | ||
|
|
96108bc1ec | ||
|
|
016f217db0 | ||
|
|
0688294f18 | ||
|
|
9ad008255d | ||
|
|
4c5a862dd6 | ||
|
|
b165a2308f | ||
|
|
8757b08cd2 | ||
|
|
3800543fba | ||
|
|
02ef60c818 | ||
|
|
88f3b30266 | ||
|
|
9203dc0112 | ||
|
|
4c683bec68 | ||
|
|
0cfd1eb453 | ||
|
|
19744dab37 | ||
|
|
12d58e5aa7 | ||
|
|
e46dd37a26 | ||
|
|
49c3ebc36b | ||
|
|
11e9bc2235 | ||
|
|
3029b3bf0e | ||
|
|
9a6c6f67ce | ||
|
|
a6ed0baef2 | ||
|
|
d3b43d80da | ||
|
|
46d4316d49 | ||
|
|
ade2864351 | ||
|
|
365fc56e9d | ||
|
|
54a5cd21ea | ||
|
|
38c0399b09 | ||
|
|
2b67858453 | ||
|
|
87fdbdbb6e | ||
|
|
bab77a4116 | ||
|
|
d20756ab96 | ||
|
|
dc75a753c3 | ||
|
|
4712d47903 | ||
|
|
c5561801e1 | ||
|
|
5c259fa07a | ||
|
|
60e8b18702 | ||
|
|
a8317824a9 | ||
|
|
0c3c78cc72 | ||
|
|
cfd4a8faac | ||
|
|
7f3fb0db0d | ||
|
|
385d3f0d1b | ||
|
|
8fa6bd12a2 | ||
|
|
57c2004e46 | ||
|
|
c6b069bbfb | ||
|
|
c18bffd08f | ||
|
|
47ec181439 | ||
|
|
90ad40b1b7 | ||
|
|
4d3f20cf98 | ||
|
|
86df9d52bc | ||
|
|
1bd025e070 | ||
|
|
86ee239c71 | ||
|
|
27d0c01e1f | ||
|
|
7a9507be01 | ||
|
|
1490035893 | ||
|
|
a6afcb0ed0 | ||
|
|
ea7e8584cb | ||
|
|
608c6e6a1d | ||
|
|
bb2c91145f | ||
|
|
db074df0f7 | ||
|
|
f7c45df9a6 | ||
|
|
44e3d16cd6 | ||
|
|
a973cdfe0b | ||
|
|
fca42c79a8 | ||
|
|
f236775599 | ||
|
|
360decd37c | ||
|
|
998433479b | ||
|
|
c7e75aacf0 | ||
|
|
690338273a | ||
|
|
4207ea494d | ||
|
|
265473a15a | ||
|
|
b907d36770 | ||
|
|
fee280341a | ||
|
|
0f1ef70752 | ||
|
|
0f8c68b22e | ||
|
|
701017d2ca | ||
|
|
be6903ca12 | ||
|
|
1521bc1223 | ||
|
|
7ed66b827f | ||
|
|
df3a478ef3 | ||
|
|
974ddf69d5 | ||
|
|
56a91268de | ||
|
|
3dda2f9a1c | ||
|
|
ed20456f9f | ||
|
|
281d4a0023 | ||
|
|
2170403662 | ||
|
|
b1c1e96135 | ||
|
|
a8de1429c1 | ||
|
|
3ba6cb81ae | ||
|
|
acc85da80f | ||
|
|
b53de8624d | ||
|
|
6e2eeb29cc | ||
|
|
62eb28ac01 | ||
|
|
fd298529bf | ||
|
|
297ce506b1 | ||
|
|
18c6954be3 | ||
|
|
cea3fb1e65 | ||
|
|
7f274fd238 | ||
|
|
439a8e93ec | ||
|
|
83801feee9 | ||
|
|
8a6860c96e | ||
|
|
5c959f2987 | ||
|
|
4e4397287a | ||
|
|
fe02abc9e8 | ||
|
|
59347ab317 | ||
|
|
f408a91176 | ||
|
|
6f6956ce27 | ||
|
|
4ecad8eccc | ||
|
|
486fbe46a0 | ||
|
|
1ddb636dd0 | ||
|
|
081c890b4e | ||
|
|
86d528ba13 | ||
|
|
6bda3cb75a | ||
|
|
12d8949c9e | ||
|
|
ffc7c2aa67 | ||
|
|
5ec67488eb | ||
|
|
be64703d3c | ||
|
|
705925a050 | ||
|
|
29665be34d | ||
|
|
1edf986acf | ||
|
|
37be8ccf7f | ||
|
|
ead68b5201 | ||
|
|
4409664698 | ||
|
|
fc6bc7965c | ||
|
|
f70eccb1da | ||
|
|
861994e804 | ||
|
|
2b8facfb97 | ||
|
|
9583897ada | ||
|
|
7704c96955 | ||
|
|
c96d609803 | ||
|
|
aa0e5000ab | ||
|
|
7ca4418a50 | ||
|
|
fdd9b02388 | ||
|
|
ece127e982 | ||
|
|
5488e14f32 | ||
|
|
3558d826fb | ||
|
|
68c94d1d8b | ||
|
|
1a4ae5dfc6 | ||
|
|
1a95afe266 | ||
|
|
6579db3cc8 | ||
|
|
ceac01533a | ||
|
|
216914882c | ||
|
|
735dbab695 | ||
|
|
dbaab152ef | ||
|
|
9da1b30984 | ||
|
|
9415ab4ef9 | ||
|
|
647294daf2 | ||
|
|
6ebc386474 | ||
|
|
3e657bdc09 | ||
|
|
0b0adb76a1 | ||
|
|
17b3e010aa | ||
|
|
20003acd73 | ||
|
|
2ab7672092 | ||
|
|
c317abe64b | ||
|
|
bc33ce1ebc | ||
|
|
684c5cf38b | ||
|
|
c34e15f0a1 | ||
|
|
bad004f892 | ||
|
|
828d3de020 | ||
|
|
132b3b9be1 | ||
|
|
388bc6fda5 | ||
|
|
a93edc184d | ||
|
|
08672d10ac | ||
|
|
b563dae3a8 | ||
|
|
917f9672dd | ||
|
|
9ddb19530b | ||
|
|
431e56a9f1 | ||
|
|
71093aac4c | ||
|
|
47c9e8127e | ||
|
|
24b801b346 | ||
|
|
70608c3ed9 | ||
|
|
f185196e85 | ||
|
|
a8766a8bbe | ||
|
|
27a8c93cfe | ||
|
|
a3cd29fda9 | ||
|
|
adda8ab640 | ||
|
|
1538ea5fc8 | ||
|
|
2367a97a54 | ||
|
|
090ec0e4af | ||
|
|
de7f552e5c | ||
|
|
d763f5dca0 | ||
|
|
9f41116241 | ||
|
|
57faada201 | ||
|
|
1edb95f0c5 | ||
|
|
9f363d8900 | ||
|
|
0bf2f1b6e1 | ||
|
|
68c7a38390 | ||
|
|
841c8a7a15 | ||
|
|
6c9688183b | ||
|
|
ccd84c91f6 | ||
|
|
318d6f9b52 | ||
|
|
8f5d612ee0 | ||
|
|
56b2a05596 | ||
|
|
4db0022d6a | ||
|
|
67f37d3188 | ||
|
|
ed81cc7207 | ||
|
|
065845f1be | ||
|
|
902f705e89 | ||
|
|
ec2e0ef773 | ||
|
|
d28c5741d0 | ||
|
|
e6e3f9e8f8 | ||
|
|
90e1dc59bd | ||
|
|
0b1c9b097c | ||
|
|
2b553d1116 | ||
|
|
567eec8bc5 | ||
|
|
293ca5b31d | ||
|
|
0d0f2bd827 | ||
|
|
5bc4610061 | ||
|
|
e6b7c107f2 | ||
|
|
51a9bf2570 | ||
|
|
8385f6f390 | ||
|
|
772e9daf57 | ||
|
|
8adc4405c5 | ||
|
|
349da7aa81 | ||
|
|
01a01d481d | ||
|
|
2f8445fb83 | ||
|
|
b04a5fc150 | ||
|
|
bbe29941df | ||
|
|
2720e445ea | ||
|
|
49ba579a59 | ||
|
|
3198c6cbfd | ||
|
|
b3feee6d9d | ||
|
|
f0f53e6bce | ||
|
|
24486d13f2 | ||
|
|
20bc9461de | ||
|
|
c8e94cc295 | ||
|
|
b2bfb0c237 | ||
|
|
0a003da724 | ||
|
|
b4f2a33016 | ||
|
|
ee7ede2885 | ||
|
|
6abc404eb7 | ||
|
|
61afe01e36 | ||
|
|
c3e60f9988 | ||
|
|
593197cd7e | ||
|
|
ee1592b478 | ||
|
|
dfe435c4f3 | ||
|
|
69e85f8b90 | ||
|
|
c9bde3c487 | ||
|
|
65e9557d9f | ||
|
|
4f249c07e7 | ||
|
|
5fd35b492c | ||
|
|
9bddf95013 | ||
|
|
03444f070f | ||
|
|
2f1a63eb64 | ||
|
|
9d0898b26c | ||
|
|
994aa99797 | ||
|
|
8204a15276 | ||
|
|
4a8bff0b98 | ||
|
|
a4336cd954 | ||
|
|
4f0dbead79 | ||
|
|
c0e7c87ca4 | ||
|
|
b967bf9a26 | ||
|
|
764a265053 | ||
|
|
68c2b2dbfa | ||
|
|
061f1263f4 | ||
|
|
2a27355479 | ||
|
|
ae2a8e8ada | ||
|
|
68dcc2333b | ||
|
|
66fb2e9a62 | ||
|
|
1dbfc64f37 | ||
|
|
98d1f88579 | ||
|
|
bb6fadc182 | ||
|
|
ac1ca71299 | ||
|
|
0d93785581 | ||
|
|
69a9d63e1d | ||
|
|
5dea35343b | ||
|
|
5c768d2121 | ||
|
|
4d5834821a | ||
|
|
ca077c4fee | ||
|
|
85d01f60f1 | ||
|
|
066d73b217 | ||
|
|
ba069d8f8e | ||
|
|
275684c9ce | ||
|
|
49d87a08d2 | ||
|
|
04c500f3d8 | ||
|
|
d05c1e4d08 | ||
|
|
bb63959678 | ||
|
|
842148647f | ||
|
|
19308d840a | ||
|
|
46bd1318cd | ||
|
|
9d1998fe52 | ||
|
|
a714a8230b | ||
|
|
b5432cd0b4 | ||
|
|
5634e94f3e | ||
|
|
c1a71b0db3 | ||
|
|
d93e7f8834 | ||
|
|
3175b2c45c | ||
|
|
547b6e8e3b | ||
|
|
d88ac27e72 | ||
|
|
e551a40d08 | ||
|
|
e810abe33a | ||
|
|
6172a73719 | ||
|
|
7455e68a45 | ||
|
|
748495ca64 | ||
|
|
f6d9c7f550 | ||
|
|
384e6c61b0 | ||
|
|
d49c9cec20 | ||
|
|
4b27f1aba1 | ||
|
|
a0a989c785 | ||
|
|
ecaecc1b91 | ||
|
|
938156aa71 | ||
|
|
d30c51bb3a | ||
|
|
874606bff9 | ||
|
|
07643e4b4c | ||
|
|
20bc5423cf | ||
|
|
b84cddffdc | ||
|
|
e46d1123df | ||
|
|
48f90faf4e | ||
|
|
615b52c4fa | ||
|
|
2c9c8e223c | ||
|
|
01a653835e | ||
|
|
9d80857a38 | ||
|
|
8a9ab6b36c | ||
|
|
4edc87c197 | ||
|
|
10712e6e62 | ||
|
|
d73dc19d3d | ||
|
|
c204353220 | ||
|
|
37123a2cd5 | ||
|
|
a39484b6ea | ||
|
|
e81b5a4e3a | ||
|
|
0b87c57fbf | ||
|
|
5fd985ba39 | ||
|
|
8c64548513 | ||
|
|
a6de64ceb9 | ||
|
|
16ebb437a3 | ||
|
|
683118a3f4 | ||
|
|
08e38ed45c | ||
|
|
7abf08f1fb | ||
|
|
f3019e9b84 | ||
|
|
9ea55664b6 | ||
|
|
c468764234 | ||
|
|
31c3178430 | ||
|
|
e81c189afc | ||
|
|
e0ccac13c1 | ||
|
|
93228459d7 | ||
|
|
63e07f56e0 | ||
|
|
ee87122bb2 | ||
|
|
290dda9018 | ||
|
|
1d3d78b936 | ||
|
|
a947bc6415 | ||
|
|
9ca891b2f5 | ||
|
|
48e0ebc8ae | ||
|
|
b323353006 | ||
|
|
c85d3ebe81 | ||
|
|
ce843abec8 | ||
|
|
6b43faa70e | ||
|
|
2d0c997b2e | ||
|
|
1db5118377 | ||
|
|
26b53ed7ac | ||
|
|
2c85ea6443 | ||
|
|
cbc2b30f47 | ||
|
|
0b58deb92c | ||
|
|
ed1cf23c91 | ||
|
|
6fbb644e4b | ||
|
|
774867502d | ||
|
|
c8b1439aeb | ||
|
|
38c16adffe | ||
|
|
18aede2701 | ||
|
|
c59d08a0a1 | ||
|
|
66ae29eb5b | ||
|
|
7d9cb3e150 | ||
|
|
9922a9f82a | ||
|
|
445b9b4673 | ||
|
|
0ef7b358e0 | ||
|
|
2d3fb75576 | ||
|
|
d55ff6d68e | ||
|
|
079654a9c7 | ||
|
|
30263c6260 | ||
|
|
3159c343c1 | ||
|
|
ceaa930623 | ||
|
|
6a8539106b | ||
|
|
7a24c3c08e | ||
|
|
251abeb090 | ||
|
|
a61fe9f98c | ||
|
|
d29c7bf91a | ||
|
|
ed4911c441 | ||
|
|
d40b4f3748 | ||
|
|
f3c4fe1914 | ||
|
|
55ee841bd0 | ||
|
|
657fb488ee | ||
|
|
4eef0b93fb | ||
|
|
f2be56435c | ||
|
|
fa6b3ad7ba | ||
|
|
52c05e6888 | ||
|
|
865bf0ba83 | ||
|
|
3f827d1bad | ||
|
|
0561d5f55c | ||
|
|
1bf2e1dacc | ||
|
|
db5a221b56 | ||
|
|
295285f132 | ||
|
|
5052b6c074 | ||
|
|
f98f45dc54 | ||
|
|
8d16950f46 | ||
|
|
74033b9f4a | ||
|
|
e497d47374 | ||
|
|
a97af59260 | ||
|
|
2197de98ea | ||
|
|
c004c7f71a | ||
|
|
69fc3ad4e8 | ||
|
|
678a8f0914 | ||
|
|
08c4c0bf1f | ||
|
|
f2a2656837 | ||
|
|
2011572270 | ||
|
|
3b682667e1 | ||
|
|
6da8de6463 | ||
|
|
039d415871 | ||
|
|
776f53bde0 | ||
|
|
58e535595e | ||
|
|
96ad5f6a6c | ||
|
|
043f7bedd8 | ||
|
|
69bcd8f7c0 | ||
|
|
8a58564812 | ||
|
|
d346cf431f | ||
|
|
c0bce4f3b1 | ||
|
|
94d258ddbb | ||
|
|
6bdba49284 | ||
|
|
9b99baf4bc | ||
|
|
5ad2a538bc | ||
|
|
28703e9bf2 | ||
|
|
e664efefe9 | ||
|
|
27a8694938 | ||
|
|
e0a6102d4d | ||
|
|
7106cf04ed | ||
|
|
2afdc5591a | ||
|
|
8eed4b67c3 | ||
|
|
edacef0f2b | ||
|
|
d28894f8cd | ||
|
|
ee8e921e1a | ||
|
|
480d4b1e9a | ||
|
|
a79c023220 | ||
|
|
efc50df243 | ||
|
|
905ea766b1 | ||
|
|
bce26f4557 | ||
|
|
474d3ad80a | ||
|
|
a74b2c9b49 | ||
|
|
22bdf61bb3 | ||
|
|
69f9b099b7 | ||
|
|
1d812487a6 | ||
|
|
7c2bf8fb9d | ||
|
|
dfb78bed69 | ||
|
|
fb42b48880 | ||
|
|
bb0988a188 | ||
|
|
c64b6f112b | ||
|
|
9ac7fb490e | ||
|
|
bd88a8a8d3 | ||
|
|
1eb75acb40 | ||
|
|
5ccc96aeb9 | ||
|
|
ef72d10344 | ||
|
|
573f0b40d1 | ||
|
|
48f49edb19 | ||
|
|
aa22d9fdd8 | ||
|
|
8410a2fdb3 | ||
|
|
ec98e4e9a4 | ||
|
|
dca6ba457b | ||
|
|
5b10a781a6 | ||
|
|
b103188faf | ||
|
|
29637b234c | ||
|
|
34dc238ef1 | ||
|
|
3c2675e650 | ||
|
|
7e87bb6838 | ||
|
|
3992a07340 | ||
|
|
bd4b61d7ac | ||
|
|
2046d87031 | ||
|
|
0618d8c6f8 | ||
|
|
5bfc27835b | ||
|
|
cdc545ea32 | ||
|
|
449db97a2b | ||
|
|
e01380090d | ||
|
|
6d1505241e | ||
|
|
f303e49e97 | ||
|
|
0e6b50e302 | ||
|
|
868af1e6a2 | ||
|
|
34f7b111ee | ||
|
|
df27907c57 | ||
|
|
75583b9e65 |
47
.gitignore
vendored
@@ -1,16 +1,33 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
#Github pages
|
||||
/gh-pages
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.apk
|
||||
output.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
122
.idea/codeStyles/Project.xml
generated
@@ -1,122 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="USE_CUSTOM_SETTINGS" value="true" />
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<XML>
|
||||
<option name="XML_KEEP_LINE_BREAKS" value="false" />
|
||||
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
|
||||
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
|
||||
</XML>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
@@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
4
.idea/encodings.xml
generated
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
|
||||
</project>
|
||||
20
.idea/gradle.xml
generated
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/libpupil" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,10 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/misc.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/classes" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/runConfigurations.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
28
README.md
@@ -1,2 +1,26 @@
|
||||
# Pupil
|
||||
Hitomi.la viewer for Android
|
||||

|
||||
*Pupil, Hitomi.la viewer for Android*
|
||||
|
||||

|
||||
[](https://github.com/tom5079/Pupil/releases/download/5.1.6-hotfix7/Pupil-v5.1.6-hotfix7.apk)
|
||||
[](https://discord.gg/Stj4b5v)
|
||||
|
||||
# Features
|
||||

|
||||
|
||||
# Installation
|
||||
|
||||
Go [Releases page](https://github.com/tom5079/Pupil/releases) and get latest version or
|
||||
Visit [github page](https://tom5079.github.io/Pupil/) (only available in Korean)
|
||||
or Build app yourself
|
||||
|
||||
# Manual
|
||||
|
||||
[Manual](https://tom5079.github.io/Pupil/2019/06/06/manual-kr.html) is only available in Korean. Consider using translator.
|
||||
|
||||
# Contribution
|
||||
|
||||
Any kind of contribution is appriciated. Feel free to leave PR!
|
||||
|
||||
## Tag Translation
|
||||
Head over to [tags branch](https://github.com/tom5079/Pupil/tree/tags)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'io.fabric'
|
||||
apply plugin: 'com.google.firebase.firebase-perf'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "xyz.quaver.pupil"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 29
|
||||
versionCode 20
|
||||
versionName "2.11.1"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += '-Xuse-experimental=kotlin.Experimental'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def markwonVersion = "3.0.1"
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.11.0"
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.preference:preference:1.1.0-beta01'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'com.google.firebase:firebase-core:17.0.0'
|
||||
implementation 'com.google.firebase:firebase-perf:18.0.1'
|
||||
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
|
||||
implementation 'com.github.arimorty:floatingsearchview:2.1.1'
|
||||
implementation 'com.github.clans:fab:1.6.4'
|
||||
implementation 'com.andrognito.patternlockview:patternlockview:1.0.0'
|
||||
implementation "ru.noties.markwon:core:${markwonVersion}"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
implementation project(path: ':libpupil')
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
183
app/build.gradle.kts
Normal file
@@ -0,0 +1,183 @@
|
||||
import com.google.protobuf.gradle.*
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("kapt")
|
||||
id("kotlin-parcelize")
|
||||
id("kotlinx-serialization")
|
||||
id("com.google.android.gms.oss-licenses-plugin")
|
||||
id("com.google.gms.google-services")
|
||||
id("com.google.firebase.crashlytics")
|
||||
id("com.google.firebase.firebase-perf")
|
||||
id("com.google.protobuf")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = File(System.getenv("SIGNING_STORE_FILE"))
|
||||
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "xyz.quaver.pupil"
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
versionCode = 600
|
||||
versionName = VERSION
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-DEBUG"
|
||||
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
|
||||
extra.set("enableCrashlytics", false)
|
||||
extra.set("alwaysUpdateBuildId", false)
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = false
|
||||
applicationIdSuffix = ".beta"
|
||||
|
||||
isCrunchPngs = false
|
||||
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = Versions.JETPACK_COMPOSE
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"META-INF/AL2.0",
|
||||
"META-INF/LGPL2.1"
|
||||
)
|
||||
)
|
||||
}
|
||||
namespace = "xyz.quaver.pupil"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
implementation(Kotlin.SERIALIZATION)
|
||||
implementation(Kotlin.COROUTINE)
|
||||
|
||||
implementation("androidx.activity:activity-compose:1.6.1")
|
||||
implementation("androidx.navigation:navigation-compose:2.5.3")
|
||||
|
||||
implementation(JetpackCompose.FOUNDATION)
|
||||
implementation(JetpackCompose.UI)
|
||||
implementation(JetpackCompose.UI_UTIL)
|
||||
implementation(JetpackCompose.UI_TOOLING)
|
||||
implementation(JetpackCompose.ANIMATION)
|
||||
implementation(JetpackCompose.MATERIAL)
|
||||
implementation(JetpackCompose.MATERIAL_ICONS)
|
||||
implementation(JetpackCompose.RUNTIME_LIVEDATA)
|
||||
|
||||
// implementation(JetpackCompose.MARKDOWN)
|
||||
|
||||
implementation(Accompanist.INSETS)
|
||||
implementation(Accompanist.INSETS_UI)
|
||||
implementation(Accompanist.FLOW_LAYOUT)
|
||||
implementation(Accompanist.SYSTEM_UI_CONTROLLER)
|
||||
implementation(Accompanist.DRAWABLE_PAINTER)
|
||||
implementation(Accompanist.APPCOMPAT_THEME)
|
||||
|
||||
implementation("io.coil-kt:coil-compose:2.0.0-rc03")
|
||||
|
||||
implementation(KtorClient.CORE)
|
||||
implementation(KtorClient.OKHTTP)
|
||||
implementation(KtorClient.CONTENT_NEGOTIATION)
|
||||
implementation(KtorClient.SERIALIZATION)
|
||||
|
||||
implementation("androidx.room:room-runtime:2.4.3")
|
||||
annotationProcessor("androidx.room:room-compiler:2.4.3")
|
||||
kapt("androidx.room:room-compiler:2.4.3")
|
||||
implementation("androidx.room:room-ktx:2.4.3")
|
||||
|
||||
implementation("androidx.datastore:datastore:1.0.0")
|
||||
implementation("androidx.datastore:datastore-preferences:1.0.0")
|
||||
|
||||
implementation("org.kodein.di:kodein-di-framework-compose:7.11.0")
|
||||
|
||||
implementation(platform("com.google.firebase:firebase-bom:29.0.3"))
|
||||
implementation("com.google.firebase:firebase-analytics-ktx")
|
||||
implementation("com.google.firebase:firebase-crashlytics-ktx")
|
||||
implementation("com.google.firebase:firebase-perf-ktx")
|
||||
|
||||
implementation("com.google.protobuf:protobuf-javalite:3.19.1")
|
||||
|
||||
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
|
||||
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
|
||||
implementation("xyz.quaver.pupil.sources:core:0.0.1-alpha01-DEV29")
|
||||
|
||||
implementation("xyz.quaver:documentfilex:0.7.2")
|
||||
implementation("xyz.quaver:subsampledimage:0.0.1-alpha22-SNAPSHOT")
|
||||
|
||||
implementation("org.kodein.log:kodein-log:0.12.0")
|
||||
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.8.1")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("org.mockito:mockito-inline:4.4.0")
|
||||
testImplementation(KtorClient.TEST)
|
||||
testImplementation(Kotlin.COROUTINE_TEST)
|
||||
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test:rules:1.4.0")
|
||||
androidTestImplementation("androidx.test:runner:1.4.0")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||
androidTestImplementation(KtorClient.TEST)
|
||||
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.1.1")
|
||||
}
|
||||
|
||||
protobuf {
|
||||
protoc {
|
||||
artifact = "com.google.protobuf:protoc:3.19.1"
|
||||
}
|
||||
generateProtoTasks {
|
||||
all().forEach { task ->
|
||||
task.builtins {
|
||||
id("java") {
|
||||
option("lite")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task<Exec>("clearAppCache") {
|
||||
commandLine("adb", "shell", "pm", "clear", "xyz.quaver.pupil.debug")
|
||||
}
|
||||
1
app/credentials.json
Normal file
@@ -0,0 +1 @@
|
||||
{"installed":{"client_id":"644157827114-rnbcmlqiaqgg295o45kavchnvi3dedbo.apps.googleusercontent.com","project_id":"pupil-1598439316578","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
|
||||
26
app/proguard-rules.pro
vendored
@@ -1,6 +1,6 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
@@ -19,3 +19,27 @@
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
-keepattributes *Annotation*, InnerClasses
|
||||
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
|
||||
|
||||
# kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer
|
||||
-keepclassmembers class kotlinx.serialization.json.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-keep,includedescriptorclasses class xyz.quaver.**$$serializer { *; }
|
||||
-keepclassmembers class xyz.quaver.** {
|
||||
*** Companion;
|
||||
}
|
||||
-keepclasseswithmembers class xyz.quaver.** {
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keepclassmembers class * extends androidx.datastore.preferences.protobuf.GeneratedMessageLite {
|
||||
<fields>;
|
||||
}
|
||||
20
app/release/output-metadata.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "xyz.quaver.pupil.beta",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 600,
|
||||
"versionName": "6.0.0-alpha02",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
[{"outputType":{"type":"APK"},"apkData":{"type":"MAIN","splits":[],"versionCode":20,"versionName":"2.11.1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
|
||||
@@ -1,21 +1,39 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2019 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@file:Suppress("UNUSED_VARIABLE")
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import com.google.api.Http
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import xyz.quaver.hitomi.fetchNozomi
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.getReader
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
@@ -29,34 +47,5 @@ class ExampleInstrumentedTest {
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("xyz.quaver.pupil", appContext.packageName)
|
||||
|
||||
Log.d("Pupil", fetchNozomi().first.size.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun checkCacheDir() {
|
||||
val activityTestRule = ActivityTestRule<LockActivity>(LockActivity::class.java)
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
activityTestRule.launchActivity(Intent())
|
||||
|
||||
while(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_doSearch() {
|
||||
val reader = getReader(1426382)
|
||||
|
||||
val data: ByteArray
|
||||
|
||||
with(URL(reader[0].url).openConnection() as HttpsURLConnection) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
|
||||
data = inputStream.readBytes()
|
||||
}
|
||||
|
||||
Log.d("Pupil", data.size.toString())
|
||||
}
|
||||
}
|
||||
22
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2020 tom5079
|
||||
~
|
||||
~ 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
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name" translatable="false" tools:override="true">Pupil-Debug</string>
|
||||
</resources>
|
||||
@@ -1,9 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="xyz.quaver.pupil">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="21"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
|
||||
<application
|
||||
android:name=".Pupil"
|
||||
@@ -13,114 +19,38 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity android:name=".ui.LockActivity"/>
|
||||
<activity
|
||||
android:name=".ui.ReaderActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:parentActivityName=".ui.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
android:theme="@style/AppTheme"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
tools:replace="android:theme"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<provider
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/galleries"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="히요비.asia"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="xn--9w3b15m8vo.asia"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="e-hentai.org"
|
||||
android:pathPrefix="/g"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="hitomi.la"
|
||||
android:pathPrefix="/galleries"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="히요비.asia"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="xn--9w3b15m8vo.asia"
|
||||
android:pathPrefix="/reader"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="e-hentai.org"
|
||||
android:pathPrefix="/g"
|
||||
android:scheme="http" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:label="@string/settings_title" />
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:theme="@style/NoActionBarAppTheme">
|
||||
android:theme="@style/NoActionBarAppTheme"
|
||||
android:exported="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
@@ -1,36 +1,56 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2019 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil
|
||||
|
||||
import android.app.Application
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import java.io.File
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import org.kodein.di.*
|
||||
import org.kodein.di.android.x.androidXModule
|
||||
import xyz.quaver.pupil.sources.core.NetworkCache
|
||||
import xyz.quaver.pupil.sources.core.settingsDataStore
|
||||
import xyz.quaver.pupil.util.PupilHttpClient
|
||||
|
||||
class Pupil : MultiDexApplication() {
|
||||
class Pupil : Application(), DIAware {
|
||||
|
||||
lateinit var histories: Histories
|
||||
lateinit var downloads: Histories
|
||||
lateinit var favorites: Histories
|
||||
override val di: DI by DI.lazy {
|
||||
import(androidXModule(this@Pupil))
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
bind { singleton { NetworkCache(this@Pupil) } }
|
||||
|
||||
bindSingleton { settingsDataStore }
|
||||
|
||||
bind { singleton { PupilHttpClient(OkHttp.create()) } }
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
||||
downloads = Histories(File(ContextCompat.getDataDir(this), "downloads.json"))
|
||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
||||
super.onCreate()
|
||||
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(this)
|
||||
@@ -40,22 +60,29 @@ class Pupil : MultiDexApplication() {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (!preference.getBoolean("channel_created", false)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val channel = NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
|
||||
manager.createNotificationChannel(NotificationChannel("download", getString(R.string.channel_download), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_download_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
}
|
||||
manager.createNotificationChannel(channel)
|
||||
}
|
||||
})
|
||||
|
||||
preference.edit().putBoolean("channel_created", true).apply()
|
||||
}
|
||||
manager.createNotificationChannel(NotificationChannel("downloader", getString(R.string.channel_downloader), NotificationManager.IMPORTANCE_LOW).apply {
|
||||
description = getString(R.string.channel_downloader_description)
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
|
||||
super.onCreate()
|
||||
manager.createNotificationChannel(NotificationChannel("update", getString(R.string.channel_update), NotificationManager.IMPORTANCE_HIGH).apply {
|
||||
description = getString(R.string.channel_update_description)
|
||||
enableLights(true)
|
||||
enableVibration(true)
|
||||
lockscreenVisibility = Notification.VISIBILITY_SECRET
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import android.util.SparseBooleanArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.android.synthetic.main.item_galleryblock.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.ReaderItem
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tag
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.getCachedGallery
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class GalleryBlockAdapter(private val galleries: List<Pair<GalleryBlock, Deferred<String>>>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
enum class ViewType {
|
||||
NEXT,
|
||||
GALLERY,
|
||||
PREV
|
||||
}
|
||||
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
inner class GalleryViewHolder(private val view: CardView) : RecyclerView.ViewHolder(view) {
|
||||
fun bind(item: Pair<GalleryBlock, Deferred<String>>) {
|
||||
with(view) {
|
||||
val resources = context.resources
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
val (galleryBlock: GalleryBlock, thumbnail: Deferred<String>) = item
|
||||
|
||||
val artists = galleryBlock.artists
|
||||
val series = galleryBlock.series
|
||||
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val cache = thumbnail.await()
|
||||
|
||||
if (!File(cache).exists())
|
||||
return@launch
|
||||
|
||||
val bitmap = BitmapFactory.decodeFile(thumbnail.await())
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
galleryblock_thumbnail.setImageBitmap(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
//Check cache
|
||||
val readerCache = { File(getCachedGallery(context, galleryBlock.id), "reader.json") }
|
||||
val imageCache = { File(getCachedGallery(context, galleryBlock.id), "images") }
|
||||
|
||||
if (readerCache.invoke().exists()) {
|
||||
val reader = Json(JsonConfiguration.Stable)
|
||||
.parse(ReaderItem.serializer().list, readerCache.invoke().readText())
|
||||
|
||||
with(galleryblock_progressbar) {
|
||||
max = reader.size
|
||||
progress = imageCache.invoke().list()?.size ?: 0
|
||||
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
galleryblock_progressbar.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (refreshTasks[this@GalleryViewHolder] == null) {
|
||||
val refresh = Timer(false).schedule(0, 1000) {
|
||||
post {
|
||||
with(view.galleryblock_progressbar) {
|
||||
progress = imageCache.invoke().list()?.size ?: 0
|
||||
|
||||
if (!readerCache.invoke().exists()) {
|
||||
visibility = View.GONE
|
||||
max = 0
|
||||
progress = 0
|
||||
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
} else {
|
||||
if (visibility == View.GONE) {
|
||||
val reader = Json(JsonConfiguration.Stable)
|
||||
.parse(ReaderItem.serializer().list, readerCache.invoke().readText())
|
||||
max = reader.size
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
if (progress == max) {
|
||||
if (completeFlag.get(galleryBlock.id, false)) {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageResource(R.drawable.ic_progressbar)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
with(view.galleryblock_progress_complete) {
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.ic_progressbar_complete).apply {
|
||||
this?.start()
|
||||
})
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
completeFlag.put(galleryBlock.id, true)
|
||||
}
|
||||
} else
|
||||
view.galleryblock_progress_complete.visibility = View.INVISIBLE
|
||||
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshTasks[this@GalleryViewHolder] = refresh
|
||||
}
|
||||
|
||||
galleryblock_title.text = galleryBlock.title
|
||||
with(galleryblock_artist) {
|
||||
text = artists.joinToString(", ") { it.wordCapitalize() }
|
||||
visibility = when {
|
||||
artists.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
setOnClickListener {
|
||||
if (artists.size > 1) {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, artists)) { _, index ->
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke(Tag("artist", artists[index]))
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
for(callback in onChipClickedHandler)
|
||||
callback.invoke(Tag("artist", artists.first()))
|
||||
}
|
||||
}
|
||||
}
|
||||
with(galleryblock_series) {
|
||||
text =
|
||||
resources.getString(
|
||||
R.string.galleryblock_series,
|
||||
series.joinToString(", ") { it.wordCapitalize() })
|
||||
visibility = when {
|
||||
series.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
setOnClickListener {
|
||||
setOnClickListener {
|
||||
if (series.size > 1) {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setAdapter(ArrayAdapter(context, android.R.layout.select_dialog_item, series)) { _, index ->
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke(Tag("series", series[index]))
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
for(callback in onChipClickedHandler)
|
||||
callback.invoke(Tag("series", series.first()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
with(galleryblock_type) {
|
||||
text = resources.getString(R.string.galleryblock_type, galleryBlock.type).wordCapitalize()
|
||||
setOnClickListener {
|
||||
setOnClickListener {
|
||||
for(callback in onChipClickedHandler)
|
||||
callback.invoke(Tag("type", galleryBlock.type))
|
||||
}
|
||||
}
|
||||
}
|
||||
with(galleryblock_language) {
|
||||
text =
|
||||
resources.getString(R.string.galleryblock_language, languages[galleryBlock.language])
|
||||
visibility = when {
|
||||
galleryBlock.language.isNotEmpty() -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
setOnClickListener {
|
||||
setOnClickListener {
|
||||
for(callback in onChipClickedHandler)
|
||||
callback.invoke(Tag("language", galleryBlock.language))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
galleryblock_tag_group.removeAllViews()
|
||||
galleryBlock.relatedTags.forEach {
|
||||
val tag = Tag.parse(it).let { tag ->
|
||||
when {
|
||||
tag.area != null -> tag
|
||||
else -> Tag("tag", it)
|
||||
}
|
||||
}
|
||||
|
||||
val chip = LayoutInflater.from(context)
|
||||
.inflate(R.layout.tag_chip, this, false) as Chip
|
||||
|
||||
val icon = when(tag.area) {
|
||||
"male" -> {
|
||||
chip.setChipBackgroundColorResource(R.color.material_blue_700)
|
||||
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_male_white)
|
||||
}
|
||||
"female" -> {
|
||||
chip.setChipBackgroundColorResource(R.color.material_pink_600)
|
||||
chip.setTextColor(ContextCompat.getColor(context, android.R.color.white))
|
||||
ContextCompat.getDrawable(context, R.drawable.ic_gender_female_white)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
chip.chipIcon = icon
|
||||
chip.text = tag.tag.wordCapitalize()
|
||||
chip.setOnClickListener {
|
||||
for (callback in onChipClickedHandler)
|
||||
callback.invoke(tag)
|
||||
}
|
||||
|
||||
galleryblock_tag_group.addView(chip)
|
||||
}
|
||||
|
||||
galleryblock_id.text = galleryBlock.id.toString()
|
||||
|
||||
if (!::favorites.isInitialized)
|
||||
favorites = (context.applicationContext as Pupil).favorites
|
||||
|
||||
with(galleryblock_favorite) {
|
||||
setImageResource(if (favorites.contains(galleryBlock.id)) R.drawable.ic_star_filled else R.drawable.ic_star_empty)
|
||||
setOnClickListener {
|
||||
when {
|
||||
favorites.contains(galleryBlock.id) -> {
|
||||
favorites.remove(galleryBlock.id)
|
||||
|
||||
setImageResource(R.drawable.ic_star_empty)
|
||||
}
|
||||
else -> {
|
||||
favorites.add(galleryBlock.id)
|
||||
|
||||
setImageDrawable(AnimatedVectorDrawableCompat.create(context, R.drawable.avd_star).apply {
|
||||
this ?: return@apply
|
||||
|
||||
registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
setImageResource(R.drawable.ic_star_filled)
|
||||
}
|
||||
})
|
||||
start()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class NextViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
||||
class PrevViewHolder(view: LinearLayout) : RecyclerView.ViewHolder(view)
|
||||
|
||||
class ViewHolderFactory {
|
||||
companion object {
|
||||
fun getLayoutID(type: Int): Int {
|
||||
return when(ViewType.values()[type]) {
|
||||
ViewType.NEXT -> R.layout.item_next
|
||||
ViewType.PREV -> R.layout.item_prev
|
||||
ViewType.GALLERY -> R.layout.item_galleryblock
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.wordCapitalize() : String {
|
||||
val result = ArrayList<String>()
|
||||
|
||||
for (word in this.split(" "))
|
||||
result.add(word.capitalize())
|
||||
|
||||
return result.joinToString(" ")
|
||||
}
|
||||
|
||||
private val refreshTasks = HashMap<GalleryViewHolder, TimerTask>()
|
||||
val completeFlag = SparseBooleanArray()
|
||||
|
||||
val onChipClickedHandler = ArrayList<((Tag) -> Unit)>()
|
||||
|
||||
var showNext = false
|
||||
var showPrev = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
fun getViewHolder(type: Int, view: View): RecyclerView.ViewHolder {
|
||||
return when(ViewType.values()[type]) {
|
||||
ViewType.NEXT -> NextViewHolder(view as LinearLayout)
|
||||
ViewType.PREV -> PrevViewHolder(view as LinearLayout)
|
||||
ViewType.GALLERY -> GalleryViewHolder(view as CardView)
|
||||
}
|
||||
}
|
||||
|
||||
return getViewHolder(
|
||||
viewType,
|
||||
LayoutInflater.from(parent.context).inflate(
|
||||
ViewHolderFactory.getLayoutID(viewType),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (holder is GalleryViewHolder)
|
||||
holder.bind(galleries[position-(if (showPrev) 1 else 0)])
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewDetachedFromWindow(holder)
|
||||
|
||||
if (holder is GalleryViewHolder) {
|
||||
val task = refreshTasks[holder] ?: return
|
||||
|
||||
task.cancel()
|
||||
refreshTasks.remove(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() =
|
||||
(if (galleries.isEmpty()) 0 else galleries.size)+
|
||||
(if (showNext) 1 else 0)+
|
||||
(if (showPrev) 1 else 0)
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when {
|
||||
showPrev && position == 0 -> ViewType.PREV
|
||||
showNext && position == galleries.size+(if (showPrev) 1 else 0) -> ViewType.NEXT
|
||||
else -> ViewType.GALLERY
|
||||
}.ordinal
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package xyz.quaver.pupil.adapters
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import xyz.quaver.pupil.R
|
||||
|
||||
class ReaderAdapter(private val images: List<String>) : RecyclerView.Adapter<ReaderAdapter.ViewHolder>() {
|
||||
|
||||
var isFullScreen = false
|
||||
|
||||
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.item_reader, parent, false
|
||||
).let {
|
||||
return ViewHolder(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
|
||||
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||
// Raw height and width of image
|
||||
val (height: Int, width: Int) = options.run { outHeight to outWidth }
|
||||
var inSampleSize = 1
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
|
||||
val halfHeight: Int = height / 2
|
||||
val halfWidth: Int = width / 2
|
||||
|
||||
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
|
||||
// height and width larger than the requested height and width.
|
||||
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
with(holder.view as ImageView) {
|
||||
val options = BitmapFactory.Options()
|
||||
|
||||
options.inJustDecodeBounds = true
|
||||
BitmapFactory.decodeFile(images[position], options)
|
||||
|
||||
val (reqWidth, reqHeight) = context.resources.displayMetrics.let {
|
||||
Pair(it.widthPixels, it.heightPixels)
|
||||
}
|
||||
|
||||
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
|
||||
|
||||
options.inPreferredConfig = Bitmap.Config.RGB_565
|
||||
|
||||
options.inJustDecodeBounds = false
|
||||
|
||||
val image = BitmapFactory.decodeFile(images[position], options)
|
||||
|
||||
setImageBitmap(image)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = images.size
|
||||
|
||||
}
|
||||
108
app/src/main/java/xyz/quaver/pupil/sources/LocalSources.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.sources
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import dalvik.system.PathClassLoader
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import xyz.quaver.pupil.sources.core.Source
|
||||
|
||||
@Composable
|
||||
fun rememberLocalSourceList(context: Context = LocalContext.current): State<List<SourceEntry>> = produceState(emptyList()) {
|
||||
while (true) {
|
||||
value = loadSourceList(context)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadSource(context: Context, sourceEntry: SourceEntry): Source = coroutineScope {
|
||||
sourceCacheMutex.withLock {
|
||||
sourceCache[sourceEntry.packageName] ?: run {
|
||||
val classLoader = PathClassLoader(sourceEntry.sourceDir, null, context.classLoader)
|
||||
|
||||
Class.forName("${sourceEntry.packagePath}${sourceEntry.sourcePath}", false, classLoader)
|
||||
.getConstructor(Application::class.java)
|
||||
.newInstance(context.applicationContext) as Source
|
||||
}.also { sourceCache[sourceEntry.packageName] = it }
|
||||
}
|
||||
}
|
||||
|
||||
private const val SOURCES_FEATURE = "pupil.sources"
|
||||
private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources"
|
||||
private const val SOURCES_PATH = "pupil.sources.path"
|
||||
|
||||
private val PackageInfo.isSourceFeatureEnabled
|
||||
get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE }
|
||||
|
||||
private fun loadSource(context: Context, packageInfo: PackageInfo): List<SourceEntry> {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
val applicationInfo = packageInfo.applicationInfo
|
||||
|
||||
val packageName = packageManager.getApplicationLabel(applicationInfo).toString().substringAfter("[Pupil] ")
|
||||
val packagePath = packageInfo.packageName
|
||||
|
||||
val icon = packageManager.getApplicationIcon(applicationInfo)
|
||||
|
||||
val version = packageInfo.versionName
|
||||
|
||||
return packageInfo
|
||||
.applicationInfo
|
||||
.metaData
|
||||
?.getString(SOURCES_PATH)
|
||||
?.split(';')
|
||||
?.map { source ->
|
||||
val (sourceName, sourcePath) = source.split(':', limit = 2)
|
||||
SourceEntry(
|
||||
packageName,
|
||||
packagePath,
|
||||
sourceName,
|
||||
sourcePath,
|
||||
applicationInfo.sourceDir,
|
||||
icon,
|
||||
version
|
||||
)
|
||||
}.orEmpty()
|
||||
}
|
||||
|
||||
private val sourceCacheMutex = Mutex()
|
||||
private val sourceCache = mutableMapOf<String, Source>()
|
||||
|
||||
private fun loadSourceList(context: Context): List<SourceEntry> {
|
||||
val packageManager = context.packageManager
|
||||
|
||||
val packages = packageManager.getInstalledPackages(
|
||||
PackageManager.GET_CONFIGURATIONS or PackageManager.GET_META_DATA
|
||||
)
|
||||
|
||||
return packages.flatMap { packageInfo ->
|
||||
if (packageInfo.isSourceFeatureEnabled)
|
||||
loadSource(context, packageInfo)
|
||||
else
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
36
app/src/main/java/xyz/quaver/pupil/sources/RemoteSources.kt
Normal file
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2022 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.sources
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import kotlinx.coroutines.delay
|
||||
import org.kodein.di.compose.localDI
|
||||
import org.kodein.di.compose.rememberInstance
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.pupil.util.PupilHttpClient
|
||||
import xyz.quaver.pupil.util.RemoteSourceInfo
|
||||
|
||||
@Composable
|
||||
fun rememberRemoteSourceList(client: PupilHttpClient = localDI().direct.instance()) = produceState<Map<String, RemoteSourceInfo>?>(null) {
|
||||
while (true) {
|
||||
value = client.getRemoteSourceList()
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
31
app/src/main/java/xyz/quaver/pupil/sources/common.kt
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2022 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.sources
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
|
||||
data class SourceEntry(
|
||||
val packageName: String,
|
||||
val packagePath: String,
|
||||
val sourceName: String,
|
||||
val sourcePath: String,
|
||||
val sourceDir: String,
|
||||
val icon: Drawable,
|
||||
val version: String
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
package xyz.quaver.pupil.types
|
||||
|
||||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import xyz.quaver.hitomi.Suggestion
|
||||
|
||||
@Parcelize
|
||||
data class TagSuggestion constructor(val s: String, val t: Int, val u: String, val n: String) : SearchSuggestion {
|
||||
constructor(s: Suggestion) : this(s.s, s.t, s.u, s.n)
|
||||
|
||||
override fun getBody(): String {
|
||||
return s
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package xyz.quaver.pupil.types
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
|
||||
companion object {
|
||||
fun parse(tag: String) : Tag {
|
||||
if (tag.first() == '-') {
|
||||
tag.substring(1).split(Regex(":"), 2).let {
|
||||
return when(it.size) {
|
||||
2 -> Tag(it[0], it[1], true)
|
||||
else -> Tag(null, tag, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
tag.split(Regex(":"), 2).let {
|
||||
return when(it.size) {
|
||||
2 -> Tag(it[0], it[1])
|
||||
else -> Tag(null, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return (if (isNegative) "-" else "") + when(area) {
|
||||
null -> tag
|
||||
else -> "$area:$tag"
|
||||
}
|
||||
}
|
||||
|
||||
fun toQuery(): String {
|
||||
return toString().replace(' ', '_')
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is Tag)
|
||||
return false
|
||||
|
||||
if (other.area == area && other.tag == tag)
|
||||
return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return super.hashCode()
|
||||
}
|
||||
}
|
||||
|
||||
class Tags(tag: List<Tag?>?) : ArrayList<Tag>() {
|
||||
|
||||
companion object {
|
||||
fun parse(tags: String) : Tags {
|
||||
return Tags(
|
||||
tags.split(' ').map {
|
||||
if (it.isNotEmpty())
|
||||
Tag.parse(it)
|
||||
else
|
||||
null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
tag?.forEach {
|
||||
if (it != null)
|
||||
add(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun contains(element: String): Boolean {
|
||||
forEach {
|
||||
if (it.toString() == element)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun add(element: String): Boolean {
|
||||
return super.add(Tag.parse(element))
|
||||
}
|
||||
|
||||
fun remove(element: String) {
|
||||
filter { it.toString() == element }.forEach {
|
||||
remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeByArea(area: String) {
|
||||
filter { it.area == area }.forEach {
|
||||
remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return joinToString(" ") { it.toString() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.andrognito.patternlockview.PatternLockView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
|
||||
class LockActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_lock)
|
||||
|
||||
val lockManager = LockManager(this)
|
||||
|
||||
val mode = intent.getStringExtra("mode")
|
||||
|
||||
lock_pattern.isEnabled = false
|
||||
lock_pin.isEnabled = false
|
||||
lock_fingerprint.isEnabled = false
|
||||
lock_password.isEnabled = false
|
||||
|
||||
when(mode) {
|
||||
null -> {
|
||||
if (lockManager.empty()) {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
"add_lock" -> {
|
||||
when(intent.getStringExtra("type")!!) {
|
||||
"pattern" -> {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content,
|
||||
PatternLockFragment().apply {
|
||||
var lastPass = ""
|
||||
onPatternDrawn = {
|
||||
when(mode) {
|
||||
null -> {
|
||||
val result = lockManager.check(it)
|
||||
|
||||
if (result == true) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
}
|
||||
"add_lock" -> {
|
||||
if (lastPass.isEmpty()) {
|
||||
lastPass = it
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
if (lastPass == it) {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
||||
finish()
|
||||
} else {
|
||||
lock_pattern_view.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
lastPass = ""
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
).commit()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.andrognito.patternlockview.PatternLockView
|
||||
import com.andrognito.patternlockview.listener.PatternLockViewListener
|
||||
import com.andrognito.patternlockview.utils.PatternLockUtils
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.*
|
||||
import kotlinx.android.synthetic.main.fragment_pattern_lock.view.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.hash
|
||||
import xyz.quaver.pupil.util.hashWithSalt
|
||||
|
||||
class PatternLockFragment : Fragment(), PatternLockViewListener {
|
||||
|
||||
var onPatternDrawn: ((String) -> Unit)? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_pattern_lock, container, false).apply {
|
||||
lock_pattern_view.addPatternLockListener(this@PatternLockFragment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
||||
}
|
||||
|
||||
override fun onComplete(pattern: MutableList<PatternLockView.Dot>?) {
|
||||
val password = PatternLockUtils.patternToMD5(lock_pattern_view, pattern)
|
||||
onPatternDrawn?.invoke(password)
|
||||
}
|
||||
|
||||
override fun onProgress(progressPattern: MutableList<PatternLockView.Dot>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStarted() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.PagerSnapHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import com.crashlytics.android.Crashlytics
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.android.synthetic.main.activity_reader.*
|
||||
import kotlinx.android.synthetic.main.activity_reader.view.*
|
||||
import kotlinx.android.synthetic.main.dialog_numberpicker.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.io.IOException
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import xyz.quaver.hitomi.GalleryBlock
|
||||
import xyz.quaver.hitomi.getGalleryBlock
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.adapters.ReaderAdapter
|
||||
import xyz.quaver.pupil.util.GalleryDownloader
|
||||
import xyz.quaver.pupil.util.Histories
|
||||
import xyz.quaver.pupil.util.ItemClickSupport
|
||||
|
||||
class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
private val images = ArrayList<String>()
|
||||
private lateinit var galleryBlock: GalleryBlock
|
||||
private var gallerySize = 0
|
||||
private var currentPage = 0
|
||||
|
||||
private var isScroll = true
|
||||
private var isFullscreen = false
|
||||
set(value) {
|
||||
field = value
|
||||
|
||||
(reader_recyclerview.adapter as ReaderAdapter).isFullScreen = value
|
||||
|
||||
reader_progressbar.visibility = when {
|
||||
value -> View.VISIBLE
|
||||
else -> View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var downloader: GalleryDownloader
|
||||
|
||||
private val snapHelper = PagerSnapHelper()
|
||||
|
||||
private var menu: Menu? = null
|
||||
|
||||
private lateinit var favorites: Histories
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
favorites = (application as Pupil).favorites
|
||||
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
setContentView(R.layout.activity_reader)
|
||||
|
||||
handleIntent(intent)
|
||||
|
||||
Crashlytics.setInt("GalleryID", galleryBlock.id)
|
||||
|
||||
if (!::galleryBlock.isInitialized) {
|
||||
onBackPressed()
|
||||
return
|
||||
}
|
||||
|
||||
supportActionBar?.title = galleryBlock.title
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(false)
|
||||
|
||||
initDownloader()
|
||||
|
||||
initView()
|
||||
|
||||
if (!downloader.download)
|
||||
downloader.start()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
handleIntent(intent)
|
||||
}
|
||||
|
||||
private fun handleIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_VIEW) {
|
||||
val uri = intent.data
|
||||
val lastPathSegment = uri?.lastPathSegment
|
||||
if (uri != null && lastPathSegment != null) {
|
||||
val nonNumber = Regex("[^-?0-9]+")
|
||||
|
||||
val galleryID = when (uri.host) {
|
||||
"hitomi.la" -> lastPathSegment.replace(nonNumber, "").toInt()
|
||||
"히요비.asia" -> lastPathSegment.toInt()
|
||||
"xn--9w3b15m8vo.asia" -> lastPathSegment.toInt()
|
||||
"e-hentai.org" -> uri.pathSegments[1].toInt()
|
||||
else -> return
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
galleryBlock = getGalleryBlock(galleryID) ?: return@launch
|
||||
}.join()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
galleryBlock = Json(JsonConfiguration.Stable).parse(
|
||||
GalleryBlock.serializer(),
|
||||
intent.getStringExtra("galleryblock")!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (preferences.getBoolean("security_mode", false))
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.reader, menu)
|
||||
|
||||
with(menu?.findItem(R.id.reader_menu_favorite)) {
|
||||
this ?: return@with
|
||||
|
||||
if (favorites.contains(galleryBlock.id))
|
||||
(icon as Animatable).start()
|
||||
}
|
||||
|
||||
this.menu = menu
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when(item?.itemId) {
|
||||
R.id.reader_menu_page_indicator -> {
|
||||
val view = LayoutInflater.from(this).inflate(R.layout.dialog_numberpicker, findViewById(android.R.id.content), false)
|
||||
with(view.dialog_number_picker) {
|
||||
minValue=1
|
||||
maxValue=gallerySize
|
||||
value=currentPage
|
||||
}
|
||||
val dialog = AlertDialog.Builder(this).apply {
|
||||
setView(view)
|
||||
}.create()
|
||||
view.dialog_ok.setOnClickListener {
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPositionWithOffset(view.dialog_number_picker.value-1, 0)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
R.id.reader_menu_favorite -> {
|
||||
val id = galleryBlock.id
|
||||
val favorite = menu?.findItem(R.id.reader_menu_favorite) ?: return true
|
||||
|
||||
if (favorites.contains(id)) {
|
||||
favorites.remove(id)
|
||||
favorite.icon = AnimatedVectorDrawableCompat.create(this, R.drawable.avd_star)
|
||||
} else {
|
||||
favorites.add(id)
|
||||
(favorite.icon as Animatable).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (::downloader.isInitialized && !downloader.download)
|
||||
downloader.cancel()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (isScroll and !isFullscreen)
|
||||
super.onBackPressed()
|
||||
|
||||
if (isFullscreen) {
|
||||
isFullscreen = false
|
||||
fullscreen(false)
|
||||
}
|
||||
|
||||
if (!isScroll) {
|
||||
isScroll = true
|
||||
scrollMode(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDownloader() {
|
||||
var d: GalleryDownloader? = GalleryDownloader.get(galleryBlock.id)
|
||||
|
||||
if (d == null) {
|
||||
try {
|
||||
d = GalleryDownloader(this, galleryBlock)
|
||||
} catch (e: IOException) {
|
||||
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
downloader = d.apply {
|
||||
onReaderLoadedHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
with(reader_download_progressbar) {
|
||||
max = it.size
|
||||
progress = 0
|
||||
}
|
||||
with(reader_progressbar) {
|
||||
max = it.size
|
||||
progress = 0
|
||||
}
|
||||
|
||||
gallerySize = it.size
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/${it.size}"
|
||||
}
|
||||
}
|
||||
onProgressHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
reader_download_progressbar.progress = it
|
||||
menu?.findItem(R.id.reader_menu_use_hiyobi)?.isVisible = downloader.useHiyobi
|
||||
}
|
||||
}
|
||||
onDownloadedHandler = {
|
||||
val item = it.toList()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (images.isEmpty()) {
|
||||
images.addAll(item)
|
||||
reader_recyclerview.adapter?.notifyDataSetChanged()
|
||||
} else {
|
||||
images.add(item.last())
|
||||
reader_recyclerview.adapter?.notifyItemInserted(images.size-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
onErrorHandler = {
|
||||
if (it is IOException)
|
||||
Snackbar.make(reader_layout, R.string.unable_to_connect, Snackbar.LENGTH_LONG).show()
|
||||
downloader.download = false
|
||||
}
|
||||
onCompleteHandler = {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
reader_download_progressbar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
onNotifyChangedHandler = { notify ->
|
||||
val fab = reader_fab_download
|
||||
|
||||
runOnUiThread {
|
||||
if (notify) {
|
||||
val icon = AnimatedVectorDrawableCompat.create(this, R.drawable.ic_downloading)
|
||||
icon?.registerAnimationCallback(object: Animatable2Compat.AnimationCallback() {
|
||||
override fun onAnimationEnd(drawable: Drawable?) {
|
||||
if (downloader.download)
|
||||
fab.post {
|
||||
icon.start()
|
||||
fab.labelText = getString(R.string.reader_fab_download_cancel)
|
||||
}
|
||||
else
|
||||
fab.post {
|
||||
fab.setImageResource(R.drawable.ic_download)
|
||||
fab.labelText = getString(R.string.reader_fab_download)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
fab.setImageDrawable(icon)
|
||||
icon?.start()
|
||||
} else {
|
||||
runOnUiThread {
|
||||
fab.setImageResource(R.drawable.ic_download)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloader.download) {
|
||||
downloader.invokeOnReaderLoaded()
|
||||
downloader.invokeOnNotifyChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
with(reader_recyclerview) {
|
||||
adapter = ReaderAdapter(images)
|
||||
|
||||
addOnScrollListener(object: RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
|
||||
if (layoutManager.findFirstVisibleItemPosition() == -1)
|
||||
return
|
||||
currentPage = layoutManager.findFirstVisibleItemPosition()+1
|
||||
menu?.findItem(R.id.reader_menu_page_indicator)?.title = "$currentPage/$gallerySize"
|
||||
this@ReaderActivity.reader_progressbar.progress = currentPage
|
||||
}
|
||||
})
|
||||
|
||||
ItemClickSupport.addTo(this)
|
||||
.setOnItemClickListener { _, _, _ ->
|
||||
if (isScroll) {
|
||||
isScroll = false
|
||||
isFullscreen = true
|
||||
|
||||
scrollMode(false)
|
||||
fullscreen(true)
|
||||
} else {
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager?)?.scrollToPosition(currentPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reader_fab_fullscreen.setOnClickListener {
|
||||
isFullscreen = true
|
||||
fullscreen(isFullscreen)
|
||||
|
||||
reader_fab.close(true)
|
||||
}
|
||||
|
||||
reader_fab_download.setOnClickListener {
|
||||
downloader.download = !downloader.download
|
||||
|
||||
if (!downloader.download)
|
||||
downloader.clearNotification()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fullscreen(isFullscreen: Boolean) {
|
||||
with(window.attributes) {
|
||||
if (isFullscreen) {
|
||||
flags = flags or WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
supportActionBar?.hide()
|
||||
this@ReaderActivity.reader_fab.visibility = View.INVISIBLE
|
||||
} else {
|
||||
flags = flags and WindowManager.LayoutParams.FLAG_FULLSCREEN.inv()
|
||||
supportActionBar?.show()
|
||||
this@ReaderActivity.reader_fab.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
window.attributes = this
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollMode(isScroll: Boolean) {
|
||||
if (isScroll) {
|
||||
snapHelper.attachToRecyclerView(null)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this)
|
||||
} else {
|
||||
snapHelper.attachToRecyclerView(reader_recyclerview)
|
||||
reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
}
|
||||
|
||||
(reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0)
|
||||
}
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.WindowManager
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.android.synthetic.main.dialog_default_query.view.*
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.getDownloadDirectory
|
||||
import java.io.File
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
val REQUEST_LOCK = 38238
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
setContentView(R.layout.settings_activity)
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, SettingsFragment())
|
||||
.commit()
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
if (preferences.getBoolean("security_mode", false))
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val suffix = listOf(
|
||||
"B",
|
||||
"kB",
|
||||
"MB",
|
||||
"GB",
|
||||
"TB" //really?
|
||||
)
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
findPreference<Preference>("app_lock")?.summary = if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
} else {
|
||||
lockManager.locks?.joinToString(", ") {
|
||||
when(it.type) {
|
||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDirSize(dir: File) : String {
|
||||
var size = dir.walk().map { it.length() }.sum()
|
||||
var suffixIndex = 0
|
||||
|
||||
while (size >= 1024) {
|
||||
size /= 1024
|
||||
suffixIndex++
|
||||
}
|
||||
|
||||
return getString(R.string.settings_clear_summary, size, suffix[suffixIndex])
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.root_preferences, rootKey)
|
||||
|
||||
with(findPreference<Preference>("app_version")) {
|
||||
this!!
|
||||
|
||||
val manager = context.packageManager
|
||||
val info = manager.getPackageInfo(context.packageName, 0)
|
||||
|
||||
summary = info.versionName
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("delete_cache")) {
|
||||
this!!
|
||||
|
||||
val dir = File(context.cacheDir, "imageCache")
|
||||
|
||||
summary = getDirSize(dir)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_cache_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
summary = getDirSize(dir)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("delete_downloads")) {
|
||||
this!!
|
||||
|
||||
val dir = getDownloadDirectory(context)!!
|
||||
|
||||
summary = getDirSize(dir)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_downloads_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
if (dir.exists())
|
||||
dir.deleteRecursively()
|
||||
|
||||
val downloads = (activity!!.application as Pupil).downloads
|
||||
|
||||
downloads.clear()
|
||||
|
||||
summary = getDirSize(dir)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
with(findPreference<Preference>("clear_history")) {
|
||||
this!!
|
||||
|
||||
val histories = (activity!!.application as Pupil).histories
|
||||
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_clear_history_alert_message)
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
histories.clear()
|
||||
summary = getString(R.string.settings_clear_history_summary, histories.size)
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with(findPreference<Preference>("default_query")) {
|
||||
this!!
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
summary = preferences.getString("default_query", "") ?: ""
|
||||
|
||||
val languages = resources.getStringArray(R.array.languages).map {
|
||||
it.split("|").let { split ->
|
||||
Pair(split[0], split[1])
|
||||
}
|
||||
}.toMap()
|
||||
val reverseLanguages = languages.entries.associate { (k, v) -> v to k }
|
||||
|
||||
val excludeBL = "-male:yaoi"
|
||||
val excludeGuro = listOf("-female:guro", "-male:guro")
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val dialogView = LayoutInflater.from(context).inflate(
|
||||
R.layout.dialog_default_query,
|
||||
LinearLayout(context),
|
||||
false
|
||||
)
|
||||
|
||||
val tags = Tags.parse(
|
||||
preferences.getString("default_query", "") ?: ""
|
||||
)
|
||||
|
||||
summary = tags.toString()
|
||||
|
||||
with(dialogView.default_query_dialog_language_selector) {
|
||||
adapter =
|
||||
ArrayAdapter(
|
||||
context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
arrayListOf(
|
||||
getString(R.string.default_query_dialog_language_selector_none)
|
||||
).apply {
|
||||
addAll(languages.values)
|
||||
}
|
||||
)
|
||||
if (tags.any { it.area == "language" }) {
|
||||
val tag = languages[tags.first { it.area == "language" }.tag]
|
||||
if (tag != null) {
|
||||
setSelection(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(adapter as ArrayAdapter<String>).getPosition(tag)
|
||||
)
|
||||
tags.removeByArea("language")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_BL_checkbox) {
|
||||
isChecked = tags.contains(excludeBL)
|
||||
if (tags.contains(excludeBL))
|
||||
tags.remove(excludeBL)
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_guro_checkbox) {
|
||||
isChecked = excludeGuro.all { tags.contains(it) }
|
||||
if (excludeGuro.all { tags.contains(it) })
|
||||
excludeGuro.forEach {
|
||||
tags.remove(it)
|
||||
}
|
||||
}
|
||||
|
||||
with(dialogView.default_query_dialog_edittext) {
|
||||
setText(tags.toString(), TextView.BufferType.EDITABLE)
|
||||
addTextChangedListener(object : 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?) {
|
||||
s ?: return
|
||||
|
||||
if (s.any { it.isUpperCase() })
|
||||
s.replace(0, s.length, s.toString().toLowerCase())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(context!!).apply {
|
||||
setView(dialogView)
|
||||
}.create()
|
||||
|
||||
dialogView.default_query_dialog_ok.setOnClickListener {
|
||||
val newTags = Tags.parse(dialogView.default_query_dialog_edittext.text.toString())
|
||||
|
||||
with(dialogView.default_query_dialog_language_selector) {
|
||||
if (selectedItemPosition != 0)
|
||||
newTags.add("language:${reverseLanguages[selectedItem]}")
|
||||
}
|
||||
|
||||
if (dialogView.default_query_dialog_BL_checkbox.isChecked)
|
||||
newTags.add(excludeBL)
|
||||
|
||||
if (dialogView.default_query_dialog_guro_checkbox.isChecked)
|
||||
excludeGuro.forEach { tag ->
|
||||
newTags.add(tag)
|
||||
}
|
||||
|
||||
preferenceManager.sharedPreferences.edit().putString("default_query", newTags.toString()).apply()
|
||||
summary = preferences.getString("default_query", "") ?: ""
|
||||
tags.clear()
|
||||
tags.addAll(newTags)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
with(findPreference<Preference>("app_lock")) {
|
||||
this!!
|
||||
|
||||
val lockManager = LockManager(context)
|
||||
|
||||
summary = if (lockManager.locks.isNullOrEmpty()) {
|
||||
getString(R.string.settings_lock_none)
|
||||
} else {
|
||||
lockManager.locks?.joinToString(", ") {
|
||||
when(it.type) {
|
||||
Lock.Type.PATTERN -> getString(R.string.settings_lock_pattern)
|
||||
Lock.Type.PIN -> getString(R.string.settings_lock_pin)
|
||||
Lock.Type.PASSWORD -> getString(R.string.settings_lock_password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val intent = Intent(context, LockActivity::class.java)
|
||||
activity?.startActivityForResult(intent, (activity as SettingsActivity).REQUEST_LOCK)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LockFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
findPreference<Preference>("lock_pattern")?.summary =
|
||||
if (lockManager.contains(Lock.Type.PATTERN))
|
||||
getString(R.string.settings_lock_enabled)
|
||||
else
|
||||
""
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
||||
|
||||
with(findPreference<Preference>("lock_pattern")) {
|
||||
this!!
|
||||
|
||||
if (LockManager(context!!).contains(Lock.Type.PATTERN))
|
||||
summary = getString(R.string.settings_lock_enabled)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val lockManager = LockManager(context!!)
|
||||
|
||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
||||
AlertDialog.Builder(context).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_lock_remove_message)
|
||||
|
||||
setPositiveButton(android.R.string.yes) { _, _ ->
|
||||
lockManager.remove(Lock.Type.PATTERN)
|
||||
onResume()
|
||||
}
|
||||
setNegativeButton(android.R.string.no) { _, _ -> }
|
||||
}.show()
|
||||
} else {
|
||||
val intent = Intent(context, LockActivity::class.java).apply {
|
||||
putExtra("mode", "add_lock")
|
||||
putExtra("type", "pattern")
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
|
||||
when (item?.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when(requestCode) {
|
||||
REQUEST_LOCK -> {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, LockFragment())
|
||||
.addToBackStack("Lock")
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
352
app/src/main/java/xyz/quaver/pupil/ui/SourceSelector.kt
Normal file
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.DownloadDone
|
||||
import androidx.compose.material.icons.filled.Explore
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import com.google.accompanist.insets.rememberInsetsPaddingValues
|
||||
import com.google.accompanist.insets.systemBarsPadding
|
||||
import com.google.accompanist.insets.ui.BottomNavigation
|
||||
import com.google.accompanist.insets.ui.Scaffold
|
||||
import com.google.accompanist.insets.ui.TopAppBar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.compose.localDI
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.pupil.sources.SourceEntry
|
||||
import xyz.quaver.pupil.sources.rememberLocalSourceList
|
||||
import xyz.quaver.pupil.sources.rememberRemoteSourceList
|
||||
import xyz.quaver.pupil.util.PupilHttpClient
|
||||
import xyz.quaver.pupil.util.RemoteSourceInfo
|
||||
import xyz.quaver.pupil.util.launchApkInstaller
|
||||
import java.io.File
|
||||
|
||||
private sealed class SourceSelectorScreen(val route: String, val icon: ImageVector) {
|
||||
object Local: SourceSelectorScreen("local", Icons.Default.DownloadDone)
|
||||
object Explore: SourceSelectorScreen("explore", Icons.Default.Explore)
|
||||
}
|
||||
|
||||
private val sourceSelectorScreens = listOf(
|
||||
SourceSelectorScreen.Local,
|
||||
SourceSelectorScreen.Explore
|
||||
)
|
||||
|
||||
private val RemoteSourceInfo.apkUrl: String
|
||||
get() = "https://github.com/tom5079/PupilSources/releases/download/$name-$version/$projectName-release.apk"
|
||||
|
||||
|
||||
class DownloadApkActionState(override val di: DI) : DIAware {
|
||||
private val app: Application by instance()
|
||||
private val client: PupilHttpClient by instance()
|
||||
|
||||
var progress by mutableStateOf<Float?>(null)
|
||||
private set
|
||||
|
||||
suspend fun download(url: String): File? = withContext(Dispatchers.IO) {
|
||||
progress = 0f
|
||||
|
||||
val file = File.createTempFile("pupil", ".apk", File(app.cacheDir, "apks").also {
|
||||
it.mkdirs()
|
||||
})
|
||||
|
||||
client.downloadFile(url, file).collect { progress = it }
|
||||
|
||||
if (progress == Float.POSITIVE_INFINITY) file else null
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
progress = null
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberDownloadApkActionState(di: DI = localDI()) = remember { DownloadApkActionState(di) }
|
||||
|
||||
@Composable
|
||||
fun DownloadApkAction(
|
||||
state: DownloadApkActionState,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
state.progress?.let { progress ->
|
||||
Box(
|
||||
Modifier.padding(12.dp, 0.dp)
|
||||
) {
|
||||
when {
|
||||
progress.isFinite() && progress > 0f ->
|
||||
CircularProgressIndicator(progress, modifier = Modifier.size(24.dp))
|
||||
else ->
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} ?: content()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SourceListItem(icon: @Composable (Modifier) -> Unit = { }, name: String, version: String, actions: @Composable () -> Unit = { }) {
|
||||
Card(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
elevation = 4.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
icon(Modifier.size(48.dp))
|
||||
|
||||
Column(
|
||||
Modifier.weight(1f)
|
||||
) {
|
||||
Text(name.capitalize(Locale.current))
|
||||
|
||||
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
||||
Text(
|
||||
"v$version",
|
||||
style = MaterialTheme.typography.caption
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
actions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Local(onSource: (SourceEntry) -> Unit) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val localSourceList by rememberLocalSourceList()
|
||||
val remoteSourceList by rememberRemoteSourceList()
|
||||
|
||||
if (localSourceList.isEmpty()) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
Modifier.align(Alignment.Center),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentAlpha provides 0.5f) {
|
||||
Text("(´∇`)", style = MaterialTheme.typography.h2)
|
||||
}
|
||||
Text("No sources found!\nLet's go download one.", textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(localSourceList) { source ->
|
||||
val actionState = rememberDownloadApkActionState()
|
||||
|
||||
SourceListItem(
|
||||
icon = { modifier ->
|
||||
Image(
|
||||
rememberDrawablePainter(source.icon),
|
||||
contentDescription = "source icon",
|
||||
modifier = modifier
|
||||
)
|
||||
},
|
||||
source.sourceName,
|
||||
source.version
|
||||
) {
|
||||
DownloadApkAction(actionState) {
|
||||
val remoteSource = remoteSourceList?.get(source.packageName)
|
||||
if (remoteSource != null && remoteSource.version != source.version) {
|
||||
TextButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
val file = actionState.download(remoteSource.apkUrl)!! // TODO("Handle error")
|
||||
context.launchApkInstaller(file)
|
||||
actionState.reset()
|
||||
}
|
||||
}) {
|
||||
Text("UPDATE")
|
||||
}
|
||||
} else {
|
||||
TextButton(
|
||||
onClick = { onSource(source) }
|
||||
) {
|
||||
Text("GO")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Explore() {
|
||||
val localSourceList by rememberLocalSourceList()
|
||||
val localSources by derivedStateOf {
|
||||
localSourceList.associateBy {
|
||||
it.packageName
|
||||
}
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val remoteSources by rememberRemoteSourceList()
|
||||
|
||||
Box(
|
||||
Modifier.fillMaxSize()
|
||||
) {
|
||||
if (remoteSources == null)
|
||||
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||
else
|
||||
LazyColumn {
|
||||
items(remoteSources?.values?.toList().orEmpty()) { sourceInfo ->
|
||||
val actionState = rememberDownloadApkActionState()
|
||||
|
||||
SourceListItem(
|
||||
icon = { modifier ->
|
||||
AsyncImage(
|
||||
"https://raw.githubusercontent.com/tom5079/PupilSources/master/${sourceInfo.projectName}/src/main/res/mipmap-xxxhdpi/ic_launcher.png",
|
||||
contentDescription = "source icon",
|
||||
modifier = modifier
|
||||
)
|
||||
},
|
||||
sourceInfo.name,
|
||||
sourceInfo.version
|
||||
) {
|
||||
DownloadApkAction(actionState) {
|
||||
if (localSources[sourceInfo.name]?.version != sourceInfo.version) {
|
||||
TextButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
|
||||
context.launchApkInstaller(file)
|
||||
actionState.reset()
|
||||
}
|
||||
}) {
|
||||
Text("UPDATE")
|
||||
}
|
||||
} else {
|
||||
IconButton(onClick = {
|
||||
if (sourceInfo.name in localSources) {
|
||||
context.startActivity(
|
||||
Intent(
|
||||
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", localSources[sourceInfo.name]!!.packagePath, null)
|
||||
)
|
||||
)
|
||||
} else coroutineScope.launch {
|
||||
val file = actionState.download(sourceInfo.apkUrl)!! // TODO("Handle exception")
|
||||
context.launchApkInstaller(file)
|
||||
actionState.reset()
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
if (sourceInfo.name !in localSources) Icons.Default.Download
|
||||
else Icons.Outlined.Info,
|
||||
contentDescription = "download"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SourceSelector(onSource: (SourceEntry) -> Unit) {
|
||||
val bottomNavController = rememberNavController()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text("Pupil")
|
||||
},
|
||||
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.statusBars)
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomNavigation(
|
||||
contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
|
||||
) {
|
||||
val navBackStackEntry by bottomNavController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
|
||||
sourceSelectorScreens.forEach { screen ->
|
||||
BottomNavigationItem(
|
||||
icon = { Icon(screen.icon, contentDescription = screen.route) },
|
||||
label = { Text(screen.route) },
|
||||
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
||||
onClick = {
|
||||
bottomNavController.navigate(screen.route) {
|
||||
popUpTo(bottomNavController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { contentPadding ->
|
||||
NavHost(bottomNavController, startDestination = "local", modifier = Modifier
|
||||
.systemBarsPadding(top = false, bottom = false)
|
||||
.padding(contentPadding)) {
|
||||
composable(SourceSelectorScreen.Local.route) { Local(onSource) }
|
||||
composable(SourceSelectorScreen.Explore.route) { Explore() }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
95
app/src/main/java/xyz/quaver/pupil/ui/UpdateDialog.kt
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2022 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kodein.di.compose.onDIContext
|
||||
import xyz.quaver.pupil.util.Release
|
||||
import xyz.quaver.pupil.util.launchApkInstaller
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun UpdateAlertDialog(
|
||||
show: Boolean,
|
||||
release: Release,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val state = rememberDownloadApkActionState()
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
if (show) {
|
||||
Dialog(onDismissRequest = { if (state.progress == null) onDismiss() }) {
|
||||
Card {
|
||||
val progress = state.progress
|
||||
|
||||
if (progress != null) {
|
||||
if (progress.isFinite() && progress > 0)
|
||||
LinearProgressIndicator(progress)
|
||||
else
|
||||
LinearProgressIndicator()
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(start = 8.dp, top = 8.dp, end = 8.dp, bottom = 0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
"Update Available",
|
||||
style = MaterialTheme.typography.h6
|
||||
)
|
||||
|
||||
Text(release.releaseNotes.getOrElse(Locale.getDefault()) { release.releaseNotes[Locale.ENGLISH]!! })
|
||||
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = onDismiss, enabled = progress == null) {
|
||||
Text("DISMISS")
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
val file = state.download(release.apkUrl)!! // TODO("Handle exception")
|
||||
context.launchApkInstaller(file)
|
||||
state.reset()
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
enabled = progress == null
|
||||
) {
|
||||
Text("UPDATE")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
app/src/main/java/xyz/quaver/pupil/ui/theme/Color.kt
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val LightBlue300 = Color(0xFF4FC3F7)
|
||||
val LightBlue700 = Color(0xFF0288D1)
|
||||
val Pink600 = Color(0xFFD81B60)
|
||||
val Blue700 = Color(0xFF1976D2)
|
||||
val GreenA700 = Color(0xFF00C853)
|
||||
val Orange500 = Color(0xFFFF9800)
|
||||
29
app/src/main/java/xyz/quaver/pupil/ui/theme/Shape.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.theme
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
57
app/src/main/java/xyz/quaver/pupil/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.theme
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
primary = LightBlue300,
|
||||
primaryVariant = LightBlue700,
|
||||
secondary = Pink600,
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White
|
||||
)
|
||||
private val LightColorPalette = lightColors(
|
||||
primary = LightBlue300,
|
||||
primaryVariant = LightBlue700,
|
||||
secondary = Pink600,
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun PupilTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colors = if (darkTheme) DarkColorPalette else LightColorPalette
|
||||
|
||||
MaterialTheme(
|
||||
colors = colors,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
33
app/src/main/java/xyz/quaver/pupil/ui/theme/Type.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.theme
|
||||
|
||||
import androidx.compose.material.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
val Typography = Typography(
|
||||
body1 = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
)
|
||||
@@ -1,288 +0,0 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.util.SparseArray
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.preference.PreferenceManager
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.io.IOException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.list
|
||||
import xyz.quaver.hitomi.*
|
||||
import xyz.quaver.hiyobi.cookie
|
||||
import xyz.quaver.hiyobi.user_agent
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.Pupil
|
||||
import xyz.quaver.pupil.ui.ReaderActivity
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
class GalleryDownloader(
|
||||
base: Context,
|
||||
private val galleryBlock: GalleryBlock,
|
||||
_notify: Boolean = false
|
||||
) : ContextWrapper(base) {
|
||||
|
||||
private val downloads = (applicationContext as Pupil).downloads
|
||||
var useHiyobi = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("use_hiyobi", false)
|
||||
|
||||
var download: Boolean = false
|
||||
set(value) {
|
||||
if (value) {
|
||||
field = true
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
|
||||
val data = getCachedGallery(this, galleryBlock.id)
|
||||
val cache = File(cacheDir, "imageCache/${galleryBlock.id}")
|
||||
|
||||
if (File(cache, "images").exists() && !data.exists()) {
|
||||
cache.copyRecursively(data, true)
|
||||
cache.deleteRecursively()
|
||||
}
|
||||
|
||||
if (reader?.isActive == false && downloadJob?.isActive != true)
|
||||
field = false
|
||||
|
||||
downloads.add(galleryBlock.id)
|
||||
} else {
|
||||
field = false
|
||||
}
|
||||
|
||||
onNotifyChangedHandler?.invoke(value)
|
||||
}
|
||||
|
||||
private val reader: Deferred<Reader>?
|
||||
private var downloadJob: Job? = null
|
||||
|
||||
private lateinit var notificationBuilder: NotificationCompat.Builder
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
|
||||
var onReaderLoadedHandler: ((Reader) -> Unit)? = null
|
||||
var onProgressHandler: ((Int) -> Unit)? = null
|
||||
var onDownloadedHandler: ((List<String>) -> Unit)? = null
|
||||
var onErrorHandler: ((Exception) -> Unit)? = null
|
||||
var onCompleteHandler: (() -> Unit)? = null
|
||||
var onNotifyChangedHandler: ((Boolean) -> Unit)? = null
|
||||
|
||||
companion object : SparseArray<GalleryDownloader>()
|
||||
|
||||
init {
|
||||
put(galleryBlock.id, this)
|
||||
|
||||
initNotification()
|
||||
|
||||
reader = CoroutineScope(Dispatchers.IO).async {
|
||||
download = _notify
|
||||
val json = Json(JsonConfiguration.Stable)
|
||||
val serializer = ReaderItem.serializer().list
|
||||
|
||||
//Check cache
|
||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "reader.json")
|
||||
|
||||
if (cache.exists()) {
|
||||
val cached = json.parse(serializer, cache.readText())
|
||||
|
||||
if (cached.isNotEmpty()) {
|
||||
useHiyobi = when {
|
||||
cached.first().url.contains("hitomi.la") -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
onReaderLoadedHandler?.invoke(cached)
|
||||
|
||||
return@async cached
|
||||
}
|
||||
}
|
||||
|
||||
//Cache doesn't exist. Load from internet
|
||||
val reader = when {
|
||||
useHiyobi -> {
|
||||
xyz.quaver.hiyobi.getReader(galleryBlock.id).let {
|
||||
when {
|
||||
it.isEmpty() -> {
|
||||
useHiyobi = false
|
||||
getReader(galleryBlock.id)
|
||||
}
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
getReader(galleryBlock.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (reader.isNotEmpty()) {
|
||||
//Save cache
|
||||
if (cache.parentFile?.exists() == false)
|
||||
cache.parentFile!!.mkdirs()
|
||||
|
||||
cache.writeText(json.stringify(serializer, reader))
|
||||
}
|
||||
|
||||
reader
|
||||
}
|
||||
}
|
||||
|
||||
private fun webpUrlFromUrl(url: String) = url.replace("/galleries/", "/webp/") + ".webp"
|
||||
|
||||
fun start() {
|
||||
downloadJob = CoroutineScope(Dispatchers.Default).launch {
|
||||
val reader = reader!!.await()
|
||||
|
||||
if (reader.isEmpty())
|
||||
onErrorHandler?.invoke(IOException("Couldn't retrieve Reader"))
|
||||
|
||||
val list = ArrayList<String>()
|
||||
|
||||
onReaderLoadedHandler?.invoke(reader)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(reader.size, 0, false)
|
||||
.setContentText("0/${reader.size}")
|
||||
|
||||
reader.chunked(4).forEachIndexed { chunkIndex, chunked ->
|
||||
chunked.mapIndexed { i, it ->
|
||||
val index = chunkIndex*4+i
|
||||
|
||||
onProgressHandler?.invoke(index)
|
||||
|
||||
notificationBuilder
|
||||
.setProgress(reader.size, index, false)
|
||||
.setContentText("$index/${reader.size}")
|
||||
|
||||
if (download)
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
|
||||
async(Dispatchers.IO) {
|
||||
val url = if (it.galleryInfo?.haswebp == 1) webpUrlFromUrl(it.url) else it.url
|
||||
|
||||
val name = "$index".padStart(4, '0')
|
||||
val ext = url.split('.').last()
|
||||
|
||||
val cache = File(getCachedGallery(this@GalleryDownloader, galleryBlock.id), "images/$name.$ext")
|
||||
|
||||
if (!cache.exists())
|
||||
try {
|
||||
with(URL(url).openConnection() as HttpsURLConnection) {
|
||||
if (useHiyobi) {
|
||||
setRequestProperty("User-Agent", user_agent)
|
||||
setRequestProperty("Cookie", cookie)
|
||||
} else
|
||||
setRequestProperty("Referer", getReferer(galleryBlock.id))
|
||||
|
||||
if (cache.parentFile?.exists() == false)
|
||||
cache.parentFile!!.mkdirs()
|
||||
|
||||
inputStream.copyTo(FileOutputStream(cache))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cache.delete()
|
||||
|
||||
onErrorHandler?.invoke(e)
|
||||
|
||||
notificationBuilder
|
||||
.setContentTitle(galleryBlock.title)
|
||||
.setContentText(getString(R.string.reader_notification_error))
|
||||
.setProgress(0, 0, false)
|
||||
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
}
|
||||
|
||||
cache.absolutePath
|
||||
}
|
||||
}.forEach {
|
||||
list.add(it.await())
|
||||
onDownloadedHandler?.invoke(list)
|
||||
}
|
||||
}
|
||||
|
||||
Timer(false).schedule(1000) {
|
||||
notificationBuilder
|
||||
.setContentTitle(galleryBlock.title)
|
||||
.setContentText(getString(R.string.reader_notification_complete))
|
||||
.setProgress(0, 0, false)
|
||||
|
||||
if (download) {
|
||||
File(cacheDir, "imageCache/${galleryBlock.id}").let {
|
||||
if (it.exists()) {
|
||||
val target = File(getDownloadDirectory(this@GalleryDownloader), galleryBlock.id.toString())
|
||||
|
||||
if (!target.exists())
|
||||
target.mkdirs()
|
||||
|
||||
it.copyRecursively(target, true)
|
||||
it.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
notificationManager.notify(galleryBlock.id, notificationBuilder.build())
|
||||
|
||||
download = false
|
||||
}
|
||||
|
||||
onCompleteHandler?.invoke()
|
||||
}
|
||||
|
||||
remove(galleryBlock.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
downloadJob?.cancel()
|
||||
|
||||
remove(galleryBlock.id)
|
||||
}
|
||||
|
||||
suspend fun cancelAndJoin() {
|
||||
downloadJob?.cancelAndJoin()
|
||||
|
||||
remove(galleryBlock.id)
|
||||
}
|
||||
|
||||
fun invokeOnReaderLoaded() {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
onReaderLoadedHandler?.invoke(reader?.await() ?: return@launch)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearNotification() {
|
||||
notificationManager.cancel(galleryBlock.id)
|
||||
}
|
||||
|
||||
fun invokeOnNotifyChanged() {
|
||||
onNotifyChangedHandler?.invoke(download)
|
||||
}
|
||||
|
||||
private fun initNotification() {
|
||||
val intent = Intent(this, ReaderActivity::class.java).apply {
|
||||
putExtra("galleryblock", Json(JsonConfiguration.Stable).stringify(GalleryBlock.serializer(), galleryBlock))
|
||||
}
|
||||
val pendingIntent = TaskStackBuilder.create(this).run {
|
||||
addNextIntentWithParentStack(intent)
|
||||
getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
notificationBuilder = NotificationCompat.Builder(this, "download").apply {
|
||||
setContentTitle(galleryBlock.title)
|
||||
setContentText(getString(R.string.reader_notification_text))
|
||||
setSmallIcon(R.drawable.ic_download)
|
||||
setContentIntent(pendingIntent)
|
||||
setProgress(0, 0, true)
|
||||
priority = NotificationCompat.PRIORITY_LOW
|
||||
}
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package xyz.quaver.pupil.util;
|
||||
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import xyz.quaver.pupil.R;
|
||||
|
||||
/*
|
||||
Source: http://www.littlerobots.nl/blog/Handle-Android-RecyclerView-Clicks/
|
||||
USAGE:
|
||||
|
||||
ItemClickSupport.addTo(mRecyclerView).setOnItemClickListener(new ItemClickSupport.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
|
||||
// do it
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
public class ItemClickSupport {
|
||||
private final RecyclerView mRecyclerView;
|
||||
private OnItemClickListener mOnItemClickListener;
|
||||
private OnItemLongClickListener mOnItemLongClickListener;
|
||||
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mOnItemClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
mOnItemClickListener.onItemClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
}
|
||||
};
|
||||
private View.OnLongClickListener mOnLongClickListener = new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (mOnItemLongClickListener != null) {
|
||||
RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(v);
|
||||
return mOnItemLongClickListener.onItemLongClicked(mRecyclerView, holder.getAdapterPosition(), v);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
private RecyclerView.OnChildAttachStateChangeListener mAttachListener
|
||||
= new RecyclerView.OnChildAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onChildViewAttachedToWindow(@NonNull View view) {
|
||||
if (mOnItemClickListener != null) {
|
||||
view.setOnClickListener(mOnClickListener);
|
||||
}
|
||||
if (mOnItemLongClickListener != null) {
|
||||
view.setOnLongClickListener(mOnLongClickListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildViewDetachedFromWindow(@NonNull View view) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
private ItemClickSupport(RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
mRecyclerView.setTag(R.id.item_click_support, this);
|
||||
mRecyclerView.addOnChildAttachStateChangeListener(mAttachListener);
|
||||
}
|
||||
|
||||
public static ItemClickSupport addTo(RecyclerView view) {
|
||||
ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
|
||||
if (support == null) {
|
||||
support = new ItemClickSupport(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public static ItemClickSupport removeFrom(RecyclerView view) {
|
||||
ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
|
||||
if (support != null) {
|
||||
support.detach(view);
|
||||
}
|
||||
return support;
|
||||
}
|
||||
|
||||
public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
|
||||
mOnItemClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
|
||||
mOnItemLongClickListener = listener;
|
||||
return this;
|
||||
}
|
||||
|
||||
private void detach(RecyclerView view) {
|
||||
view.removeOnChildAttachStateChangeListener(mAttachListener);
|
||||
view.setTag(R.id.item_click_support, null);
|
||||
}
|
||||
|
||||
public interface OnItemClickListener {
|
||||
|
||||
void onItemClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
|
||||
public interface OnItemLongClickListener {
|
||||
|
||||
boolean onItemLongClicked(RecyclerView recyclerView, int position, View v);
|
||||
}
|
||||
}
|
||||
156
app/src/main/java/xyz/quaver/pupil/util/PupilHttpClient.kt
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2022 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.utils.io.core.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
data class RemoteSourceInfo(
|
||||
val projectName: String,
|
||||
val name: String,
|
||||
val version: String
|
||||
)
|
||||
|
||||
class Release(
|
||||
val version: String,
|
||||
val apkUrl: String,
|
||||
val releaseNotes: Map<Locale, String>
|
||||
)
|
||||
|
||||
private val localeMap = mapOf(
|
||||
"한국어" to Locale.KOREAN,
|
||||
"日本語" to Locale.JAPANESE,
|
||||
"English" to Locale.ENGLISH
|
||||
)
|
||||
|
||||
class PupilHttpClient(engine: HttpClientEngine) {
|
||||
private val httpClient = HttpClient(engine) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a list of available sources from PupilSources repository.
|
||||
* Returns empty map when exception occurs
|
||||
*/
|
||||
suspend fun getRemoteSourceList(): Map<String, RemoteSourceInfo> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
httpClient.get("https://tom5079.github.io/PupilSources/versions.json").body<Map<String, RemoteSourceInfo>>()
|
||||
}.getOrDefault(emptyMap())
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads specific file from :url to :dest.
|
||||
* Returns flow that emits progress.
|
||||
* when value emitted by flow {
|
||||
* in 0f .. 1f -> downloading
|
||||
* POSITIVE_INFINITY -> download finised
|
||||
* NEGATIVE_INFINITY -> exception occured
|
||||
* }
|
||||
*/
|
||||
fun downloadFile(url: String, dest: File) = flow {
|
||||
runCatching {
|
||||
httpClient.prepareGet(url).execute { response ->
|
||||
val channel = response.bodyAsChannel()
|
||||
val contentLength = response.contentLength() ?: -1
|
||||
var readBytes = 0f
|
||||
|
||||
dest.outputStream().use { outputStream ->
|
||||
while (!channel.isClosedForRead) {
|
||||
val packet = channel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
|
||||
while (!packet.isEmpty) {
|
||||
val bytes = packet.readBytes()
|
||||
outputStream.write(bytes)
|
||||
|
||||
readBytes += bytes.size
|
||||
emit(readBytes / contentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit(Float.POSITIVE_INFINITY)
|
||||
}.onFailure {
|
||||
emit(Float.NEGATIVE_INFINITY)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* Latest application release info from Github API.
|
||||
* Returns null when exception occurs.
|
||||
*/
|
||||
suspend fun latestRelease(beta: Boolean = true): Release? = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val releases = Json.parseToJsonElement(
|
||||
httpClient.get("https://api.github.com/repos/tom5079/Pupil/releases").bodyAsText()
|
||||
).jsonArray
|
||||
|
||||
val latestRelease = releases.first { release ->
|
||||
beta || !release.jsonObject["prerelease"]!!.jsonPrimitive.boolean
|
||||
}.jsonObject
|
||||
|
||||
val version = latestRelease["tag_name"]!!.jsonPrimitive.content
|
||||
|
||||
val apkUrl = latestRelease["assets"]!!.jsonArray.first { asset ->
|
||||
val name = asset.jsonObject["name"]!!.jsonPrimitive.content
|
||||
name.startsWith("Pupil-v") && name.endsWith(".apk")
|
||||
}.jsonObject["browser_download_url"]!!.jsonPrimitive.content
|
||||
|
||||
val releaseNotes: Map<Locale, String> = buildMap {
|
||||
val body = latestRelease["body"]!!.jsonPrimitive.content
|
||||
|
||||
var locale: Locale? = null
|
||||
val stringBuilder = StringBuilder()
|
||||
body.lineSequence().forEach { line ->
|
||||
localeMap[line.drop(3)]?.let { newLocale ->
|
||||
if (locale != null) {
|
||||
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
||||
stringBuilder.clear()
|
||||
}
|
||||
locale = newLocale
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (locale != null) stringBuilder.appendLine(line)
|
||||
}
|
||||
put(locale!!, stringBuilder.deleteCharAt(stringBuilder.length-1).toString())
|
||||
}
|
||||
|
||||
Release(version, apkUrl, releaseNotes)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.text.style.LineHeightSpan
|
||||
|
||||
class SetLineOverlap(private val overlap: Boolean) : LineHeightSpan {
|
||||
companion object {
|
||||
private var originalBottom = 15
|
||||
private var originalDescent = 13
|
||||
private var overlapSaved = false
|
||||
}
|
||||
|
||||
override fun chooseHeight(
|
||||
text: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
spanstartv: Int,
|
||||
lineHeight: Int,
|
||||
fm: Paint.FontMetricsInt?
|
||||
) {
|
||||
fm ?: return
|
||||
|
||||
if (overlap) {
|
||||
if (overlapSaved) {
|
||||
originalBottom = fm.bottom
|
||||
originalDescent = fm.descent
|
||||
overlapSaved = true
|
||||
}
|
||||
fm.bottom += fm.top
|
||||
fm.descent += fm.top
|
||||
} else {
|
||||
fm.bottom = originalBottom
|
||||
fm.descent = originalDescent
|
||||
overlapSaved = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.core.content.ContextCompat
|
||||
import java.io.File
|
||||
|
||||
fun getCachedGallery(context: Context, galleryID: Int): File {
|
||||
return File(getDownloadDirectory(context), galleryID.toString()).let {
|
||||
when {
|
||||
it.exists() -> it
|
||||
else -> File(context.cacheDir, "imageCache/$galleryID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getDownloadDirectory(context: Context): File? {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
context.getExternalFilesDir("Pupil")
|
||||
else
|
||||
File(Environment.getExternalStorageDirectory(), "Pupil")
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import kotlinx.serialization.ImplicitReflectionSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import kotlinx.serialization.parseList
|
||||
import kotlinx.serialization.stringify
|
||||
import java.io.File
|
||||
|
||||
class Histories(private val file: File) : ArrayList<Int>() {
|
||||
|
||||
init {
|
||||
if (!file.exists())
|
||||
file.parentFile.mkdirs()
|
||||
|
||||
try {
|
||||
load()
|
||||
} catch (e: Exception) {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun load() : Histories {
|
||||
return apply {
|
||||
super.clear()
|
||||
addAll(
|
||||
Json(JsonConfiguration.Stable).parseList(
|
||||
file.bufferedReader().use { it.readText() }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
fun save() {
|
||||
file.writeText(Json(JsonConfiguration.Stable).stringify(this))
|
||||
}
|
||||
|
||||
override fun add(element: Int): Boolean {
|
||||
load()
|
||||
|
||||
if (contains(element))
|
||||
super.remove(element)
|
||||
|
||||
super.add(0, element)
|
||||
|
||||
save()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun remove(element: Int): Boolean {
|
||||
load()
|
||||
val retval = super.remove(element)
|
||||
save()
|
||||
|
||||
return retval
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
super.clear()
|
||||
save()
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonConfiguration
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
fun hash(password: String): String {
|
||||
val bytes = password.toByteArray()
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
return md.digest(bytes).fold("") { str, it -> str + "%02x".format(it) }
|
||||
}
|
||||
|
||||
// Ret1: SHA-256 Hash
|
||||
// Ret2: Hash salt
|
||||
fun hashWithSalt(password: String): Pair<String, String> {
|
||||
val salt = (0 until 12).map { source.random() }.joinToString()
|
||||
|
||||
return Pair(hash(password+salt), salt)
|
||||
}
|
||||
|
||||
val source = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
@Serializable
|
||||
data class Lock(val type: Type, val hash: String, val salt: String) {
|
||||
|
||||
enum class Type {
|
||||
PATTERN,
|
||||
PIN,
|
||||
PASSWORD
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun generate(type: Type, password: String): Lock {
|
||||
val (hash, salt) = hashWithSalt(password)
|
||||
return Lock(type, hash, salt)
|
||||
}
|
||||
}
|
||||
|
||||
fun match(password: String): Boolean {
|
||||
return hash(password+salt) == hash
|
||||
}
|
||||
}
|
||||
|
||||
class LockManager(base: Context): ContextWrapper(base) {
|
||||
|
||||
var locks: ArrayList<Lock>? = null
|
||||
|
||||
init {
|
||||
load()
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun load() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
if (!lock.exists()) {
|
||||
lock.createNewFile()
|
||||
lock.writeText("[]")
|
||||
}
|
||||
|
||||
locks = ArrayList(Json(JsonConfiguration.Stable).parseList(lock.readText()))
|
||||
}
|
||||
|
||||
@UseExperimental(ImplicitReflectionSerializer::class)
|
||||
private fun save() {
|
||||
val lock = File(ContextCompat.getDataDir(this), "lock.json")
|
||||
|
||||
if (!lock.exists())
|
||||
lock.createNewFile()
|
||||
|
||||
lock.writeText(Json(JsonConfiguration.Stable).stringify(locks?.toList() ?: listOf()))
|
||||
}
|
||||
|
||||
fun add(lock: Lock) {
|
||||
remove(lock.type)
|
||||
locks?.add(lock)
|
||||
save()
|
||||
}
|
||||
|
||||
fun remove(type: Lock.Type) {
|
||||
locks?.removeAll { it.type == type }
|
||||
save()
|
||||
}
|
||||
|
||||
fun check(password: String): Boolean? {
|
||||
return locks?.any {
|
||||
it.match(password)
|
||||
}
|
||||
}
|
||||
|
||||
fun empty(): Boolean {
|
||||
return locks.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun contains(type: Lock.Type): Boolean {
|
||||
return locks?.any { it.type == type } ?: false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.serialization.json.*
|
||||
import java.net.URL
|
||||
|
||||
fun getReleases(url: String) : JsonArray {
|
||||
return try {
|
||||
URL(url).readText().let {
|
||||
Json(JsonConfiguration.Stable).parse(JsonArray.serializer(), it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
JsonArray(emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
fun checkUpdate(url: String, currentVersion: String) : JsonObject? {
|
||||
val releases = getReleases(url)
|
||||
|
||||
if (releases.isEmpty())
|
||||
return null
|
||||
|
||||
val latestVersion = releases[0].jsonObject["tag_name"]?.content
|
||||
|
||||
return when {
|
||||
currentVersion.split('-').size == 1 -> {
|
||||
when {
|
||||
currentVersion != latestVersion -> releases[0].jsonObject
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
(currentVersion.split('-')[0] == latestVersion) -> releases[0].jsonObject
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
app/src/main/java/xyz/quaver/pupil/util/util.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2022 tom5079
|
||||
*
|
||||
* 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
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
|
||||
fun Context.launchApkInstaller(file: File) {
|
||||
val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
setDataAndType(uri, MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"))
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
tools:ignore="NewApi">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 19 9 L 15 9 L 15 3 L 9 3 L 9 9 L 5 9 L 12 16 L 19 9 Z"
|
||||
android:fillColor="#fff"
|
||||
android:strokeWidth="1"/>
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:pathData="M 5 19 L 19 19"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#fff"
|
||||
android:strokeWidth="2"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="path_2">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathEnd"
|
||||
android:duration="500"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="0.8"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathStart"
|
||||
android:startOffset="500"
|
||||
android:duration="500"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="0.8"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
<objectAnimator
|
||||
android:propertyName="trimPathOffset"
|
||||
android:duration="1000"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="0.2"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#333333">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#333333">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#333333">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#333333">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#333333">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#333333">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
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"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.1,12.9a2.8,2.8 0,0 0,0.1 -0.9,2.8 2.8,0 0,0 -0.1,-0.9l2.1,-1.6a0.7,0.7 0,0 0,0.1 -0.6L19.4,5.5a0.7,0.7 0,0 0,-0.6 -0.2l-2.4,1a6.5,6.5 0,0 0,-1.6 -0.9l-0.4,-2.6a0.5,0.5 0,0 0,-0.5 -0.4H10.1a0.5,0.5 0,0 0,-0.5 0.4L9.3,5.4a5.6,5.6 0,0 0,-1.7 0.9l-2.4,-1a0.4,0.4 0,0 0,-0.5 0.2l-2,3.4c-0.1,0.2 0,0.4 0.2,0.6l2,1.6a2.8,2.8 0,0 0,-0.1 0.9,2.8 2.8,0 0,0 0.1,0.9L2.8,14.5a0.7,0.7 0,0 0,-0.1 0.6l1.9,3.4a0.7,0.7 0,0 0,0.6 0.2l2.4,-1a6.5,6.5 0,0 0,1.6 0.9l0.4,2.6a0.5,0.5 0,0 0,0.5 0.4h3.8a0.5,0.5 0,0 0,0.5 -0.4l0.3,-2.6a5.6,5.6 0,0 0,1.7 -0.9l2.4,1a0.4,0.4 0,0 0,0.5 -0.2l2,-3.4c0.1,-0.2 0,-0.4 -0.2,-0.6ZM12,15.6A3.6,3.6 0,1 1,15.6 12,3.6 3.6,0 0,1 12,15.6Z"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 620 B |
|
Before Width: | Height: | Size: 975 B |
|
Before Width: | Height: | Size: 325 B |
|
Before Width: | Height: | Size: 197 B |
|
Before Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 571 B |
|
Before Width: | Height: | Size: 585 B |
|
Before Width: | Height: | Size: 279 B |
|
Before Width: | Height: | Size: 361 B |
|
Before Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 469 B |
|
Before Width: | Height: | Size: 965 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 793 B |
|
Before Width: | Height: | Size: 802 B |
|
Before Width: | Height: | Size: 495 B |
|
Before Width: | Height: | Size: 639 B |
|
Before Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 817 B |
|
Before Width: | Height: | Size: 670 B |
|
Before Width: | Height: | Size: 934 B |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 979 B |
|
Before Width: | Height: | Size: 636 B |
|
Before Width: | Height: | Size: 760 B |
|
Before Width: | Height: | Size: 235 B |
|
Before Width: | Height: | Size: 145 B |
|
Before Width: | Height: | Size: 99 B |
|
Before Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 1001 B |
|
Before Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 366 B |
|
Before Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 224 B |
|
Before Width: | Height: | Size: 236 B |
|
Before Width: | Height: | Size: 352 B |
|
Before Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |