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 files
|
||||||
.gradle
|
.gradle/
|
||||||
/local.properties
|
build/
|
||||||
/.idea/caches
|
|
||||||
/.idea/libraries
|
|
||||||
/.idea/modules.xml
|
|
||||||
/.idea/workspace.xml
|
|
||||||
/.idea/navEditor.xml
|
|
||||||
/.idea/assetWizardSettings.xml
|
|
||||||
.DS_Store
|
|
||||||
/build
|
|
||||||
/captures
|
|
||||||
.externalNativeBuild
|
|
||||||
|
|
||||||
#Github pages
|
# Local configuration file (sdk path, etc)
|
||||||
/gh-pages
|
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>
|
|
||||||
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
License is specified in following module separately
|
License is specified in following module separately
|
||||||
|
|
||||||
app/
|
app/
|
||||||
libpupil/
|
libpupil/
|
||||||
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"]}}
|
||||||
28
app/proguard-rules.pro
vendored
@@ -1,6 +1,6 @@
|
|||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# 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
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
@@ -18,4 +18,28 @@
|
|||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-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
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import androidx.test.rule.ActivityTestRule
|
import com.google.api.Http
|
||||||
import org.junit.Assert.assertEquals
|
import io.ktor.client.*
|
||||||
import org.junit.Rule
|
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.Test
|
||||||
import org.junit.runner.RunWith
|
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.
|
* Instrumented test, which will execute on an Android device.
|
||||||
@@ -29,34 +47,5 @@ class ExampleInstrumentedTest {
|
|||||||
fun useAppContext() {
|
fun useAppContext() {
|
||||||
// Context of the app under test.
|
// Context of the app under test.
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="xyz.quaver.pupil">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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
|
<application
|
||||||
android:name=".Pupil"
|
android:name=".Pupil"
|
||||||
@@ -13,114 +19,38 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
<activity android:name=".ui.LockActivity"/>
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
<activity
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:name=".ui.ReaderActivity"
|
tools:replace="android:theme"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
tools:ignore="UnusedAttribute">
|
||||||
android:parentActivityName=".ui.MainActivity">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<provider
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
|
||||||
<data
|
<meta-data
|
||||||
android:host="hitomi.la"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:pathPrefix="/galleries"
|
android:resource="@xml/file_paths" />
|
||||||
android:scheme="https" />
|
</provider>
|
||||||
</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="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
|
<activity
|
||||||
android:name=".ui.MainActivity"
|
android:name=".ui.MainActivity"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:theme="@style/NoActionBarAppTheme">
|
android:theme="@style/NoActionBarAppTheme"
|
||||||
|
android:exported="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</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
|
package xyz.quaver.pupil
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
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.GooglePlayServicesNotAvailableException
|
||||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||||
import com.google.android.gms.security.ProviderInstaller
|
import com.google.android.gms.security.ProviderInstaller
|
||||||
import xyz.quaver.pupil.util.Histories
|
import io.ktor.client.*
|
||||||
import java.io.File
|
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
|
override val di: DI by DI.lazy {
|
||||||
lateinit var downloads: Histories
|
import(androidXModule(this@Pupil))
|
||||||
lateinit var favorites: Histories
|
|
||||||
|
|
||||||
init {
|
bind { singleton { NetworkCache(this@Pupil) } }
|
||||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
|
||||||
|
bindSingleton { settingsDataStore }
|
||||||
|
|
||||||
|
bind { singleton { PupilHttpClient(OkHttp.create()) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
val preference = PreferenceManager.getDefaultSharedPreferences(this)
|
super.onCreate()
|
||||||
|
|
||||||
histories = Histories(File(ContextCompat.getDataDir(this), "histories.json"))
|
|
||||||
downloads = Histories(File(ContextCompat.getDataDir(this), "downloads.json"))
|
|
||||||
favorites = Histories(File(ContextCompat.getDataDir(this), "favorites.json"))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProviderInstaller.installIfNeeded(this)
|
ProviderInstaller.installIfNeeded(this)
|
||||||
@@ -40,22 +60,29 @@ class Pupil : MultiDexApplication() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!preference.getBoolean("channel_created", false)) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
val channel = 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("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(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
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -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 |