mirror of
https://github.com/rebelonion/Dantotsu.git
synced 2026-01-12 22:07:39 +00:00
Compare commits
2192 Commits
v2.1.0
...
l10n_dev_c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53807ff68d | ||
|
|
902bf96a29 | ||
|
|
91743dc0b7 | ||
|
|
384f8603b5 | ||
|
|
6c4edda017 | ||
|
|
2f4f7345e4 | ||
|
|
8847d37a7d | ||
|
|
4e1f82aada | ||
|
|
2395a57271 | ||
|
|
43cbf35761 | ||
|
|
d095f87a1a | ||
|
|
6be5fbd0c8 | ||
|
|
5450d331b8 | ||
|
|
d53cdce2e2 | ||
|
|
bb8d03f776 | ||
|
|
df65eec0d3 | ||
|
|
b112db7803 | ||
|
|
178d7f7820 | ||
|
|
afaa162907 | ||
|
|
1c4a7ff8af | ||
|
|
19c14d81c3 | ||
|
|
a794550969 | ||
|
|
dfa55741cf | ||
|
|
2a32c92cfc | ||
|
|
eaaf0f355a | ||
|
|
7262795cb0 | ||
|
|
4c7a644e0a | ||
|
|
b802338fdf | ||
|
|
d4346d292a | ||
|
|
ba3e5b8ae2 | ||
|
|
08b9820ff6 | ||
|
|
65cd62a94a | ||
|
|
0154eeace4 | ||
|
|
bb8db3cc67 | ||
|
|
b03908ca5c | ||
|
|
67b8e51fdb | ||
|
|
3bf67b1997 | ||
|
|
486b3fbaa9 | ||
|
|
7d5264760d | ||
|
|
2707e461e7 | ||
|
|
d82f4b633a | ||
|
|
17ab29dc03 | ||
|
|
b66e625a53 | ||
|
|
b54384fe33 | ||
|
|
edfa6f0ed0 | ||
|
|
53e5f86ae2 | ||
|
|
a6531ac9cc | ||
|
|
7b9e4d4870 | ||
|
|
aa2e0cd5e0 | ||
|
|
4d0e777401 | ||
|
|
9f6882888f | ||
|
|
2044681859 | ||
|
|
7b17f9a29c | ||
|
|
46e094bdf8 | ||
|
|
a60e371b0f | ||
|
|
aa5f94cd14 | ||
|
|
baf6f59f8a | ||
|
|
fa0c69aecf | ||
|
|
ebd1a22c75 | ||
|
|
16cce05bbd | ||
|
|
0daf2cf26f | ||
|
|
8bbfa6166b | ||
|
|
dbc3884b1c | ||
|
|
f2b5b5ef62 | ||
|
|
a5c57ab6c2 | ||
|
|
6ac7978d7c | ||
|
|
b26c764999 | ||
|
|
de3012692f | ||
|
|
0ca0474920 | ||
|
|
8328759f95 | ||
|
|
393afa4159 | ||
|
|
a25f701e8f | ||
|
|
903a77bd6d | ||
|
|
353f7023a8 | ||
|
|
4b8d96849f | ||
|
|
090fcf8e10 | ||
|
|
8df7cb9dd2 | ||
|
|
c6188c45dc | ||
|
|
25b5ea1474 | ||
|
|
35bdfe3999 | ||
|
|
bd70ee1031 | ||
|
|
640fff73f5 | ||
|
|
a271c5740f | ||
|
|
31b8d7c7f9 | ||
|
|
983afff3f7 | ||
|
|
e89c180f09 | ||
|
|
aac0246fd6 | ||
|
|
aa10bd8000 | ||
|
|
ebd0cca6b6 | ||
|
|
4b21582059 | ||
|
|
1ca92afb65 | ||
|
|
49d92e1867 | ||
|
|
120a91b591 | ||
|
|
bf74fbfd15 | ||
|
|
a564a2f48b | ||
|
|
2687769c64 | ||
|
|
a17991be84 | ||
|
|
700cdf4a93 | ||
|
|
773c1d17c4 | ||
|
|
3222931be6 | ||
|
|
fd63096466 | ||
|
|
f4a799d514 | ||
|
|
9f65441677 | ||
|
|
ceb10ba179 | ||
|
|
17547ad7b6 | ||
|
|
02a6c85575 | ||
|
|
963ab68f83 | ||
|
|
e85425ede8 | ||
|
|
7835a90aed | ||
|
|
2ac94487ac | ||
|
|
7ec12e5ee1 | ||
|
|
840c5635d2 | ||
|
|
bc7c46b3e8 | ||
|
|
2136181d2c | ||
|
|
783eb588b1 | ||
|
|
9345295270 | ||
|
|
e257a8ca99 | ||
|
|
c062780519 | ||
|
|
8fa3a995db | ||
|
|
7e0ade4a38 | ||
|
|
e0fb5530f6 | ||
|
|
d94e8be220 | ||
|
|
472e4a788c | ||
|
|
300d0600af | ||
|
|
87ae95dffc | ||
|
|
4873b8b6ee | ||
|
|
26ed3bacc5 | ||
|
|
5fbf9fdb26 | ||
|
|
f0bcbb5fee | ||
|
|
b494b60251 | ||
|
|
e621306f6d | ||
|
|
94d69d530e | ||
|
|
5fb343972a | ||
|
|
edbe16959d | ||
|
|
8d77450384 | ||
|
|
6a69567cdc | ||
|
|
645972c451 | ||
|
|
114504cfa2 | ||
|
|
0a1d9090c0 | ||
|
|
12cb670af4 | ||
|
|
3ff66d7792 | ||
|
|
b34d6c1cb0 | ||
|
|
1fe5e5f0c0 | ||
|
|
58251d19ef | ||
|
|
1715cebaea | ||
|
|
ba6c162e1f | ||
|
|
8137ef0acc | ||
|
|
836cdde7fb | ||
|
|
dc3d48edd6 | ||
|
|
2dffa1c4ac | ||
|
|
f829fd29e0 | ||
|
|
d47e0bf6af | ||
|
|
cb495dffbd | ||
|
|
3d109b1ff0 | ||
|
|
b8d6b097fe | ||
|
|
999b6f8f4c | ||
|
|
71d85838c7 | ||
|
|
9d509c581f | ||
|
|
7c8b8b17a2 | ||
|
|
126ed7a2fb | ||
|
|
93ac9b1e81 | ||
|
|
840190bd1d | ||
|
|
113e156900 | ||
|
|
e1a07c7c3b | ||
|
|
c1e657d63a | ||
|
|
019b4e3389 | ||
|
|
44b904ac81 | ||
|
|
22223af035 | ||
|
|
90f7256edc | ||
|
|
927435a1c6 | ||
|
|
47b9e8cc3b | ||
|
|
f5a03146ea | ||
|
|
2c8bff7b78 | ||
|
|
00084bd87d | ||
|
|
b46de0bd62 | ||
|
|
d317f9bf36 | ||
|
|
6226f933ff | ||
|
|
bbc0d97d2a | ||
|
|
7fa74c755b | ||
|
|
9d0708f2ff | ||
|
|
ab9ad65e5b | ||
|
|
81bb2586ea | ||
|
|
51c9d1c3b9 | ||
|
|
4e568a1d82 | ||
|
|
f892fab256 | ||
|
|
f8e98da62f | ||
|
|
18664fb239 | ||
|
|
7f2edc4b63 | ||
|
|
96fa35f0a9 | ||
|
|
916c585c38 | ||
|
|
e13f001379 | ||
|
|
f4e5bf8927 | ||
|
|
327b843f8a | ||
|
|
cab38e8bff | ||
|
|
bcdf515f95 | ||
|
|
2f1dbceb6c | ||
|
|
652bb76bdf | ||
|
|
ee9c9e0134 | ||
|
|
3763363199 | ||
|
|
9197f163e3 | ||
|
|
08c8e83d15 | ||
|
|
7fab2e8cb5 | ||
|
|
4f0056e0dc | ||
|
|
5aa78c739f | ||
|
|
ac747dfcc1 | ||
|
|
a7a07de396 | ||
|
|
526670ff6d | ||
|
|
60b6254167 | ||
|
|
8329ed0ce8 | ||
|
|
952beb75b3 | ||
|
|
e2729c3274 | ||
|
|
1e2dac074c | ||
|
|
ee59b5aea1 | ||
|
|
f3ba74f46a | ||
|
|
f74c32e4b8 | ||
|
|
d1860831e3 | ||
|
|
c89ca91f4a | ||
|
|
50f375f187 | ||
|
|
2cb26442d3 | ||
|
|
07f161baf9 | ||
|
|
b2ceaa01fa | ||
|
|
dfbf849691 | ||
|
|
ddc8cbf6fa | ||
|
|
bc2e05c75b | ||
|
|
2bd1f9affb | ||
|
|
b376ab2f5d | ||
|
|
494280b4aa | ||
|
|
93154eef9e | ||
|
|
007bbd3154 | ||
|
|
a54af8d312 | ||
|
|
294b6b0aa6 | ||
|
|
713fa77a0b | ||
|
|
8004a5e194 | ||
|
|
ac470df370 | ||
|
|
0eef242112 | ||
|
|
286db2e1a9 | ||
|
|
aeb00cc790 | ||
|
|
555554d3fd | ||
|
|
e148ac132d | ||
|
|
ae2f19709a | ||
|
|
f2a0897f1e | ||
|
|
97eb752000 | ||
|
|
a92d41398d | ||
|
|
4c360d306a | ||
|
|
aaa4751c17 | ||
|
|
20498bc429 | ||
|
|
c4062af91a | ||
|
|
2d7fb67ad5 | ||
|
|
8eae6906d8 | ||
|
|
54d60c9603 | ||
|
|
a577970705 | ||
|
|
2e97a62365 | ||
|
|
abe4a1e394 | ||
|
|
b42ec620df | ||
|
|
58a68271fc | ||
|
|
c58eb14c59 | ||
|
|
7726abcf00 | ||
|
|
93f9547e3e | ||
|
|
4f72028284 | ||
|
|
caa4ff6d7a | ||
|
|
92655c62c4 | ||
|
|
671565b80c | ||
|
|
020e0e385e | ||
|
|
aa271aa26d | ||
|
|
ccae92a605 | ||
|
|
ca3057326d | ||
|
|
417acee7da | ||
|
|
b95da94f7f | ||
|
|
2cbb78f804 | ||
|
|
a2903ec1bf | ||
|
|
48fae1cc73 | ||
|
|
cc5faa8f9a | ||
|
|
5dfa5001fa | ||
|
|
4859fa2532 | ||
|
|
d6b3cd3e4f | ||
|
|
9ab29af5af | ||
|
|
a4a82d6a56 | ||
|
|
2aa742aaf0 | ||
|
|
1e951baeb4 | ||
|
|
1a0d912083 | ||
|
|
0071286153 | ||
|
|
6a966e7166 | ||
|
|
734805458d | ||
|
|
8de9831954 | ||
|
|
04a2cea6fe | ||
|
|
2f34669301 | ||
|
|
4f6ab17ea1 | ||
|
|
ccbb50bc3a | ||
|
|
a3bf2bc18e | ||
|
|
b616850895 | ||
|
|
ed2c8902fd | ||
|
|
d59c96fad9 | ||
|
|
1ad544d0df | ||
|
|
942bf83d24 | ||
|
|
408d1abe16 | ||
|
|
0e198b3423 | ||
|
|
d1b0cf65b0 | ||
|
|
323ab1d4ab | ||
|
|
7950982bfc | ||
|
|
d70b3fd541 | ||
|
|
6877c82315 | ||
|
|
860d108f82 | ||
|
|
bacc7708c0 | ||
|
|
d4766e27be | ||
|
|
bf130bfc72 | ||
|
|
09aa955299 | ||
|
|
4efbdd0554 | ||
|
|
12f0a83c7d | ||
|
|
26c345d130 | ||
|
|
660a40ce23 | ||
|
|
2fda4d62b7 | ||
|
|
e00942001a | ||
|
|
13d9084456 | ||
|
|
5ee1d2d5b7 | ||
|
|
87e31ff7f7 | ||
|
|
842f24af91 | ||
|
|
fb2373c385 | ||
|
|
ca50c3c1d4 | ||
|
|
ad7f452b8e | ||
|
|
a7e6773428 | ||
|
|
aa1c46caf3 | ||
|
|
0ef412d5f3 | ||
|
|
ba8772b54d | ||
|
|
13595fe1b9 | ||
|
|
59d3d4f816 | ||
|
|
b0618733a3 | ||
|
|
1d5dc80d43 | ||
|
|
1b4e675c68 | ||
|
|
d883a30a75 | ||
|
|
4bc900c74b | ||
|
|
303965ea97 | ||
|
|
b50241a123 | ||
|
|
728a02a034 | ||
|
|
005601ab53 | ||
|
|
649cab0bcc | ||
|
|
00728d1fed | ||
|
|
a398674276 | ||
|
|
bcf10dd11a | ||
|
|
1f2d7899ca | ||
|
|
c60dc58173 | ||
|
|
5fa1e8d863 | ||
|
|
441e296afa | ||
|
|
e7763b97e3 | ||
|
|
9f9076844e | ||
|
|
097ab3915e | ||
|
|
891a5e6264 | ||
|
|
3c32490d38 | ||
|
|
0964d1f531 | ||
|
|
9ffa3c4176 | ||
|
|
f8861d7a61 | ||
|
|
368a166b5c | ||
|
|
5d1dc0e70a | ||
|
|
75dd72b5d6 | ||
|
|
12d76ead3c | ||
|
|
edb24815ff | ||
|
|
c5bf3886a4 | ||
|
|
1a692e7779 | ||
|
|
4f80667943 | ||
|
|
e583701755 | ||
|
|
0f0d3e577a | ||
|
|
c655a04c41 | ||
|
|
869da4b0d8 | ||
|
|
323e725c74 | ||
|
|
965e8dc1b8 | ||
|
|
917902a695 | ||
|
|
64fb8b06ee | ||
|
|
7bf2b9cc8f | ||
|
|
f03ed32617 | ||
|
|
c7d01058e5 | ||
|
|
a039f73f4e | ||
|
|
5bd6ce8f1a | ||
|
|
5c3ad3f7a0 | ||
|
|
d297215db2 | ||
|
|
cabafc02fd | ||
|
|
becfda37a2 | ||
|
|
97882ffea7 | ||
|
|
ac4a0bb214 | ||
|
|
677dca710b | ||
|
|
1bd821edb8 | ||
|
|
4714d6b0e7 | ||
|
|
979dd05298 | ||
|
|
ac39785d4a | ||
|
|
15e900d51b | ||
|
|
2e7da05f0e | ||
|
|
5c882d43c7 | ||
|
|
5be0c6bd89 | ||
|
|
748d2f8ba1 | ||
|
|
b0ae50eabb | ||
|
|
bcf3c0f2a1 | ||
|
|
a6cee5586a | ||
|
|
2222cfe991 | ||
|
|
9f9a97998e | ||
|
|
e95181958a | ||
|
|
7777f87ab4 | ||
|
|
1b821b1524 | ||
|
|
6c83fd97c8 | ||
|
|
4046539610 | ||
|
|
63d50a73d2 | ||
|
|
b2a789d882 | ||
|
|
4222e587c5 | ||
|
|
3cc3618782 | ||
|
|
c371639aee | ||
|
|
c1db16262e | ||
|
|
0d0c5cea05 | ||
|
|
92c2e40e12 | ||
|
|
a64865603f | ||
|
|
323dc38918 | ||
|
|
6d44ac01e5 | ||
|
|
01a0288cf8 | ||
|
|
db1d66e3fd | ||
|
|
b1d821451c | ||
|
|
a0ce7464a5 | ||
|
|
91f1546d10 | ||
|
|
89a5b7812b | ||
|
|
4315c09d2e | ||
|
|
0bb02a50c1 | ||
|
|
27b3a28c8b | ||
|
|
97edd35840 | ||
|
|
3913e4f27a | ||
|
|
ce4f7f1884 | ||
|
|
6c68b48e89 | ||
|
|
de5f238b64 | ||
|
|
2d698d2a62 | ||
|
|
6db124b4cc | ||
|
|
549a34e153 | ||
|
|
9eaf959a5a | ||
|
|
ee62edcc7e | ||
|
|
2968d91535 | ||
|
|
2fb95c6099 | ||
|
|
a7825297ed | ||
|
|
e72c235078 | ||
|
|
89a4847c9f | ||
|
|
15719ad29b | ||
|
|
cc0f7113e9 | ||
|
|
004290c5fe | ||
|
|
83b28e40db | ||
|
|
737fce70fb | ||
|
|
8759c336dc | ||
|
|
d85f99e32a | ||
|
|
b2740a8b21 | ||
|
|
30fc3ff713 | ||
|
|
406007f4ed | ||
|
|
7c5e42f941 | ||
|
|
e890c417a0 | ||
|
|
0c01f435d8 | ||
|
|
dbb7759b0f | ||
|
|
905f72b965 | ||
|
|
77ee082809 | ||
|
|
3a19684dcb | ||
|
|
2890ca1c4a | ||
|
|
3b00572b91 | ||
|
|
ed89ddb2ba | ||
|
|
4de630b08a | ||
|
|
6bc26e3144 | ||
|
|
579411aefb | ||
|
|
7c9f52d986 | ||
|
|
2a55c7a846 | ||
|
|
1268763d99 | ||
|
|
1338bc0ebb | ||
|
|
423a657da7 | ||
|
|
d36d818bfa | ||
|
|
4a851ade4f | ||
|
|
8dba9243a4 | ||
|
|
b4013d1d28 | ||
|
|
6bc23ba8e1 | ||
|
|
4d6aaf6f36 | ||
|
|
291f516e6f | ||
|
|
5fb0fb14e7 | ||
|
|
c40d61f471 | ||
|
|
80da5e3480 | ||
|
|
cfd5790589 | ||
|
|
98e700ab11 | ||
|
|
5c55a9a597 | ||
|
|
2833c58856 | ||
|
|
38ccb8926d | ||
|
|
76b5e2f3c2 | ||
|
|
da02ee2706 | ||
|
|
92cae18d53 | ||
|
|
2e70f13a42 | ||
|
|
0358341e42 | ||
|
|
5a65d2ef5f | ||
|
|
ad713a357a | ||
|
|
d8776b6ab0 | ||
|
|
d2de96f486 | ||
|
|
acf1a4ea32 | ||
|
|
2fdafedc2f | ||
|
|
d04fa0eaa7 | ||
|
|
9d23caab12 | ||
|
|
2121400e78 | ||
|
|
e02da10e23 | ||
|
|
3bada69085 | ||
|
|
7845d3059f | ||
|
|
5921dd8e9b | ||
|
|
43e0aff3ab | ||
|
|
ed5a4cb9be | ||
|
|
824b13cf6e | ||
|
|
6e1f5c7993 | ||
|
|
05eceec6ce | ||
|
|
35d3e35004 | ||
|
|
3a4dec04df | ||
|
|
dd3f5e74f8 | ||
|
|
6c434bbbce | ||
|
|
8f5416080c | ||
|
|
c633c62c4c | ||
|
|
7e5622ba0a | ||
|
|
9f1253274c | ||
|
|
2c7a43b32f | ||
|
|
a1c5d5d818 | ||
|
|
851f137723 | ||
|
|
706af34e90 | ||
|
|
9d650bf3c7 | ||
|
|
f575cc8a70 | ||
|
|
ad44e77934 | ||
|
|
788bd4dd0b | ||
|
|
911e7432e4 | ||
|
|
d62b26813a | ||
|
|
32be2cf617 | ||
|
|
bbd6e7ec45 | ||
|
|
8655a53519 | ||
|
|
0b9fbe31b9 | ||
|
|
c95ee7ce82 | ||
|
|
a25dff9106 | ||
|
|
0835f879b3 | ||
|
|
b336ce3915 | ||
|
|
271f71fe21 | ||
|
|
afec155692 | ||
|
|
b08497d7cb | ||
|
|
9600fb2efb | ||
|
|
6540fb511b | ||
|
|
55689edc6e | ||
|
|
5adae74ad8 | ||
|
|
7187e681e8 | ||
|
|
b106f4e66b | ||
|
|
869d982bfb | ||
|
|
11b7de55ca | ||
|
|
11912ddff3 | ||
|
|
ba7fc2cf6d | ||
|
|
114f34feed | ||
|
|
a611265534 | ||
|
|
ceaa5d6445 | ||
|
|
08b858a28f | ||
|
|
da24a8e348 | ||
|
|
2db010d4b7 | ||
|
|
35cc7a6374 | ||
|
|
0b2b7f3769 | ||
|
|
4fa97d8abc | ||
|
|
fc9fe84744 | ||
|
|
fc27b92ef8 | ||
|
|
7d508ba843 | ||
|
|
b312535e83 | ||
|
|
319cfc7542 | ||
|
|
7a81f941c1 | ||
|
|
b5e2d5c2e9 | ||
|
|
60872f9095 | ||
|
|
ded8525d23 | ||
|
|
1c9799cdab | ||
|
|
204747fc94 | ||
|
|
13444a2176 | ||
|
|
5f7cc092c7 | ||
|
|
b026d8ffef | ||
|
|
035344677f | ||
|
|
76756bed4a | ||
|
|
78d49f7ad1 | ||
|
|
d35c9d3ea9 | ||
|
|
dbf25344a9 | ||
|
|
e83edad329 | ||
|
|
656e03f799 | ||
|
|
72d5f555ca | ||
|
|
36f52612dc | ||
|
|
dac5ee262a | ||
|
|
f693c8288d | ||
|
|
40a3a192a7 | ||
|
|
814da6172e | ||
|
|
e729b096bb | ||
|
|
5d888d6dac | ||
|
|
f7fe4d09f9 | ||
|
|
ce50b9d241 | ||
|
|
a4a53a3e0e | ||
|
|
a20c8477d7 | ||
|
|
0d73819d17 | ||
|
|
ee9e2ad4a3 | ||
|
|
9664cdc72a | ||
|
|
b2caa71f23 | ||
|
|
b31424b529 | ||
|
|
b1446f87d7 | ||
|
|
fce0b9279b | ||
|
|
cc7d7cfb22 | ||
|
|
01aa52caa1 | ||
|
|
82cb690138 | ||
|
|
194b1b1428 | ||
|
|
55bdf10282 | ||
|
|
c87034ff39 | ||
|
|
53e6509db6 | ||
|
|
d81ab5abbe | ||
|
|
50268e609e | ||
|
|
46db6182b3 | ||
|
|
321cc097e8 | ||
|
|
97fde6a5c1 | ||
|
|
b92a2999a6 | ||
|
|
736801ef1f | ||
|
|
e774678ce8 | ||
|
|
28a0e09b64 | ||
|
|
57c5d332b0 | ||
|
|
96bc7bfc53 | ||
|
|
0b2082763b | ||
|
|
9975f76f3f | ||
|
|
8de08bd0e0 | ||
|
|
9289674ff2 | ||
|
|
bde36d85dc | ||
|
|
15ece3d939 | ||
|
|
3cd60c004c | ||
|
|
cd9a64f4b7 | ||
|
|
989d3dbb77 | ||
|
|
8b9e4eca77 | ||
|
|
91207627ba | ||
|
|
d458d30e6e | ||
|
|
75b382f53c | ||
|
|
650ab0a9ac | ||
|
|
a07bef0613 | ||
|
|
01b061890c | ||
|
|
a970337ef7 | ||
|
|
878b8af0b3 | ||
|
|
bde4e73cd1 | ||
|
|
b13a6e0943 | ||
|
|
0be9ee6c49 | ||
|
|
c19d15dbf6 | ||
|
|
27bab50e57 | ||
|
|
3ea85878c9 | ||
|
|
1ff415900a | ||
|
|
19cd0596a5 | ||
|
|
603fbf6254 | ||
|
|
317958257c | ||
|
|
51cc16013a | ||
|
|
6b565ba0ab | ||
|
|
c1bbd34f51 | ||
|
|
795ceb9f5b | ||
|
|
74e73a663b | ||
|
|
d3f34f0bae | ||
|
|
1e1cd929c9 | ||
|
|
b19f9326d1 | ||
|
|
cbe3e556ca | ||
|
|
ea2a3fb56d | ||
|
|
3f5eb4b858 | ||
|
|
898bba3827 | ||
|
|
63c3b1b5e8 | ||
|
|
a19a896da3 | ||
|
|
ac5f810a12 | ||
|
|
b03ad95e4c | ||
|
|
ed7d49644b | ||
|
|
73a7143ea3 | ||
|
|
80de313a26 | ||
|
|
fe2d89f6a3 | ||
|
|
e53ad65049 | ||
|
|
fcda0eae62 | ||
|
|
01323c4d2c | ||
|
|
d62705f509 | ||
|
|
3f26e78de1 | ||
|
|
b47f231701 | ||
|
|
ec8bb5634a | ||
|
|
bc39551fb1 | ||
|
|
a3e84ce41a | ||
|
|
11ffca8650 | ||
|
|
4584b1b74d | ||
|
|
1afee8c9ee | ||
|
|
5012c4efa6 | ||
|
|
83af384e80 | ||
|
|
6b32ea4acb | ||
|
|
f677121794 | ||
|
|
7118dfc74a | ||
|
|
55f400c205 | ||
|
|
9ba281f552 | ||
|
|
beece59947 | ||
|
|
0d43fc0781 | ||
|
|
e14142b4f6 | ||
|
|
765b6d2452 | ||
|
|
076d821e8c | ||
|
|
17985bf61f | ||
|
|
03dbeddabb | ||
|
|
ae372998f0 | ||
|
|
262ce571ba | ||
|
|
c0eb96f3cd | ||
|
|
cfbf4e109d | ||
|
|
e05af0dfcc | ||
|
|
64f078263f | ||
|
|
c4aa8c91f8 | ||
|
|
a91285a40f | ||
|
|
d56578b8be | ||
|
|
b05c14e880 | ||
|
|
60644590f4 | ||
|
|
a8b1dc11a5 | ||
|
|
2cbeb28cd7 | ||
|
|
579c23907f | ||
|
|
a16ae62fb3 | ||
|
|
0bf623c9d4 | ||
|
|
67dd9b8e01 | ||
|
|
7b1824cf36 | ||
|
|
085adbfdaa | ||
|
|
7fb70627f9 | ||
|
|
ea12e5a054 | ||
|
|
a5b24ac8b7 | ||
|
|
830ca60883 | ||
|
|
f0c1bc88a5 | ||
|
|
cea67261d5 | ||
|
|
c33934c211 | ||
|
|
3ec0b9aba2 | ||
|
|
748b359615 | ||
|
|
e22734dd54 | ||
|
|
6dc6f801a8 | ||
|
|
c6c95cdde7 | ||
|
|
c0d0d931b0 | ||
|
|
991a040f3f | ||
|
|
29b9e57bd8 | ||
|
|
f52bd0e51c | ||
|
|
b8c03ecc10 | ||
|
|
7c09419fae | ||
|
|
576c99d702 | ||
|
|
a7c2f60307 | ||
|
|
2a2101d926 | ||
|
|
1379617f23 | ||
|
|
85fba27644 | ||
|
|
17084b4ec3 | ||
|
|
b77d6076ab | ||
|
|
2997aa9784 | ||
|
|
df77ec6ee1 | ||
|
|
d5409a80a3 | ||
|
|
c42c0b49f9 | ||
|
|
23893f88d5 | ||
|
|
240d4faf09 | ||
|
|
0e8eb5eb16 | ||
|
|
827dae53a5 | ||
|
|
6d49928a11 | ||
|
|
f63028c98c | ||
|
|
a4d8100b73 | ||
|
|
34d328507d | ||
|
|
245384c720 | ||
|
|
fb7a6e57b2 | ||
|
|
5af59582df | ||
|
|
8e7647a3b5 | ||
|
|
c57e1ad0e9 | ||
|
|
953183a956 | ||
|
|
29ab73e5c9 | ||
|
|
eced8e1ea6 | ||
|
|
600a6898f1 | ||
|
|
9e14c03322 | ||
|
|
0125d075b2 | ||
|
|
5e68906a22 | ||
|
|
a0618d1b5d | ||
|
|
e5a0a2fa34 | ||
|
|
25cc16c408 | ||
|
|
3f430fc8e0 | ||
|
|
5a3bb27566 | ||
|
|
31fed55966 | ||
|
|
d056cd2f13 | ||
|
|
06029f2827 | ||
|
|
c07efc1595 | ||
|
|
93a320ded2 | ||
|
|
b14778c7ac | ||
|
|
73544c3fc1 | ||
|
|
bfaf4f6f96 | ||
|
|
84c0fe7100 | ||
|
|
a541161bd0 | ||
|
|
d6496ba028 | ||
|
|
3d6d0c70a0 | ||
|
|
7461de1292 | ||
|
|
4a3deb30b8 | ||
|
|
bba3cbfb47 | ||
|
|
bba915b898 | ||
|
|
f8934ef315 | ||
|
|
4e84e5d32c | ||
|
|
536ccfd38f | ||
|
|
514c24fefa | ||
|
|
e3b556bdd0 | ||
|
|
dea15469c1 | ||
|
|
442fdec098 | ||
|
|
3b65bfa2d5 | ||
|
|
ae5b47db1e | ||
|
|
cb4accec08 | ||
|
|
1349640272 | ||
|
|
f65e1f6e55 | ||
|
|
6eb7cd0f26 | ||
|
|
bff337ac03 | ||
|
|
11a6cdd2e5 | ||
|
|
d6a72097c0 | ||
|
|
d166294de6 | ||
|
|
35cd54a7d9 | ||
|
|
4a12d8e9b3 | ||
|
|
0a4164431f | ||
|
|
d04673204e | ||
|
|
8a56ca74a5 | ||
|
|
b1a2f89693 | ||
|
|
3c3eb138e0 | ||
|
|
01599086f7 | ||
|
|
744dc3082b | ||
|
|
7363a7c2d4 | ||
|
|
79136ba5b7 | ||
|
|
c951eefdd4 | ||
|
|
f44cb5e57d | ||
|
|
e5cbb6afeb | ||
|
|
18722a45ba | ||
|
|
88f6fb61c9 | ||
|
|
68802999ce | ||
|
|
fd8c84d3c0 | ||
|
|
25e7496927 | ||
|
|
72a24df665 | ||
|
|
d832ce6410 | ||
|
|
5d4d91c737 | ||
|
|
c617b72483 | ||
|
|
83dc58cc82 | ||
|
|
79b9a4a649 | ||
|
|
4794037b2b | ||
|
|
f7a4853e78 | ||
|
|
27fef48103 | ||
|
|
6ec9ab7c81 | ||
|
|
332d607f42 | ||
|
|
95da8c4197 | ||
|
|
a4cb07eb3b | ||
|
|
9dcc7b7a25 | ||
|
|
4ea399a6e4 | ||
|
|
ec3366bcce | ||
|
|
35a217ae6e | ||
|
|
3ef634693d | ||
|
|
3a925edd63 | ||
|
|
e7e85e0c2f | ||
|
|
aec6d40af8 | ||
|
|
128ba28da8 | ||
|
|
297f21a9dd | ||
|
|
77a0371c4b | ||
|
|
7d1528ca1e | ||
|
|
b82b0c6167 | ||
|
|
bd59707f61 | ||
|
|
4130ea2dd3 | ||
|
|
86db993344 | ||
|
|
accf7e117b | ||
|
|
63fe89f064 | ||
|
|
8ce0b64fe3 | ||
|
|
5a2610fd9e | ||
|
|
f3977f54d5 | ||
|
|
e82e6200b0 | ||
|
|
4b9036a4ce | ||
|
|
33c4d9c244 | ||
|
|
14115678ef | ||
|
|
d5fb143f28 | ||
|
|
6ab5eb31ca | ||
|
|
3e75bc405c | ||
|
|
8405c38557 | ||
|
|
9818eb699e | ||
|
|
21a378e078 | ||
|
|
9a3a96ed19 | ||
|
|
bd30cc4b0f | ||
|
|
02d04ecb57 | ||
|
|
e23311e817 | ||
|
|
60fe855813 | ||
|
|
5c0bd7bf90 | ||
|
|
d4d6e8c5a0 | ||
|
|
e779dfbe9d | ||
|
|
9e39a3cc64 | ||
|
|
7e02a7d928 | ||
|
|
8c3fe1bd80 | ||
|
|
0b72160c93 | ||
|
|
f529a0cc3f | ||
|
|
b4520d91b5 | ||
|
|
771985d063 | ||
|
|
feba28f55d | ||
|
|
32d3376d44 | ||
|
|
7ae51daf39 | ||
|
|
b5a7e69040 | ||
|
|
7f431a1e7b | ||
|
|
c2a7aae848 | ||
|
|
9a3fff9c09 | ||
|
|
90ad8b016e | ||
|
|
ed6fc25507 | ||
|
|
1b13ef9f34 | ||
|
|
7f7e7e2e82 | ||
|
|
dfdc866dfc | ||
|
|
58021c5813 | ||
|
|
aa1fb1d921 | ||
|
|
25cb70e766 | ||
|
|
48dd69fbf6 | ||
|
|
bfa2bcb3e4 | ||
|
|
ebad32f0d3 | ||
|
|
167dd4dff0 | ||
|
|
f9cf7365fd | ||
|
|
c03b5100d3 | ||
|
|
745a605052 | ||
|
|
f6e63144fc | ||
|
|
65b6ff90c3 | ||
|
|
0261a20572 | ||
|
|
878d0b7e42 | ||
|
|
1705f29cbf | ||
|
|
22abc94065 | ||
|
|
5ad1ee917e | ||
|
|
37dcf7e9a4 | ||
|
|
bf270794e9 | ||
|
|
e8c43da0c9 | ||
|
|
f910c6da18 | ||
|
|
8a76fd33a9 | ||
|
|
5c4153cb6b | ||
|
|
7f0ee95236 | ||
|
|
c3e47fc417 | ||
|
|
555490129c | ||
|
|
2dcc49c6a9 | ||
|
|
0d294990c5 | ||
|
|
9924aa4e93 | ||
|
|
4f44cf6840 | ||
|
|
d9e4d2da95 | ||
|
|
fdc46093c6 | ||
|
|
0cff00c426 | ||
|
|
5f22941164 | ||
|
|
4f6c587388 | ||
|
|
b2349c9757 | ||
|
|
47cd0648ac | ||
|
|
70746f8786 | ||
|
|
919fd87bbc | ||
|
|
a80c0a6a3b | ||
|
|
7f78515dd0 | ||
|
|
d466d47751 | ||
|
|
a834620606 | ||
|
|
b898001fa3 | ||
|
|
c9eb224aaa | ||
|
|
885c764c87 | ||
|
|
99996bb82f | ||
|
|
9b42b21d44 | ||
|
|
d1fcd3394c | ||
|
|
e54cab033b | ||
|
|
9a01ae279c | ||
|
|
cfc67e2700 | ||
|
|
996d35408a | ||
|
|
333af057f4 | ||
|
|
45d7d06e0f | ||
|
|
acce63ce24 | ||
|
|
80c9ce35cb | ||
|
|
e51a64c242 | ||
|
|
59cf2e4d79 | ||
|
|
7edfc676f1 | ||
|
|
f197b71950 | ||
|
|
c61404545c | ||
|
|
9d349dae5b | ||
|
|
779ee45b20 | ||
|
|
f0c5802471 | ||
|
|
4b7dc3921c | ||
|
|
100e96f843 | ||
|
|
30391805dc | ||
|
|
49f0424956 | ||
|
|
eea76dde54 | ||
|
|
74be4a8e7f | ||
|
|
4452839d76 | ||
|
|
eae3785c21 | ||
|
|
d818abc578 | ||
|
|
80f8024ae8 | ||
|
|
294a3a168a | ||
|
|
59b1e4203a | ||
|
|
598613e189 | ||
|
|
2e69f2f967 | ||
|
|
ee60f5e59b | ||
|
|
c652792404 | ||
|
|
af04890cd9 | ||
|
|
6bdae9f53e | ||
|
|
10657bfd78 | ||
|
|
400570f4b9 | ||
|
|
48eacbc5cc | ||
|
|
787d2a4456 | ||
|
|
a8088cdae7 | ||
|
|
29f3ce0278 | ||
|
|
b787d9a27d | ||
|
|
8f72a4d6eb | ||
|
|
b4faf8a0a5 | ||
|
|
f8042d2e99 | ||
|
|
f3a3707b95 | ||
|
|
82e5f6b50f | ||
|
|
b06784d9c1 | ||
|
|
6910fde9a2 | ||
|
|
d70f82b26e | ||
|
|
0279b837ee | ||
|
|
510d02c7cf | ||
|
|
cc5cd9f21a | ||
|
|
8bdf09b71b | ||
|
|
ffa8a19eca | ||
|
|
dcfdb48dc2 | ||
|
|
a418295ece | ||
|
|
fda5cf70fc | ||
|
|
40d3602f55 | ||
|
|
3e6382a051 | ||
|
|
3dbd4f9cfc | ||
|
|
a807c251d1 | ||
|
|
9ec5532ea6 | ||
|
|
eb5f3045f4 | ||
|
|
3e72fa3430 | ||
|
|
59c6bc6e96 | ||
|
|
28fc390837 | ||
|
|
1a32f5d161 | ||
|
|
05642967a7 | ||
|
|
a6ac55f9f2 | ||
|
|
d83061b2bb | ||
|
|
cb9a4960d2 | ||
|
|
96ed8f2867 | ||
|
|
743b73f0bf | ||
|
|
7ad26fe339 | ||
|
|
359b6e8d28 | ||
|
|
f0051fcc22 | ||
|
|
2dca6fe24c | ||
|
|
ce662ee561 | ||
|
|
fb06244e1d | ||
|
|
ee2d700298 | ||
|
|
adcdadd0f1 | ||
|
|
1d5b752c42 | ||
|
|
b70629c592 | ||
|
|
3fb72e39d4 | ||
|
|
69275c7fc7 | ||
|
|
b747f9fc62 | ||
|
|
a66237d108 | ||
|
|
97d53b7850 | ||
|
|
c9a2a93908 | ||
|
|
7509e117c7 | ||
|
|
899dd5cb52 | ||
|
|
751c020935 | ||
|
|
6a751a66cd | ||
|
|
c21355e1af | ||
|
|
ba90993038 | ||
|
|
c3841e14e0 | ||
|
|
4e20be58a1 | ||
|
|
418fd2565d | ||
|
|
464578bdb8 | ||
|
|
f4712ed3d6 | ||
|
|
b48b335afb | ||
|
|
c77710fa95 | ||
|
|
402116ff4c | ||
|
|
c362239d15 | ||
|
|
bed2816fb8 | ||
|
|
fd5bdecf88 | ||
|
|
a96a1ec7e2 | ||
|
|
4e588cd59a | ||
|
|
354f223d7e | ||
|
|
eacd16331c | ||
|
|
5b215fe72a | ||
|
|
7ba70f4d44 | ||
|
|
732b61c160 | ||
|
|
19f23f4ea1 | ||
|
|
8c8641c1a8 | ||
|
|
495157a5c3 | ||
|
|
d0b1eb44b5 | ||
|
|
aea4081784 | ||
|
|
a2b70003ec | ||
|
|
e4b37dde33 | ||
|
|
8236c18239 | ||
|
|
aefff852bc | ||
|
|
f23187ba10 | ||
|
|
bf6c35388f | ||
|
|
99fe7170f8 | ||
|
|
488822e454 | ||
|
|
a9dc432786 | ||
|
|
a9632a9024 | ||
|
|
e26ada0623 | ||
|
|
05f5548747 | ||
|
|
cb95978428 | ||
|
|
f3371acb3b | ||
|
|
7f930cf0c9 | ||
|
|
3136511af3 | ||
|
|
aff8fd93db | ||
|
|
f6a4181658 | ||
|
|
760489f6f9 | ||
|
|
3407301b98 | ||
|
|
399c6c877c | ||
|
|
df98acdc9a | ||
|
|
ca9f976241 | ||
|
|
1d715cf631 | ||
|
|
704c604ee4 | ||
|
|
8c8d9f15d2 | ||
|
|
94c53a0d33 | ||
|
|
5e48094d77 | ||
|
|
57042c593a | ||
|
|
e930a24f48 | ||
|
|
7052c19af4 | ||
|
|
59bb4270fb | ||
|
|
8906a9b93e | ||
|
|
201007a46a | ||
|
|
41aa4edb91 | ||
|
|
5635dfd447 | ||
|
|
f48ca609e5 | ||
|
|
2df8d8690b | ||
|
|
2782f89a21 | ||
|
|
6e1a831f69 | ||
|
|
08f189753a | ||
|
|
048c1b0cc7 | ||
|
|
946ce1873b | ||
|
|
01d4dc7c2e | ||
|
|
705abf68be | ||
|
|
13eeaf5321 | ||
|
|
0a160bfba5 | ||
|
|
ee70ab63b6 | ||
|
|
6b6e49efd3 | ||
|
|
949d88736c | ||
|
|
22c2bf33c0 | ||
|
|
39a62977fe | ||
|
|
6aea176253 | ||
|
|
45e088a635 | ||
|
|
113db63e2d | ||
|
|
8fa9d696b1 | ||
|
|
f64e68beb7 | ||
|
|
a19ee36bf8 | ||
|
|
f6cab9cc54 | ||
|
|
8beebf67e2 | ||
|
|
ab9a7cfa5a | ||
|
|
45277ecfef | ||
|
|
dcb08b00f9 | ||
|
|
126f526f9f | ||
|
|
c750cf78a4 | ||
|
|
4e369e372b | ||
|
|
150010839d | ||
|
|
1dc4dfdef8 | ||
|
|
2175796c6d | ||
|
|
42ad2e5a3d | ||
|
|
ebe4ad6b96 | ||
|
|
9f70c63544 | ||
|
|
afb7c6cd7f | ||
|
|
9114c2a8c2 | ||
|
|
33e29746b7 | ||
|
|
b2900c5751 | ||
|
|
f368c066d6 | ||
|
|
5129d6a698 | ||
|
|
65f7b25668 | ||
|
|
b7b05be5af | ||
|
|
863de3bfa4 | ||
|
|
e4a925969d | ||
|
|
6be81dfbea | ||
|
|
72709618b0 | ||
|
|
28e968cad4 | ||
|
|
3ba5e319a5 | ||
|
|
45ce1ce649 | ||
|
|
67998c5535 | ||
|
|
3dfe673b93 | ||
|
|
1ebfea677b | ||
|
|
ac8bb3a9cf | ||
|
|
30b0c06eb3 | ||
|
|
1e3a5892e9 | ||
|
|
db94c1e5ee | ||
|
|
7a043abc64 | ||
|
|
651684c8e4 | ||
|
|
b50eebca22 | ||
|
|
8f8d293439 | ||
|
|
3a6b97aed1 | ||
|
|
35bd0ab3bc | ||
|
|
0a85ffb366 | ||
|
|
7bfbad11a0 | ||
|
|
3b1339b60a | ||
|
|
726db46420 | ||
|
|
45c9d3495f | ||
|
|
0258c235d5 | ||
|
|
c2f9d7abfe | ||
|
|
e53758abd9 | ||
|
|
4e6e61b736 | ||
|
|
7ac82a64c1 | ||
|
|
aef0d31d98 | ||
|
|
e521032291 | ||
|
|
6d34e1f594 | ||
|
|
a8bcc48530 | ||
|
|
52332fe578 | ||
|
|
9b42bdba7b | ||
|
|
73e073b5fe | ||
|
|
506b6383e2 | ||
|
|
fd4f675615 | ||
|
|
280e9cce25 | ||
|
|
498e857d07 | ||
|
|
46c80e8a37 | ||
|
|
8923a9dcc1 | ||
|
|
167a56c962 | ||
|
|
bd6bca881d | ||
|
|
60109beab1 | ||
|
|
b3026581d8 | ||
|
|
4705e9fd0f | ||
|
|
6a6514365d | ||
|
|
7679753b51 | ||
|
|
c27a009d02 | ||
|
|
3974dac2ee | ||
|
|
c8bb46178a | ||
|
|
2c92d4afec | ||
|
|
356c83eb5a | ||
|
|
425f8e4758 | ||
|
|
d389b54a40 | ||
|
|
9ec9867bf5 | ||
|
|
73f7552f28 | ||
|
|
8349936447 | ||
|
|
4b98ddcf21 | ||
|
|
f1b041dd47 | ||
|
|
766c77f32b | ||
|
|
1f051b2ef0 | ||
|
|
3676e52899 | ||
|
|
4ad70054ae | ||
|
|
dbd1ef9ff8 | ||
|
|
803a9843ea | ||
|
|
ae1a98211a | ||
|
|
6a3e0dabbd | ||
|
|
7900b7b44e | ||
|
|
16a8a37bd4 | ||
|
|
dee2b68a8a | ||
|
|
4182f7ca04 | ||
|
|
d29d3c6e2a | ||
|
|
3522f3e7fd | ||
|
|
1f46ac6e32 | ||
|
|
7de46bd778 | ||
|
|
40a4c23839 | ||
|
|
c32b3312ea | ||
|
|
8fa0a10103 | ||
|
|
6b4028a456 | ||
|
|
2f780447b6 | ||
|
|
ed4164d03e | ||
|
|
fef05061b4 | ||
|
|
8fb0d9ffa1 | ||
|
|
2c17e80367 | ||
|
|
dcdcc3432b | ||
|
|
42d719d3b5 | ||
|
|
8e019e8ff9 | ||
|
|
2720887870 | ||
|
|
69fe1670c3 | ||
|
|
1fcf0bf3e6 | ||
|
|
86c0d671e4 | ||
|
|
338a450746 | ||
|
|
c84b1effba | ||
|
|
8be2d6a7d0 | ||
|
|
8830b2b08a | ||
|
|
f4cd9b78b4 | ||
|
|
179c77f09c | ||
|
|
911fd058a6 | ||
|
|
8d8def030a | ||
|
|
ac89630e11 | ||
|
|
f05e1f1693 | ||
|
|
e59ab24424 | ||
|
|
4254320fc9 | ||
|
|
f5ff73d87d | ||
|
|
ed5ffbd813 | ||
|
|
afc8a2f76d | ||
|
|
8f8561d175 | ||
|
|
36b54ad384 | ||
|
|
668b7ca1b2 | ||
|
|
9bc48c16c7 | ||
|
|
a980793630 | ||
|
|
94a2f544d5 | ||
|
|
3ad3c163ed | ||
|
|
714a92af06 | ||
|
|
c7b1879ba9 | ||
|
|
64a53dccd1 | ||
|
|
15d0e3d702 | ||
|
|
d3ded0c897 | ||
|
|
d473d3fe95 | ||
|
|
9688633de9 | ||
|
|
b4f860990d | ||
|
|
e7771046d3 | ||
|
|
ce86b82eed | ||
|
|
3a3e470ac8 | ||
|
|
029807ec6b | ||
|
|
affdd0454a | ||
|
|
fd3172e5a9 | ||
|
|
2db8f8f3ff | ||
|
|
78c2a92949 | ||
|
|
75cd2e1422 | ||
|
|
1231089989 | ||
|
|
86f14fc006 | ||
|
|
f9966694b8 | ||
|
|
737012db0f | ||
|
|
f3d4bdcc3a | ||
|
|
5863d697be | ||
|
|
29627a396b | ||
|
|
ab72d6c3ef | ||
|
|
77f2e7f756 | ||
|
|
83cbc23e71 | ||
|
|
01d1453453 | ||
|
|
0b733a7743 | ||
|
|
afa99d3034 | ||
|
|
67f63f6afe | ||
|
|
257a3ecb40 | ||
|
|
69283ec1e5 | ||
|
|
3fa5e0c105 | ||
|
|
45fa72005b | ||
|
|
7f1681944e | ||
|
|
7a8c0ba119 | ||
|
|
bc0b86652f | ||
|
|
6174cdabde | ||
|
|
83b002cb91 | ||
|
|
80f06334aa | ||
|
|
9125cc3af1 | ||
|
|
308bba8ca2 | ||
|
|
5659a33a7c | ||
|
|
7d8144a1c3 | ||
|
|
fbec5ec038 | ||
|
|
bc2f283ea8 | ||
|
|
3f822ae3eb | ||
|
|
bc9ab7c639 | ||
|
|
fa153e57ce | ||
|
|
0516c4fb9a | ||
|
|
3e9253a78e | ||
|
|
9dbb221082 | ||
|
|
fd3f0a22e2 | ||
|
|
2d05133150 | ||
|
|
4fb65c7090 | ||
|
|
e7c8d17097 | ||
|
|
ddacc4ff4b | ||
|
|
011ccf495e | ||
|
|
460b430fa3 | ||
|
|
fef54c8599 | ||
|
|
f2535a4b0c | ||
|
|
a876d967b7 | ||
|
|
8a3c448712 | ||
|
|
ac199cf65a | ||
|
|
e272c8b7f3 | ||
|
|
b86b4ff67f | ||
|
|
15233b73b6 | ||
|
|
75deddff97 | ||
|
|
7817706d73 | ||
|
|
15baf7f5dc | ||
|
|
b4b5228d00 | ||
|
|
52312c5450 | ||
|
|
ec837e3877 | ||
|
|
b5bfd0831d | ||
|
|
3bc80163e3 | ||
|
|
6879a16b8c | ||
|
|
ea18684ccd | ||
|
|
9ccaf2c7e5 | ||
|
|
329c3b1320 | ||
|
|
1ca8af3c4a | ||
|
|
a9e5167ccd | ||
|
|
46b3567fc9 | ||
|
|
724ce1e509 | ||
|
|
afc91edbb2 | ||
|
|
850921b289 | ||
|
|
133af98ad1 | ||
|
|
4b6a88ac32 | ||
|
|
22afe4d49f | ||
|
|
21b1e0d7f5 | ||
|
|
6ae813378c | ||
|
|
99c1902c1e | ||
|
|
883d8efe9b | ||
|
|
dc72e1d18f | ||
|
|
55a9290c44 | ||
|
|
b25d072dab | ||
|
|
72af12b41e | ||
|
|
66919726ca | ||
|
|
f91ea98d7e | ||
|
|
f37444d50a | ||
|
|
43580be7e8 | ||
|
|
a6dd46d129 | ||
|
|
757bd5aa9f | ||
|
|
e16ea086a6 | ||
|
|
4c876ac6c7 | ||
|
|
cbff202864 | ||
|
|
3ab3f9bb1b | ||
|
|
7a382c24af | ||
|
|
94ac5f6689 | ||
|
|
f35fa44c8b | ||
|
|
2acb388bdd | ||
|
|
4f79cf0b8c | ||
|
|
e593341696 | ||
|
|
a48e5a6ea0 | ||
|
|
f8a78d1604 | ||
|
|
2f889a8704 | ||
|
|
4e4766497d | ||
|
|
032bf88d55 | ||
|
|
8ea84f7d96 | ||
|
|
a00cf15fa4 | ||
|
|
155f570f17 | ||
|
|
d7485623be | ||
|
|
c4ef64472d | ||
|
|
8b986f76fa | ||
|
|
66ea2ff9d7 | ||
|
|
34f6de4efd | ||
|
|
cf58724481 | ||
|
|
1db981ae20 | ||
|
|
22d98b8991 | ||
|
|
ce77a63ed8 | ||
|
|
b307f0aa00 | ||
|
|
94ac7c8bf2 | ||
|
|
3de880dd3a | ||
|
|
5443d7c61a | ||
|
|
8ec7a8139f | ||
|
|
efa6d077c1 | ||
|
|
fa71c538c5 | ||
|
|
a983b3c318 | ||
|
|
1e9922a62e | ||
|
|
3e24113e1b | ||
|
|
d02b9cfab5 | ||
|
|
8c33ac660d | ||
|
|
22f6f2d7b5 | ||
|
|
bf1e35bba0 | ||
|
|
df48a08fe8 | ||
|
|
c6258a6174 | ||
|
|
a38c2086d0 | ||
|
|
4db3876129 | ||
|
|
1c5782ebec | ||
|
|
03856c1827 | ||
|
|
bcda9d7a9b | ||
|
|
c401b1bde7 | ||
|
|
31b0cfe1b6 | ||
|
|
f4f411ab75 | ||
|
|
7856855de5 | ||
|
|
ee55ca3e37 | ||
|
|
a40a9e9be6 | ||
|
|
e45f0ab83f | ||
|
|
fff90c3904 | ||
|
|
d06effc960 | ||
|
|
8d08743360 | ||
|
|
7c77c8a229 | ||
|
|
9b683dbb6a | ||
|
|
4328f0f58e | ||
|
|
062da007a8 | ||
|
|
e31b4a603d | ||
|
|
196b7497e4 | ||
|
|
8cfdce6e1b | ||
|
|
8ed6d69da1 | ||
|
|
1d62aa80a0 | ||
|
|
b81dea93ab | ||
|
|
364a1588a2 | ||
|
|
9a88f0b09a | ||
|
|
92a614b4b5 | ||
|
|
f4262f39c4 | ||
|
|
f17af3ca50 | ||
|
|
261ef768de | ||
|
|
7fba2bdcba | ||
|
|
7d760b9f42 | ||
|
|
a3a65d7a3b | ||
|
|
24fc2c2ce1 | ||
|
|
796003063f | ||
|
|
53412c31b6 | ||
|
|
023c19ee7c | ||
|
|
532da69263 | ||
|
|
ec63af57e6 | ||
|
|
56ca158f29 | ||
|
|
e143c87bfd | ||
|
|
3a03e5040e | ||
|
|
c147658497 | ||
|
|
bc4b354421 | ||
|
|
dd60a6ea88 | ||
|
|
cab18a8347 | ||
|
|
7a7aaba633 | ||
|
|
2cfa16f328 | ||
|
|
a73db6b36d | ||
|
|
2c859734a3 | ||
|
|
4dac190cbd | ||
|
|
8136f8eff6 | ||
|
|
13959496e8 | ||
|
|
e079a06796 | ||
|
|
1ece2bd8e2 | ||
|
|
93e9f0cc1c | ||
|
|
962f065410 | ||
|
|
ffd609ae8e | ||
|
|
f047ae115c | ||
|
|
036dae1f5f | ||
|
|
cff63e6469 | ||
|
|
f9be2d1bec | ||
|
|
7046766386 | ||
|
|
8b4c5498b9 | ||
|
|
af62fab676 | ||
|
|
defe4749f0 | ||
|
|
ff2086e4c9 | ||
|
|
a3ccf78bf7 | ||
|
|
b1285b61ef | ||
|
|
9cc80f0dda | ||
|
|
1f38eed471 | ||
|
|
d8c7856966 | ||
|
|
420c16ad8c | ||
|
|
a07e5eeddb | ||
|
|
250179ee59 | ||
|
|
d2279aa36b | ||
|
|
fbd98588f2 | ||
|
|
877c321664 | ||
|
|
c6efa74d23 | ||
|
|
9f196828ef | ||
|
|
2eb72ad756 | ||
|
|
590be6b406 | ||
|
|
5201752c8e | ||
|
|
92691578dd | ||
|
|
cbc3172627 | ||
|
|
fac4729dfe | ||
|
|
ca6fcb5441 | ||
|
|
3b12d31f1b | ||
|
|
d93daf50fd | ||
|
|
8ec35f5d5f | ||
|
|
d61e1ca97e | ||
|
|
b284d9cb74 | ||
|
|
ef9e6e9e6c | ||
|
|
f558dc5a5b | ||
|
|
544e5cd964 | ||
|
|
3dfb48c7d7 | ||
|
|
ecd7f2fefd | ||
|
|
73a7fe09f3 | ||
|
|
fec8eb3ae2 | ||
|
|
db84a65931 | ||
|
|
c9a8625199 | ||
|
|
0c316a7ea3 | ||
|
|
0ff1db33bd | ||
|
|
b2283db2dc | ||
|
|
1d90e47ef2 | ||
|
|
461ee0b583 | ||
|
|
4b47ad45d4 | ||
|
|
8a15f32b00 | ||
|
|
cc1ee25cd3 | ||
|
|
f482b18147 | ||
|
|
c1329578f9 | ||
|
|
4d92dafb4f | ||
|
|
2516a1c3f3 | ||
|
|
dc01193951 | ||
|
|
12eae5a462 | ||
|
|
078cfba980 | ||
|
|
34d1f12ae2 | ||
|
|
96c205bf0d | ||
|
|
5005d77666 | ||
|
|
f4d0274899 | ||
|
|
c9f23aeee1 | ||
|
|
589b28532b | ||
|
|
9e462acb32 | ||
|
|
7c80270673 | ||
|
|
59c2ed2459 | ||
|
|
d14d0734e8 | ||
|
|
e10a09fc61 | ||
|
|
8ed44ada20 | ||
|
|
54e83ed342 | ||
|
|
fe423d1e68 | ||
|
|
559ae7592c | ||
|
|
ab5f6817cf | ||
|
|
54b080695e | ||
|
|
4e66e4e8e1 | ||
|
|
ab29f07c13 | ||
|
|
82150eca62 | ||
|
|
64c0045ad6 | ||
|
|
04ee4c5a0f | ||
|
|
6afeab4fad | ||
|
|
e49e1a1eea | ||
|
|
105d7f9e1d | ||
|
|
a4c6ae7fe4 | ||
|
|
2279ab984e | ||
|
|
0b8e0fa179 | ||
|
|
4c34730904 | ||
|
|
0e0b991fa2 | ||
|
|
9d6950d80c | ||
|
|
07db1871a6 | ||
|
|
f87f2f7058 | ||
|
|
3e253a0978 | ||
|
|
d44bb97e2b | ||
|
|
1c52a960c7 | ||
|
|
bfa4e6941a | ||
|
|
7452eee4a3 | ||
|
|
119a701d60 | ||
|
|
f2de9a4b3b | ||
|
|
896304e6fb | ||
|
|
71c5e456e3 | ||
|
|
a5047813f0 | ||
|
|
8dd78abe91 | ||
|
|
7e5f7389a5 | ||
|
|
414a98b879 | ||
|
|
ca2343a2f4 | ||
|
|
ac836e2011 | ||
|
|
f7c47e3a8b | ||
|
|
92dc364364 | ||
|
|
1a1919971a | ||
|
|
595055073c | ||
|
|
832befc9a7 | ||
|
|
3ef4a6388d | ||
|
|
63a5800025 | ||
|
|
11c875c4a3 | ||
|
|
0f57f83093 | ||
|
|
33276a1724 | ||
|
|
adeb627556 | ||
|
|
d45a5c0b46 | ||
|
|
3c46c21a25 | ||
|
|
44178b2de2 | ||
|
|
13f5d0978d | ||
|
|
70a50ece43 | ||
|
|
24147e746a | ||
|
|
386e02a564 | ||
|
|
865b96a219 | ||
|
|
dd38bb156b | ||
|
|
72c07b7d7a | ||
|
|
3f19cadffc | ||
|
|
670d16bd8e | ||
|
|
3d1040b280 | ||
|
|
cd3bb20afd | ||
|
|
91d1d2cf1d | ||
|
|
f8a6fad513 | ||
|
|
9d3d394c7d | ||
|
|
820a09b28f | ||
|
|
108285021e | ||
|
|
1d005585c8 | ||
|
|
714591dd2e | ||
|
|
6e399b32e1 | ||
|
|
4b413b78fe | ||
|
|
126bc6134e | ||
|
|
bf33f5d9c8 | ||
|
|
a8ff4fdc26 | ||
|
|
7ca44480a9 | ||
|
|
ea29449413 | ||
|
|
9ec448e503 | ||
|
|
70be4e92fb | ||
|
|
c0e3243ee6 | ||
|
|
b961701189 | ||
|
|
3619355cb4 | ||
|
|
674a512630 | ||
|
|
5e5277404e | ||
|
|
c242d9dd99 | ||
|
|
a5a94e5003 | ||
|
|
9b6dc1318d | ||
|
|
87535a9239 | ||
|
|
6be589618c | ||
|
|
a51e025c03 | ||
|
|
29e115ce41 | ||
|
|
f96d2ffaa5 | ||
|
|
47d05e737d | ||
|
|
3666758e6e | ||
|
|
e49f0dbf32 | ||
|
|
abe3f883ae | ||
|
|
e5cb7c7fdf | ||
|
|
79337b5e7f | ||
|
|
ae5907e6b3 | ||
|
|
04fb31eff9 | ||
|
|
9f7e01a1fb | ||
|
|
9ace8e5235 | ||
|
|
771cdcc163 | ||
|
|
58d5b5bc41 | ||
|
|
04538c52f2 | ||
|
|
dd994dcfab | ||
|
|
594b71dc16 | ||
|
|
cf7ccaebd1 | ||
|
|
8bde831794 | ||
|
|
2f30bdb6a8 | ||
|
|
4d28ae2e3e | ||
|
|
5fcbfeb3db | ||
|
|
f6c7b09d9b | ||
|
|
72c69e7c79 | ||
|
|
13a65c2bfa | ||
|
|
d2f118a86c | ||
|
|
ce11c71e95 | ||
|
|
e4574d6c03 | ||
|
|
d8c311fbd7 | ||
|
|
d6e6c6f8fb | ||
|
|
63c3058f5b | ||
|
|
0d5815d3c9 | ||
|
|
dec4996760 | ||
|
|
e0df092a70 | ||
|
|
da56aecd5e | ||
|
|
7688ffa39f | ||
|
|
d08e89bb63 | ||
|
|
5979479619 | ||
|
|
e1b968bfe0 | ||
|
|
36c476bc36 | ||
|
|
6bfadfa962 | ||
|
|
720b40afa7 | ||
|
|
75e90541c9 | ||
|
|
47b1940ace | ||
|
|
012b1cd79d | ||
|
|
ff3131d988 | ||
|
|
ba1725224a | ||
|
|
55bc2add85 | ||
|
|
9e96fd1e20 | ||
|
|
79d20b0b63 | ||
|
|
b2a44cfe09 | ||
|
|
146805af49 | ||
|
|
aabbe9198a | ||
|
|
a815bac15d | ||
|
|
86427a4c3c | ||
|
|
0d8a82568a | ||
|
|
95b2939532 | ||
|
|
76e11e5a3e | ||
|
|
2d5d02fd67 | ||
|
|
f30e6b7809 | ||
|
|
04f2034dd1 | ||
|
|
99b3bbaaad | ||
|
|
c0bccc027f | ||
|
|
51beac2d03 | ||
|
|
63a5150cea | ||
|
|
e34a20bce6 | ||
|
|
ca482ea9d4 | ||
|
|
e31d2ada4f | ||
|
|
c29147a681 | ||
|
|
92be9bf626 | ||
|
|
a02b8b7b0a | ||
|
|
1c1d14fff1 | ||
|
|
eff0a34c54 | ||
|
|
2dc3035a7c | ||
|
|
78f6ec27b3 | ||
|
|
6b868fa824 | ||
|
|
7951c2cf37 | ||
|
|
ea678ef55e | ||
|
|
fbbbf41595 | ||
|
|
f83d1d8d84 | ||
|
|
7bcc01b94e | ||
|
|
ff72f9dbdf | ||
|
|
b1210570d1 | ||
|
|
ef97b5679e | ||
|
|
6dfe0269bf | ||
|
|
77c57846ed | ||
|
|
19b5b11b07 | ||
|
|
27d4ce3c5b | ||
|
|
859aa01ec2 | ||
|
|
6d102f7be3 | ||
|
|
5ae1ead2c9 | ||
|
|
b1982013dc | ||
|
|
954fdde1c4 | ||
|
|
f177e2cf7c | ||
|
|
845ebb4868 | ||
|
|
b43171bb31 | ||
|
|
be07fad8f1 | ||
|
|
3375496ef2 | ||
|
|
df23b2f62f | ||
|
|
95cddbd409 | ||
|
|
d46f1b25eb | ||
|
|
378abe73c9 | ||
|
|
b5eda797b5 | ||
|
|
f704e322af | ||
|
|
dc21d28b83 | ||
|
|
eb17862177 | ||
|
|
fc023f307a | ||
|
|
ad1905c8fe | ||
|
|
85d54e8f5e | ||
|
|
ba09f7533c | ||
|
|
fa6e3a34b5 | ||
|
|
85ef4b3c12 | ||
|
|
89e18b0e2f | ||
|
|
1b50ffcf11 | ||
|
|
b3f83816c5 | ||
|
|
75b78886ae | ||
|
|
26d97da066 | ||
|
|
ab7bc15573 | ||
|
|
d43d643bbd | ||
|
|
3ca5efc177 | ||
|
|
04c858e6fd | ||
|
|
25046e4c11 | ||
|
|
5134776e2f | ||
|
|
cc29ebd75b | ||
|
|
2233f1ce44 | ||
|
|
a189802061 | ||
|
|
dca6ffdbbe | ||
|
|
859946a751 | ||
|
|
08bf1a2336 | ||
|
|
27743e3427 | ||
|
|
52b0cc4129 | ||
|
|
22abc2e21d | ||
|
|
fc8425b12a | ||
|
|
60fc1fa74b | ||
|
|
190e3ce7bb | ||
|
|
012024ab77 | ||
|
|
529bdd74c8 | ||
|
|
6e349b84c0 | ||
|
|
ab9b92035e | ||
|
|
37ec165319 | ||
|
|
958aa634b1 | ||
|
|
125a95285d | ||
|
|
bbaae2e776 | ||
|
|
f9090f59b7 | ||
|
|
1d740d33a0 | ||
|
|
633ec19c90 | ||
|
|
9b2015f4cf | ||
|
|
e65e7a79a5 | ||
|
|
0996639cac | ||
|
|
e5f58f20c7 | ||
|
|
d1e03b8237 | ||
|
|
917ffe644f | ||
|
|
02efc01a10 | ||
|
|
3016792f95 | ||
|
|
e1b50c86f3 | ||
|
|
42f23e4345 | ||
|
|
adb304f138 | ||
|
|
3bbf9efe63 | ||
|
|
b454a2e3d9 | ||
|
|
23e6323f92 | ||
|
|
b0dbd7a348 | ||
|
|
f707f8cc33 | ||
|
|
fa7126d80d | ||
|
|
7d5f69888a | ||
|
|
51841cf05f | ||
|
|
6d2c01ff2b | ||
|
|
0bd4755814 | ||
|
|
927ba5ac86 | ||
|
|
808d4e6bf5 | ||
|
|
a39db5ea93 | ||
|
|
ca2409ef91 | ||
|
|
7b1f1a1357 | ||
|
|
9471683501 | ||
|
|
deeefb8e35 | ||
|
|
c777888fdb | ||
|
|
ce50627989 | ||
|
|
9f84845ada | ||
|
|
6a8e422a30 | ||
|
|
39d6f0fbd6 | ||
|
|
c240664fda | ||
|
|
a0f6320eee | ||
|
|
3434aa9744 | ||
|
|
3e84cfe09a | ||
|
|
22dccaa24b | ||
|
|
ffe921a223 | ||
|
|
1fd91b9ec6 | ||
|
|
cf10229574 | ||
|
|
5c2ae57d77 | ||
|
|
353452dd21 | ||
|
|
92fa0c117d | ||
|
|
9f4cd0ba0d | ||
|
|
385198e69a | ||
|
|
89fe3b82a3 | ||
|
|
af1bc944d8 | ||
|
|
a0b22e8d56 | ||
|
|
c47d1afa1a | ||
|
|
12a5b602e9 | ||
|
|
25b85569fe | ||
|
|
b373a52218 | ||
|
|
b0e46cd904 | ||
|
|
89a54b4509 | ||
|
|
56aefef693 | ||
|
|
a8ad018c44 | ||
|
|
726f461ff6 | ||
|
|
9c0861a8e4 | ||
|
|
ca0162fb9c | ||
|
|
cfb8c3c733 | ||
|
|
fea448f850 | ||
|
|
c033bb0445 | ||
|
|
bb110be9ab | ||
|
|
fd39c4f391 | ||
|
|
9a3f9c6de2 | ||
|
|
cf9799da7c | ||
|
|
c054e2f2ac | ||
|
|
8177dfdcef | ||
|
|
813b64980d | ||
|
|
fda809bc8a | ||
|
|
5d1b220105 | ||
|
|
de21365c90 | ||
|
|
a24d1515b3 | ||
|
|
a3d6f841c6 | ||
|
|
b770bca6ba | ||
|
|
eaefbc13f9 | ||
|
|
7fcc23c5bf | ||
|
|
29364bf30a | ||
|
|
a9b4916dd8 | ||
|
|
d4ab0ad57d | ||
|
|
e3f8096749 | ||
|
|
94aae33d10 | ||
|
|
96e29a8c59 | ||
|
|
34a9a55d4f | ||
|
|
cf93f6d657 | ||
|
|
91bcacc978 | ||
|
|
e79a824a04 | ||
|
|
e00bbb2d8e | ||
|
|
12c77604f1 | ||
|
|
9a1ec8567c | ||
|
|
5dbc01dba3 | ||
|
|
c5abfa15e0 | ||
|
|
b69e466853 | ||
|
|
9e371778b7 | ||
|
|
ff036165df | ||
|
|
4ed74b664b | ||
|
|
ddd59643c5 | ||
|
|
6122eb3669 | ||
|
|
1b5149f143 | ||
|
|
b654824eb7 | ||
|
|
4d2a08c258 | ||
|
|
19697f4f39 | ||
|
|
41eea667e5 | ||
|
|
f0040b8392 | ||
|
|
291f61551a | ||
|
|
6e8bd08828 | ||
|
|
e915dd619d | ||
|
|
8fb6357fb5 | ||
|
|
07662a91f4 | ||
|
|
37c618cb28 | ||
|
|
5536f3b994 | ||
|
|
bdbbe62570 | ||
|
|
4838e69aea | ||
|
|
408737d510 | ||
|
|
a0f05928e0 | ||
|
|
a35887d4ac | ||
|
|
dbce7c5b29 | ||
|
|
1028ac66cb | ||
|
|
eb5e2623a0 | ||
|
|
867a4f36b3 | ||
|
|
913d74b285 | ||
|
|
eb2eae7d6c | ||
|
|
8e5e548e16 | ||
|
|
af1a481bdb | ||
|
|
92089067f1 | ||
|
|
d04ced94ea | ||
|
|
14115ada4c | ||
|
|
7f36eba709 | ||
|
|
7504bb9081 | ||
|
|
64df08f91c | ||
|
|
98f4d4f30b | ||
|
|
a9b03c45c6 | ||
|
|
3af7926d20 | ||
|
|
e0cd43c63c | ||
|
|
2742f58af5 | ||
|
|
49175a962a | ||
|
|
46d8248ffd | ||
|
|
4ba1408f0f | ||
|
|
95fa5dcd9b | ||
|
|
a2ca16355a | ||
|
|
7ac679f927 | ||
|
|
e2eae6250b | ||
|
|
2855093f5f | ||
|
|
e50a65571f | ||
|
|
acef7c3d5e | ||
|
|
18778f3c5a | ||
|
|
03dae8c1b0 | ||
|
|
c862c072b5 | ||
|
|
251f1e89cf | ||
|
|
3632055081 | ||
|
|
bd64454c15 | ||
|
|
31afbd547e | ||
|
|
8da0092561 | ||
|
|
36c64951c7 | ||
|
|
120e63ea8a | ||
|
|
ad82faba3f | ||
|
|
4c4bbe3214 | ||
|
|
ecbc7efebc | ||
|
|
89b6f28b9f | ||
|
|
8a1097cd35 | ||
|
|
47d74de7ce | ||
|
|
f3c89b3ac5 | ||
|
|
a2ecc5e30e | ||
|
|
db50975174 | ||
|
|
ab14c4815f | ||
|
|
7ad586c994 | ||
|
|
db979de829 | ||
|
|
5218d5cd28 | ||
|
|
9e4684e61c | ||
|
|
9b408e7520 | ||
|
|
10bd7d0918 | ||
|
|
5279b0cd65 | ||
|
|
d181dcf837 | ||
|
|
49dc9d55b5 | ||
|
|
852e9d0d29 | ||
|
|
7a1ed4f83e | ||
|
|
2673b7d9bc | ||
|
|
1587aff433 | ||
|
|
26b3f50fe0 | ||
|
|
286297aa38 | ||
|
|
99a805826d | ||
|
|
be1711b51e | ||
|
|
51beea1dc8 | ||
|
|
297e9dd756 | ||
|
|
03b8e7dab6 | ||
|
|
dbe837be28 | ||
|
|
945d5886ea | ||
|
|
93fa29829f | ||
|
|
a9f8d223e9 | ||
|
|
d2876d04f5 | ||
|
|
fcd5c621de | ||
|
|
5fb0204376 | ||
|
|
790ab1a343 | ||
|
|
86ed721796 | ||
|
|
2837cad762 | ||
|
|
500de4e45e | ||
|
|
533148069f | ||
|
|
42b0a3b62b | ||
|
|
c720aed4fc | ||
|
|
00dad2ad48 | ||
|
|
ce62a9c645 | ||
|
|
1e4e2fd701 | ||
|
|
103be31a43 | ||
|
|
63fa3c829e | ||
|
|
ab5c623e53 | ||
|
|
5e307bb796 | ||
|
|
a5567ef909 | ||
|
|
da22347267 | ||
|
|
a401ab89f3 | ||
|
|
6e6429db82 | ||
|
|
05fc97a933 | ||
|
|
b9eb9d82f1 | ||
|
|
976acd4af2 | ||
|
|
c5cbe408c1 | ||
|
|
1316d5a698 | ||
|
|
89aaef8355 | ||
|
|
94e3dff909 | ||
|
|
6240c61a11 | ||
|
|
f226614980 | ||
|
|
0ab283b254 | ||
|
|
449485f06a | ||
|
|
60752e83ed | ||
|
|
dfd8b597cd | ||
|
|
e256fb1560 | ||
|
|
2f7c6e734e | ||
|
|
efe5f546a2 | ||
|
|
a8bd9ef97b | ||
|
|
e93ca5d86e | ||
|
|
7f943d34ac | ||
|
|
8a922bd083 | ||
|
|
d5c87c46aa | ||
|
|
f128dee3e4 | ||
|
|
9de129a35b | ||
|
|
6d6b0b975a | ||
|
|
bff8983b23 | ||
|
|
55e156579b | ||
|
|
a251dd4ffb | ||
|
|
526098f2bf | ||
|
|
6ccdc10208 | ||
|
|
ce355c108e | ||
|
|
612936476d | ||
|
|
7ba117ec25 | ||
|
|
8ea6bf85b8 | ||
|
|
70c87b4067 | ||
|
|
4628282715 | ||
|
|
6c14a2eccf | ||
|
|
8944941d80 | ||
|
|
78da98bd1d | ||
|
|
57833be7df | ||
|
|
506a0576df | ||
|
|
458f4d1ff9 | ||
|
|
21b9d51a35 | ||
|
|
82922b9792 | ||
|
|
160f783c6d | ||
|
|
ba7c665a9d | ||
|
|
b2f01a24b2 | ||
|
|
84ae520f93 | ||
|
|
4f61f4cf2c | ||
|
|
eba774618e | ||
|
|
a74b9e985d | ||
|
|
0c2e2db1dc | ||
|
|
98b227876b | ||
|
|
1fe50d2cca | ||
|
|
420c0348f9 | ||
|
|
9be81aa4a9 | ||
|
|
64c8f4225c | ||
|
|
a7c9604c43 | ||
|
|
68cc81e56c | ||
|
|
c9a64b1638 | ||
|
|
ee7cff0fea | ||
|
|
4c35f9a0cf | ||
|
|
9d9c4f026d | ||
|
|
b4c7ea5f26 | ||
|
|
093bee94c6 | ||
|
|
fb99429dd7 | ||
|
|
a73c4cd678 | ||
|
|
1694a1cb24 | ||
|
|
aaf9bdd00c | ||
|
|
129adc5825 | ||
|
|
07e7456ed8 | ||
|
|
7168e08587 | ||
|
|
efb3b27317 | ||
|
|
2c3247c194 | ||
|
|
d37ebf8cdd | ||
|
|
a73b049fd4 | ||
|
|
b3de2f805f | ||
|
|
0bec4f4d61 | ||
|
|
4be4a0968d | ||
|
|
97b957a0ab | ||
|
|
a22083dfcd | ||
|
|
9dbc3db1b8 | ||
|
|
7af71ba217 | ||
|
|
80f3523f2e | ||
|
|
0afad1d9ae | ||
|
|
915c6c1dfe | ||
|
|
e319aeb342 | ||
|
|
ac20426689 | ||
|
|
83c07467a9 | ||
|
|
0225b28fea | ||
|
|
1e2a740dae | ||
|
|
22e687b9d8 | ||
|
|
f088b90964 | ||
|
|
2493935349 | ||
|
|
c9699ba1fc | ||
|
|
051012be2d | ||
|
|
a38f1a2b2b | ||
|
|
0b66275995 | ||
|
|
7879fe4355 | ||
|
|
4aa88244ed | ||
|
|
bfe4d69b8a | ||
|
|
69fead70d1 | ||
|
|
92c663cd38 | ||
|
|
b829ed26f3 | ||
|
|
3d4834507d | ||
|
|
0758241e06 | ||
|
|
b093b5f979 | ||
|
|
d86481a0f7 | ||
|
|
de1788950f | ||
|
|
1e7b546b75 | ||
|
|
d53781e75a | ||
|
|
aa1830f12c | ||
|
|
17a87aa8c8 | ||
|
|
2662017fb7 | ||
|
|
f2b9cc3b3e | ||
|
|
2912ee5f73 | ||
|
|
9bb4c702f3 | ||
|
|
c4630f9243 | ||
|
|
57581d6f54 | ||
|
|
b5cfb5d567 | ||
|
|
04306a981f | ||
|
|
95409f7eda | ||
|
|
8741d820ad | ||
|
|
6020636a66 | ||
|
|
d15ffe7708 | ||
|
|
f77269e468 | ||
|
|
a2e44da99d | ||
|
|
8d7b86a667 | ||
|
|
0c6fad91ca | ||
|
|
c0f3fed142 | ||
|
|
21f5d503cd | ||
|
|
18b4f858d9 | ||
|
|
a101cac503 | ||
|
|
3f4c1953f8 | ||
|
|
a599b5b632 | ||
|
|
9fad3597bb | ||
|
|
f4b9889d67 | ||
|
|
d27b4f6905 | ||
|
|
855c7e623a | ||
|
|
a7e7bd0230 | ||
|
|
1d46897086 | ||
|
|
7af0721a75 | ||
|
|
884d738de9 | ||
|
|
ef71ca8a76 | ||
|
|
d4df12505b | ||
|
|
670d9b3913 | ||
|
|
19df5355ba | ||
|
|
8ca1c3be1f | ||
|
|
ed19aac553 | ||
|
|
7a67fbb980 | ||
|
|
d80b250650 | ||
|
|
300f2c2fb0 | ||
|
|
c2f108bf44 | ||
|
|
15abcd77d0 | ||
|
|
462f82e3fb | ||
|
|
eade3ce341 | ||
|
|
b4487159ed | ||
|
|
d12022266e | ||
|
|
13074a0f72 | ||
|
|
dc2c0c1027 | ||
|
|
97ed84127e | ||
|
|
d3f097f675 | ||
|
|
402e0576c8 | ||
|
|
aa8d41eecf | ||
|
|
54b53dbe56 | ||
|
|
2214c47c0c | ||
|
|
ed6275b0e8 | ||
|
|
97d062ffc2 | ||
|
|
c57c33c088 | ||
|
|
025d31102e | ||
|
|
cd96b6ab06 | ||
|
|
9358f86d43 | ||
|
|
7e51e067cd | ||
|
|
10ae66d1d0 | ||
|
|
cbdd1a2538 | ||
|
|
829292399b | ||
|
|
f7c7b8050f | ||
|
|
a3be59bd02 | ||
|
|
49e90a27b8 | ||
|
|
b559a13bab | ||
|
|
4d682f014d | ||
|
|
1a346e6b5a | ||
|
|
37a53748a2 | ||
|
|
9c2c932e75 | ||
|
|
c853d5bdf8 | ||
|
|
1f98e349dc | ||
|
|
883c14bf0d | ||
|
|
178abf0f83 | ||
|
|
c242fedfee | ||
|
|
922ccdfb27 | ||
|
|
965adddf8d | ||
|
|
8020b32541 | ||
|
|
33d798727c | ||
|
|
a0949c4e36 | ||
|
|
f00a0367cf | ||
|
|
eb5b83564f | ||
|
|
6b9dce10cf | ||
|
|
5d789bf96c | ||
|
|
17431734fb | ||
|
|
63c80fa526 | ||
|
|
2b79d437fc | ||
|
|
1e6041f99e | ||
|
|
fad4032a78 | ||
|
|
36ad006021 | ||
|
|
f6db690454 | ||
|
|
26575cfa0d | ||
|
|
73be639397 | ||
|
|
0ebd067bc2 | ||
|
|
49b3c33fbc | ||
|
|
4a5eab13c9 | ||
|
|
00ce6ce755 | ||
|
|
0aa95889aa | ||
|
|
3fdec074c6 | ||
|
|
29c6863b00 | ||
|
|
97eacb58a6 | ||
|
|
67bb28d027 | ||
|
|
513ed31b08 | ||
|
|
da5d95c7e5 | ||
|
|
7b9450807b | ||
|
|
8bc3631964 | ||
|
|
ab038983e5 | ||
|
|
79cff1ec9d | ||
|
|
4893cd0b03 | ||
|
|
b8fbeed785 | ||
|
|
8a9668bc79 | ||
|
|
d02d542207 | ||
|
|
4c797c5eb1 | ||
|
|
3fa2690277 | ||
|
|
67d482bad6 | ||
|
|
60981ba224 | ||
|
|
cb8ebfccb6 | ||
|
|
e5f0b71cf0 | ||
|
|
4218d81c49 | ||
|
|
9fa422ebf3 | ||
|
|
3ec488675f | ||
|
|
78b7d07500 |
108
.github/workflows/beta.yml
vendored
108
.github/workflows/beta.yml
vendored
@@ -15,53 +15,115 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
- name: Download last SHA artifact
|
||||
uses: dawidd6/action-download-artifact@v3
|
||||
with:
|
||||
workflow: beta.yml
|
||||
name: last-sha
|
||||
path: .
|
||||
|
||||
continue-on-error: true
|
||||
|
||||
- name: Get Commits Since Last Run
|
||||
run: |
|
||||
if [ -f last_sha.txt ]; then
|
||||
LAST_SHA=$(cat last_sha.txt)
|
||||
else
|
||||
# Fallback to first commit if no previous SHA available
|
||||
LAST_SHA=$(git rev-list --max-parents=0 HEAD)
|
||||
fi
|
||||
echo "Commits since $LAST_SHA:"
|
||||
# Accumulate commit logs in a shell variable
|
||||
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
|
||||
# URL-encode the newline characters for GitHub Actions
|
||||
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
|
||||
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
|
||||
COMMIT_LOGS="${COMMIT_LOGS//$'\r'/'%0D'}"
|
||||
# Append the encoded commit logs to the COMMIT_LOG environment variable
|
||||
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
|
||||
# Debugging: Print the variable to check its content
|
||||
echo "$COMMIT_LOGS"
|
||||
echo "$COMMIT_LOGS" > commit_log.txt
|
||||
shell: /usr/bin/bash -e {0}
|
||||
env:
|
||||
CI: true
|
||||
continue-on-error: true
|
||||
|
||||
- name: Save Current SHA for Next Run
|
||||
run: echo ${{ github.sha }} > last_sha.txt
|
||||
|
||||
- name: Set variables
|
||||
run: |
|
||||
VER=$(grep -E -o "versionName \".*\"" app/build.gradle | sed -e 's/versionName //g' | tr -d '"')
|
||||
SHA=${{ github.sha }}
|
||||
VERSION="$VER.${SHA:0:7}"
|
||||
VERSION="$VER+${SHA:0:7}"
|
||||
echo "Version $VERSION"
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 17
|
||||
cache: gradle
|
||||
|
||||
|
||||
- name: Decode Keystore File
|
||||
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
|
||||
|
||||
|
||||
- name: List files in the directory
|
||||
run: ls -l
|
||||
|
||||
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew assembleDebug -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload a Build Artifact
|
||||
uses: actions/upload-artifact@v3.0.0
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Dantotsu
|
||||
path: "app/build/outputs/apk/debug/app-debug.apk"
|
||||
|
||||
- name: Upload APK to Discord
|
||||
retention-days: 5
|
||||
compression-level: 9
|
||||
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
|
||||
|
||||
- name: Upload APK to Discord and Telegram
|
||||
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
|
||||
shell: bash
|
||||
run: |
|
||||
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" )
|
||||
curl -F "payload_json={\"content\":\" Debug-Build: <@719439449423085569> **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
#Discord
|
||||
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
|
||||
# Truncate commit messages if they are too long
|
||||
max_length=1900 # Adjust this value as needed
|
||||
if [ ${#commit_messages} -gt $max_length ]; then
|
||||
commit_messages="${commit_messages:0:$max_length}... (truncated)"
|
||||
fi
|
||||
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
|
||||
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
#Telegram
|
||||
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
|
||||
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
|
||||
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
|
||||
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
|
||||
|
||||
- name: Delete Old Pre-Releases
|
||||
id: delete-pre-releases
|
||||
uses: sgpublic/delete-release-action@master
|
||||
with:
|
||||
pre-release-drop: true
|
||||
pre-release-keep-count: 3
|
||||
pre-release-drop-tag: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMMIT_LOG: ${{ env.COMMIT_LOG }}
|
||||
VERSION: ${{ env.VERSION }}
|
||||
|
||||
- name: Upload Current SHA as Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: last-sha
|
||||
path: last_sha.txt
|
||||
|
||||
- name: Upload Commit log as Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: commit-log
|
||||
path: commit_log.txt
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,6 +8,9 @@ local.properties
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Secrets
|
||||
apikey.properties
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
@@ -28,3 +31,6 @@ output.json
|
||||
|
||||
#other
|
||||
scripts/
|
||||
|
||||
#crowdin
|
||||
crowdin.yml
|
||||
4
app/.gitignore
vendored
4
app/.gitignore
vendored
@@ -1,4 +1,8 @@
|
||||
/build
|
||||
/debug
|
||||
/debug/output-metadata.json
|
||||
/alpha
|
||||
/alpha/output-metadata.json
|
||||
/google/*
|
||||
/fdroid/*
|
||||
/release
|
||||
113
app/build.gradle
113
app/build.gradle
@@ -1,12 +1,9 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'com.google.devtools.ksp'
|
||||
|
||||
}
|
||||
|
||||
def gitCommitHash = providers.exec {
|
||||
@@ -18,27 +15,57 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "ani.dantotsu"
|
||||
minSdk 23
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
versionCode ((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "2.0.0-beta01-iv1"
|
||||
versionCode((System.currentTimeMillis() / 60000).toInteger())
|
||||
versionName "3.0.0"
|
||||
versionCode 300000000
|
||||
signingConfig signingConfigs.debug
|
||||
|
||||
}
|
||||
|
||||
flavorDimensions += "store"
|
||||
productFlavors {
|
||||
fdroid {
|
||||
// F-Droid specific configuration
|
||||
dimension "store"
|
||||
versionNameSuffix "-fdroid"
|
||||
}
|
||||
google {
|
||||
// Google Play specific configuration
|
||||
dimension "store"
|
||||
isDefault true
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
alpha {
|
||||
applicationIdSuffix ".beta" // keep as beta by popular request
|
||||
versionNameSuffix "-alpha01-" + gitCommitHash
|
||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
|
||||
debuggable System.getenv("CI") == null
|
||||
isDefault true
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix ".beta"
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
|
||||
debuggable System.getenv("CI") == null
|
||||
versionNameSuffix "-beta02"
|
||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
|
||||
debuggable false
|
||||
}
|
||||
release {
|
||||
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
|
||||
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher"
|
||||
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_round"
|
||||
debuggable false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
@@ -52,9 +79,14 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Core
|
||||
|
||||
// FireBase
|
||||
googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
|
||||
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
|
||||
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
|
||||
// Core
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.browser:browser:1.7.0'
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.2'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
@@ -63,13 +95,14 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:2.9.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
implementation 'androidx.webkit:webkit:1.9.0'
|
||||
implementation 'androidx.webkit:webkit:1.10.0'
|
||||
implementation "com.anggrayudi:storage:1.5.5"
|
||||
|
||||
// Glide
|
||||
// Glide
|
||||
ext.glide_version = '4.16.0'
|
||||
api "com.github.bumptech.glide:glide:$glide_version"
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
@@ -77,51 +110,61 @@ dependencies {
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
// FireBase
|
||||
implementation platform('com.google.firebase:firebase-bom:32.2.3')
|
||||
implementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.0'
|
||||
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.2.0'
|
||||
// Exoplayer
|
||||
ext.exo_version = '1.3.1'
|
||||
implementation "androidx.media3:media3-exoplayer:$exo_version"
|
||||
implementation "androidx.media3:media3-ui:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
|
||||
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
|
||||
implementation "androidx.media3:media3-session:$exo_version"
|
||||
//media3 casting
|
||||
// Media3 Casting
|
||||
implementation "androidx.media3:media3-cast:$exo_version"
|
||||
implementation "androidx.mediarouter:mediarouter:1.6.0"
|
||||
implementation "androidx.mediarouter:mediarouter:1.7.0"
|
||||
|
||||
// UI
|
||||
// UI
|
||||
implementation 'com.google.android.material:material:1.11.0'
|
||||
implementation 'nl.joery.animatedbottombar:library:1.1.0'
|
||||
implementation 'io.noties.markwon:core:4.6.2'
|
||||
implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
|
||||
implementation 'com.flaviofaria:kenburnsview:1.0.7'
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
|
||||
implementation 'com.alexvasilkov:gesture-views:2.8.3'
|
||||
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
|
||||
implementation 'com.github.eltos:simpledialogfragments:v3.7'
|
||||
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
|
||||
|
||||
// string matching
|
||||
// Markwon
|
||||
ext.markwon_version = '4.6.2'
|
||||
implementation "io.noties.markwon:core:$markwon_version"
|
||||
implementation "io.noties.markwon:editor:$markwon_version"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
|
||||
implementation "io.noties.markwon:ext-tables:$markwon_version"
|
||||
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
|
||||
implementation "io.noties.markwon:html:$markwon_version"
|
||||
implementation "io.noties.markwon:image-glide:$markwon_version"
|
||||
|
||||
// Groupie
|
||||
ext.groupie_version = '2.10.1'
|
||||
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
|
||||
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
|
||||
|
||||
// String Matching
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
|
||||
// Aniyomi
|
||||
// Aniyomi
|
||||
implementation 'io.reactivex:rxjava:1.3.8'
|
||||
implementation 'io.reactivex:rxandroid:1.2.1'
|
||||
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
|
||||
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
|
||||
implementation 'com.squareup.logcat:logcat:0.1'
|
||||
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
|
||||
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
|
||||
implementation 'com.squareup.okio:okio:3.7.0'
|
||||
implementation 'ch.acra:acra-http:5.11.3'
|
||||
implementation 'org.jsoup:jsoup:1.15.4'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
|
||||
implementation 'com.squareup.okio:okio:3.8.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
|
||||
implementation 'org.jsoup:jsoup:1.16.1'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
|
||||
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
|
||||
implementation 'com.github.tachiyomiorg:unifile:17bec43'
|
||||
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
|
||||
|
||||
@@ -43,6 +43,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||
"android_client_info": {
|
||||
"package_name": "ani.dantotsu.alpha"
|
||||
}
|
||||
},
|
||||
"oauth_client": [],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
|
||||
}
|
||||
],
|
||||
"services": {
|
||||
"appinvite_service": {
|
||||
"other_platform_oauth_client": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
|
||||
|
||||
19
app/proguard-rules.pro
vendored
19
app/proguard-rules.pro
vendored
@@ -43,6 +43,25 @@
|
||||
public static <1> INSTANCE;
|
||||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
-keep class ani.dantotsu.** { *; }
|
||||
-keep class ani.dantotsu.download.DownloadsManager { *; }
|
||||
-keepattributes Signature
|
||||
-keep class uy.kohesive.injekt.** { *; }
|
||||
-keep class eu.kanade.tachiyomi.** { *; }
|
||||
-keep class kotlin.** { *; }
|
||||
-dontwarn kotlin.**
|
||||
-keep class kotlinx.** { *; }
|
||||
-keepclassmembers class uy.kohesive.injekt.api.FullTypeReference {
|
||||
<init>(...);
|
||||
}
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keepattributes *Annotation*
|
||||
-keepattributes EnclosingMethod
|
||||
-keep class com.google.gson.reflect.TypeToken { *; }
|
||||
-keep class org.jsoup.** { *; }
|
||||
-keepclassmembers class org.jsoup.nodes.Document { *; }
|
||||
|
||||
|
||||
|
||||
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
|
||||
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
|
||||
|
||||
376
app/src/alpha/res/drawable/anim_splash.xml
Normal file
376
app/src/alpha/res/drawable/anim_splash.xml
Normal file
@@ -0,0 +1,376 @@
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
android:name="vector"
|
||||
android:width="768dp"
|
||||
android:height="768dp"
|
||||
android:viewportWidth="768"
|
||||
android:viewportHeight="768">
|
||||
<group
|
||||
android:name="wrapper"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="clippath"
|
||||
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z" />
|
||||
<group android:name="group">
|
||||
<group android:name="group_1">
|
||||
<path
|
||||
android:name="path"
|
||||
android:fillColor="#ED0021"
|
||||
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
|
||||
android:strokeWidth="1" />
|
||||
<group
|
||||
android:name="group_12"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:fillColor="#D40037"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="group_2">
|
||||
<group android:name="group_7">
|
||||
<group android:name="group_10">
|
||||
<group
|
||||
android:name="group_11"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:fillColor="#A70060"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
<clip-path
|
||||
android:name="mask_2"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_13"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="mask_1"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z" />
|
||||
<group
|
||||
android:name="group_9"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:fillColor="#BF005E"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_6"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-5"
|
||||
android:scaleX="1.2"
|
||||
android:scaleY="1.2" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_8"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-90">
|
||||
<group
|
||||
android:name="group_14"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440">
|
||||
<path
|
||||
android:name="path_4"
|
||||
android:fillColor="#C70051"
|
||||
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_3"
|
||||
android:translateX="-360">
|
||||
<path
|
||||
android:name="path_6"
|
||||
android:fillColor="#251528"
|
||||
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_4"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:scaleX="1.5"
|
||||
android:scaleY="1.5">
|
||||
<path
|
||||
android:name="path_5"
|
||||
android:fillColor="#251528"
|
||||
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_5"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:rotation="-15"
|
||||
android:scaleX="3"
|
||||
android:scaleY="3">
|
||||
<path
|
||||
android:name="path_7"
|
||||
android:fillAlpha="0"
|
||||
android:fillColor="#FFD8DF"
|
||||
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="wrapper">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_6">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="-10"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_3">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="400"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="translateX"
|
||||
android:startOffset="250"
|
||||
android:valueFrom="-360"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_4">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="400"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="400"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_7">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_5">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="-45"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_8">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_9">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_11">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_12">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_13">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_14">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="200"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:valueFrom="5"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="250"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="5"
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
4
app/src/alpha/res/values/strings.xml
Normal file
4
app/src/alpha/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dantotsu α</string>
|
||||
</resources>
|
||||
@@ -1,5 +1,4 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector
|
||||
@@ -14,23 +13,23 @@
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="clippath"
|
||||
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z"/>
|
||||
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z" />
|
||||
<group android:name="group">
|
||||
<group android:name="group_1">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
|
||||
android:fillColor="#6901fd"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
|
||||
android:strokeWidth="1" />
|
||||
<group
|
||||
android:name="group_12"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384">
|
||||
<path
|
||||
android:name="path_2"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
|
||||
android:fillColor="#4800e5"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="group_2">
|
||||
@@ -43,12 +42,12 @@
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_1"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:fillColor="#2000bd"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
<clip-path
|
||||
android:name="mask_2"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"/>
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
@@ -57,7 +56,7 @@
|
||||
android:pivotY="384">
|
||||
<clip-path
|
||||
android:name="mask_1"
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"/>
|
||||
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z" />
|
||||
<group
|
||||
android:name="group_9"
|
||||
android:pivotX="94"
|
||||
@@ -65,18 +64,18 @@
|
||||
android:rotation="-90">
|
||||
<path
|
||||
android:name="path_3"
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:fillColor="#1e00d1"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
android:name="group_6"
|
||||
android:pivotX="94"
|
||||
android:pivotY="440"
|
||||
android:rotation="-5"
|
||||
android:scaleX="1.2"
|
||||
android:scaleY="1.2"
|
||||
android:rotation="-5"/>
|
||||
android:scaleY="1.2" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_8"
|
||||
@@ -89,9 +88,9 @@
|
||||
android:pivotY="440">
|
||||
<path
|
||||
android:name="path_4"
|
||||
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
|
||||
android:fillColor="#2900da"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
@@ -100,9 +99,9 @@
|
||||
android:translateX="-360">
|
||||
<path
|
||||
android:name="path_6"
|
||||
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
|
||||
android:fillColor="#1f1f30"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_4"
|
||||
@@ -112,23 +111,23 @@
|
||||
android:scaleY="1.5">
|
||||
<path
|
||||
android:name="path_5"
|
||||
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
|
||||
android:fillColor="#1f1f30"
|
||||
android:strokeWidth="1"/>
|
||||
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
<group
|
||||
android:name="group_5"
|
||||
android:pivotX="384"
|
||||
android:pivotY="384"
|
||||
android:rotation="-15"
|
||||
android:scaleX="3"
|
||||
android:scaleY="3"
|
||||
android:rotation="-15">
|
||||
android:scaleY="3">
|
||||
<path
|
||||
android:name="path_7"
|
||||
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
|
||||
android:fillColor="#efe7ff"
|
||||
android:fillAlpha="0"
|
||||
android:strokeWidth="1"/>
|
||||
android:fillColor="#efe7ff"
|
||||
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
|
||||
android:strokeWidth="1" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
@@ -138,19 +137,19 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="500"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="500"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -158,177 +157,177 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="-10"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:duration="300"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="300"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:duration="300"
|
||||
android:valueFrom="1.2"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_3">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="400"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="translateX"
|
||||
android:startOffset="250"
|
||||
android:duration="400"
|
||||
android:valueFrom="-360"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_4">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="400"
|
||||
android:duration="350"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="400"
|
||||
android:duration="350"
|
||||
android:valueFrom="1.5"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="path_7">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="fillAlpha"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_5">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="-45"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:startOffset="350"
|
||||
android:duration="550"
|
||||
android:valueFrom="3"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_8">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:duration="350"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_9">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:duration="350"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="350"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_11">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
android:duration="350"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="100"
|
||||
android:duration="350"
|
||||
android:valueFrom="-90"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:interpolator/fast_out_slow_in"/>
|
||||
android:valueType="floatType" />
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="group_12">
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="550"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -336,19 +335,19 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleX"
|
||||
android:duration="550"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:propertyName="scaleY"
|
||||
android:duration="550"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"
|
||||
android:propertyName="scaleY"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="1"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/overshoot_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -356,21 +355,21 @@
|
||||
<aapt:attr name="android:animation">
|
||||
<set>
|
||||
<objectAnimator
|
||||
android:duration="200"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="350"
|
||||
android:duration="200"
|
||||
android:valueFrom="5"
|
||||
android:valueTo="0"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
<objectAnimator
|
||||
android:duration="100"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
|
||||
android:propertyName="rotation"
|
||||
android:startOffset="250"
|
||||
android:duration="100"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="5"
|
||||
android:valueType="floatType"
|
||||
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
|
||||
android:valueType="floatType" />
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
class CrashlyticsFactory {
|
||||
companion object {
|
||||
fun createCrashlytics(): CrashlyticsInterface {
|
||||
return CrashlyticsStub()
|
||||
}
|
||||
}
|
||||
}
|
||||
9
app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
Normal file
9
app/src/fdroid/java/ani/dantotsu/others/AppUpdater.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.others
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
class CrashlyticsFactory {
|
||||
companion object {
|
||||
fun createCrashlytics(): CrashlyticsInterface {
|
||||
return FirebaseCrashlytics()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
import com.google.firebase.FirebaseApp
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
|
||||
class FirebaseCrashlytics : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
FirebaseApp.initializeApp(context)
|
||||
}
|
||||
|
||||
override fun logException(e: Throwable) {
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
}
|
||||
|
||||
override fun log(message: String) {
|
||||
FirebaseCrashlytics.getInstance().log(message)
|
||||
}
|
||||
|
||||
override fun setUserId(id: String) {
|
||||
Firebase.crashlytics.setUserId(id)
|
||||
}
|
||||
|
||||
override fun setCustomKey(key: String, value: String) {
|
||||
FirebaseCrashlytics.getInstance().setCustomKey(key, value)
|
||||
}
|
||||
|
||||
override fun setCrashlyticsCollectionEnabled(enabled: Boolean) {
|
||||
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,12 +11,21 @@ import android.net.Uri
|
||||
import android.os.Environment
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.*
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.buildMarkwon
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -24,9 +33,8 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
object AppUpdater {
|
||||
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
|
||||
@@ -38,9 +46,10 @@ object AppUpdater {
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
val r = res.filter { it.prerelease }.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
|
||||
.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
} else {
|
||||
@@ -49,8 +58,8 @@ object AppUpdater {
|
||||
res to res.substringAfter("# ").substringBefore("\n")
|
||||
}
|
||||
|
||||
logger("Git Version : $version")
|
||||
val dontShow = loadData("dont_ask_for_update_$version") ?: false
|
||||
Logger.log("Git Version : $version")
|
||||
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
|
||||
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
setTitleText(
|
||||
@@ -60,8 +69,7 @@ object AppUpdater {
|
||||
)
|
||||
addView(
|
||||
TextView(activity).apply {
|
||||
val markWon = Markwon.builder(activity)
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
val markWon = buildMarkwon(activity, false)
|
||||
markWon.setMarkdown(this, md)
|
||||
}
|
||||
)
|
||||
@@ -71,19 +79,24 @@ object AppUpdater {
|
||||
false
|
||||
) { isChecked ->
|
||||
if (isChecked) {
|
||||
saveData("dont_ask_for_update_$version", true)
|
||||
PrefManager.setCustomVal("dont_ask_for_update_$version", true)
|
||||
}
|
||||
}
|
||||
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.find {
|
||||
it.browserDownloadURL.endsWith("apk")
|
||||
}?.browserDownloadURL.apply {
|
||||
if (this != null) activity.downloadUpdate(version, this)
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
val apkToDownload = apks?.first()
|
||||
apkToDownload?.browserDownloadURL.apply {
|
||||
if (this != null) activity.downloadUpdate(version, this)
|
||||
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
@@ -103,24 +116,25 @@ object AppUpdater {
|
||||
}
|
||||
|
||||
private fun compareVersion(version: String): Boolean {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
"debug" -> BuildConfig.VERSION_NAME != version
|
||||
"alpha" -> false
|
||||
else -> {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
return BuildConfig.VERSION_NAME != version
|
||||
} else {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
val new = toDouble(version.split("."))
|
||||
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
||||
new > curr
|
||||
}
|
||||
|
||||
val new = toDouble(version.split("."))
|
||||
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
|
||||
return new > curr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,21 +174,8 @@ object AppUpdater {
|
||||
DownloadManager.EXTRA_DOWNLOAD_ID, id
|
||||
) ?: id
|
||||
|
||||
val query = DownloadManager.Query()
|
||||
query.setFilterById(downloadId)
|
||||
val c = downloadManager.query(query)
|
||||
|
||||
if (c.moveToFirst()) {
|
||||
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||
if (DownloadManager.STATUS_SUCCESSFUL == c
|
||||
.getInt(columnIndex)
|
||||
) {
|
||||
c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)
|
||||
val uri = Uri.parse(
|
||||
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
)
|
||||
openApk(this@downloadUpdate, uri)
|
||||
}
|
||||
downloadManager.getUriForDownloadedFile(downloadId)?.let {
|
||||
openApk(this@downloadUpdate, it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
@@ -186,19 +187,14 @@ object AppUpdater {
|
||||
return true
|
||||
}
|
||||
|
||||
fun openApk(context: Context, uri: Uri) {
|
||||
private fun openApk(context: Context, uri: Uri) {
|
||||
try {
|
||||
uri.path?.let {
|
||||
val contentUri = FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".provider",
|
||||
File(it)
|
||||
)
|
||||
val installIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||
data = contentUri
|
||||
data = uri
|
||||
}
|
||||
context.startActivity(installIntent)
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="go.server.gojni" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
@@ -10,15 +12,16 @@
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||
<uses-permission
|
||||
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
|
||||
tools:ignore="LeanbackUsesWifi" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" /> <!-- For background jobs -->
|
||||
@@ -37,6 +40,17 @@
|
||||
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
<!-- ExoPlayer: Bluetooth Headsets -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.bluetooth"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.BLUETOOTH"
|
||||
android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<!-- ExoPlayer: Bluetooth Headsets -->
|
||||
|
||||
<queries>
|
||||
<package android:name="idm.internet.download.manager.plus" />
|
||||
<package android:name="idm.internet.download.manager" />
|
||||
@@ -48,6 +62,7 @@
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/ic_banner_foreground"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="${icon_placeholder}"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
@@ -56,9 +71,30 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Dantotsu"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="AllowBackup">
|
||||
tools:ignore="AllowBackup"
|
||||
tools:targetApi="tiramisu">
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.CurrentlyAiringWidget"
|
||||
android:name=".widgets.upcoming.UpcomingWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/upcoming_widget_info" />
|
||||
</receiver>
|
||||
<activity
|
||||
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".widgets.statistics.ProfileStatsWidget"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
@@ -66,10 +102,9 @@
|
||||
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/currently_airing_widget_info" />
|
||||
android:resource="@xml/statistics_widget_info" />
|
||||
</receiver>
|
||||
<receiver android:name=".subcriptions.NotificationClickReceiver" />
|
||||
|
||||
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
|
||||
|
||||
<activity
|
||||
android:name=".media.novel.novelreader.NovelReaderActivity"
|
||||
@@ -101,8 +136,61 @@
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAboutActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAccountActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAnimeActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsCommonActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsExtensionsActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsAddonActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsMangaActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsNotificationActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.SettingsThemeActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".settings.ExtensionsActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity
|
||||
android:name=".widgets.statistics.ProfileStatsConfigure"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".profile.ProfileActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity
|
||||
android:name=".profile.FollowActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity
|
||||
android:name=".profile.activity.FeedActivity"
|
||||
android:configChanges="orientation|screenSize|screenLayout"
|
||||
android:label="Inbox Activity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".profile.activity.NotificationActivity"
|
||||
android:label="Inbox Activity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity
|
||||
android:name=".others.imagesearch.ImageSearchActivity"
|
||||
@@ -116,6 +204,9 @@
|
||||
android:name=".media.CalendarActivity"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name=".media.user.ListActivity" />
|
||||
<activity
|
||||
android:name=".profile.SingleStatActivity"
|
||||
android:parentActivityName=".profile.ProfileActivity" />
|
||||
<activity
|
||||
android:name=".media.manga.mangareader.MangaReaderActivity"
|
||||
android:excludeFromRecents="true"
|
||||
@@ -126,7 +217,8 @@
|
||||
<activity
|
||||
android:name=".media.MediaDetailsActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@style/Theme.Dantotsu.NeverCutout" />
|
||||
android:theme="@style/Theme.Dantotsu.NeverCutout"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
<activity android:name=".media.CharacterDetailsActivity" />
|
||||
<activity android:name=".home.NoInternet" />
|
||||
<activity
|
||||
@@ -238,6 +330,17 @@
|
||||
<data android:host="myanimelist.net" />
|
||||
<data android:pathPrefix="/anime" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/view_profile_in_dantotsu">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="anilist.co" />
|
||||
<data android:pathPrefix="/user" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
@@ -253,24 +356,40 @@
|
||||
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:pathPattern=".*\\.ani" />
|
||||
<data android:pathPattern=".*\\.sani" />
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
<activity
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
|
||||
android:exported="false"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<receiver
|
||||
android:name=".subcriptions.AlarmReceiver"
|
||||
android:name=".notifications.AlarmPermissionStateReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".notifications.BootCompletedReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="Aani.dantotsu.ACTION_ALARM" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
|
||||
<receiver android:name=".notifications.comment.CommentNotificationReceiver" />
|
||||
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
|
||||
|
||||
<meta-data
|
||||
android:name="preloaded_fonts"
|
||||
@@ -288,25 +407,11 @@
|
||||
</provider>
|
||||
|
||||
<service
|
||||
android:name=".widgets.CurrentlyAiringRemoteViewsService"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS"
|
||||
android:exported="true" />
|
||||
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name=".download.video.ExoplayerDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
|
||||
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
@@ -317,19 +422,27 @@
|
||||
android:name=".download.novel.NovelDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name=".download.anime.AnimeDownloaderService"
|
||||
<service
|
||||
android:name=".download.anime.AnimeDownloaderService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service
|
||||
android:name=".connections.discord.DiscordService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
android:exported="true"/>
|
||||
<service
|
||||
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service
|
||||
android:name=".addons.torrent.ServerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="true" />
|
||||
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
app/src/main/ic_launcher_alpha-playstore.png
Normal file
BIN
app/src/main/ic_launcher_alpha-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -6,18 +6,24 @@ import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.aniyomi.anime.custom.AppModule
|
||||
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.notifications.TaskScheduler
|
||||
import ani.dantotsu.others.DisabledReports
|
||||
import ani.dantotsu.parsers.AnimeSources
|
||||
import ani.dantotsu.parsers.MangaSources
|
||||
import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import ani.dantotsu.settings.SettingsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.FinalExceptionHandler
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.crashlytics.ktx.crashlytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
@@ -28,7 +34,6 @@ import kotlinx.coroutines.launch
|
||||
import logcat.AndroidLogcatLogger
|
||||
import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
@@ -38,6 +43,9 @@ class App : MultiDexApplication() {
|
||||
private lateinit var animeExtensionManager: AnimeExtensionManager
|
||||
private lateinit var mangaExtensionManager: MangaExtensionManager
|
||||
private lateinit var novelExtensionManager: NovelExtensionManager
|
||||
private lateinit var torrentAddonManager: TorrentAddonManager
|
||||
private lateinit var downloadAddonManager: DownloadAddonManager
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
MultiDex.install(this)
|
||||
@@ -51,38 +59,39 @@ class App : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
|
||||
if (useMaterialYou) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
//TODO: HarmonizedColors
|
||||
}
|
||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||
|
||||
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).getBoolean("shared_user_id", true).let {
|
||||
if (!it) return@let
|
||||
val dUsername = getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).getString("discord_username", null)
|
||||
val aUsername = getSharedPreferences(
|
||||
getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
).getString("anilist_username", null)
|
||||
if (dUsername != null || aUsername != null) {
|
||||
Firebase.crashlytics.setUserId("$dUsername - $aUsername")
|
||||
}
|
||||
}
|
||||
FirebaseCrashlytics.getInstance().setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||
|
||||
PrefManager.init(this)
|
||||
Injekt.importModule(AppModule(this))
|
||||
Injekt.importModule(PreferenceModule(this))
|
||||
|
||||
initializeNetwork(baseContext)
|
||||
val crashlytics = Injekt.get<CrashlyticsInterface>()
|
||||
crashlytics.initialize(this)
|
||||
|
||||
val useMaterialYou: Boolean = PrefManager.getVal(PrefName.UseMaterialYou)
|
||||
if (useMaterialYou) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
|
||||
|
||||
crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
|
||||
(PrefManager.getVal(PrefName.SharedUserID) as Boolean).let {
|
||||
if (!it) return@let
|
||||
val dUsername = PrefManager.getVal(PrefName.DiscordUserName, null as String?)
|
||||
val aUsername = PrefManager.getVal(PrefName.AnilistUserName, null as String?)
|
||||
if (dUsername != null) {
|
||||
crashlytics.setCustomKey("dUsername", dUsername)
|
||||
}
|
||||
if (aUsername != null) {
|
||||
crashlytics.setCustomKey("aUsername", aUsername)
|
||||
}
|
||||
}
|
||||
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
|
||||
|
||||
Logger.init(this)
|
||||
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
|
||||
Logger.log("App: Logging started")
|
||||
|
||||
initializeNetwork()
|
||||
|
||||
setupNotificationChannels()
|
||||
if (!LogcatLogger.isInstalled) {
|
||||
@@ -92,33 +101,49 @@ class App : MultiDexApplication() {
|
||||
animeExtensionManager = Injekt.get()
|
||||
mangaExtensionManager = Injekt.get()
|
||||
novelExtensionManager = Injekt.get()
|
||||
torrentAddonManager = Injekt.get()
|
||||
downloadAddonManager = Injekt.get()
|
||||
|
||||
val animeScope = CoroutineScope(Dispatchers.Default)
|
||||
animeScope.launch {
|
||||
animeExtensionManager.findAvailableExtensions()
|
||||
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
|
||||
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
|
||||
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val mangaScope = CoroutineScope(Dispatchers.Default)
|
||||
mangaScope.launch {
|
||||
mangaExtensionManager.findAvailableExtensions()
|
||||
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
|
||||
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
|
||||
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
val novelScope = CoroutineScope(Dispatchers.Default)
|
||||
novelScope.launch {
|
||||
novelExtensionManager.findAvailableExtensions()
|
||||
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
|
||||
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
|
||||
}
|
||||
}
|
||||
val addonScope = CoroutineScope(Dispatchers.Default)
|
||||
addonScope.launch {
|
||||
torrentAddonManager.init()
|
||||
downloadAddonManager.init()
|
||||
}
|
||||
val commentsScope = CoroutineScope(Dispatchers.Default)
|
||||
commentsScope.launch {
|
||||
CommentsAPI.fetchAuthToken()
|
||||
}
|
||||
|
||||
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
|
||||
val scheduler = TaskScheduler.create(this, useAlarmManager)
|
||||
scheduler.scheduleAllTasks(this)
|
||||
scheduler.scheduleSingleWork(this)
|
||||
}
|
||||
|
||||
private fun setupNotificationChannels() {
|
||||
try {
|
||||
Notifications.createChannels(this)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
|
||||
Logger.log("Failed to modify notification channels")
|
||||
Logger.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +166,10 @@ class App : MultiDexApplication() {
|
||||
|
||||
companion object {
|
||||
private var instance: App? = null
|
||||
|
||||
/** Reference to the application context.
|
||||
*
|
||||
* USE WITH EXTREME CAUTION!**/
|
||||
var context: Context? = null
|
||||
fun currentContext(): Context? {
|
||||
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,9 @@ package ani.dantotsu
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.net.Uri
|
||||
@@ -12,12 +13,11 @@ import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnticipateInterpolator
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.OptIn
|
||||
@@ -26,42 +26,55 @@ import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import ani.dantotsu.addons.torrent.ServerService
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.databinding.ActivityMainBinding
|
||||
import ani.dantotsu.databinding.SplashScreenBinding
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.home.AnimeFragment
|
||||
import ani.dantotsu.home.HomeFragment
|
||||
import ani.dantotsu.home.LoginFragment
|
||||
import ani.dantotsu.home.MangaFragment
|
||||
import ani.dantotsu.home.NoInternet
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
|
||||
import ani.dantotsu.notifications.comment.CommentNotificationWorker
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.SharedPreferenceBooleanLiveData
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.activity.FeedActivity
|
||||
import ani.dantotsu.profile.activity.NotificationActivity
|
||||
import ani.dantotsu.settings.ExtensionsActivity
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.Serializable
|
||||
@@ -73,37 +86,92 @@ class MainActivity : AppCompatActivity() {
|
||||
private val scope = lifecycleScope
|
||||
private var load = false
|
||||
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
|
||||
|
||||
@kotlin.OptIn(DelicateCoroutinesApi::class)
|
||||
@SuppressLint("InternalInsetResource", "DiscouragedApi")
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeManager(this).applyTheme()
|
||||
LangSet.setLocale(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
//get FRAGMENT_CLASS_NAME from intent
|
||||
val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
val fragment = intent.getStringExtra("FRAGMENT_CLASS_NAME")
|
||||
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
|
||||
androidx.work.WorkManager.getInstance(this)
|
||||
.enqueue(OneTimeWorkRequest.Companion.from(CommentNotificationWorker::class.java))
|
||||
|
||||
androidx.work.WorkManager.getInstance(this)
|
||||
.enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
|
||||
|
||||
val action = intent.action
|
||||
val type = intent.type
|
||||
if (Intent.ACTION_VIEW == action && type != null) {
|
||||
val uri: Uri? = intent.data
|
||||
try {
|
||||
if (uri == null) {
|
||||
throw Exception("Uri is null")
|
||||
}
|
||||
val jsonString =
|
||||
contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val name =
|
||||
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
val decryptedJson = try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password,
|
||||
encrypted,
|
||||
salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast("Incorrect password")
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson)) {
|
||||
val intent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||
if (PreferencePackager.unpack(decryptedJson)) {
|
||||
val intent = Intent(this, this.javaClass)
|
||||
this.finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
toast("Invalid file type")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
toast("Error importing settings")
|
||||
}
|
||||
}
|
||||
|
||||
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val backgroundDrawable = bottomNavBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
}
|
||||
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
bottomNavBar.background = backgroundDrawable
|
||||
}
|
||||
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
val offset = try {
|
||||
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
|
||||
@@ -114,11 +182,10 @@ class MainActivity : AppCompatActivity() {
|
||||
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
|
||||
layoutParams.topMargin = 11 * offset / 12
|
||||
binding.incognito.layoutParams = layoutParams
|
||||
incognitoLiveData = SharedPreferenceBooleanLiveData(
|
||||
sharedPreferences,
|
||||
"incognito",
|
||||
incognitoLiveData = PrefManager.getLiveVal(
|
||||
PrefName.Incognito,
|
||||
false
|
||||
)
|
||||
).asLiveBool()
|
||||
incognitoLiveData.observe(this) {
|
||||
if (it) {
|
||||
val slideDownAnim = ObjectAnimator.ofFloat(
|
||||
@@ -154,20 +221,14 @@ class MainActivity : AppCompatActivity() {
|
||||
finish()
|
||||
}
|
||||
doubleBackToExitPressedOnce = true
|
||||
snackString(this@MainActivity.getString(R.string.back_to_exit))
|
||||
Handler(Looper.getMainLooper()).postDelayed(
|
||||
{ doubleBackToExitPressedOnce = false },
|
||||
2000
|
||||
)
|
||||
}
|
||||
|
||||
val preferences: SourcePreferences = Injekt.get()
|
||||
if (preferences.animeExtensionUpdatesCount().get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
"You have extension updates available!",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
|
||||
this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
super.onDismissed(transientBottomBar, event)
|
||||
doubleBackToExitPressedOnce = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
binding.root.isMotionEventSplittingEnabled = false
|
||||
@@ -213,24 +274,66 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = if (FRAGMENT_CLASS_NAME != null) {
|
||||
when (FRAGMENT_CLASS_NAME) {
|
||||
val preferences: SourcePreferences = Injekt.get()
|
||||
if (preferences.animeExtensionUpdatesCount()
|
||||
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
|
||||
) {
|
||||
snackString(R.string.extension_updates_available)
|
||||
?.setDuration(Snackbar.LENGTH_LONG)
|
||||
?.setAction(R.string.review) {
|
||||
startActivity(Intent(this, ExtensionsActivity::class.java))
|
||||
}
|
||||
}
|
||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
||||
selectedOption = if (fragment != null) {
|
||||
when (fragment) {
|
||||
AnimeFragment::class.java.name -> 0
|
||||
HomeFragment::class.java.name -> 1
|
||||
MangaFragment::class.java.name -> 2
|
||||
else -> 1
|
||||
}
|
||||
} else {
|
||||
uiSettings.defaultStartUpTab
|
||||
PrefManager.getVal(PrefName.DefaultStartUpTab)
|
||||
}
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
|
||||
}
|
||||
}
|
||||
val offlineMode = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("offlineMode", false)
|
||||
|
||||
var launched = false
|
||||
intent.extras?.let { extras ->
|
||||
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
|
||||
val mediaId = extras.getInt("mediaId", -1)
|
||||
val commentId = extras.getInt("commentId", -1)
|
||||
val activityId = extras.getInt("activityId", -1)
|
||||
|
||||
if (fragmentToLoad != null && mediaId != -1 && commentId != -1) {
|
||||
val detailIntent = Intent(this, MediaDetailsActivity::class.java).apply {
|
||||
putExtra("FRAGMENT_TO_LOAD", fragmentToLoad)
|
||||
putExtra("mediaId", mediaId)
|
||||
putExtra("commentId", commentId)
|
||||
}
|
||||
launched = true
|
||||
startActivity(detailIntent)
|
||||
} else if (fragmentToLoad == "FEED" && activityId != -1) {
|
||||
val feedIntent = Intent(this, FeedActivity::class.java).apply {
|
||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
||||
putExtra("activityId", activityId)
|
||||
|
||||
}
|
||||
launched = true
|
||||
startActivity(feedIntent)
|
||||
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
|
||||
Logger.log("MainActivity, onCreate: $activityId")
|
||||
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
|
||||
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
|
||||
putExtra("activityId", activityId)
|
||||
}
|
||||
launched = true
|
||||
startActivity(notificationIntent)
|
||||
}
|
||||
}
|
||||
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
if (!isOnline(this)) {
|
||||
snackString(this@MainActivity.getString(R.string.no_internet_connection))
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
@@ -240,7 +343,7 @@ class MainActivity : AppCompatActivity() {
|
||||
startActivity(Intent(this, NoInternet::class.java))
|
||||
} else {
|
||||
val model: AnilistHomeViewModel by viewModels()
|
||||
model.genres.observe(this) { it ->
|
||||
model.genres.observe(this) {
|
||||
if (it != null) {
|
||||
if (it) {
|
||||
val navbar = binding.includedNavbar.navbar
|
||||
@@ -251,7 +354,7 @@ class MainActivity : AppCompatActivity() {
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
@@ -265,12 +368,14 @@ class MainActivity : AppCompatActivity() {
|
||||
mainViewPager.setCurrentItem(newIndex, false)
|
||||
}
|
||||
})
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
selectedOption,
|
||||
false
|
||||
)
|
||||
if (mainViewPager.currentItem != selectedOption) {
|
||||
navbar.selectTabAt(selectedOption)
|
||||
mainViewPager.post {
|
||||
mainViewPager.setCurrentItem(
|
||||
selectedOption,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.mainProgressBar.visibility = View.GONE
|
||||
@@ -278,7 +383,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
//Load Data
|
||||
if (!load) {
|
||||
if (!load && !launched) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.loadMain(this@MainActivity)
|
||||
val id = intent.extras?.getInt("mediaId", 0)
|
||||
@@ -298,14 +403,27 @@ class MainActivity : AppCompatActivity() {
|
||||
snackString(this@MainActivity.getString(R.string.anilist_not_found))
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
startSubscription()
|
||||
val username = intent.extras?.getString("username")
|
||||
if (username != null) {
|
||||
val nameInt = username.toIntOrNull()
|
||||
if (nameInt != null) {
|
||||
startActivity(
|
||||
Intent(this@MainActivity, ProfileActivity::class.java)
|
||||
.putExtra("userId", nameInt)
|
||||
)
|
||||
} else {
|
||||
startActivity(
|
||||
Intent(this@MainActivity, ProfileActivity::class.java)
|
||||
.putExtra("username", username)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
load = true
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (loadData<Boolean>("allow_opening_links", this) != true) {
|
||||
if (!(PrefManager.getVal(PrefName.AllowOpeningLinks) as Boolean)) {
|
||||
CustomBottomDialog.newInstance().apply {
|
||||
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
|
||||
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
|
||||
@@ -317,45 +435,97 @@ class MainActivity : AppCompatActivity() {
|
||||
})
|
||||
|
||||
setNegativeButton(this@MainActivity.getString(R.string.no)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
|
||||
saveData("allow_opening_links", true, this@MainActivity)
|
||||
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
|
||||
tryWith(true) {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
}.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: Remove this
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val index = Helper.downloadManager(this@MainActivity).downloadIndex
|
||||
val downloadCursor = index.getDownloads()
|
||||
while (downloadCursor.moveToNext()) {
|
||||
val download = downloadCursor.download
|
||||
Log.e("Downloader", download.request.uri.toString())
|
||||
Log.e("Downloader", download.request.id.toString())
|
||||
Log.e("Downloader", download.request.mimeType.toString())
|
||||
Log.e("Downloader", download.request.data.size.toString())
|
||||
Log.e("Downloader", download.bytesDownloaded.toString())
|
||||
Log.e("Downloader", download.state.toString())
|
||||
Log.e("Downloader", download.failureReason.toString())
|
||||
|
||||
if (download.state == Download.STATE_FAILED) { //simple cleanup
|
||||
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
|
||||
val torrentManager = Injekt.get<TorrentAddonManager>()
|
||||
fun startTorrent() {
|
||||
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
|
||||
launchIO {
|
||||
if (!ServerService.isRunning()) {
|
||||
ServerService.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (torrentManager.isInitialized.value == false) {
|
||||
torrentManager.isInitialized.observe(this) {
|
||||
if (it) {
|
||||
startTorrent()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
startTorrent()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestart() {
|
||||
super.onRestart()
|
||||
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
|
||||
val params: ViewGroup.MarginLayoutParams =
|
||||
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.updateMargins(bottom = margin.toPx)
|
||||
}
|
||||
|
||||
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView =
|
||||
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = getString(R.string.enter_password_to_decrypt_file)
|
||||
|
||||
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
password.fill('0')
|
||||
dialog.dismiss()
|
||||
callback(null)
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
|
||||
// Override the positive button here
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
||||
if (editText?.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
dialog.dismiss()
|
||||
callback(password)
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//ViewPager
|
||||
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package ani.dantotsu
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.others.webview.CloudFlare
|
||||
import ani.dantotsu.others.webview.WebViewBottomDialog
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import com.lagradost.nicehttp.addGenericDns
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -34,13 +35,13 @@ lateinit var defaultHeaders: Map<String, String>
|
||||
lateinit var okHttpClient: OkHttpClient
|
||||
lateinit var client: Requests
|
||||
|
||||
fun initializeNetwork(context: Context) {
|
||||
fun initializeNetwork() {
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
defaultHeaders = mapOf(
|
||||
"User-Agent" to
|
||||
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
|
||||
defaultUserAgentProvider()
|
||||
.format(Build.VERSION.RELEASE, Build.MODEL)
|
||||
)
|
||||
|
||||
@@ -104,6 +105,7 @@ fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
|
||||
toast(e.localizedMessage)
|
||||
}
|
||||
e.printStackTrace()
|
||||
Logger.log(e)
|
||||
}
|
||||
|
||||
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
|
||||
@@ -134,7 +136,7 @@ suspend fun <T> tryWithSuspend(
|
||||
* A url, which can also have headers
|
||||
* **/
|
||||
data class FileUrl(
|
||||
val url: String,
|
||||
var url: String,
|
||||
val headers: Map<String, String> = mapOf()
|
||||
) : Serializable {
|
||||
companion object {
|
||||
|
||||
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
15
app/src/main/java/ani/dantotsu/addons/Addon.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
abstract class Addon {
|
||||
abstract val name: String
|
||||
abstract val pkgName: String
|
||||
abstract val versionName: String
|
||||
abstract val versionCode: Long
|
||||
|
||||
abstract class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
) : Addon()
|
||||
}
|
||||
129
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
129
app/src/main/java/ani/dantotsu/addons/AddonDownloader.kt
Normal file
@@ -0,0 +1,129 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.InstallerSteps
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
||||
class AddonDownloader {
|
||||
companion object {
|
||||
private suspend fun check(repo: String): Pair<String, String> {
|
||||
return try {
|
||||
val res = client.get("https://api.github.com/repos/$repo/releases")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
|
||||
}
|
||||
val r = res.maxByOrNull {
|
||||
it.timeStamp()
|
||||
} ?: throw Exception("No Pre Release Found")
|
||||
val v = r.tagName.substringAfter("v", "")
|
||||
val md = r.body ?: ""
|
||||
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
|
||||
|
||||
Logger.log("Git Version : $version")
|
||||
Pair(md, version)
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error checking for update")
|
||||
Logger.log(e)
|
||||
Pair("", "")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
|
||||
val (_, version) = check(repo)
|
||||
return compareVersion(version, currentVersion)
|
||||
}
|
||||
|
||||
suspend fun update(
|
||||
activity: Activity,
|
||||
manager: AddonManager<*>,
|
||||
repo: String,
|
||||
currentVersion: String
|
||||
) {
|
||||
val (_, version) = check(repo)
|
||||
if (!compareVersion(version, currentVersion)) {
|
||||
toast(activity.getString(R.string.no_update_found))
|
||||
return
|
||||
}
|
||||
MainScope().launch(Dispatchers.IO) {
|
||||
try {
|
||||
val apks =
|
||||
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
|
||||
.parsed<AppUpdater.GithubResponse>().assets?.filter {
|
||||
it.browserDownloadURL.endsWith(
|
||||
".apk"
|
||||
)
|
||||
}
|
||||
val apkToDownload =
|
||||
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
|
||||
?: apks?.find { it.browserDownloadURL.contains("universal") }
|
||||
?: apks?.first()
|
||||
apkToDownload?.browserDownloadURL.apply {
|
||||
if (this != null) {
|
||||
val notificationManager =
|
||||
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val installerSteps = InstallerSteps(notificationManager, activity)
|
||||
manager.install(this)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ installStep -> installerSteps.onInstallStep(installStep) {} },
|
||||
{ error -> installerSteps.onError(error) {} },
|
||||
{ installerSteps.onComplete {} }
|
||||
)
|
||||
} else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ABI that the app is most likely running on.
|
||||
* @return The primary ABI for the device.
|
||||
*/
|
||||
private fun getCurrentABI(): String {
|
||||
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
|
||||
Build.SUPPORTED_ABIS[0]
|
||||
} else "Unknown"
|
||||
}
|
||||
|
||||
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
|
||||
fun toDouble(list: List<String>): Double {
|
||||
return try {
|
||||
list.mapIndexed { i: Int, s: String ->
|
||||
when (i) {
|
||||
0 -> s.toDouble() * 100
|
||||
1 -> s.toDouble() * 10
|
||||
2 -> s.toDouble()
|
||||
else -> s.toDoubleOrNull() ?: 0.0
|
||||
}
|
||||
}.sum()
|
||||
} catch (e: NumberFormatException) {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
val new = toDouble(newVersion.split("."))
|
||||
val curr = toDouble(oldVersion.split("."))
|
||||
return new > curr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
11
app/src/main/java/ani/dantotsu/addons/AddonListener.kt
Normal file
@@ -0,0 +1,11 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
interface AddonListener {
|
||||
fun onAddonInstalled(result: LoadResult?)
|
||||
fun onAddonUpdated(result: LoadResult?)
|
||||
fun onAddonUninstalled(pkgName: String)
|
||||
|
||||
enum class ListenerAction {
|
||||
INSTALL, UPDATE, UNINSTALL
|
||||
}
|
||||
}
|
||||
143
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
143
app/src/main/java/ani/dantotsu/addons/AddonLoader.kt
Normal file
@@ -0,0 +1,143 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import ani.dantotsu.addons.download.DownloadAddon
|
||||
import ani.dantotsu.addons.download.DownloadAddonApi
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.download.DownloadLoadResult
|
||||
import ani.dantotsu.addons.torrent.TorrentAddon
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonApi
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentLoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import dalvik.system.PathClassLoader
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.util.system.getApplicationIcon
|
||||
|
||||
class AddonLoader {
|
||||
companion object {
|
||||
fun loadExtension(
|
||||
context: Context,
|
||||
packageName: String,
|
||||
className: String,
|
||||
type: AddonType
|
||||
): LoadResult? {
|
||||
val pkgManager = context.packageManager
|
||||
|
||||
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
|
||||
} else {
|
||||
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
|
||||
}
|
||||
|
||||
val extPkgs = installedPkgs.filter {
|
||||
isPackageAnExtension(
|
||||
packageName,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
||||
if (extPkgs.isEmpty()) return null
|
||||
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
|
||||
|
||||
val pkgName = extPkgs.first().packageName
|
||||
val pkgInfo = extPkgs.first()
|
||||
|
||||
val appInfo = try {
|
||||
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
|
||||
} catch (error: PackageManager.NameNotFoundException) {
|
||||
// Unlikely, but the package may have been uninstalled at this point
|
||||
Logger.log(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
val extName =
|
||||
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
|
||||
val versionName = pkgInfo.versionName
|
||||
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
|
||||
|
||||
if (versionName.isNullOrEmpty()) {
|
||||
Logger.log("Missing versionName for extension $extName")
|
||||
throw IllegalStateException("Missing versionName for extension $extName")
|
||||
}
|
||||
val classLoader =
|
||||
PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
|
||||
val loadedClass = try {
|
||||
Class.forName(className, false, classLoader)
|
||||
} catch (e: ClassNotFoundException) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: NoClassDefFoundError) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Extension load error: $extName ($className)")
|
||||
Logger.log(e)
|
||||
throw e
|
||||
}
|
||||
val instance = loadedClass.getDeclaredConstructor().newInstance()
|
||||
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> {
|
||||
val extension = instance as? TorrentAddonApi
|
||||
?: throw IllegalStateException("Extension is not a TorrentAddonApi")
|
||||
TorrentLoadResult.Success(
|
||||
TorrentAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
AddonType.DOWNLOAD -> {
|
||||
val extension = instance as? DownloadAddonApi
|
||||
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
|
||||
DownloadLoadResult.Success(
|
||||
DownloadAddon.Installed(
|
||||
name = extName,
|
||||
pkgName = pkgName,
|
||||
versionName = versionName,
|
||||
versionCode = versionCode,
|
||||
extension = extension,
|
||||
icon = context.getApplicationIcon(pkgName),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
|
||||
return when (type) {
|
||||
AddonType.TORRENT -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
TorrentAddonManager.TORRENT_CLASS,
|
||||
type
|
||||
)
|
||||
|
||||
AddonType.DOWNLOAD -> loadExtension(
|
||||
context,
|
||||
packageName,
|
||||
DownloadAddonManager.DOWNLOAD_CLASS,
|
||||
type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
|
||||
return pkgInfo.packageName.equals(type)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
46
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
46
app/src/main/java/ani/dantotsu/addons/AddonManager.kt
Normal file
@@ -0,0 +1,46 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
|
||||
import rx.Observable
|
||||
|
||||
abstract class AddonManager<T : Addon.Installed>(
|
||||
private val context: Context
|
||||
) {
|
||||
abstract var extension: T?
|
||||
abstract var name: String
|
||||
abstract var type: AddonType
|
||||
protected val installer by lazy { ExtensionInstaller(context) }
|
||||
var hasUpdate: Boolean = false
|
||||
protected set
|
||||
|
||||
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
|
||||
|
||||
abstract suspend fun init()
|
||||
abstract fun isAvailable(): Boolean
|
||||
abstract fun getVersion(): String?
|
||||
abstract fun getPackageName(): String?
|
||||
abstract fun hadError(context: Context): String?
|
||||
abstract fun updateInstallStep(id: Long, step: InstallStep)
|
||||
abstract fun setInstalling(id: Long)
|
||||
|
||||
fun uninstall() {
|
||||
getPackageName()?.let {
|
||||
installer.uninstallApk(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
|
||||
onListenerAction = action
|
||||
}
|
||||
|
||||
fun removeListenerAction() {
|
||||
onListenerAction = null
|
||||
}
|
||||
|
||||
fun install(url: String): Observable<InstallStep> {
|
||||
return installer.downloadAndInstall(url, getPackageName() ?: "", name, type)
|
||||
}
|
||||
}
|
||||
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
8
app/src/main/java/ani/dantotsu/addons/LoadResult.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package ani.dantotsu.addons
|
||||
|
||||
abstract class LoadResult {
|
||||
|
||||
abstract class Success : LoadResult()
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.media.AddonType
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import tachiyomi.core.util.lang.launchNow
|
||||
|
||||
internal class AddonInstallReceiver : BroadcastReceiver() {
|
||||
private var listener: AddonListener? = null
|
||||
private var type: AddonType? = null
|
||||
|
||||
/**
|
||||
* Registers this broadcast receiver
|
||||
*/
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
|
||||
}
|
||||
|
||||
fun setListener(listener: AddonListener, type: AddonType): AddonInstallReceiver {
|
||||
this.listener = listener
|
||||
this.type = type
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one of the events of the [filter] is received. When the package is an extension,
|
||||
* it's loaded in background and it notifies the [listener] when finished.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_PACKAGE_ADDED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonInstalled(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REPLACED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
launchNow {
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.DOWNLOAD
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
|
||||
listener?.onAddonUpdated(
|
||||
AddonLoader.loadFromPkgName(
|
||||
context,
|
||||
packageName,
|
||||
AddonType.TORRENT
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent.ACTION_PACKAGE_REMOVED -> {
|
||||
if (ExtensionInstallReceiver.isReplacing(intent)) return
|
||||
getPackageNameFromIntent(intent)?.let { packageName ->
|
||||
when (type) {
|
||||
AddonType.DOWNLOAD -> {
|
||||
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
AddonType.TORRENT -> {
|
||||
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
|
||||
listener?.onAddonUninstalled(packageName)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class DownloadAddon : Addon() {
|
||||
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: DownloadAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
|
||||
interface DownloadAddonApi {
|
||||
|
||||
fun cancelDownload(sessionId: Long)
|
||||
|
||||
fun setDownloadPath(context: Context, uri: Uri): String
|
||||
|
||||
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
|
||||
|
||||
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
|
||||
|
||||
fun getState(sessionId: Long): String
|
||||
|
||||
fun getStackTrace(sessionId: Long): String?
|
||||
|
||||
fun hadError(sessionId: Long): Boolean
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<DownloadAddon.Installed>(context) {
|
||||
|
||||
override var extension: DownloadAddon.Installed? = null
|
||||
override var name: String = "Download Addon"
|
||||
override var type = AddonType.DOWNLOAD
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
DOWNLOAD_PACKAGE,
|
||||
DOWNLOAD_CLASS,
|
||||
AddonType.DOWNLOAD
|
||||
) as? DownloadLoadResult
|
||||
result?.let {
|
||||
if (it is DownloadLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing Download extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is DownloadLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (extension?.pkgName == pkgName) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
|
||||
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Download-Addon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ani.dantotsu.addons.download
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class DownloadLoadResult : LoadResult() {
|
||||
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import ani.dantotsu.addons.Addon
|
||||
|
||||
sealed class TorrentAddon : Addon() {
|
||||
data class Installed(
|
||||
override val name: String,
|
||||
override val pkgName: String,
|
||||
override val versionName: String,
|
||||
override val versionCode: Long,
|
||||
val extension: TorrentAddonApi,
|
||||
val icon: Drawable?,
|
||||
val hasUpdate: Boolean = false,
|
||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
|
||||
|
||||
interface TorrentAddonApi {
|
||||
|
||||
fun startServer(path: String)
|
||||
|
||||
fun stopServer()
|
||||
|
||||
fun echo(): String
|
||||
|
||||
fun removeTorrent(torrent: String)
|
||||
|
||||
fun addTorrent(
|
||||
link: String,
|
||||
title: String,
|
||||
poster: String,
|
||||
data: String,
|
||||
save: Boolean,
|
||||
): Torrent
|
||||
|
||||
fun getLink(torrent: Torrent, index: Int): String
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
|
||||
import ani.dantotsu.addons.AddonListener
|
||||
import ani.dantotsu.addons.AddonLoader
|
||||
import ani.dantotsu.addons.AddonManager
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
import ani.dantotsu.addons.download.AddonInstallReceiver
|
||||
import ani.dantotsu.media.AddonType
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.extension.InstallStep
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TorrentAddonManager(
|
||||
private val context: Context
|
||||
) : AddonManager<TorrentAddon.Installed>(context) {
|
||||
override var extension: TorrentAddon.Installed? = null
|
||||
override var name: String = "Torrent Addon"
|
||||
override var type: AddonType = AddonType.TORRENT
|
||||
var torrentHash: String? = null
|
||||
|
||||
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
|
||||
val isInitialized: LiveData<Boolean> = _isInitialized
|
||||
|
||||
private var error: String? = null
|
||||
|
||||
override suspend fun init() {
|
||||
extension = null
|
||||
error = null
|
||||
hasUpdate = false
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
Logger.log("Torrent extension is not supported on this device.")
|
||||
error = context.getString(R.string.torrent_extension_not_supported)
|
||||
return
|
||||
}
|
||||
|
||||
AddonInstallReceiver()
|
||||
.setListener(InstallationListener(), type)
|
||||
.register(context)
|
||||
try {
|
||||
val result = AddonLoader.loadExtension(
|
||||
context,
|
||||
TORRENT_PACKAGE,
|
||||
TORRENT_CLASS,
|
||||
type
|
||||
) as TorrentLoadResult?
|
||||
result?.let {
|
||||
if (it is TorrentLoadResult.Success) {
|
||||
extension = it.extension
|
||||
hasUpdate = hasUpdate(REPO, it.extension.versionName)
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isInitialized.value = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log("Error initializing torrent extension")
|
||||
Logger.log(e)
|
||||
error = e.message
|
||||
}
|
||||
}
|
||||
|
||||
override fun isAvailable(): Boolean {
|
||||
return extension?.extension != null
|
||||
}
|
||||
|
||||
override fun getVersion(): String? {
|
||||
return extension?.versionName
|
||||
}
|
||||
|
||||
override fun getPackageName(): String? {
|
||||
return extension?.pkgName
|
||||
}
|
||||
|
||||
override fun hadError(context: Context): String? {
|
||||
return if (isInitialized.value == true) {
|
||||
if (error != null) {
|
||||
error
|
||||
} else if (extension != null) {
|
||||
context.getString(R.string.loaded_successfully)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class InstallationListener : AddonListener {
|
||||
override fun onAddonInstalled(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUpdated(result: LoadResult?) {
|
||||
if (result is TorrentLoadResult.Success) {
|
||||
extension = result.extension
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddonUninstalled(pkgName: String) {
|
||||
if (pkgName == TORRENT_PACKAGE) {
|
||||
extension = null
|
||||
hasUpdate = false
|
||||
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateInstallStep(id: Long, step: InstallStep) {
|
||||
installer.updateInstallStep(id, step)
|
||||
}
|
||||
|
||||
override fun setInstalling(id: Long) {
|
||||
installer.updateInstallStep(id, InstallStep.Installing)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
|
||||
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
|
||||
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import ani.dantotsu.addons.LoadResult
|
||||
|
||||
open class TorrentLoadResult : LoadResult() {
|
||||
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
|
||||
}
|
||||
168
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
168
app/src/main/java/ani/dantotsu/addons/torrent/TorrentService.kt
Normal file
@@ -0,0 +1,168 @@
|
||||
package ani.dantotsu.addons.torrent
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.util.Logger
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
|
||||
class ServerService : Service() {
|
||||
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
|
||||
private val applicationContext = Injekt.get<Application>()
|
||||
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
intent?.let {
|
||||
if (it.action != null) {
|
||||
when (it.action) {
|
||||
ACTION_START -> {
|
||||
startServer()
|
||||
notification(applicationContext)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
ACTION_STOP -> {
|
||||
stopServer()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun startServer() {
|
||||
serviceScope.launch {
|
||||
val echo = extension.echo()
|
||||
if (echo == "") {
|
||||
extension.startServer(filesDir.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopServer() {
|
||||
serviceScope.launch {
|
||||
extension.stopServer()
|
||||
applicationContext.cancelNotification(ID_TORRENT_SERVER)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun notification(context: Context) {
|
||||
val exitPendingIntent =
|
||||
PendingIntent.getService(
|
||||
applicationContext,
|
||||
0,
|
||||
Intent(applicationContext, ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setContentText("Torrent Server")
|
||||
setContentTitle("Server is running…")
|
||||
setAutoCancel(false)
|
||||
setOngoing(true)
|
||||
setUsesChronometer(true)
|
||||
addAction(
|
||||
R.drawable.ic_circle_cancel,
|
||||
"Stop",
|
||||
exitPendingIntent,
|
||||
)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
ID_TORRENT_SERVER,
|
||||
builder.build(),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
startForeground(ID_TORRENT_SERVER, builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ACTION_START = "start_torrent_server"
|
||||
const val ACTION_STOP = "stop_torrent_server"
|
||||
|
||||
fun isRunning(): Boolean {
|
||||
with(Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
|
||||
@Suppress("DEPRECATION") // We only need our services
|
||||
getRunningServices(Int.MAX_VALUE).forEach {
|
||||
if (ServerService::class.java.name.equals(it.service.className)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun start() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_START
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun stop() {
|
||||
try {
|
||||
val intent =
|
||||
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
|
||||
action = ACTION_STOP
|
||||
}
|
||||
Injekt.get<Application>().startService(intent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun wait(timeout: Int = -1): Boolean {
|
||||
var count = 0
|
||||
if (timeout < 0) {
|
||||
count = -20
|
||||
}
|
||||
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
while (echo == "") {
|
||||
Thread.sleep(1000)
|
||||
count++
|
||||
if (count > timeout) {
|
||||
return false
|
||||
}
|
||||
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
|
||||
}
|
||||
Logger.log("ServerService: Server started: $echo")
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package ani.dantotsu.aniyomi.anime.custom
|
||||
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.addons.torrent.TorrentAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.media.manga.MangaCache
|
||||
import ani.dantotsu.parsers.novel.NovelExtensionManager
|
||||
@@ -16,9 +18,9 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
|
||||
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
|
||||
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
import tachiyomi.domain.source.anime.service.AnimeSourceManager
|
||||
@@ -30,24 +32,25 @@ import uy.kohesive.injekt.api.addSingletonFactory
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class AppModule(val app: Application) : InjektModule {
|
||||
@kotlin.OptIn(ExperimentalSerializationApi::class)
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { DownloadsManager(app) }
|
||||
|
||||
addSingletonFactory { NetworkHelper(app, get()) }
|
||||
addSingletonFactory { NetworkHelper(app) }
|
||||
addSingletonFactory { NetworkHelper(app).client }
|
||||
|
||||
addSingletonFactory { AnimeExtensionManager(app) }
|
||||
addSingletonFactory { MangaExtensionManager(app) }
|
||||
addSingletonFactory { NovelExtensionManager(app) }
|
||||
addSingletonFactory { TorrentAddonManager(app) }
|
||||
addSingletonFactory { DownloadAddonManager(app) }
|
||||
|
||||
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
|
||||
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
|
||||
|
||||
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
addSingleton(sharedPreferences)
|
||||
|
||||
addSingletonFactory {
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
@@ -57,6 +60,10 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { StandaloneDatabaseProvider(app) }
|
||||
|
||||
addSingletonFactory<CrashlyticsInterface> {
|
||||
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
|
||||
}
|
||||
|
||||
addSingletonFactory { MangaCache() }
|
||||
|
||||
ContextCompat.getMainExecutor(app).execute {
|
||||
@@ -72,13 +79,6 @@ class PreferenceModule(val application: Application) : InjektModule {
|
||||
AndroidPreferenceStore(application)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
NetworkPreferences(
|
||||
preferenceStore = get(),
|
||||
verboseLogging = false,
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
SourcePreferences(get())
|
||||
}
|
||||
|
||||
@@ -6,19 +6,20 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun updateProgress(media: Media, number: String) {
|
||||
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
|
||||
?.getBoolean("incognito", false) ?: false
|
||||
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
|
||||
if (!incognito) {
|
||||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val a = number.toFloatOrNull()?.toInt()
|
||||
if ((a ?: 0) > (media.userProgress ?: 0)) {
|
||||
if ((a ?: 0) > (media.userProgress ?: -1)) {
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
a,
|
||||
|
||||
@@ -6,10 +6,14 @@ import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.comments.CommentsAPI
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import java.io.File
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import java.util.Calendar
|
||||
|
||||
object Anilist {
|
||||
@@ -24,28 +28,65 @@ object Anilist {
|
||||
var bg: String? = null
|
||||
var episodesWatched: Int? = null
|
||||
var chapterRead: Int? = null
|
||||
var unreadNotificationCount: Int = 0
|
||||
|
||||
var genres: ArrayList<String>? = null
|
||||
var tags: Map<Boolean, List<String>>? = null
|
||||
|
||||
var rateLimitReset: Long = 0
|
||||
|
||||
val sortBy = listOf(
|
||||
"SCORE_DESC",
|
||||
"POPULARITY_DESC",
|
||||
"TRENDING_DESC",
|
||||
"START_DATE_DESC",
|
||||
"TITLE_ENGLISH",
|
||||
"TITLE_ENGLISH_DESC",
|
||||
"SCORE"
|
||||
)
|
||||
|
||||
val source = listOf(
|
||||
"ORIGINAL",
|
||||
"MANGA",
|
||||
"LIGHT NOVEL",
|
||||
"VISUAL NOVEL",
|
||||
"VIDEO GAME",
|
||||
"OTHER",
|
||||
"NOVEL",
|
||||
"DOUJINSHI",
|
||||
"ANIME",
|
||||
"WEB NOVEL",
|
||||
"LIVE ACTION",
|
||||
"GAME",
|
||||
"COMIC",
|
||||
"MULTIMEDIA PROJECT",
|
||||
"PICTURE BOOK"
|
||||
)
|
||||
|
||||
val animeStatus = listOf(
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT YET RELEASED",
|
||||
"CANCELLED"
|
||||
)
|
||||
|
||||
val mangaStatus = listOf(
|
||||
"FINISHED",
|
||||
"RELEASING",
|
||||
"NOT YET RELEASED",
|
||||
"HIATUS",
|
||||
"CANCELLED"
|
||||
)
|
||||
|
||||
val seasons = listOf(
|
||||
"WINTER", "SPRING", "SUMMER", "FALL"
|
||||
)
|
||||
|
||||
val anime_formats = listOf(
|
||||
val animeFormats = listOf(
|
||||
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
||||
)
|
||||
|
||||
val manga_formats = listOf(
|
||||
val mangaFormats = listOf(
|
||||
"MANGA", "NOVEL", "ONE SHOT"
|
||||
)
|
||||
|
||||
@@ -94,15 +135,12 @@ object Anilist {
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
token = File(context.filesDir, "anilistToken").readText()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
fun getSavedToken(): Boolean {
|
||||
token = PrefManager.getVal(PrefName.AnilistToken, null as String?)
|
||||
return !token.isNullOrEmpty()
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
fun removeSavedToken() {
|
||||
token = null
|
||||
username = null
|
||||
adult = false
|
||||
@@ -111,9 +149,10 @@ object Anilist {
|
||||
bg = null
|
||||
episodesWatched = null
|
||||
chapterRead = null
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
File(context.filesDir, "anilistToken").delete()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.AnilistToken)
|
||||
//logout from comments api
|
||||
CommentsAPI.logout()
|
||||
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> executeQuery(
|
||||
@@ -124,7 +163,12 @@ object Anilist {
|
||||
show: Boolean = false,
|
||||
cache: Int? = null
|
||||
): T? {
|
||||
return tryWithSuspend {
|
||||
return try {
|
||||
if (show) Logger.log("Anilist Query: $query")
|
||||
if (rateLimitReset > System.currentTimeMillis() / 1000) {
|
||||
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
|
||||
}
|
||||
val data = mapOf(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
@@ -143,10 +187,28 @@ object Anilist {
|
||||
data = data,
|
||||
cacheTime = cache ?: 10
|
||||
)
|
||||
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
if (show) println("Response : ${json.text}")
|
||||
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
|
||||
Logger.log("Remaining requests: $remaining")
|
||||
if (json.code == 429) {
|
||||
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
|
||||
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
|
||||
if (retry > 0) {
|
||||
rateLimitReset = passedLimitReset
|
||||
}
|
||||
|
||||
toast("Rate limited. Try after $retry seconds")
|
||||
throw Exception("Rate limited after $retry seconds")
|
||||
}
|
||||
if (!json.text.startsWith("{")) {
|
||||
throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
}
|
||||
if (show) Logger.log("Anilist Response: ${json.text}")
|
||||
json.parsed()
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
if (show) snackString("Error fetching Anilist data: ${e.message}")
|
||||
Logger.log("Anilist Query Error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,23 @@ class AnilistMutations {
|
||||
executeQuery<JsonObject>(query, variables)
|
||||
}
|
||||
|
||||
suspend fun toggleFav(type: FavType, id: Int): Boolean {
|
||||
val filter = when (type) {
|
||||
FavType.ANIME -> "animeId"
|
||||
FavType.MANGA -> "mangaId"
|
||||
FavType.CHARACTER -> "characterId"
|
||||
FavType.STAFF -> "staffId"
|
||||
FavType.STUDIO -> "studioId"
|
||||
}
|
||||
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
|
||||
val result = executeQuery<JsonObject>(query)
|
||||
return result?.get("errors") == null && result != null
|
||||
}
|
||||
|
||||
enum class FavType {
|
||||
ANIME, MANGA, CHARACTER, STAFF, STUDIO
|
||||
}
|
||||
|
||||
suspend fun editList(
|
||||
mediaID: Int,
|
||||
progress: Int? = null,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,26 +5,25 @@ import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.BuildConfig
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val token = sharedPref.getString("discord_token", null)
|
||||
val userid = sharedPref.getString("discord_id", null)
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
|
||||
if (userid == null && token != null) {
|
||||
/*if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))*/
|
||||
@@ -59,52 +58,57 @@ class AnilistHomeViewModel : ViewModel() {
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
||||
|
||||
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||
suspend fun setAnimePlanned() =
|
||||
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||
|
||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
||||
|
||||
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||
suspend fun setMangaPlanned() =
|
||||
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||
|
||||
private val recommendation: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||
|
||||
suspend fun initHomePage() {
|
||||
val res = Anilist.query.initHomePage()
|
||||
Logger.log("AnilistHomeViewModel : res=$res")
|
||||
res["currentAnime"]?.let { animeContinue.postValue(it) }
|
||||
res["favoriteAnime"]?.let { animeFav.postValue(it) }
|
||||
res["plannedAnime"]?.let { animePlanned.postValue(it) }
|
||||
res["currentManga"]?.let { mangaContinue.postValue(it) }
|
||||
res["favoriteManga"]?.let { mangaFav.postValue(it) }
|
||||
res["plannedManga"]?.let { mangaPlanned.postValue(it) }
|
||||
res["recommendations"]?.let { recommendation.postValue(it) }
|
||||
}
|
||||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
Anilist.getSavedToken(context)
|
||||
MAL.getSavedToken(context)
|
||||
Discord.getSavedToken(context)
|
||||
if (loadData<Boolean>("check_update") != false) AppUpdater.check(context)
|
||||
genres.postValue(Anilist.query.getGenresAndTags(context))
|
||||
Anilist.getSavedToken()
|
||||
MAL.getSavedToken()
|
||||
Discord.getSavedToken()
|
||||
if (!BuildConfig.FLAVOR.contains("fdroid")) {
|
||||
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
|
||||
}
|
||||
genres.postValue(Anilist.query.getGenresAndTags())
|
||||
}
|
||||
|
||||
val empty = MutableLiveData<Boolean>(null)
|
||||
@@ -131,22 +135,19 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
sort = Anilist.sortBy[2],
|
||||
season = season,
|
||||
seasonYear = year,
|
||||
hd = true
|
||||
hd = true,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)?.results
|
||||
)
|
||||
}
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
||||
|
||||
private val animePopular = MutableLiveData<SearchResults?>(null)
|
||||
|
||||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
searchVal: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
@@ -154,10 +155,11 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
animePopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
genres = genres,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -172,13 +174,43 @@ class AnilistAnimeViewModel : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList
|
||||
r.onList,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
|
||||
)
|
||||
)
|
||||
|
||||
var loaded: Boolean = false
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||
|
||||
private val popularMovies: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
|
||||
|
||||
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
|
||||
|
||||
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
|
||||
suspend fun loadAll() {
|
||||
val list = Anilist.query.loadAnimeList()
|
||||
updated.postValue(list["recentUpdates"])
|
||||
popularMovies.postValue(list["trendingMovies"])
|
||||
topRatedAnime.postValue(list["topRated"])
|
||||
mostFavAnime.postValue(list["mostFav"])
|
||||
}
|
||||
}
|
||||
|
||||
class AnilistMangaViewModel : ViewModel() {
|
||||
@@ -196,29 +228,17 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
hd = true
|
||||
hd = true,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)?.results
|
||||
)
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadTrendingNovel() =
|
||||
updated.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
perPage = 10,
|
||||
sort = Anilist.sortBy[2],
|
||||
format = "NOVEL"
|
||||
)?.results
|
||||
)
|
||||
|
||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
searchVal: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
@@ -226,10 +246,11 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
mangaPopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
search = searchVal,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
genres = genres,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -244,17 +265,55 @@ class AnilistMangaViewModel : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
r.season,
|
||||
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
|
||||
)
|
||||
)
|
||||
|
||||
var loaded: Boolean = false
|
||||
|
||||
private val popularManga: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
|
||||
|
||||
private val popularManhwa: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
|
||||
|
||||
private val popularNovel: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
|
||||
|
||||
private val topRatedManga: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
|
||||
|
||||
private val mostFavManga: MutableLiveData<MutableList<Media>> =
|
||||
MutableLiveData<MutableList<Media>>(null)
|
||||
|
||||
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
|
||||
suspend fun loadAll() {
|
||||
val list = Anilist.query.loadMangaList()
|
||||
popularManga.postValue(list["trendingManga"])
|
||||
popularManhwa.postValue(list["trendingManhwa"])
|
||||
popularNovel.postValue(list["trendingNovel"])
|
||||
topRatedManga.postValue(list["topRated"])
|
||||
mostFavManga.postValue(list["mostFav"])
|
||||
}
|
||||
}
|
||||
|
||||
class AnilistSearch : ViewModel() {
|
||||
@@ -273,13 +332,17 @@ class AnilistSearch : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
r.season,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -292,11 +355,15 @@ class AnilistSearch : ViewModel() {
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.status,
|
||||
r.source,
|
||||
r.format,
|
||||
r.countryOfOrigin,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.startYear,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
)
|
||||
@@ -320,4 +387,40 @@ class GenresViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileViewModel : ViewModel() {
|
||||
|
||||
private val mangaFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||
|
||||
private val animeFav: MutableLiveData<ArrayList<Media>> =
|
||||
MutableLiveData<ArrayList<Media>>(null)
|
||||
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
|
||||
suspend fun setData(id: Int) {
|
||||
val res = Anilist.query.initProfilePage(id)
|
||||
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
|
||||
it.node?.let { i ->
|
||||
Media(i).apply { isFav = true }
|
||||
}
|
||||
}
|
||||
mangaFav.postValue(ArrayList(mangaList ?: arrayListOf()))
|
||||
val animeList = res?.data?.favoriteAnime?.favourites?.anime?.edges?.mapNotNull {
|
||||
it.node?.let { i ->
|
||||
Media(i).apply { isFav = true }
|
||||
}
|
||||
}
|
||||
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
|
||||
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
mangaFav.postValue(mangaFav.value)
|
||||
animeFav.postValue(animeFav.value)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,24 @@
|
||||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
val data: Uri? = intent?.data
|
||||
logger(data.toString())
|
||||
try {
|
||||
Anilist.token =
|
||||
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
val filename = "anilistToken"
|
||||
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
||||
it.write(Anilist.token!!.toByteArray())
|
||||
}
|
||||
PrefManager.setVal(PrefName.AnilistToken, Anilist.token ?: "")
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
@@ -11,13 +11,17 @@ data class SearchResults(
|
||||
var onList: Boolean? = null,
|
||||
var perPage: Int? = null,
|
||||
var search: String? = null,
|
||||
var countryOfOrigin: String? = null,
|
||||
var sort: String? = null,
|
||||
var genres: MutableList<String>? = null,
|
||||
var excludedGenres: MutableList<String>? = null,
|
||||
var tags: MutableList<String>? = null,
|
||||
var excludedTags: MutableList<String>? = null,
|
||||
var status: String? = null,
|
||||
var source: String? = null,
|
||||
var format: String? = null,
|
||||
var seasonYear: Int? = null,
|
||||
var startYear: Int? = null,
|
||||
var season: String? = null,
|
||||
var page: Int = 1,
|
||||
var results: MutableList<Media>,
|
||||
@@ -37,12 +41,24 @@ data class SearchResults(
|
||||
)
|
||||
)
|
||||
}
|
||||
status?.let {
|
||||
list.add(SearchChip("STATUS", currContext()!!.getString(R.string.filter_status, it)))
|
||||
}
|
||||
source?.let {
|
||||
list.add(SearchChip("SOURCE", currContext()!!.getString(R.string.filter_source, it)))
|
||||
}
|
||||
format?.let {
|
||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||
}
|
||||
countryOfOrigin?.let {
|
||||
list.add(SearchChip("COUNTRY", currContext()!!.getString(R.string.filter_country, it)))
|
||||
}
|
||||
season?.let {
|
||||
list.add(SearchChip("SEASON", it))
|
||||
}
|
||||
startYear?.let {
|
||||
list.add(SearchChip("START_YEAR", it.toString()))
|
||||
}
|
||||
seasonYear?.let {
|
||||
list.add(SearchChip("SEASON_YEAR", it.toString()))
|
||||
}
|
||||
@@ -74,8 +90,12 @@ data class SearchResults(
|
||||
fun removeChip(chip: SearchChip) {
|
||||
when (chip.type) {
|
||||
"SORT" -> sort = null
|
||||
"STATUS" -> status = null
|
||||
"SOURCE" -> source = null
|
||||
"FORMAT" -> format = null
|
||||
"COUNTRY" -> countryOfOrigin = null
|
||||
"SEASON" -> season = null
|
||||
"START_YEAR" -> startYear = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||
|
||||
@@ -5,27 +5,31 @@ import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import ani.dantotsu.loadMedia
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
class UrlMedia : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
ThemeManager(this).applyTheme()
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
var continueMedia = true
|
||||
if (id == 0) {
|
||||
continueMedia = false
|
||||
val data: Uri? = intent?.data
|
||||
isMAL = data?.host != "anilist.co"
|
||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||
} else loadMedia = id
|
||||
startMainActivity(
|
||||
this,
|
||||
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
||||
)
|
||||
val data: Uri? = intent?.data
|
||||
val type = data?.pathSegments?.getOrNull(0)
|
||||
if (type != "user") {
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
var continueMedia = true
|
||||
if (id == 0) {
|
||||
continueMedia = false
|
||||
isMAL = data?.host != "anilist.co"
|
||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||
} else loadMedia = id
|
||||
startMainActivity(
|
||||
this,
|
||||
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
|
||||
)
|
||||
} else {
|
||||
val username = data.pathSegments?.getOrNull(1)
|
||||
startMainActivity(this, bundleOf("username" to username))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ data class Character(
|
||||
|
||||
// Notes for site moderators
|
||||
@SerialName("modNotes") var modNotes: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterConnection(
|
||||
@@ -55,8 +55,8 @@ data class CharacterConnection(
|
||||
@SerialName("nodes") var nodes: List<Character>?,
|
||||
|
||||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterEdge(
|
||||
@@ -72,7 +72,7 @@ data class CharacterEdge(
|
||||
@SerialName("name") var name: String?,
|
||||
|
||||
// The voice actors of the character
|
||||
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
@SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
|
||||
// The voice actors of the character with role date
|
||||
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
||||
@@ -82,7 +82,7 @@ data class CharacterEdge(
|
||||
|
||||
// The order the character should be displayed from the users favourites
|
||||
@SerialName("favouriteOrder") var favouriteOrder: Int?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterName(
|
||||
@@ -109,7 +109,7 @@ data class CharacterName(
|
||||
|
||||
// The currently authenticated users preferred name language. Default romaji for non-authenticated
|
||||
@SerialName("userPreferred") var userPreferred: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CharacterImage(
|
||||
@@ -118,4 +118,4 @@ data class CharacterImage(
|
||||
|
||||
// The character's image of media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
@@ -24,7 +24,9 @@ class Query {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Media")
|
||||
val media: ani.dantotsu.connections.anilist.api.Media?
|
||||
val media: ani.dantotsu.connections.anilist.api.Media?,
|
||||
@SerialName("Page")
|
||||
val page: ani.dantotsu.connections.anilist.api.Page?
|
||||
)
|
||||
}
|
||||
|
||||
@@ -105,41 +107,620 @@ class Query {
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CombinedMediaListResponse(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("current") val current: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("planned") val planned: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("repeating") val repeating: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class HomePageMedia(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("currentAnime") val currentAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("repeatingAnime") val repeatingAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("plannedAnime") val plannedAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("currentManga") val currentManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("repeatingManga") val repeatingManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("plannedManga") val plannedManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
@SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ProfilePageMedia(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
|
||||
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AnimeList(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MangaList(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
|
||||
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ToggleFollow(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("ToggleFollow")
|
||||
val toggleFollow: FollowData
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GenreCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) {
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("GenreCollection")
|
||||
val genreCollection: List<String>?
|
||||
)
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaTagCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) {
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("MediaTagCollection")
|
||||
val mediaTagCollection: List<MediaTag>?
|
||||
)
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) {
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserProfileResponse(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("followerPage")
|
||||
val followerPage: UserProfilePage?,
|
||||
@SerialName("followingPage")
|
||||
val followingPage: UserProfilePage?,
|
||||
@SerialName("user")
|
||||
val user: UserProfile?
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserProfilePage(
|
||||
@SerialName("pageInfo")
|
||||
val pageInfo: PageInfo,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Following(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page: FollowingPage?
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Follower(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page: FollowerPage?
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class FollowerPage(
|
||||
@SerialName("followers")
|
||||
val followers: List<ani.dantotsu.connections.anilist.api.User>?
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FollowingPage(
|
||||
@SerialName("following")
|
||||
val following: List<ani.dantotsu.connections.anilist.api.User>?
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserProfile(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("about")
|
||||
val about: String?,
|
||||
@SerialName("avatar")
|
||||
val avatar: UserAvatar?,
|
||||
@SerialName("bannerImage")
|
||||
val bannerImage: String?,
|
||||
@SerialName("isFollowing")
|
||||
var isFollowing: Boolean,
|
||||
@SerialName("isFollower")
|
||||
val isFollower: Boolean,
|
||||
@SerialName("isBlocked")
|
||||
val isBlocked: Boolean,
|
||||
@SerialName("favourites")
|
||||
val favourites: UserFavourites?,
|
||||
@SerialName("statistics")
|
||||
val statistics: NNUserStatisticTypes,
|
||||
@SerialName("siteUrl")
|
||||
val siteUrl: String,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NNUserStatisticTypes(
|
||||
@SerialName("anime") var anime: NNUserStatistics,
|
||||
@SerialName("manga") var manga: NNUserStatistics
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NNUserStatistics(
|
||||
@SerialName("count") var count: Int,
|
||||
@SerialName("meanScore") var meanScore: Float,
|
||||
@SerialName("standardDeviation") var standardDeviation: Float,
|
||||
@SerialName("minutesWatched") var minutesWatched: Int,
|
||||
@SerialName("episodesWatched") var episodesWatched: Int,
|
||||
@SerialName("chaptersRead") var chaptersRead: Int,
|
||||
@SerialName("volumesRead") var volumesRead: Int,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserFavourites(
|
||||
@SerialName("anime")
|
||||
val anime: UserMediaFavouritesCollection,
|
||||
@SerialName("manga")
|
||||
val manga: UserMediaFavouritesCollection,
|
||||
@SerialName("characters")
|
||||
val characters: UserCharacterFavouritesCollection,
|
||||
@SerialName("staff")
|
||||
val staff: UserStaffFavouritesCollection,
|
||||
@SerialName("studios")
|
||||
val studios: UserStudioFavouritesCollection,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserMediaFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserMediaImageFavorite>,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserMediaImageFavorite(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("coverImage")
|
||||
val coverImage: MediaCoverImage
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserCharacterFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserCharacterImageFavorite>,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserCharacterImageFavorite(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: CharacterName,
|
||||
@SerialName("image")
|
||||
val image: CharacterImage,
|
||||
@SerialName("isFavourite")
|
||||
val isFavourite: Boolean
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStaffFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStudioFavouritesCollection(
|
||||
@SerialName("nodes")
|
||||
val nodes: List<UserStudioFavorite>,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStudioFavorite(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
) : java.io.Serializable
|
||||
|
||||
//----------------------------------------
|
||||
// Statistics
|
||||
|
||||
@Serializable
|
||||
data class StatisticsResponse(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: StatisticsUser?
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class StatisticsUser(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("mediaListOptions")
|
||||
val mediaListOptions: MediaListOptions,
|
||||
@SerialName("statistics")
|
||||
val statistics: StatisticsTypes
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsTypes(
|
||||
@SerialName("anime")
|
||||
val anime: Statistics,
|
||||
@SerialName("manga")
|
||||
val manga: Statistics
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Statistics(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("standardDeviation")
|
||||
val standardDeviation: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("episodesWatched")
|
||||
val episodesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("volumesRead")
|
||||
val volumesRead: Int,
|
||||
@SerialName("formats")
|
||||
val formats: List<StatisticsFormat>,
|
||||
@SerialName("statuses")
|
||||
val statuses: List<StatisticsStatus>,
|
||||
@SerialName("scores")
|
||||
val scores: List<StatisticsScore>,
|
||||
@SerialName("lengths")
|
||||
val lengths: List<StatisticsLength>,
|
||||
@SerialName("releaseYears")
|
||||
val releaseYears: List<StatisticsReleaseYear>,
|
||||
@SerialName("startYears")
|
||||
val startYears: List<StatisticsStartYear>,
|
||||
@SerialName("genres")
|
||||
val genres: List<StatisticsGenre>,
|
||||
@SerialName("tags")
|
||||
val tags: List<StatisticsTag>,
|
||||
@SerialName("countries")
|
||||
val countries: List<StatisticsCountry>,
|
||||
@SerialName("voiceActors")
|
||||
val voiceActors: List<StatisticsVoiceActor>,
|
||||
@SerialName("staff")
|
||||
val staff: List<StatisticsStaff>,
|
||||
@SerialName("studios")
|
||||
val studios: List<StatisticsStudio>
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsFormat(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("format")
|
||||
val format: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStatus(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("status")
|
||||
val status: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsScore(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("score")
|
||||
val score: Int
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsLength(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("length")
|
||||
val length: String? //can be null for manga
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsReleaseYear(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("releaseYear")
|
||||
val releaseYear: Int
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStartYear(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("startYear")
|
||||
val startYear: Int
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsGenre(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("genre")
|
||||
val genre: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsTag(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("tag")
|
||||
val tag: Tag
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Tag(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsCountry(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("country")
|
||||
val country: String
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsVoiceActor(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("voiceActor")
|
||||
val voiceActor: VoiceActor,
|
||||
@SerialName("characterIds")
|
||||
val characterIds: List<Int>
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VoiceActor(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: StaffName
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StaffName(
|
||||
@SerialName("first")
|
||||
val first: String?,
|
||||
@SerialName("middle")
|
||||
val middle: String?,
|
||||
@SerialName("last")
|
||||
val last: String?,
|
||||
@SerialName("full")
|
||||
val full: String?,
|
||||
@SerialName("native")
|
||||
val native: String?,
|
||||
@SerialName("alternative")
|
||||
val alternative: List<String>?,
|
||||
@SerialName("userPreferred")
|
||||
val userPreferred: String?
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStaff(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("staff")
|
||||
val staff: VoiceActor
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatisticsStudio(
|
||||
@SerialName("count")
|
||||
val count: Int,
|
||||
@SerialName("meanScore")
|
||||
val meanScore: Float,
|
||||
@SerialName("minutesWatched")
|
||||
val minutesWatched: Int,
|
||||
@SerialName("chaptersRead")
|
||||
val chaptersRead: Int,
|
||||
@SerialName("mediaIds")
|
||||
val mediaIds: List<Int>,
|
||||
@SerialName("studio")
|
||||
val studio: StatStudio
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatStudio(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@SerialName("isAnimationStudio")
|
||||
val isAnimationStudio: Boolean
|
||||
) : java.io.Serializable
|
||||
|
||||
}
|
||||
|
||||
//data class WhaData(
|
||||
@@ -169,7 +750,7 @@ class Query {
|
||||
// // Activity reply query
|
||||
// val ActivityReply: ActivityReply?,
|
||||
|
||||
// // Comment query
|
||||
// // CommentNotificationWorker query
|
||||
// val ThreadComment: List<ThreadComment>?,
|
||||
|
||||
// // Notification query
|
||||
|
||||
114
app/src/main/java/ani/dantotsu/connections/anilist/api/Feed.kt
Normal file
114
app/src/main/java/ani/dantotsu/connections/anilist/api/Feed.kt
Normal file
@@ -0,0 +1,114 @@
|
||||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FeedResponse(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page: ActivityPage
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ActivityPage(
|
||||
@SerialName("activities")
|
||||
val activities: List<Activity>
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Activity(
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("recipientId")
|
||||
val recipientId: Int?,
|
||||
@SerialName("messengerId")
|
||||
val messengerId: Int?,
|
||||
@SerialName("userId")
|
||||
val userId: Int?,
|
||||
@SerialName("type")
|
||||
val type: String,
|
||||
@SerialName("replyCount")
|
||||
val replyCount: Int,
|
||||
@SerialName("status")
|
||||
val status: String?,
|
||||
@SerialName("progress")
|
||||
val progress: String?,
|
||||
@SerialName("text")
|
||||
val text: String?,
|
||||
@SerialName("message")
|
||||
val message: String?,
|
||||
@SerialName("siteUrl")
|
||||
val siteUrl: String?,
|
||||
@SerialName("isLocked")
|
||||
val isLocked: Boolean,
|
||||
@SerialName("isSubscribed")
|
||||
val isSubscribed: Boolean,
|
||||
@SerialName("likeCount")
|
||||
var likeCount: Int?,
|
||||
@SerialName("isLiked")
|
||||
var isLiked: Boolean?,
|
||||
@SerialName("isPinned")
|
||||
val isPinned: Boolean?,
|
||||
@SerialName("isPrivate")
|
||||
val isPrivate: Boolean?,
|
||||
@SerialName("createdAt")
|
||||
val createdAt: Int,
|
||||
@SerialName("user")
|
||||
val user: User?,
|
||||
@SerialName("recipient")
|
||||
val recipient: User?,
|
||||
@SerialName("messenger")
|
||||
val messenger: User?,
|
||||
@SerialName("media")
|
||||
val media: Media?,
|
||||
@SerialName("replies")
|
||||
val replies: List<ActivityReply>?,
|
||||
@SerialName("likes")
|
||||
val likes: List<User>?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ActivityReply(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("userId")
|
||||
val userId: Int,
|
||||
@SerialName("text")
|
||||
val text: String,
|
||||
@SerialName("likeCount")
|
||||
val likeCount: Int,
|
||||
@SerialName("isLiked")
|
||||
val isLiked: Boolean,
|
||||
@SerialName("createdAt")
|
||||
val createdAt: Int,
|
||||
@SerialName("user")
|
||||
val user: User,
|
||||
@SerialName("likes")
|
||||
val likes: List<User>?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ToggleLike(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("ToggleLikeV2")
|
||||
val toggleLike: LikeData
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class LikeData(
|
||||
@SerialName("__typename")
|
||||
val typename: String
|
||||
) : java.io.Serializable
|
||||
@@ -251,7 +251,7 @@ data class MediaCoverImage(
|
||||
|
||||
// Average #hex color of cover image
|
||||
@SerialName("color") var color: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MediaList(
|
||||
@@ -490,7 +490,7 @@ data class MediaExternalLink(
|
||||
|
||||
// isDisabled: Boolean
|
||||
@SerialName("notes") var notes: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class ExternalLinkType {
|
||||
@@ -512,7 +512,13 @@ data class MediaListCollection(
|
||||
// If there is another chunk
|
||||
@SerialName("hasNextChunk") var hasNextChunk: Boolean?,
|
||||
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FollowData(
|
||||
@SerialName("id") var id: Int,
|
||||
@SerialName("isFollowing") var isFollowing: Boolean,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MediaListGroup(
|
||||
@@ -526,4 +532,4 @@ data class MediaListGroup(
|
||||
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
|
||||
|
||||
@SerialName("status") var status: MediaListStatus?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
@@ -0,0 +1,123 @@
|
||||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
enum class NotificationType(val value: String) {
|
||||
ACTIVITY_MESSAGE("ACTIVITY_MESSAGE"),
|
||||
ACTIVITY_REPLY("ACTIVITY_REPLY"),
|
||||
FOLLOWING("FOLLOWING"),
|
||||
ACTIVITY_MENTION("ACTIVITY_MENTION"),
|
||||
THREAD_COMMENT_MENTION("THREAD_COMMENT_MENTION"),
|
||||
THREAD_SUBSCRIBED("THREAD_SUBSCRIBED"),
|
||||
THREAD_COMMENT_REPLY("THREAD_COMMENT_REPLY"),
|
||||
AIRING("AIRING"),
|
||||
ACTIVITY_LIKE("ACTIVITY_LIKE"),
|
||||
ACTIVITY_REPLY_LIKE("ACTIVITY_REPLY_LIKE"),
|
||||
THREAD_LIKE("THREAD_LIKE"),
|
||||
THREAD_COMMENT_LIKE("THREAD_COMMENT_LIKE"),
|
||||
ACTIVITY_REPLY_SUBSCRIBED("ACTIVITY_REPLY_SUBSCRIBED"),
|
||||
RELATED_MEDIA_ADDITION("RELATED_MEDIA_ADDITION"),
|
||||
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
|
||||
MEDIA_MERGE("MEDIA_MERGE"),
|
||||
MEDIA_DELETION("MEDIA_DELETION"),
|
||||
|
||||
//custom
|
||||
COMMENT_REPLY("COMMENT_REPLY"),
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NotificationResponse(
|
||||
@SerialName("data")
|
||||
val data: Data,
|
||||
) : java.io.Serializable {
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: NotificationUser,
|
||||
@SerialName("Page")
|
||||
val page: NotificationPage,
|
||||
) : java.io.Serializable
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NotificationUser(
|
||||
@SerialName("unreadNotificationCount")
|
||||
var unreadNotificationCount: Int,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NotificationPage(
|
||||
@SerialName("pageInfo")
|
||||
val pageInfo: PageInfo,
|
||||
@SerialName("notifications")
|
||||
val notifications: List<Notification>,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Notification(
|
||||
@SerialName("__typename")
|
||||
val typename: String,
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("userId")
|
||||
val userId: Int? = null,
|
||||
@SerialName("CommentId")
|
||||
val commentId: Int?,
|
||||
@SerialName("type")
|
||||
val notificationType: String,
|
||||
@SerialName("activityId")
|
||||
val activityId: Int? = null,
|
||||
@SerialName("animeId")
|
||||
val mediaId: Int? = null,
|
||||
@SerialName("episode")
|
||||
val episode: Int? = null,
|
||||
@SerialName("contexts")
|
||||
val contexts: List<String>? = null,
|
||||
@SerialName("context")
|
||||
val context: String? = null,
|
||||
@SerialName("reason")
|
||||
val reason: String? = null,
|
||||
@SerialName("deletedMediaTitle")
|
||||
val deletedMediaTitle: String? = null,
|
||||
@SerialName("deletedMediaTitles")
|
||||
val deletedMediaTitles: List<String>? = null,
|
||||
@SerialName("createdAt")
|
||||
val createdAt: Int,
|
||||
@SerialName("media")
|
||||
val media: Media? = null,
|
||||
@SerialName("user")
|
||||
val user: User? = null,
|
||||
@SerialName("message")
|
||||
val message: MessageActivity? = null,
|
||||
@SerialName("activity")
|
||||
val activity: ActivityUnion? = null,
|
||||
@SerialName("Thread")
|
||||
val thread: Thread? = null,
|
||||
@SerialName("comment")
|
||||
val comment: ThreadComment? = null,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MessageActivity(
|
||||
@SerialName("id")
|
||||
val id: Int?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ActivityUnion(
|
||||
@SerialName("id")
|
||||
val id: Int?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Thread(
|
||||
@SerialName("id")
|
||||
val id: Int?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ThreadComment(
|
||||
@SerialName("id")
|
||||
val id: Int?,
|
||||
) : java.io.Serializable
|
||||
@@ -15,7 +15,7 @@ data class Staff(
|
||||
@SerialName("languageV2") var languageV2: String?,
|
||||
|
||||
// The staff images
|
||||
// @SerialName("image") var image: StaffImage?,
|
||||
@SerialName("image") var image: StaffImage?,
|
||||
|
||||
// A general description of the staff member
|
||||
@SerialName("description") var description: String?,
|
||||
@@ -94,6 +94,15 @@ data class StaffConnection(
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaffImage(
|
||||
// The character's image of media at its largest size
|
||||
@SerialName("large") var large: String?,
|
||||
|
||||
// The character's image of media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StaffEdge(
|
||||
var role: String?,
|
||||
|
||||
@@ -46,7 +46,7 @@ data class User(
|
||||
@SerialName("statistics") var statistics: UserStatisticTypes?,
|
||||
|
||||
// The number of unread notifications the user has
|
||||
// @SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
|
||||
@SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
|
||||
|
||||
// The url for the user page on the AniList website
|
||||
// @SerialName("siteUrl") var siteUrl: String?,
|
||||
@@ -111,7 +111,7 @@ data class UserAvatar(
|
||||
|
||||
// The avatar of user at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
)
|
||||
) : java.io.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UserStatisticTypes(
|
||||
@@ -164,7 +164,7 @@ data class Favourites(
|
||||
@Serializable
|
||||
data class MediaListOptions(
|
||||
// The score format the user is using for media lists
|
||||
// @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
|
||||
@SerialName("scoreFormat") var scoreFormat: String?,
|
||||
|
||||
// The default order list rows should be displayed in
|
||||
@SerialName("rowOrder") var rowOrder: String?,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package ani.dantotsu.connections.bakaupdates
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okio.ByteString.Companion.encode
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
class MangaUpdates {
|
||||
|
||||
private val Int?.dateFormat get() = String.format("%02d", this)
|
||||
|
||||
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
|
||||
|
||||
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
|
||||
return tryWithSuspend {
|
||||
val query = JSONObject().apply {
|
||||
try {
|
||||
put("search", title.encode(Charset.forName("UTF-8")))
|
||||
startDate?.let {
|
||||
put(
|
||||
"start_date",
|
||||
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
|
||||
)
|
||||
}
|
||||
put("include_metadata", true)
|
||||
} catch (e: JSONException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
|
||||
coroutineScope {
|
||||
res.results?.map {
|
||||
async(Dispatchers.IO) {
|
||||
Logger.log(it.toString())
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
res.results?.first {
|
||||
it.metadata.series.lastUpdated?.timestamp != null
|
||||
&& (it.metadata.series.latestChapter != null
|
||||
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
|
||||
return results.metadata.series.latestChapter?.let {
|
||||
context.getString(R.string.chapter_number, it)
|
||||
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
|
||||
chapter.takeIf {
|
||||
it.toIntOrNull() == null
|
||||
} ?: context.getString(R.string.chapter_number, chapter.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MangaUpdatesResponse(
|
||||
@SerialName("total_hits")
|
||||
val totalHits: Int?,
|
||||
@SerialName("page")
|
||||
val page: Int?,
|
||||
@SerialName("per_page")
|
||||
val perPage: Int?,
|
||||
val results: List<Results>? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Results(
|
||||
val record: Record,
|
||||
val metadata: MetaData
|
||||
) {
|
||||
@Serializable
|
||||
data class Record(
|
||||
@SerialName("id")
|
||||
val id: Int,
|
||||
@SerialName("title")
|
||||
val title: String,
|
||||
@SerialName("volume")
|
||||
val volume: String?,
|
||||
@SerialName("chapter")
|
||||
val chapter: String?,
|
||||
@SerialName("release_date")
|
||||
val releaseDate: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MetaData(
|
||||
val series: Series
|
||||
) {
|
||||
@Serializable
|
||||
data class Series(
|
||||
@SerialName("series_id")
|
||||
val seriesId: Long?,
|
||||
@SerialName("title")
|
||||
val title: String?,
|
||||
@SerialName("latest_chapter")
|
||||
val latestChapter: Int?,
|
||||
@SerialName("last_updated")
|
||||
val lastUpdated: LastUpdated?
|
||||
) {
|
||||
@Serializable
|
||||
data class LastUpdated(
|
||||
@SerialName("timestamp")
|
||||
val timestamp: Long,
|
||||
@SerialName("as_rfc3339")
|
||||
val asRfc3339: String,
|
||||
@SerialName("as_string")
|
||||
val asString: String
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
package ani.dantotsu.connections.comments
|
||||
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.toast
|
||||
import com.lagradost.nicehttp.NiceResponse
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.OkHttpClient
|
||||
import okio.IOException
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
object CommentsAPI {
|
||||
private const val ADDRESS: String = "https://1224665.xyz:443"
|
||||
var authToken: String? = null
|
||||
var userId: String? = null
|
||||
var isBanned: Boolean = false
|
||||
var isAdmin: Boolean = false
|
||||
var isMod: Boolean = false
|
||||
var totalVotes: Int = 0
|
||||
|
||||
suspend fun getCommentsForId(
|
||||
id: Int,
|
||||
page: Int = 1,
|
||||
tag: Int?,
|
||||
sort: String?
|
||||
): CommentResponse? {
|
||||
var url = "$ADDRESS/comments/$id/$page"
|
||||
val request = requestBuilder()
|
||||
tag?.let {
|
||||
url += "?tag=$it"
|
||||
}
|
||||
sort?.let {
|
||||
url += if (tag != null) "&sort=$it" else "?sort=$it"
|
||||
}
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to fetch comments")
|
||||
return null
|
||||
}
|
||||
if (!json.text.startsWith("{")) return null
|
||||
val res = json.code == 200
|
||||
if (!res && json.code != 404) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
val parsed = try {
|
||||
Json.decodeFromString<CommentResponse>(json.text)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
|
||||
val url = "$ADDRESS/comments/parent/$id/$page"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to fetch comments")
|
||||
return null
|
||||
}
|
||||
if (!json.text.startsWith("{")) return null
|
||||
val res = json.code == 200
|
||||
if (!res && json.code != 404) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
val parsed = try {
|
||||
Json.decodeFromString<CommentResponse>(json.text)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
suspend fun getSingleComment(id: Int): Comment? {
|
||||
val url = "$ADDRESS/comments/$id"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to fetch comment")
|
||||
return null
|
||||
}
|
||||
if (!json.text.startsWith("{")) return null
|
||||
val res = json.code == 200
|
||||
if (!res && json.code != 404) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
val parsed = try {
|
||||
Json.decodeFromString<Comment>(json.text)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
suspend fun vote(commentId: Int, voteType: Int): Boolean {
|
||||
val url = "$ADDRESS/comments/vote/$commentId/$voteType"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to vote")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
if (!res) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
|
||||
val url = "$ADDRESS/comments"
|
||||
val body = FormBody.Builder()
|
||||
.add("user_id", userId ?: return null)
|
||||
.add("media_id", mediaId.toString())
|
||||
.add("content", content)
|
||||
if (tag != null) {
|
||||
body.add("tag", tag.toString())
|
||||
}
|
||||
parentCommentId?.let {
|
||||
body.add("parent_comment_id", it.toString())
|
||||
}
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url, requestBody = body.build())
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to comment")
|
||||
return null
|
||||
}
|
||||
val res = json.code == 200
|
||||
if (!res) {
|
||||
errorReason(json.code, json.text)
|
||||
return null
|
||||
}
|
||||
val parsed = try {
|
||||
Json.decodeFromString<ReturnedComment>(json.text)
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to parse comment")
|
||||
return null
|
||||
}
|
||||
return Comment(
|
||||
parsed.id,
|
||||
parsed.userId,
|
||||
parsed.mediaId,
|
||||
parsed.parentCommentId,
|
||||
parsed.content,
|
||||
parsed.timestamp,
|
||||
parsed.deleted,
|
||||
parsed.tag,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
Anilist.username ?: "",
|
||||
Anilist.avatar,
|
||||
totalVotes = totalVotes
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun deleteComment(commentId: Int): Boolean {
|
||||
val url = "$ADDRESS/comments/$commentId"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.delete(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to delete comment")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
if (!res) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
suspend fun editComment(commentId: Int, content: String): Boolean {
|
||||
val url = "$ADDRESS/comments/$commentId"
|
||||
val body = FormBody.Builder()
|
||||
.add("content", content)
|
||||
.build()
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.put(url, requestBody = body)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to edit comment")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
if (!res) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
suspend fun banUser(userId: String): Boolean {
|
||||
val url = "$ADDRESS/ban/$userId"
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to ban user")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
if (!res) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
suspend fun reportComment(
|
||||
commentId: Int,
|
||||
username: String,
|
||||
mediaTitle: String,
|
||||
reportedId: String
|
||||
): Boolean {
|
||||
val url = "$ADDRESS/report/$commentId"
|
||||
val body = FormBody.Builder()
|
||||
.add("username", username)
|
||||
.add("mediaName", mediaTitle)
|
||||
.add("reporter", Anilist.username ?: "unknown")
|
||||
.add("reportedId", reportedId)
|
||||
.build()
|
||||
val request = requestBuilder()
|
||||
val json = try {
|
||||
request.post(url, requestBody = body)
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to report comment")
|
||||
return false
|
||||
}
|
||||
val res = json.code == 200
|
||||
if (!res) {
|
||||
errorReason(json.code, json.text)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
|
||||
val url = "$ADDRESS/notification/reply"
|
||||
val request = requestBuilder(client)
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
}
|
||||
if (!json.text.startsWith("{")) return null
|
||||
val res = json.code == 200
|
||||
if (!res) {
|
||||
return null
|
||||
}
|
||||
val parsed = try {
|
||||
Json.decodeFromString<NotificationResponse>(json.text)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
|
||||
val url = "$ADDRESS/user"
|
||||
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
||||
val json = try {
|
||||
request.get(url)
|
||||
} catch (e: IOException) {
|
||||
return null
|
||||
}
|
||||
if (json.code == 200) {
|
||||
val parsed = try {
|
||||
Json.decodeFromString<UserResponse>(json.text)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return null
|
||||
}
|
||||
isBanned = parsed.user.isBanned ?: false
|
||||
isAdmin = parsed.user.isAdmin ?: false
|
||||
isMod = parsed.user.isMod ?: false
|
||||
totalVotes = parsed.user.totalVotes
|
||||
return parsed.user
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun fetchAuthToken(client: OkHttpClient? = null) {
|
||||
if (authToken != null) return
|
||||
val MAX_RETRIES = 5
|
||||
val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days
|
||||
val tokenExpiry = PrefManager.getVal<Long>(PrefName.CommentTokenExpiry)
|
||||
if (tokenExpiry < System.currentTimeMillis() + tokenLifetime) {
|
||||
val commentResponse =
|
||||
PrefManager.getNullableVal<AuthResponse>(PrefName.CommentAuthResponse, null)
|
||||
if (commentResponse != null) {
|
||||
authToken = commentResponse.authToken
|
||||
userId = commentResponse.user.id
|
||||
isBanned = commentResponse.user.isBanned ?: false
|
||||
isAdmin = commentResponse.user.isAdmin ?: false
|
||||
isMod = commentResponse.user.isMod ?: false
|
||||
totalVotes = commentResponse.user.totalVotes
|
||||
if (getUserDetails(client) != null) return
|
||||
}
|
||||
|
||||
}
|
||||
val url = "$ADDRESS/authenticate"
|
||||
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
|
||||
repeat(MAX_RETRIES) {
|
||||
try {
|
||||
val json = authRequest(token, url, client)
|
||||
if (json.code == 200) {
|
||||
if (!json.text.startsWith("{")) throw IOException("Invalid response")
|
||||
val parsed = try {
|
||||
Json.decodeFromString<AuthResponse>(json.text)
|
||||
} catch (e: Exception) {
|
||||
snackString("Failed to login to comments API: ${e.printStackTrace()}")
|
||||
return
|
||||
}
|
||||
PrefManager.setVal(PrefName.CommentAuthResponse, parsed)
|
||||
PrefManager.setVal(
|
||||
PrefName.CommentTokenExpiry,
|
||||
System.currentTimeMillis() + tokenLifetime
|
||||
)
|
||||
authToken = parsed.authToken
|
||||
userId = parsed.user.id
|
||||
isBanned = parsed.user.isBanned ?: false
|
||||
isAdmin = parsed.user.isAdmin ?: false
|
||||
isMod = parsed.user.isMod ?: false
|
||||
totalVotes = parsed.user.totalVotes
|
||||
return
|
||||
} else if (json.code != 429) {
|
||||
errorReason(json.code, json.text)
|
||||
return
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
snackString("Failed to login to comments API")
|
||||
return
|
||||
}
|
||||
kotlinx.coroutines.delay(60000)
|
||||
}
|
||||
snackString("Failed to login after multiple attempts")
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
PrefManager.removeVal(PrefName.CommentAuthResponse)
|
||||
PrefManager.removeVal(PrefName.CommentTokenExpiry)
|
||||
authToken = null
|
||||
userId = null
|
||||
isBanned = false
|
||||
isAdmin = false
|
||||
isMod = false
|
||||
totalVotes = 0
|
||||
}
|
||||
|
||||
private suspend fun authRequest(
|
||||
token: String,
|
||||
url: String,
|
||||
client: OkHttpClient? = null
|
||||
): NiceResponse {
|
||||
val body: FormBody = FormBody.Builder()
|
||||
.add("token", token)
|
||||
.build()
|
||||
val request = if (client != null) requestBuilder(client) else requestBuilder()
|
||||
return request.post(url, requestBody = body)
|
||||
}
|
||||
|
||||
private fun headerBuilder(): Map<String, String> {
|
||||
val map = mutableMapOf(
|
||||
"appauth" to "6*45Qp%W2RS@t38jkXoSKY588Ynj%n"
|
||||
)
|
||||
if (authToken != null) {
|
||||
map["Authorization"] = authToken!!
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
|
||||
return Requests(
|
||||
client,
|
||||
headerBuilder()
|
||||
)
|
||||
}
|
||||
|
||||
private fun errorReason(code: Int, reason: String? = null) {
|
||||
val error = when (code) {
|
||||
429 -> "Rate limited. :("
|
||||
else -> "Failed to connect"
|
||||
}
|
||||
val parsed = try {
|
||||
Json.decodeFromString<ErrorResponse>(reason!!)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val message = parsed?.message ?: reason ?: error
|
||||
val fullMessage = if (code == 500) message else "$code: $message"
|
||||
|
||||
toast(fullMessage)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ErrorResponse(
|
||||
@SerialName("message")
|
||||
val message: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NotificationResponse(
|
||||
@SerialName("notifications")
|
||||
val notifications: List<Notification>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Notification(
|
||||
@SerialName("username")
|
||||
val username: String,
|
||||
@SerialName("media_id")
|
||||
val mediaId: Int,
|
||||
@SerialName("comment_id")
|
||||
val commentId: Int,
|
||||
@SerialName("type")
|
||||
val type: Int? = null,
|
||||
@SerialName("content")
|
||||
val content: String? = null,
|
||||
@SerialName("notification_id")
|
||||
val notificationId: Int
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class AuthResponse(
|
||||
@SerialName("authToken")
|
||||
val authToken: String,
|
||||
@SerialName("user")
|
||||
val user: User
|
||||
) : java.io.Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID: Long = 1
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UserResponse(
|
||||
@SerialName("user")
|
||||
val user: User
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
@SerialName("user_id")
|
||||
val id: String,
|
||||
@SerialName("username")
|
||||
val username: String,
|
||||
@SerialName("profile_picture_url")
|
||||
val profilePictureUrl: String? = null,
|
||||
@SerialName("is_banned")
|
||||
@Serializable(with = NumericBooleanSerializer::class)
|
||||
val isBanned: Boolean? = null,
|
||||
@SerialName("is_mod")
|
||||
@Serializable(with = NumericBooleanSerializer::class)
|
||||
val isAdmin: Boolean? = null,
|
||||
@SerialName("is_admin")
|
||||
@Serializable(with = NumericBooleanSerializer::class)
|
||||
val isMod: Boolean? = null,
|
||||
@SerialName("total_votes")
|
||||
val totalVotes: Int,
|
||||
@SerialName("warnings")
|
||||
val warnings: Int
|
||||
) : java.io.Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID: Long = 1
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class CommentResponse(
|
||||
@SerialName("comments")
|
||||
val comments: List<Comment>,
|
||||
@SerialName("totalPages")
|
||||
val totalPages: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Comment(
|
||||
@SerialName("comment_id")
|
||||
val commentId: Int,
|
||||
@SerialName("user_id")
|
||||
val userId: String,
|
||||
@SerialName("media_id")
|
||||
val mediaId: Int,
|
||||
@SerialName("parent_comment_id")
|
||||
val parentCommentId: Int?,
|
||||
@SerialName("content")
|
||||
var content: String,
|
||||
@SerialName("timestamp")
|
||||
var timestamp: String,
|
||||
@SerialName("deleted")
|
||||
@Serializable(with = NumericBooleanSerializer::class)
|
||||
val deleted: Boolean?,
|
||||
@SerialName("tag")
|
||||
val tag: Int?,
|
||||
@SerialName("upvotes")
|
||||
var upvotes: Int,
|
||||
@SerialName("downvotes")
|
||||
var downvotes: Int,
|
||||
@SerialName("user_vote_type")
|
||||
var userVoteType: Int?,
|
||||
@SerialName("username")
|
||||
val username: String,
|
||||
@SerialName("profile_picture_url")
|
||||
val profilePictureUrl: String?,
|
||||
@SerialName("is_mod")
|
||||
@Serializable(with = NumericBooleanSerializer::class)
|
||||
val isMod: Boolean? = null,
|
||||
@SerialName("is_admin")
|
||||
@Serializable(with = NumericBooleanSerializer::class)
|
||||
val isAdmin: Boolean? = null,
|
||||
@SerialName("reply_count")
|
||||
val replyCount: Int? = null,
|
||||
@SerialName("total_votes")
|
||||
val totalVotes: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReturnedComment(
|
||||
@SerialName("id")
|
||||
var id: Int,
|
||||
@SerialName("comment_id")
|
||||
var commentId: Int?,
|
||||
@SerialName("user_id")
|
||||
val userId: String,
|
||||
@SerialName("media_id")
|
||||
val mediaId: Int,
|
||||
@SerialName("parent_comment_id")
|
||||
val parentCommentId: Int? = null,
|
||||
@SerialName("content")
|
||||
val content: String,
|
||||
@SerialName("timestamp")
|
||||
val timestamp: String,
|
||||
@SerialName("deleted")
|
||||
@Serializable(with = NumericBooleanSerializer::class)
|
||||
val deleted: Boolean?,
|
||||
@SerialName("tag")
|
||||
val tag: Int? = null,
|
||||
)
|
||||
|
||||
object NumericBooleanSerializer : KSerializer<Boolean> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("NumericBoolean", PrimitiveKind.INT)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Boolean) {
|
||||
encoder.encodeInt(if (value) 1 else 0)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Boolean {
|
||||
return decoder.decodeInt() != 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
|
||||
interface CrashlyticsInterface {
|
||||
fun initialize(context: Context)
|
||||
fun logException(e: Throwable)
|
||||
fun log(message: String)
|
||||
fun setUserId(id: String)
|
||||
fun setCustomKey(key: String, value: String)
|
||||
fun setCrashlyticsCollectionEnabled(enabled: Boolean)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ani.dantotsu.connections.crashlytics
|
||||
|
||||
import android.content.Context
|
||||
import ani.dantotsu.util.Logger
|
||||
|
||||
class CrashlyticsStub : CrashlyticsInterface {
|
||||
override fun initialize(context: Context) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun logException(e: Throwable) {
|
||||
Logger.log(e)
|
||||
}
|
||||
|
||||
override fun log(message: String) {
|
||||
Logger.log(message)
|
||||
}
|
||||
|
||||
override fun setUserId(id: String) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun setCustomKey(key: String, value: String) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
override fun setCrashlyticsCollectionEnabled(enabled: Boolean) {
|
||||
//no-op
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,9 +3,10 @@ package ani.dantotsu.connections.discord
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.edit
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWith
|
||||
import io.noties.markwon.Markwon
|
||||
@@ -18,37 +19,20 @@ object Discord {
|
||||
var userid: String? = null
|
||||
var avatar: String? = null
|
||||
|
||||
const val TOKEN = "discord_token"
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
fun getSavedToken(): Boolean {
|
||||
token = PrefManager.getVal(
|
||||
PrefName.DiscordToken, null as String?
|
||||
)
|
||||
token = sharedPref.getString(TOKEN, null)
|
||||
return token != null
|
||||
}
|
||||
|
||||
fun saveToken(context: Context, token: String) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
putString(TOKEN, token)
|
||||
commit()
|
||||
}
|
||||
fun saveToken(token: String) {
|
||||
PrefManager.setVal(PrefName.DiscordToken, token)
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
remove(TOKEN)
|
||||
commit()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.DiscordToken)
|
||||
|
||||
tryWith(true) {
|
||||
val dir = File(context.filesDir?.parentFile, "app_webview")
|
||||
@@ -86,19 +70,7 @@ object Discord {
|
||||
|
||||
const val application_Id = "1163925779692912771"
|
||||
const val small_Image: String =
|
||||
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
|
||||
/*fun defaultRPC(): RPC? {
|
||||
return token?.let {
|
||||
RPC(it, Dispatchers.IO).apply {
|
||||
applicationId = application_Id
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
small_Image
|
||||
)
|
||||
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
|
||||
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
|
||||
const val small_Image_AniList: String =
|
||||
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
|
||||
}
|
||||
@@ -5,17 +5,12 @@ import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
@@ -24,6 +19,9 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import ani.dantotsu.connections.discord.serializers.User
|
||||
import ani.dantotsu.isOnline
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
@@ -35,7 +33,6 @@ import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.io.File
|
||||
import java.io.OutputStreamWriter
|
||||
|
||||
class DiscordService : Service() {
|
||||
private var heartbeat: Int = 0
|
||||
@@ -47,6 +44,7 @@ class DiscordService : Service() {
|
||||
private lateinit var heartbeatThread: Thread
|
||||
private lateinit var client: OkHttpClient
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
private val shouldLog = false
|
||||
var presenceStore = ""
|
||||
val json = Json {
|
||||
encodeDefaults = true
|
||||
@@ -65,7 +63,7 @@ class DiscordService : Service() {
|
||||
PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"discordRPC:backgroundPresence"
|
||||
)
|
||||
wakeLock.acquire()
|
||||
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
|
||||
log("WakeLock Acquired")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val serviceChannel = NotificationChannel(
|
||||
@@ -149,27 +147,19 @@ class DiscordService : Service() {
|
||||
}
|
||||
|
||||
fun saveProfile(response: String) {
|
||||
val sharedPref = baseContext.getSharedPreferences(
|
||||
baseContext.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val user = json.decodeFromString<User.Response>(response).d.user
|
||||
log("User data: $user")
|
||||
with(sharedPref.edit()) {
|
||||
putString("discord_username", user.username)
|
||||
putString("discord_id", user.id)
|
||||
putString("discord_avatar", user.avatar)
|
||||
apply()
|
||||
}
|
||||
|
||||
PrefManager.setVal(PrefName.DiscordUserName, user.username)
|
||||
PrefManager.setVal(PrefName.DiscordId, user.id)
|
||||
PrefManager.setVal(PrefName.DiscordAvatar, user.avatar)
|
||||
}
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
|
||||
inner class DiscordWebSocketListener : WebSocketListener() {
|
||||
|
||||
var retryAttempts = 0
|
||||
val maxRetryAttempts = 10
|
||||
private var retryAttempts = 0
|
||||
private val maxRetryAttempts = 10
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
super.onOpen(webSocket, response)
|
||||
this@DiscordService.webSocket = webSocket
|
||||
@@ -238,7 +228,7 @@ class DiscordService : Service() {
|
||||
resume()
|
||||
resume = false
|
||||
} else {
|
||||
identify(webSocket, baseContext)
|
||||
identify(webSocket)
|
||||
log("WebSocket: Identified")
|
||||
}
|
||||
}
|
||||
@@ -251,13 +241,13 @@ class DiscordService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun identify(webSocket: WebSocket, context: Context) {
|
||||
private fun identify(webSocket: WebSocket) {
|
||||
val properties = JsonObject()
|
||||
properties.addProperty("os", "linux")
|
||||
properties.addProperty("browser", "unknown")
|
||||
properties.addProperty("device", "unknown")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(context))
|
||||
d.addProperty("token", getToken())
|
||||
d.addProperty("intents", 0)
|
||||
d.add("properties", properties)
|
||||
val payload = JsonObject()
|
||||
@@ -276,11 +266,11 @@ class DiscordService : Service() {
|
||||
retryAttempts++
|
||||
if (retryAttempts >= maxRetryAttempts) {
|
||||
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
|
||||
errorNotification("Could not set the presence", "Max Retry Attempts")
|
||||
errorNotification("Timeout setting presence", "Max Retry Attempts")
|
||||
return
|
||||
}
|
||||
}
|
||||
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
|
||||
t.message?.let { Logger.log("onFailure() $it") }
|
||||
log("WebSocket: Error, onFailure() reason: ${t.message}")
|
||||
client = OkHttpClient()
|
||||
client.newWebSocket(
|
||||
@@ -295,7 +285,7 @@ class DiscordService : Service() {
|
||||
|
||||
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
|
||||
super.onClosing(webSocket, code, reason)
|
||||
Log.d("WebSocket", "onClosing() $code $reason")
|
||||
Logger.log("onClosing() $code $reason")
|
||||
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
|
||||
heartbeatThread.interrupt()
|
||||
}
|
||||
@@ -303,7 +293,7 @@ class DiscordService : Service() {
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
super.onClosed(webSocket, code, reason)
|
||||
Log.d("WebSocket", "onClosed() $code $reason")
|
||||
Logger.log("onClosed() $code $reason")
|
||||
if (code >= 4000) {
|
||||
log("WebSocket: Error, code: $code reason: $reason")
|
||||
client = OkHttpClient()
|
||||
@@ -317,18 +307,14 @@ class DiscordService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun getToken(context: Context): String {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
val token = sharedPref.getString(Discord.TOKEN, null)
|
||||
if (token == null) {
|
||||
fun getToken(): String {
|
||||
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
|
||||
return if (token == null) {
|
||||
log("WebSocket: Token not found")
|
||||
errorNotification("Could not set the presence", "token not found")
|
||||
return ""
|
||||
""
|
||||
} else {
|
||||
return token
|
||||
token
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,13 +345,13 @@ class DiscordService : Service() {
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
//TODO: Request permission
|
||||
return
|
||||
}
|
||||
notificationManager.notify(2, builder.build())
|
||||
log("Error Notified")
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun saveSimpleTestPresence() {
|
||||
val file = File(baseContext.cacheDir, "payload")
|
||||
//fill with test payload
|
||||
@@ -385,65 +371,22 @@ class DiscordService : Service() {
|
||||
log("WebSocket: Simple Test Presence Saved")
|
||||
}
|
||||
|
||||
fun setPresence(String: String) {
|
||||
fun setPresence(string: String) {
|
||||
log("WebSocket: Sending Presence payload")
|
||||
log(String)
|
||||
webSocket.send(String)
|
||||
log(string)
|
||||
webSocket.send(string)
|
||||
}
|
||||
|
||||
fun log(string: String) {
|
||||
Log.d("WebSocket_Discord", string)
|
||||
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
|
||||
}
|
||||
|
||||
fun saveLogToFile() {
|
||||
val fileName = "log_${System.currentTimeMillis()}.txt"
|
||||
|
||||
// ContentValues to store file metadata
|
||||
val values = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
|
||||
}
|
||||
}
|
||||
|
||||
// Inserting the file in the MediaStore
|
||||
val resolver = baseContext.contentResolver
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
|
||||
} else {
|
||||
val directory =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(directory, fileName)
|
||||
|
||||
// Make sure the Downloads directory exists
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Use FileProvider to get the URI for the file
|
||||
val authority =
|
||||
"${baseContext.packageName}.provider" // Adjust with your app's package name
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
|
||||
// Writing to the file
|
||||
uri?.let {
|
||||
resolver.openOutputStream(it).use { outputStream ->
|
||||
OutputStreamWriter(outputStream).use { writer ->
|
||||
writer.write(log)
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
log("Error saving log file")
|
||||
if (shouldLog) {
|
||||
Logger.log(string)
|
||||
}
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
log("Sending Resume payload")
|
||||
val d = JsonObject()
|
||||
d.addProperty("token", getToken(baseContext))
|
||||
d.addProperty("token", getToken())
|
||||
d.addProperty("session_id", sessionId)
|
||||
d.addProperty("seq", sequence)
|
||||
val json = JsonObject()
|
||||
@@ -459,7 +402,7 @@ class DiscordService : Service() {
|
||||
Thread.sleep(heartbeat.toLong())
|
||||
heartbeatSend(webSocket, sequence)
|
||||
log("WebSocket: Heartbeat Sent")
|
||||
} catch (e: InterruptedException) {
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
|
||||
@@ -20,7 +19,7 @@ class Login : AppCompatActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val process = getProcessName()
|
||||
@@ -76,7 +75,7 @@ class Login : AppCompatActivity() {
|
||||
}
|
||||
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
saveToken(this, token)
|
||||
saveToken(token)
|
||||
startMainActivity(this@Login)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package ani.dantotsu.connections.discord
|
||||
|
||||
import ani.dantotsu.connections.discord.serializers.Activity
|
||||
import ani.dantotsu.connections.discord.serializers.Presence
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -69,8 +71,8 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
assets = Activity.Assets(
|
||||
largeImage = data.largeImage?.url?.discordUrl(),
|
||||
largeText = data.largeImage?.label,
|
||||
smallImage = data.smallImage?.url?.discordUrl(),
|
||||
smallText = data.smallImage?.label
|
||||
smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
|
||||
smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
|
||||
),
|
||||
buttons = data.buttons.map { it.label },
|
||||
metadata = Activity.Metadata(
|
||||
@@ -81,7 +83,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
),
|
||||
afk = true,
|
||||
since = data.startTimestamp,
|
||||
status = data.status
|
||||
status = PrefManager.getVal(PrefName.DiscordStatus)
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package ani.dantotsu.connections.github
|
||||
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.getAppString
|
||||
import ani.dantotsu.settings.Developer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
|
||||
class Contributors {
|
||||
|
||||
fun getContributors(): Array<Developer> {
|
||||
var developers = arrayOf<Developer>()
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val repo = getAppString(R.string.repo)
|
||||
val res = client.get("https://api.github.com/repos/$repo/contributors")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.forEach {
|
||||
if (it.login == "SunglassJerry") return@forEach
|
||||
val role = when (it.login) {
|
||||
"rebelonion" -> "Owner & Maintainer"
|
||||
"sneazy-ibo" -> "Contributor & Comment Moderator"
|
||||
"WaiWhat" -> "Icon Designer"
|
||||
else -> "Contributor"
|
||||
}
|
||||
developers = developers.plus(
|
||||
Developer(
|
||||
it.login,
|
||||
it.avatarUrl,
|
||||
role,
|
||||
it.htmlUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
developers = developers.plus(
|
||||
arrayOf(
|
||||
Developer(
|
||||
"MarshMeadow",
|
||||
"https://avatars.githubusercontent.com/u/88599122?v=4",
|
||||
"Beta Icon Designer & Website Maintainer",
|
||||
"https://github.com/MarshMeadow?tab=repositories"
|
||||
),
|
||||
Developer(
|
||||
"Zaxx69",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6342562-kxE8m4i7KUMK.png",
|
||||
"Telegram Admin",
|
||||
"https://anilist.co/user/6342562"
|
||||
),
|
||||
Developer(
|
||||
"Arif Alam",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6011177-2n994qtayiR9.jpg",
|
||||
"Discord & Comment Moderator",
|
||||
"https://anilist.co/user/6011177"
|
||||
),
|
||||
Developer(
|
||||
"SunglassJeery",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b5804776-FEKfP5wbz2xv.png",
|
||||
"Head Discord & Comment Moderator",
|
||||
"https://anilist.co/user/5804776"
|
||||
),
|
||||
Developer(
|
||||
"Excited",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6131921-toSoGWmKbRA1.png",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6131921"
|
||||
),
|
||||
Developer(
|
||||
"Gurjshan",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6363228-rWQ3Pl3WuxzL.png",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6363228"
|
||||
),
|
||||
Developer(
|
||||
"NekoMimi",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6244220-HOpImMGMQAxW.jpg",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6244220"
|
||||
),
|
||||
Developer(
|
||||
"Zaidsenior",
|
||||
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6049773"
|
||||
),
|
||||
Developer(
|
||||
"hastsu",
|
||||
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
|
||||
"Comment Moderator",
|
||||
"https://anilist.co/user/6183359"
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
return developers
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GithubResponse(
|
||||
@SerialName("login")
|
||||
val login: String,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String,
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String
|
||||
)
|
||||
}
|
||||
54
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
54
app/src/main/java/ani/dantotsu/connections/github/Forks.kt
Normal file
@@ -0,0 +1,54 @@
|
||||
package ani.dantotsu.connections.github
|
||||
|
||||
import ani.dantotsu.Mapper
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.settings.Developer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
|
||||
class Forks {
|
||||
|
||||
fun getForks(): Array<Developer> {
|
||||
var forks = arrayOf<Developer>()
|
||||
runBlocking(Dispatchers.IO) {
|
||||
val res =
|
||||
client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks?sort=stargazers")
|
||||
.parsed<JsonArray>().map {
|
||||
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
|
||||
}
|
||||
res.forEach {
|
||||
forks = forks.plus(
|
||||
Developer(
|
||||
it.name,
|
||||
it.owner.avatarUrl,
|
||||
it.owner.login,
|
||||
it.htmlUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return forks
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GithubResponse(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
val owner: Owner,
|
||||
@SerialName("html_url")
|
||||
val htmlUrl: String,
|
||||
) {
|
||||
@Serializable
|
||||
data class Owner(
|
||||
@SerialName("login")
|
||||
val login: String,
|
||||
@SerialName("avatar_url")
|
||||
val avatarUrl: String
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.connections.mal.MAL.clientId
|
||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.startMainActivity
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
@@ -21,12 +21,12 @@ import kotlinx.coroutines.launch
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
try {
|
||||
val data: Uri = intent?.data
|
||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||
val codeChallenge: String = loadData("malCodeChallenge", this)
|
||||
val codeChallenge = PrefManager.getVal(PrefName.MALCodeChallenge, null as String?)
|
||||
?: throw Exception(getString(R.string.mal_login_code_challenge_not_found))
|
||||
val code = data.getQueryParameter("code")
|
||||
?: throw Exception(getString(R.string.mal_login_code_not_present))
|
||||
|
||||
@@ -5,17 +5,15 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
object MAL {
|
||||
@@ -34,7 +32,7 @@ object MAL {
|
||||
.replace("/", "_")
|
||||
.replace("\n", "")
|
||||
|
||||
saveData("malCodeChallenge", codeChallenge, context)
|
||||
PrefManager.setVal(PrefName.MALCodeChallenge, codeChallenge)
|
||||
val request =
|
||||
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
|
||||
try {
|
||||
@@ -47,11 +45,9 @@ object MAL {
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAL_TOKEN = "malToken"
|
||||
|
||||
private suspend fun refreshToken(): ResponseToken? {
|
||||
return tryWithSuspend {
|
||||
val token = loadData<ResponseToken>(MAL_TOKEN)
|
||||
val token = PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
|
||||
val res = client.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
@@ -67,10 +63,11 @@ object MAL {
|
||||
}
|
||||
|
||||
|
||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
||||
suspend fun getSavedToken(): Boolean {
|
||||
return tryWithSuspend(false) {
|
||||
var res: ResponseToken = loadData(MAL_TOKEN, context)
|
||||
?: return@tryWithSuspend false
|
||||
var res: ResponseToken =
|
||||
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
|
||||
?: return@tryWithSuspend false
|
||||
if (System.currentTimeMillis() > res.expiresIn)
|
||||
res = refreshToken()
|
||||
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
|
||||
@@ -79,19 +76,17 @@ object MAL {
|
||||
} ?: false
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
fun removeSavedToken() {
|
||||
token = null
|
||||
username = null
|
||||
userid = null
|
||||
avatar = null
|
||||
if (MAL_TOKEN in context.fileList()) {
|
||||
File(context.filesDir, MAL_TOKEN).delete()
|
||||
}
|
||||
PrefManager.removeVal(PrefName.MALToken)
|
||||
}
|
||||
|
||||
fun saveResponse(res: ResponseToken) {
|
||||
res.expiresIn += System.currentTimeMillis()
|
||||
saveData(MAL_TOKEN, res)
|
||||
PrefManager.setVal(PrefName.MALToken, res)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -100,6 +95,10 @@ object MAL {
|
||||
@SerialName("expires_in") var expiresIn: Long,
|
||||
@SerialName("access_token") val accessToken: String,
|
||||
@SerialName("refresh_token") val refreshToken: String,
|
||||
) : java.io.Serializable
|
||||
) : java.io.Serializable {
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,34 +1,45 @@
|
||||
package ani.dantotsu.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.widget.Toast
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.callback.FolderCallback
|
||||
import com.anggrayudi.storage.file.deleteRecursively
|
||||
import com.anggrayudi.storage.file.findFolder
|
||||
import com.anggrayudi.storage.file.moveFolderTo
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import java.io.Serializable
|
||||
|
||||
class DownloadsManager(private val context: Context) {
|
||||
private val prefs: SharedPreferences =
|
||||
context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
|
||||
private val gson = Gson()
|
||||
private val downloadsList = loadDownloads().toMutableList()
|
||||
|
||||
val mangaDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
|
||||
get() = downloadsList.filter { it.type == MediaType.MANGA }
|
||||
val animeDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
|
||||
get() = downloadsList.filter { it.type == MediaType.ANIME }
|
||||
val novelDownloadedTypes: List<DownloadedType>
|
||||
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
|
||||
get() = downloadsList.filter { it.type == MediaType.NOVEL }
|
||||
|
||||
private fun saveDownloads() {
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
prefs.edit().putString("downloads_key", jsonString).apply()
|
||||
PrefManager.setVal(PrefName.DownloadsKeys, jsonString)
|
||||
}
|
||||
|
||||
private fun loadDownloads(): List<DownloadedType> {
|
||||
val jsonString = prefs.getString("downloads_key", null)
|
||||
val jsonString = PrefManager.getVal(PrefName.DownloadsKeys, null as String?)
|
||||
return if (jsonString != null) {
|
||||
val type = object : TypeToken<List<DownloadedType>>() {}.type
|
||||
gson.fromJson(jsonString, type)
|
||||
@@ -42,82 +53,70 @@ class DownloadsManager(private val context: Context) {
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeDownload(downloadedType: DownloadedType) {
|
||||
fun removeDownload(
|
||||
downloadedType: DownloadedType,
|
||||
toast: Boolean = true,
|
||||
onFinished: () -> Unit
|
||||
) {
|
||||
downloadsList.remove(downloadedType)
|
||||
removeDirectory(downloadedType)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
removeDirectory(downloadedType, toast)
|
||||
withContext(Dispatchers.Main) {
|
||||
onFinished()
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
fun removeMedia(title: String, type: DownloadedType.Type) {
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory/$title"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
fun removeMedia(title: String, type: MediaType) {
|
||||
val baseDirectory = getBaseDirectory(context, type)
|
||||
val directory = baseDirectory?.findFolder(title)
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
cleanDownloads()
|
||||
}
|
||||
when (type) {
|
||||
DownloadedType.Type.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
|
||||
MediaType.MANGA -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
|
||||
}
|
||||
DownloadedType.Type.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
|
||||
|
||||
MediaType.ANIME -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
|
||||
}
|
||||
DownloadedType.Type.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
|
||||
|
||||
MediaType.NOVEL -> {
|
||||
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
|
||||
}
|
||||
}
|
||||
saveDownloads()
|
||||
}
|
||||
|
||||
private fun cleanDownloads() {
|
||||
cleanDownload(DownloadedType.Type.MANGA)
|
||||
cleanDownload(DownloadedType.Type.ANIME)
|
||||
cleanDownload(DownloadedType.Type.NOVEL)
|
||||
cleanDownload(MediaType.MANGA)
|
||||
cleanDownload(MediaType.ANIME)
|
||||
cleanDownload(MediaType.NOVEL)
|
||||
}
|
||||
|
||||
private fun cleanDownload(type: DownloadedType.Type) {
|
||||
private fun cleanDownload(type: MediaType) {
|
||||
// remove all folders that are not in the downloads list
|
||||
val subDirectory = if (type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
val directory = getBaseDirectory(context, type)
|
||||
val downloadsSubLists = when (type) {
|
||||
MediaType.MANGA -> mangaDownloadedTypes
|
||||
MediaType.ANIME -> animeDownloadedTypes
|
||||
else -> novelDownloadedTypes
|
||||
}
|
||||
val directory = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$subDirectory"
|
||||
)
|
||||
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
|
||||
mangaDownloadedTypes
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
animeDownloadedTypes
|
||||
} else {
|
||||
novelDownloadedTypes
|
||||
}
|
||||
if (directory.exists()) {
|
||||
if (directory?.exists() == true && directory.isDirectory) {
|
||||
val files = directory.listFiles()
|
||||
if (files != null) {
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
val deleted = file.deleteRecursively()
|
||||
}
|
||||
for (file in files) {
|
||||
if (!downloadsSubLists.any { it.title == file.name }) {
|
||||
file.deleteRecursively(context, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,34 +124,92 @@ class DownloadsManager(private val context: Context) {
|
||||
val iterator = downloadsList.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
val download = iterator.next()
|
||||
val downloadDir = File(directory, download.title)
|
||||
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
|
||||
val downloadDir = directory?.findFolder(download.title)
|
||||
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
|
||||
iterator.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
|
||||
{
|
||||
val jsonString = gson.toJson(downloadsList)
|
||||
val file = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/downloads.json"
|
||||
)
|
||||
if (file.parentFile?.exists() == false) {
|
||||
file.parentFile?.mkdirs()
|
||||
fun moveDownloadsDir(
|
||||
context: Context,
|
||||
oldUri: Uri,
|
||||
newUri: Uri,
|
||||
finished: (Boolean, String) -> Unit
|
||||
) {
|
||||
try {
|
||||
if (oldUri == newUri) {
|
||||
finished(false, "Source and destination are the same")
|
||||
return
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
val oldBase =
|
||||
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
|
||||
val newBase =
|
||||
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
|
||||
val folder =
|
||||
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
|
||||
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object :
|
||||
FolderCallback() {
|
||||
override fun onFailed(errorCode: ErrorCode) {
|
||||
when (errorCode) {
|
||||
ErrorCode.CANCELED -> finished(false, "Move canceled")
|
||||
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(
|
||||
false,
|
||||
"Cannot create file in target"
|
||||
)
|
||||
|
||||
ErrorCode.INVALID_TARGET_FOLDER -> finished(
|
||||
true,
|
||||
"Invalid target folder"
|
||||
) // seems to still work
|
||||
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(
|
||||
false,
|
||||
"No space left on target path"
|
||||
)
|
||||
|
||||
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
|
||||
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(
|
||||
false,
|
||||
"Source folder not found"
|
||||
)
|
||||
|
||||
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(
|
||||
false,
|
||||
"Storage permission denied"
|
||||
)
|
||||
|
||||
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(
|
||||
false,
|
||||
"Target folder cannot have same path with source folder"
|
||||
)
|
||||
|
||||
else -> finished(false, "Failed to move downloads: $errorCode")
|
||||
}
|
||||
Logger.log("Failed to move downloads: $errorCode")
|
||||
super.onFailed(errorCode)
|
||||
}
|
||||
|
||||
override fun onCompleted(result: Result) {
|
||||
finished(true, "Successfully moved downloads")
|
||||
super.onCompleted(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
snackString("Error: ${e.message}")
|
||||
finished(false, "Failed to move downloads: ${e.message}")
|
||||
return
|
||||
}
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
}
|
||||
file.writeText(jsonString)
|
||||
}
|
||||
|
||||
fun queryDownload(downloadedType: DownloadedType): Boolean {
|
||||
return downloadsList.contains(downloadedType)
|
||||
}
|
||||
|
||||
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
|
||||
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
|
||||
return if (type == null) {
|
||||
downloadsList.any { it.title == title && it.chapter == chapter }
|
||||
} else {
|
||||
@@ -160,87 +217,35 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType) {
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
|
||||
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
|
||||
val baseDirectory = getBaseDirectory(context, downloadedType.type)
|
||||
val directory =
|
||||
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
|
||||
downloadsList.remove(downloadedType)
|
||||
// Check if the directory exists and delete it recursively
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
if (toast) snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
|
||||
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
}
|
||||
val destination = File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
|
||||
)
|
||||
if (directory.exists()) {
|
||||
val copied = directory.copyRecursively(destination, true)
|
||||
if (copied) {
|
||||
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun purgeDownloads(type: DownloadedType.Type) {
|
||||
val directory = if (type == DownloadedType.Type.MANGA) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
|
||||
} else {
|
||||
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
|
||||
}
|
||||
if (directory.exists()) {
|
||||
val deleted = directory.deleteRecursively()
|
||||
fun purgeDownloads(type: MediaType) {
|
||||
val directory = getBaseDirectory(context, type)
|
||||
if (directory?.exists() == true) {
|
||||
val deleted = directory.deleteRecursively(context, false)
|
||||
if (deleted) {
|
||||
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
|
||||
snackString("Successfully deleted")
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
|
||||
snackString("Failed to delete directory")
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
|
||||
snackString("Directory does not exist")
|
||||
}
|
||||
|
||||
downloadsList.removeAll { it.type == type }
|
||||
@@ -248,57 +253,126 @@ class DownloadsManager(private val context: Context) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val novelLocation = "Dantotsu/Novel"
|
||||
const val mangaLocation = "Dantotsu/Manga"
|
||||
const val animeLocation = "Dantotsu/Anime"
|
||||
private const val BASE_LOCATION = "Dantotsu"
|
||||
private const val MANGA_SUB_LOCATION = "Manga"
|
||||
private const val ANIME_SUB_LOCATION = "Anime"
|
||||
private const val NOVEL_SUB_LOCATION = "Novel"
|
||||
|
||||
fun getDirectory(context: Context, type: DownloadedType.Type, title: String, chapter: String? = null): File {
|
||||
return if (type == DownloadedType.Type.MANGA) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$mangaLocation/$title"
|
||||
)
|
||||
|
||||
/**
|
||||
* Get and create a base directory for the given type
|
||||
* @param context the context
|
||||
* @param type the type of media
|
||||
* @return the base directory
|
||||
*/
|
||||
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
|
||||
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
||||
if (baseDirectory == Uri.EMPTY) return null
|
||||
var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
|
||||
base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
|
||||
return when (type) {
|
||||
MediaType.MANGA -> {
|
||||
base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
|
||||
}
|
||||
} else if (type == DownloadedType.Type.ANIME) {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$animeLocation/$title"
|
||||
)
|
||||
|
||||
MediaType.ANIME -> {
|
||||
base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
|
||||
}
|
||||
} else {
|
||||
if (chapter != null) {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title/$chapter"
|
||||
)
|
||||
} else {
|
||||
File(
|
||||
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"$novelLocation/$title"
|
||||
)
|
||||
|
||||
else -> {
|
||||
base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* Get and create a subdirectory for the given type
|
||||
* @param context the context
|
||||
* @param type the type of media
|
||||
* @param title the title of the media
|
||||
* @param chapter the chapter of the media
|
||||
* @return the subdirectory
|
||||
*/
|
||||
fun getSubDirectory(
|
||||
context: Context,
|
||||
type: MediaType,
|
||||
overwrite: Boolean,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): DocumentFile? {
|
||||
val baseDirectory = getBaseDirectory(context, type) ?: return null
|
||||
return if (chapter != null) {
|
||||
baseDirectory.findOrCreateFolder(title, false)
|
||||
?.findOrCreateFolder(chapter, overwrite)
|
||||
} else {
|
||||
baseDirectory.findOrCreateFolder(title, overwrite)
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
|
||||
enum class Type {
|
||||
MANGA,
|
||||
ANIME,
|
||||
NOVEL
|
||||
fun getDirSize(
|
||||
context: Context,
|
||||
type: MediaType,
|
||||
title: String,
|
||||
chapter: String? = null
|
||||
): Long {
|
||||
val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0
|
||||
var size = 0L
|
||||
directory.listFiles().forEach {
|
||||
size += it.length()
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
fun addNoMedia(context: Context) {
|
||||
val baseDirectory = getBaseDirectory(context) ?: return
|
||||
if (baseDirectory.findFile(".nomedia") == null) {
|
||||
baseDirectory.createFile("application/octet-stream", ".nomedia")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBaseDirectory(context: Context): DocumentFile? {
|
||||
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
|
||||
if (baseDirectory == Uri.EMPTY) return null
|
||||
return DocumentFile.fromTreeUri(context, baseDirectory)
|
||||
}
|
||||
|
||||
private fun DocumentFile.findOrCreateFolder(
|
||||
name: String, overwrite: Boolean
|
||||
): DocumentFile? {
|
||||
return if (overwrite) {
|
||||
findFolder(name.findValidName())?.delete()
|
||||
createDirectory(name.findValidName())
|
||||
} else {
|
||||
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
|
||||
}
|
||||
}
|
||||
|
||||
private const val RATIO_THRESHOLD = 95
|
||||
fun Media.compareName(name: String): Boolean {
|
||||
val mainName = mainName().findValidName().lowercase()
|
||||
val ratio = FuzzySearch.ratio(mainName, name.lowercase())
|
||||
return ratio > RATIO_THRESHOLD
|
||||
}
|
||||
|
||||
fun String.compareName(name: String): Boolean {
|
||||
val mainName = findValidName().lowercase()
|
||||
val compareName = name.findValidName().lowercase()
|
||||
val ratio = FuzzySearch.ratio(mainName, compareName)
|
||||
return ratio > RATIO_THRESHOLD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
|
||||
private fun String?.findValidName(): String {
|
||||
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
|
||||
}
|
||||
|
||||
data class DownloadedType(
|
||||
val pTitle: String, val pChapter: String, val type: MediaType
|
||||
) : Serializable {
|
||||
val title: String
|
||||
get() = pTitle.findValidName()
|
||||
val chapter: String
|
||||
get() = pChapter.findValidName()
|
||||
}
|
||||
|
||||
@@ -9,31 +9,35 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import ani.dantotsu.FileUrl
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.addons.download.DownloadAddonManager
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.video.ExoplayerDownloadService
|
||||
import ani.dantotsu.download.video.Helper
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.SubtitleDownloader
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
@@ -44,9 +48,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -55,13 +57,12 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
|
||||
class AnimeDownloaderService : Service() {
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
@@ -72,6 +73,7 @@ class AnimeDownloaderService : Service() {
|
||||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
|
||||
private val ffExtension = Injekt.get<DownloadAddonManager>().extension?.extension
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
@@ -80,13 +82,19 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
if (ffExtension == null) {
|
||||
toast(getString(R.string.download_addon_not_found))
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder =
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Anime Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(100, 0, false)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
@@ -155,27 +163,14 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
@UnstableApi
|
||||
fun cancelDownload(taskName: String) {
|
||||
val url =
|
||||
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
|
||||
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
|
||||
if (url.isEmpty()) {
|
||||
snackString("Failed to cancel download")
|
||||
return
|
||||
val sessionIds =
|
||||
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
|
||||
.map { it.sessionId }.toMutableList()
|
||||
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
|
||||
sessionIds.forEach {
|
||||
ffExtension!!.cancelDownload(it)
|
||||
}
|
||||
currentTasks.removeAll { it.getTaskName() == taskName }
|
||||
DownloadService.sendSetStopReason(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
|
||||
false
|
||||
)
|
||||
DownloadService.sendRemoveDownload(
|
||||
this@AnimeDownloaderService,
|
||||
ExoplayerDownloadService::class.java,
|
||||
url,
|
||||
false
|
||||
)
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
mutex.withLock {
|
||||
downloadJobs[taskName]?.cancel()
|
||||
@@ -208,7 +203,6 @@ class AnimeDownloaderService : Service() {
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun download(task: AnimeDownloadTask) {
|
||||
try {
|
||||
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
|
||||
withContext(Dispatchers.Main) {
|
||||
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
ContextCompat.checkSelfPermission(
|
||||
@@ -219,20 +213,63 @@ class AnimeDownloaderService : Service() {
|
||||
true
|
||||
}
|
||||
|
||||
builder.setContentText("Downloading ${task.title} - ${task.episode}")
|
||||
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
broadcastDownloadStarted(task.episode)
|
||||
val outputDir = getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
task.title,
|
||||
task.episode
|
||||
) ?: throw Exception("Failed to create output directory")
|
||||
|
||||
currActivity()?.let {
|
||||
Helper.downloadVideo(
|
||||
it,
|
||||
task.video,
|
||||
task.subtitle
|
||||
)
|
||||
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
|
||||
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
|
||||
?: throw Exception("Failed to create output file")
|
||||
|
||||
var percent = 0
|
||||
var totalLength = 0.0
|
||||
val path = ffExtension!!.setDownloadPath(
|
||||
this@AnimeDownloaderService,
|
||||
outputFile.uri
|
||||
)
|
||||
val headersStringBuilder = StringBuilder()
|
||||
task.video.file.headers.forEach {
|
||||
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
|
||||
}
|
||||
if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now
|
||||
headersStringBuilder.append("\"").append("User-Agent: ")
|
||||
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
|
||||
}
|
||||
val probeRequest =
|
||||
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
|
||||
ffExtension.executeFFProbe(
|
||||
probeRequest
|
||||
) {
|
||||
if (it.toDoubleOrNull() != null) {
|
||||
totalLength = it.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
val headers = headersStringBuilder.toString()
|
||||
var request = "-headers $headers "
|
||||
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
|
||||
Logger.log("Request: $request")
|
||||
val ffTask =
|
||||
ffExtension.executeFFMpeg(request) {
|
||||
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||
val timeInMilliseconds = it
|
||||
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||
percent = ((it / 1000) / totalLength * 100).toInt()
|
||||
}
|
||||
Logger.log("Statistics: $it")
|
||||
}
|
||||
task.sessionId = ffTask
|
||||
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
|
||||
ffTask
|
||||
|
||||
saveMediaInfo(task)
|
||||
task.subtitle?.let {
|
||||
@@ -242,135 +279,150 @@ class AnimeDownloaderService : Service() {
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
}
|
||||
val downloadStarted =
|
||||
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
|
||||
|
||||
if (!downloadStarted) {
|
||||
logger("Download failed to start")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed to start")
|
||||
broadcastDownloadFailed(task.episode)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
|
||||
// periodically check if the download is complete
|
||||
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
|
||||
logger("Download failed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download failed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download failed")
|
||||
logger("Download failed: ${download.failureReason}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||
if (ffExtension.getState(ffTask) == "FAILED") {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
task.episode
|
||||
)
|
||||
)
|
||||
FirebaseCrashlytics.getInstance().recordException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${download.failureReason}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
|
||||
logger("Download completed")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download completed")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download completed")
|
||||
getSharedPreferences(
|
||||
getString(R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
DownloadedType.Type.ANIME,
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFinished(task.episode)
|
||||
break
|
||||
}
|
||||
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
|
||||
logger("Download stopped")
|
||||
builder.setContentText("${task.title} - ${task.episode} Download stopped")
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${task.title} - ${task.episode} Download stopped")
|
||||
break
|
||||
}
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
download.percentDownloaded.toInt()
|
||||
} Download failed"
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
toast("${getTaskName(task.title, task.episode)} Download failed")
|
||||
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
),
|
||||
false
|
||||
) {}
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${getTaskName(task.title, task.episode)}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
break
|
||||
}
|
||||
builder.setProgress(
|
||||
100, percent.coerceAtMost(99),
|
||||
false
|
||||
)
|
||||
broadcastDownloadProgress(
|
||||
task.episode,
|
||||
percent.coerceAtMost(99)
|
||||
)
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
kotlinx.coroutines.delay(2000)
|
||||
}
|
||||
if (ffExtension.getState(ffTask) == "COMPLETED") {
|
||||
if (ffExtension.hadError(ffTask)) {
|
||||
Logger.log("Download failed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
} Download failed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${getTaskName(task.title, task.episode)} Download failed")
|
||||
downloadsManager.removeDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
) {}
|
||||
Injekt.get<CrashlyticsInterface>().logException(
|
||||
Exception(
|
||||
"Anime Download failed:" +
|
||||
" ${getTaskName(task.title, task.episode)}" +
|
||||
" url: ${task.video.file.url}" +
|
||||
" title: ${task.title}" +
|
||||
" episode: ${task.episode}"
|
||||
)
|
||||
)
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFailed(task.episode)
|
||||
return@withContext
|
||||
}
|
||||
Logger.log("Download completed")
|
||||
builder.setContentText(
|
||||
"${
|
||||
getTaskName(
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
} Download completed"
|
||||
)
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
snackString("${getTaskName(task.title, task.episode)} Download completed")
|
||||
PrefManager.getAnimeDownloadPreferences().edit().putString(
|
||||
task.getTaskName(),
|
||||
task.video.file.url
|
||||
).apply()
|
||||
downloadsManager.addDownload(
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.episode,
|
||||
MediaType.ANIME,
|
||||
)
|
||||
)
|
||||
|
||||
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
|
||||
broadcastDownloadFinished(task.episode)
|
||||
} else throw Exception("Download failed")
|
||||
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
|
||||
logger("Exception while downloading file: ${e.message}")
|
||||
Logger.log("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
e.printStackTrace()
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
broadcastDownloadFailed(task.episode)
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
suspend fun hasDownloadStarted(
|
||||
downloadManager: DownloadManager,
|
||||
task: AnimeDownloadTask,
|
||||
timeout: Long
|
||||
): Boolean {
|
||||
val startTime = System.currentTimeMillis()
|
||||
while (System.currentTimeMillis() - startTime < timeout) {
|
||||
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
|
||||
if (download != null) {
|
||||
return true
|
||||
}
|
||||
// Delay between each poll
|
||||
kotlinx.coroutines.delay(500)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: AnimeDownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"${DownloadsManager.animeLocation}/${task.title}"
|
||||
)
|
||||
val episodeDirectory = File(directory, task.episode)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val directory =
|
||||
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val episodeDirectory =
|
||||
getSubDirectory(
|
||||
this@AnimeDownloaderService,
|
||||
MediaType.ANIME,
|
||||
false,
|
||||
task.title,
|
||||
task.episode
|
||||
)
|
||||
?: throw Exception("Directory not found")
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -404,14 +456,25 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@AnimeDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
@@ -422,13 +485,16 @@ class AnimeDownloaderService : Service() {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -495,14 +561,15 @@ class AnimeDownloaderService : Service() {
|
||||
val episodeImage: String? = null,
|
||||
val retries: Int = 2,
|
||||
val simultaneousDownloads: Int = 2,
|
||||
var sessionId: Long = -1
|
||||
) {
|
||||
fun getTaskName(): String {
|
||||
return "$title - $episode"
|
||||
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getTaskName(title: String, episode: String): String {
|
||||
return "$title - $episode"
|
||||
return "${title.replace("/", "")}/${episode.replace("/", "")}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -516,7 +583,6 @@ class AnimeDownloaderService : Service() {
|
||||
|
||||
object AnimeServiceDataSingleton {
|
||||
var video: Video? = null
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ani.dantotsu.download.anime
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -12,6 +11,8 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
|
||||
class OfflineAnimeAdapter(
|
||||
@@ -22,8 +23,7 @@ class OfflineAnimeAdapter(
|
||||
private val inflater: LayoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var originalItems: List<OfflineAnimeModel> = items
|
||||
private var style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
@@ -37,7 +37,6 @@ class OfflineAnimeAdapter(
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
|
||||
val view: View = convertView ?: when (style) {
|
||||
@@ -50,28 +49,27 @@ class OfflineAnimeAdapter(
|
||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
val totalEpisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
|
||||
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
|
||||
|
||||
if (style == 0) {
|
||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||
val episodes = view.findViewById<TextView>(R.id.itemTotal)
|
||||
episodes.text = " Episodes"
|
||||
bannerView.setImageURI(item.banner)
|
||||
totalepisodes.text = item.totalEpisodeList
|
||||
episodes.text = context.getString(R.string.episodes)
|
||||
bannerView.setImageURI(item.banner ?: item.image)
|
||||
totalEpisodes.text = item.totalEpisodeList
|
||||
} else if (style == 1) {
|
||||
val watchedEpisodes =
|
||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||
watchedEpisodes.text = item.watchedEpisode
|
||||
totalepisodes.text = " | " + item.totalEpisode
|
||||
totalEpisodes.text = context.getString(R.string.total_divider, item.totalEpisode)
|
||||
}
|
||||
|
||||
// Bind item data to the views
|
||||
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
|
||||
typeImage.setImageResource(R.drawable.ic_round_movie_filter_24)
|
||||
type.text = item.type
|
||||
typeView.visibility = View.VISIBLE
|
||||
imageView.setImageURI(item.image)
|
||||
@@ -105,8 +103,7 @@ class OfflineAnimeAdapter(
|
||||
}
|
||||
|
||||
fun notifyNewGrid() {
|
||||
style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
style = PrefManager.getVal(PrefName.OfflineView)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
package ani.dantotsu.download.anime
|
||||
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
@@ -16,7 +12,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.GridView
|
||||
@@ -25,34 +20,35 @@ import android.widget.TextView
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
@@ -61,11 +57,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
@@ -73,9 +71,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
private var downloads: List<OfflineAnimeModel> = listOf()
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineAnimeAdapter
|
||||
private lateinit var total : TextView
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private lateinit var total: TextView
|
||||
private var downloadsJob: Job = Job()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -101,15 +98,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineANIME)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
if (!uiSettings.immersiveMode) {
|
||||
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||
view.rootView.fitsSystemWindows = true
|
||||
}
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||
searchView.addTextChangedListener(object : TextWatcher {
|
||||
@@ -123,13 +117,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
onSearchQuery(s.toString())
|
||||
}
|
||||
})
|
||||
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getInt("offline_view", 0)
|
||||
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
0 -> layoutList
|
||||
1 -> layoutcompact
|
||||
1 -> layoutCompact
|
||||
else -> layoutList
|
||||
}
|
||||
selected.alpha = 1f
|
||||
@@ -143,26 +136,25 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
layoutList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putInt("offline_view", style!!)?.apply()
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
|
||||
layoutcompact.setOnClickListener {
|
||||
layoutCompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putInt("offline_view", style!!)?.apply()
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView1)
|
||||
adapter.notifyNewGrid()
|
||||
grid()
|
||||
}
|
||||
|
||||
gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||
gridView =
|
||||
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
|
||||
total = view.findViewById(R.id.total)
|
||||
grid()
|
||||
return view
|
||||
@@ -171,11 +163,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
|
||||
getDownloads()
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
@@ -183,26 +175,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val media =
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
downloadManager.animeDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
||||
media?.let {
|
||||
val mediaModel = getMedia(it)
|
||||
if (mediaModel == null) {
|
||||
snackString("Error loading media.json")
|
||||
return@let
|
||||
}
|
||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("download", true),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
lifecycleScope.launch {
|
||||
val mediaModel = getMedia(it)
|
||||
if (mediaModel == null) {
|
||||
snackString("Error loading media.json")
|
||||
return@launch
|
||||
}
|
||||
MediaDetailsActivity.mediaSingleton = mediaModel
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Pair.create(
|
||||
requireActivity().findViewById<ImageView>(R.id.itemCompactImage),
|
||||
ViewCompat.getTransitionName(requireActivity().findViewById(R.id.itemCompactImage))
|
||||
),
|
||||
).toBundle()
|
||||
)
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
@@ -210,8 +198,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineAnimeModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineAnimeModel
|
||||
val type: DownloadedType.Type =
|
||||
DownloadedType.Type.ANIME
|
||||
val type: MediaType = MediaType.ANIME
|
||||
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
@@ -220,21 +207,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
builder.setMessage("Are you sure you want to delete ${item.title}?")
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
val mediaIds = requireContext().getSharedPreferences(
|
||||
getString(R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
?.all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
|
||||
val mediaIds =
|
||||
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
|
||||
?: emptySet()
|
||||
if (mediaIds.isEmpty()) {
|
||||
snackString("No media found") // if this happens, terrible things have happened
|
||||
}
|
||||
for (mediaId in mediaIds) {
|
||||
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
|
||||
.removeDownload(mediaId.toString())
|
||||
}
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
@@ -252,41 +231,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
var height = statusBarHeight
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
height = max(
|
||||
statusBarHeight,
|
||||
min(
|
||||
displayCutout.boundingRects[0].width(),
|
||||
displayCutout.boundingRects[0].height()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
val visible = false
|
||||
|
||||
fun animate() {
|
||||
val start = if (visible) 0f else 1f
|
||||
val end = if (!visible) 0f else 1f
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop.setOnClickListener {
|
||||
gridView.smoothScrollToPositionFromTop(0, 0)
|
||||
}
|
||||
@@ -296,7 +241,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
@@ -306,8 +250,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
totalItemCount: Int
|
||||
) {
|
||||
val first = view.getChildAt(0)
|
||||
val visibility = first != null && first.top < -height
|
||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
val visibility = first != null && first.top < 0
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
scrollTop.isVisible = visibility
|
||||
}
|
||||
})
|
||||
initActivity(requireActivity())
|
||||
@@ -317,7 +263,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -337,31 +282,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val _downloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
|
||||
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
|
||||
for (title in animeTitles) {
|
||||
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineAnimeModel = loadOfflineAnimeModel(download)
|
||||
newAnimeDownloads += offlineAnimeModel
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.setItems(downloads)
|
||||
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
downloads = newAnimeDownloads
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
/**
|
||||
* Load media.json file from the directory and convert it to Media class
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return Media object
|
||||
*/
|
||||
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
return try {
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -373,41 +326,42 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
SEpisodeImpl() // Provide an instance of SEpisodeImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
}
|
||||
?: return null
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
/**
|
||||
* Load OfflineAnimeModel from the directory
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return OfflineAnimeModel object
|
||||
*/
|
||||
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
|
||||
val type = downloadedType.type.asText()
|
||||
try {
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val directory = DownloadsManager.getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||
cover.uri
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
val banner = directory?.findFile("banner.jpg")
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
@@ -437,9 +391,9 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineAnimeModel(
|
||||
"unknown",
|
||||
"0",
|
||||
@@ -448,8 +402,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
false,
|
||||
false,
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
@@ -10,18 +10,20 @@ import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.manga.ImageData
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
|
||||
@@ -29,7 +31,10 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
|
||||
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.deleteRecursively
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
|
||||
@@ -37,8 +42,8 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
@@ -47,10 +52,9 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.util.Queue
|
||||
@@ -76,7 +80,7 @@ class MangaDownloaderService : Service() {
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Manga Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
@@ -187,13 +191,20 @@ class MangaDownloaderService : Service() {
|
||||
true
|
||||
}
|
||||
|
||||
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
|
||||
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
|
||||
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
|
||||
if (notifi) {
|
||||
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
getSubDirectory(
|
||||
this@MangaDownloaderService,
|
||||
MediaType.MANGA,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
)?.deleteRecursively(this@MangaDownloaderService)
|
||||
|
||||
// Loop through each ImageData object from the task
|
||||
var farthest = 0
|
||||
for ((index, image) in task.imageData.withIndex()) {
|
||||
@@ -209,8 +220,7 @@ class MangaDownloaderService : Service() {
|
||||
while (bitmap == null && retryCount < task.retries) {
|
||||
bitmap = image.fetchAndProcessImage(
|
||||
image.page,
|
||||
image.source,
|
||||
this@MangaDownloaderService
|
||||
image.source
|
||||
)
|
||||
retryCount++
|
||||
}
|
||||
@@ -244,16 +254,16 @@ class MangaDownloaderService : Service() {
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
DownloadedType.Type.MANGA
|
||||
MediaType.MANGA
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.chapter)
|
||||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading file: ${e.message}")
|
||||
Logger.log("Exception while downloading file: ${e.message}")
|
||||
snackString("Exception while downloading file: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.chapter)
|
||||
}
|
||||
}
|
||||
@@ -262,40 +272,34 @@ class MangaDownloaderService : Service() {
|
||||
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
|
||||
try {
|
||||
// Define the directory within the private external storage space
|
||||
val directory = File(
|
||||
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/$title/$chapter"
|
||||
)
|
||||
|
||||
if (!directory.exists()) {
|
||||
directory.mkdirs()
|
||||
}
|
||||
|
||||
// Create a file reference within that directory for your image
|
||||
val file = File(directory, fileName)
|
||||
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile(fileName)?.forceDelete(this)
|
||||
// Create a file reference within that directory for the image
|
||||
val file =
|
||||
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
|
||||
|
||||
// Use a FileOutputStream to write the bitmap to the file
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
file.openOutputStream(this, false).use { outputStream ->
|
||||
if (outputStream == null) throw Exception("Output stream is null")
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
}
|
||||
|
||||
|
||||
} catch (e: Exception) {
|
||||
println("Exception while saving image: ${e.message}")
|
||||
snackString("Exception while saving image: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Manga/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
launchIO {
|
||||
val directory =
|
||||
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
|
||||
?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -310,7 +314,10 @@ class MangaDownloaderService : Service() {
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
file.writeText(jsonString)
|
||||
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
@@ -325,7 +332,7 @@ class MangaDownloaderService : Service() {
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
@@ -335,14 +342,16 @@ class MangaDownloaderService : Service() {
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@@ -11,6 +10,8 @@ import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
|
||||
|
||||
class OfflineMangaAdapter(
|
||||
@@ -21,8 +22,7 @@ class OfflineMangaAdapter(
|
||||
private val inflater: LayoutInflater =
|
||||
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
private var originalItems: List<OfflineMangaModel> = items
|
||||
private var style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
|
||||
override fun getCount(): Int {
|
||||
return items.size
|
||||
@@ -36,7 +36,6 @@ class OfflineMangaAdapter(
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
|
||||
val view: View = convertView ?: when (style) {
|
||||
@@ -49,7 +48,6 @@ class OfflineMangaAdapter(
|
||||
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
|
||||
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
|
||||
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
|
||||
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
|
||||
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
|
||||
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
|
||||
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
|
||||
@@ -59,14 +57,14 @@ class OfflineMangaAdapter(
|
||||
if (style == 0) {
|
||||
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
|
||||
val chapters = view.findViewById<TextView>(R.id.itemTotal)
|
||||
chapters.text = " Chapters"
|
||||
bannerView.setImageURI(item.banner)
|
||||
chapters.text = context.getString(R.string.chapters)
|
||||
bannerView.setImageURI(item.banner ?: item.image)
|
||||
totalChapter.text = item.totalChapter
|
||||
} else if (style == 1) {
|
||||
val readChapter =
|
||||
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
|
||||
readChapter.text = item.readChapter
|
||||
totalChapter.text = " | " + item.totalChapter
|
||||
totalChapter.text = context.getString(R.string.total_divider, item.totalChapter)
|
||||
}
|
||||
|
||||
// Bind item data to the views
|
||||
@@ -104,8 +102,7 @@ class OfflineMangaAdapter(
|
||||
}
|
||||
|
||||
fun notifyNewGrid() {
|
||||
style =
|
||||
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
|
||||
style = PrefManager.getVal(PrefName.OfflineView)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
package ani.dantotsu.download.manga
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.TypedValue
|
||||
@@ -15,7 +11,6 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.AbsListView
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.GridView
|
||||
@@ -23,42 +18,46 @@ import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.compareName
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaDetailsActivity
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.openInputStream
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.imageview.ShapeableImageView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
@@ -67,8 +66,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
private lateinit var gridView: GridView
|
||||
private lateinit var adapter: OfflineMangaAdapter
|
||||
private lateinit var total: TextView
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
private var downloadsJob: Job = Job()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -94,15 +92,12 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
if (!uiSettings.immersiveMode) {
|
||||
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||
view.rootView.fitsSystemWindows = true
|
||||
}
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
|
||||
searchView.addTextChangedListener(object : TextWatcher {
|
||||
@@ -116,8 +111,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
onSearchQuery(s.toString())
|
||||
}
|
||||
})
|
||||
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getInt("offline_view", 0)
|
||||
var style: Int = PrefManager.getVal(PrefName.OfflineView)
|
||||
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
|
||||
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
|
||||
var selected = when (style) {
|
||||
@@ -136,8 +130,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
layoutList.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 0
|
||||
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putInt("offline_view", style!!).apply()
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView)
|
||||
adapter.notifyNewGrid()
|
||||
@@ -148,8 +141,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
layoutcompact.setOnClickListener {
|
||||
selected(it as ImageView)
|
||||
style = 1
|
||||
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
|
||||
.putInt("offline_view", style!!).apply()
|
||||
PrefManager.setVal(PrefName.OfflineView, style)
|
||||
gridView.visibility = View.GONE
|
||||
gridView = view.findViewById(R.id.gridView1)
|
||||
adapter.notifyNewGrid()
|
||||
@@ -164,11 +156,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private fun grid() {
|
||||
gridView.visibility = View.VISIBLE
|
||||
getDownloads()
|
||||
val fadeIn = AlphaAnimation(0f, 1f)
|
||||
fadeIn.duration = 300 // animations pog
|
||||
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
|
||||
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
|
||||
getDownloads()
|
||||
gridView.adapter = adapter
|
||||
gridView.scheduleLayoutAnimation()
|
||||
total.text =
|
||||
@@ -177,23 +169,18 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val media =
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
|
||||
downloadManager.mangaDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
||||
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
|
||||
media?.let {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
lifecycleScope.launch {
|
||||
ContextCompat.startActivity(
|
||||
requireActivity(),
|
||||
Pair.create(
|
||||
gridView.getChildAt(position)
|
||||
.findViewById<ImageView>(R.id.itemCompactImage),
|
||||
ViewCompat.getTransitionName(requireActivity().findViewById(R.id.itemCompactImage))
|
||||
)
|
||||
).toBundle()
|
||||
)
|
||||
Intent(requireContext(), MediaDetailsActivity::class.java)
|
||||
.putExtra("media", getMedia(it))
|
||||
.putExtra("download", true),
|
||||
null
|
||||
)
|
||||
}
|
||||
} ?: run {
|
||||
snackString("no media found")
|
||||
}
|
||||
@@ -202,11 +189,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
gridView.setOnItemLongClickListener { _, _, position, _ ->
|
||||
// Get the OfflineMangaModel that was clicked
|
||||
val item = adapter.getItem(position) as OfflineMangaModel
|
||||
val type: DownloadedType.Type =
|
||||
val type: MediaType =
|
||||
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
|
||||
DownloadedType.Type.MANGA
|
||||
MediaType.MANGA
|
||||
} else {
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
}
|
||||
// Alert dialog to confirm deletion
|
||||
val builder =
|
||||
@@ -216,9 +203,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
builder.setPositiveButton("Yes") { _, _ ->
|
||||
downloadManager.removeMedia(item.title, type)
|
||||
getDownloads()
|
||||
adapter.setItems(downloads)
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
}
|
||||
builder.setNegativeButton("No") { _, _ ->
|
||||
// Do nothing
|
||||
@@ -236,41 +220,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
initActivity(requireActivity())
|
||||
var height = statusBarHeight
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
|
||||
if (displayCutout != null) {
|
||||
if (displayCutout.boundingRects.size > 0) {
|
||||
height = max(
|
||||
statusBarHeight,
|
||||
min(
|
||||
displayCutout.boundingRects[0].width(),
|
||||
displayCutout.boundingRects[0].height()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
val visible = false
|
||||
|
||||
fun animate() {
|
||||
val start = if (visible) 0f else 1f
|
||||
val end = if (!visible) 0f else 1f
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
|
||||
duration = 300
|
||||
interpolator = OvershootInterpolator(2f)
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
scrollTop.setOnClickListener {
|
||||
gridView.smoothScrollToPositionFromTop(0, 0)
|
||||
}
|
||||
@@ -280,7 +231,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
|
||||
// Implement behavior for different scroll states if needed
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
@@ -290,8 +240,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
totalItemCount: Int
|
||||
) {
|
||||
val first = view.getChildAt(0)
|
||||
val visibility = first != null && first.top < -height
|
||||
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
|
||||
val visibility = first != null && first.top < 0
|
||||
scrollTop.isVisible = visibility
|
||||
scrollTop.translationY =
|
||||
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -301,7 +253,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
getDownloads()
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -321,81 +272,87 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
|
||||
private fun getDownloads() {
|
||||
downloads = listOf()
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
if (downloadsJob.isActive) {
|
||||
downloadsJob.cancel()
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = _downloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
downloads = listOf()
|
||||
downloadsJob = Job()
|
||||
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
|
||||
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
|
||||
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in mangaTitles) {
|
||||
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newMangaDownloads += offlineMangaModel
|
||||
}
|
||||
downloads = newMangaDownloads
|
||||
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
|
||||
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
|
||||
for (title in novelTitles) {
|
||||
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
|
||||
val download = tDownloads.first()
|
||||
val offlineMangaModel = loadOfflineMangaModel(download)
|
||||
newNovelDownloads += offlineMangaModel
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.setItems(downloads)
|
||||
total.text =
|
||||
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
downloads += newNovelDownloads
|
||||
|
||||
}
|
||||
|
||||
private fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
//load media.json and convert to media class with gson
|
||||
/**
|
||||
* Load media.json file from the directory and convert it to Media class
|
||||
* @param downloadedType DownloadedType object
|
||||
* @return Media object
|
||||
*/
|
||||
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
|
||||
return try {
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
})
|
||||
.create()
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val media = directory?.findFile("media.json")
|
||||
?: return null
|
||||
val mediaJson =
|
||||
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
|
||||
it?.readText()
|
||||
}
|
||||
gson.fromJson(mediaJson, Media::class.java)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
|
||||
"Manga"
|
||||
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
|
||||
"Anime"
|
||||
} else {
|
||||
"Novel"
|
||||
}
|
||||
val directory = File(
|
||||
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/$type/${downloadedType.title}"
|
||||
)
|
||||
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
|
||||
val type = downloadedType.type.asText()
|
||||
//load media.json and convert to media class with gson
|
||||
try {
|
||||
val media = File(directory, "media.json")
|
||||
val mediaJson = media.readText()
|
||||
val directory = getSubDirectory(
|
||||
context ?: currContext()!!, downloadedType.type,
|
||||
false, downloadedType.title
|
||||
)
|
||||
val mediaModel = getMedia(downloadedType)!!
|
||||
val cover = File(directory, "cover.jpg")
|
||||
val coverUri: Uri? = if (cover.exists()) {
|
||||
Uri.fromFile(cover)
|
||||
val cover = directory?.findFile("cover.jpg")
|
||||
val coverUri: Uri? = if (cover?.exists() == true) {
|
||||
cover.uri
|
||||
} else null
|
||||
val banner = File(directory, "banner.jpg")
|
||||
val bannerUri: Uri? = if (banner.exists()) {
|
||||
Uri.fromFile(banner)
|
||||
val banner = directory?.findFile("banner.jpg")
|
||||
val bannerUri: Uri? = if (banner?.exists() == true) {
|
||||
banner.uri
|
||||
} else null
|
||||
val title = mediaModel.mainName()
|
||||
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
|
||||
@@ -403,14 +360,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
val isOngoing =
|
||||
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
val isUserScored = mediaModel.userScore != 0
|
||||
val readchapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val readChapter = (mediaModel.userProgress ?: "~").toString()
|
||||
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
|
||||
val chapters = " Chapters"
|
||||
return OfflineMangaModel(
|
||||
title,
|
||||
score,
|
||||
totalchapter,
|
||||
readchapter,
|
||||
totalChapter,
|
||||
readChapter,
|
||||
type,
|
||||
chapters,
|
||||
isOngoing,
|
||||
@@ -419,9 +376,9 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
bannerUri
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger("Error loading media.json: ${e.message}")
|
||||
logger(e.printStackTrace())
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Logger.log("Error loading media.json: ${e.message}")
|
||||
Logger.log(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
return OfflineMangaModel(
|
||||
"unknown",
|
||||
"0",
|
||||
@@ -429,8 +386,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
|
||||
"??",
|
||||
"movie",
|
||||
"hmm",
|
||||
false,
|
||||
false,
|
||||
isOngoing = false,
|
||||
isUserScored = false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
@@ -9,21 +9,25 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.IBinder
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.snackString
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.anggrayudi.storage.file.forceDelete
|
||||
import com.anggrayudi.storage.file.openOutputStream
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.InstanceCreator
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
@@ -31,8 +35,8 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SChapterImpl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -42,10 +46,9 @@ import kotlinx.coroutines.withContext
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
@@ -62,7 +65,7 @@ class NovelDownloaderService : Service() {
|
||||
private val mutex = Mutex()
|
||||
private var isCurrentlyProcessing = false
|
||||
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
private val networkHelper = Injekt.get<NetworkHelper>()
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// This is only required for bound services.
|
||||
@@ -75,7 +78,7 @@ class NovelDownloaderService : Service() {
|
||||
builder =
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
|
||||
setContentTitle("Novel Download Progress")
|
||||
setSmallIcon(R.drawable.ic_round_download_24)
|
||||
setSmallIcon(R.drawable.ic_download_24)
|
||||
priority = NotificationCompat.PRIORITY_DEFAULT
|
||||
setOnlyAlertOnce(true)
|
||||
setProgress(0, 0, false)
|
||||
@@ -186,15 +189,15 @@ class NovelDownloaderService : Service() {
|
||||
val contentType = response.header("Content-Type")
|
||||
val contentDisposition = response.header("Content-Disposition")
|
||||
|
||||
logger("Content-Type: $contentType")
|
||||
logger("Content-Disposition: $contentDisposition")
|
||||
Logger.log("Content-Type: $contentType")
|
||||
Logger.log("Content-Disposition: $contentDisposition")
|
||||
|
||||
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
|
||||
contentType == "application/epub+zip" ||
|
||||
(contentDisposition?.contains(".epub") == true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Error checking file type: ${e.message}")
|
||||
Logger.log("Error checking file type: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -225,12 +228,12 @@ class NovelDownloaderService : Service() {
|
||||
|
||||
if (!isEpubFile(task.downloadLink)) {
|
||||
if (isAlreadyDownloaded(task.originalLink)) {
|
||||
logger("Already downloaded")
|
||||
Logger.log("Already downloaded")
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
snackString("Already downloaded")
|
||||
return@withContext
|
||||
}
|
||||
logger("Download link is not an .epub file")
|
||||
Logger.log("Download link is not an .epub file")
|
||||
broadcastDownloadFailed(task.originalLink)
|
||||
snackString("Download link is not an .epub file")
|
||||
return@withContext
|
||||
@@ -245,27 +248,30 @@ class NovelDownloaderService : Service() {
|
||||
|
||||
networkHelper.downloadClient.newCall(request).execute().use { response ->
|
||||
// Ensure the response is successful and has a body
|
||||
if (!response.isSuccessful || response.body == null) {
|
||||
if (!response.isSuccessful) {
|
||||
throw IOException("Failed to download file: ${response.message}")
|
||||
}
|
||||
val directory = getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title,
|
||||
task.chapter
|
||||
) ?: throw Exception("Directory not found")
|
||||
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
|
||||
|
||||
val file = File(
|
||||
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
|
||||
)
|
||||
|
||||
// Create directories if they don't exist
|
||||
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
|
||||
|
||||
// Overwrite existing file
|
||||
if (file.exists()) file.delete()
|
||||
val file = directory.createFile("application/epub+zip", "0.epub")
|
||||
?: throw Exception("File not created")
|
||||
|
||||
//download cover
|
||||
task.coverUrl?.let {
|
||||
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
|
||||
}
|
||||
val outputStream =
|
||||
this@NovelDownloaderService.contentResolver.openOutputStream(file.uri)
|
||||
?: throw Exception("Could not open OutputStream")
|
||||
|
||||
val sink = file.sink().buffer()
|
||||
val sink = outputStream.sink().buffer()
|
||||
val responseBody = response.body
|
||||
val totalBytes = responseBody.contentLength()
|
||||
var downloadedBytes = 0L
|
||||
@@ -301,7 +307,7 @@ class NovelDownloaderService : Service() {
|
||||
withContext(Dispatchers.Main) {
|
||||
val progress =
|
||||
(downloadedBytes * 100 / totalBytes).toInt()
|
||||
logger("Download progress: $progress")
|
||||
Logger.log("Download progress: $progress")
|
||||
broadcastDownloadProgress(task.originalLink, progress)
|
||||
}
|
||||
lastBroadcastUpdate = downloadedBytes
|
||||
@@ -316,7 +322,7 @@ class NovelDownloaderService : Service() {
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading .epub inside request: ${e.message}")
|
||||
Logger.log("Exception while downloading .epub inside request: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -333,29 +339,33 @@ class NovelDownloaderService : Service() {
|
||||
DownloadedType(
|
||||
task.title,
|
||||
task.chapter,
|
||||
DownloadedType.Type.NOVEL
|
||||
MediaType.NOVEL
|
||||
)
|
||||
)
|
||||
broadcastDownloadFinished(task.originalLink)
|
||||
snackString("${task.title} - ${task.chapter} Download finished")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger("Exception while downloading .epub: ${e.message}")
|
||||
Logger.log("Exception while downloading .epub: ${e.message}")
|
||||
snackString("Exception while downloading .epub: ${e.message}")
|
||||
FirebaseCrashlytics.getInstance().recordException(e)
|
||||
Injekt.get<CrashlyticsInterface>().logException(e)
|
||||
broadcastDownloadFailed(task.originalLink)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun saveMediaInfo(task: DownloadTask) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
val directory = File(
|
||||
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
|
||||
"Dantotsu/Novel/${task.title}"
|
||||
)
|
||||
if (!directory.exists()) directory.mkdirs()
|
||||
|
||||
val file = File(directory, "media.json")
|
||||
launchIO {
|
||||
val directory =
|
||||
getSubDirectory(
|
||||
this@NovelDownloaderService,
|
||||
MediaType.NOVEL,
|
||||
false,
|
||||
task.title
|
||||
) ?: throw Exception("Directory not found")
|
||||
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
|
||||
val file = directory.createFile("application/json", "media.json")
|
||||
?: throw Exception("File not created")
|
||||
val gson = GsonBuilder()
|
||||
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
|
||||
SChapterImpl() // Provide an instance of SChapterImpl
|
||||
@@ -369,33 +379,47 @@ class NovelDownloaderService : Service() {
|
||||
|
||||
val jsonString = gson.toJson(media)
|
||||
withContext(Dispatchers.Main) {
|
||||
file.writeText(jsonString)
|
||||
try {
|
||||
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
output.write(jsonString.toByteArray())
|
||||
}
|
||||
} catch (e: android.system.ErrnoException) {
|
||||
e.printStackTrace()
|
||||
Toast.makeText(
|
||||
this@NovelDownloaderService,
|
||||
"Error while saving: ${e.localizedMessage}",
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
|
||||
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
|
||||
withContext(
|
||||
Dispatchers.IO
|
||||
) {
|
||||
var connection: HttpURLConnection? = null
|
||||
println("Downloading url $url")
|
||||
Logger.log("Downloading url $url")
|
||||
try {
|
||||
connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.connect()
|
||||
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
||||
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
|
||||
}
|
||||
|
||||
val file = File(directory, name)
|
||||
FileOutputStream(file).use { output ->
|
||||
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
|
||||
val file =
|
||||
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
|
||||
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
|
||||
if (output == null) throw Exception("Output stream is null")
|
||||
connection.inputStream.use { input ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
return@withContext file.absolutePath
|
||||
return@withContext file.uri.toString()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -470,7 +494,6 @@ class NovelDownloaderService : Service() {
|
||||
}
|
||||
|
||||
object NovelServiceDataSingleton {
|
||||
var sourceMedia: Media? = null
|
||||
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
|
||||
|
||||
@Volatile
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package ani.dantotsu.download.video
|
||||
|
||||
import android.app.Notification
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.PlatformScheduler
|
||||
import androidx.media3.exoplayer.scheduler.Scheduler
|
||||
import ani.dantotsu.R
|
||||
|
||||
@UnstableApi
|
||||
class ExoplayerDownloadService :
|
||||
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
|
||||
companion object {
|
||||
private const val JOB_ID = 1
|
||||
private const val FOREGROUND_NOTIFICATION_ID = 1
|
||||
}
|
||||
|
||||
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
|
||||
|
||||
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
|
||||
|
||||
override fun getForegroundNotification(
|
||||
downloads: MutableList<Download>,
|
||||
notMetRequirements: Int
|
||||
): Notification =
|
||||
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
|
||||
this,
|
||||
R.drawable.mono,
|
||||
null,
|
||||
null,
|
||||
downloads,
|
||||
notMetRequirements
|
||||
)
|
||||
}
|
||||
@@ -7,189 +7,26 @@ import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.ContextCompat.getString
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.TrackSelectionParameters
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.database.StandaloneDatabaseProvider
|
||||
import androidx.media3.datasource.DataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.datasource.cache.NoOpCacheEvictor
|
||||
import androidx.media3.datasource.cache.SimpleCache
|
||||
import androidx.media3.datasource.okhttp.OkHttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.offline.Download
|
||||
import androidx.media3.exoplayer.offline.DownloadHelper
|
||||
import androidx.media3.exoplayer.offline.DownloadManager
|
||||
import androidx.media3.exoplayer.offline.DownloadService
|
||||
import androidx.media3.exoplayer.scheduler.Requirements
|
||||
import androidx.media3.ui.TrackSelectionDialogBuilder
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.defaultHeaders
|
||||
import ani.dantotsu.download.DownloadedType
|
||||
import ani.dantotsu.download.DownloadsManager
|
||||
import ani.dantotsu.download.anime.AnimeDownloaderService
|
||||
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.okHttpClient
|
||||
import ani.dantotsu.media.MediaType
|
||||
import ani.dantotsu.parsers.Subtitle
|
||||
import ani.dantotsu.parsers.SubtitleType
|
||||
import ani.dantotsu.parsers.Video
|
||||
import ani.dantotsu.parsers.VideoType
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.*
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
object Helper {
|
||||
|
||||
private var simpleCache: SimpleCache? = null
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
video.file.headers.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
val mimeType = when (video.format) {
|
||||
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
|
||||
VideoType.DASH -> MimeTypes.APPLICATION_MPD
|
||||
else -> MimeTypes.APPLICATION_MP4
|
||||
}
|
||||
|
||||
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
|
||||
var sub: MediaItem.SubtitleConfiguration? = null
|
||||
if (subtitle != null) {
|
||||
sub = MediaItem.SubtitleConfiguration
|
||||
.Builder(Uri.parse(subtitle.file.url))
|
||||
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
|
||||
.setMimeType(
|
||||
when (subtitle.type) {
|
||||
SubtitleType.VTT -> MimeTypes.TEXT_VTT
|
||||
SubtitleType.ASS -> MimeTypes.TEXT_SSA
|
||||
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
|
||||
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
|
||||
}
|
||||
)
|
||||
.build()
|
||||
}
|
||||
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
|
||||
val mediaItem = builder.build()
|
||||
val downloadHelper = DownloadHelper.forMediaItem(
|
||||
context,
|
||||
mediaItem,
|
||||
DefaultRenderersFactory(context),
|
||||
dataSourceFactory
|
||||
)
|
||||
downloadHelper.prepare(object : DownloadHelper.Callback {
|
||||
override fun onPrepared(helper: DownloadHelper) {
|
||||
helper.getDownloadRequest(null).let {
|
||||
DownloadService.sendAddDownload(
|
||||
context,
|
||||
ExoplayerDownloadService::class.java,
|
||||
it,
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareError(helper: DownloadHelper, e: IOException) {
|
||||
logError(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private var download: DownloadManager? = null
|
||||
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
|
||||
|
||||
@Synchronized
|
||||
@UnstableApi
|
||||
fun downloadManager(context: Context): DownloadManager {
|
||||
return download ?: let {
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val dataSourceFactory = DataSource.Factory {
|
||||
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
val networkHelper = Injekt.get<NetworkHelper>()
|
||||
val okHttpClient = networkHelper.client
|
||||
val dataSource: HttpDataSource =
|
||||
OkHttpDataSource.Factory(okHttpClient).createDataSource()
|
||||
defaultHeaders.forEach {
|
||||
dataSource.setRequestProperty(it.key, it.value)
|
||||
}
|
||||
dataSource
|
||||
}
|
||||
val threadPoolSize = Runtime.getRuntime().availableProcessors()
|
||||
val executorService = Executors.newFixedThreadPool(threadPoolSize)
|
||||
val downloadManager = DownloadManager(
|
||||
context,
|
||||
database,
|
||||
getSimpleCache(context),
|
||||
dataSourceFactory,
|
||||
executorService
|
||||
).apply {
|
||||
requirements =
|
||||
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
|
||||
maxParallelDownloads = 3
|
||||
}
|
||||
downloadManager.addListener( //for testing
|
||||
object : DownloadManager.Listener {
|
||||
override fun onDownloadChanged(
|
||||
downloadManager: DownloadManager,
|
||||
download: Download,
|
||||
finalException: Exception?
|
||||
) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
Log.e("Downloader", "Download Completed")
|
||||
} else if (download.state == Download.STATE_FAILED) {
|
||||
Log.e("Downloader", "Download Failed")
|
||||
} else if (download.state == Download.STATE_STOPPED) {
|
||||
Log.e("Downloader", "Download Stopped")
|
||||
} else if (download.state == Download.STATE_QUEUED) {
|
||||
Log.e("Downloader", "Download Queued")
|
||||
} else if (download.state == Download.STATE_DOWNLOADING) {
|
||||
Log.e("Downloader", "Download Downloading")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
downloadManager
|
||||
}
|
||||
}
|
||||
|
||||
private var downloadDirectory: File? = null
|
||||
|
||||
@Synchronized
|
||||
private fun getDownloadDirectory(context: Context): File {
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getExternalFilesDir(null)
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.filesDir
|
||||
}
|
||||
}
|
||||
return downloadDirectory!!
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun startAnimeDownloadService(
|
||||
context: Context,
|
||||
@@ -221,43 +58,29 @@ object Helper {
|
||||
|
||||
val downloadsManger = Injekt.get<DownloadsManager>()
|
||||
val downloadCheck = downloadsManger
|
||||
.queryDownload(title, episode, DownloadedType.Type.ANIME)
|
||||
.queryDownload(title, episode, MediaType.ANIME)
|
||||
|
||||
if (downloadCheck) {
|
||||
AlertDialog.Builder(context, R.style.MyPopup)
|
||||
.setTitle("Download Exists")
|
||||
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
|
||||
.setPositiveButton("Yes") { _, _ ->
|
||||
DownloadService.sendRemoveDownload(
|
||||
context,
|
||||
ExoplayerDownloadService::class.java,
|
||||
context.getSharedPreferences(
|
||||
getString(context, R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).getString(
|
||||
animeDownloadTask.getTaskName(),
|
||||
""
|
||||
) ?: "",
|
||||
false
|
||||
)
|
||||
context.getSharedPreferences(
|
||||
getString(context, R.string.anime_downloads),
|
||||
Context.MODE_PRIVATE
|
||||
).edit()
|
||||
PrefManager.getAnimeDownloadPreferences().edit()
|
||||
.remove(animeDownloadTask.getTaskName())
|
||||
.apply()
|
||||
downloadsManger.removeDownload(
|
||||
DownloadedType(
|
||||
title,
|
||||
episode,
|
||||
DownloadedType.Type.ANIME
|
||||
MediaType.ANIME
|
||||
)
|
||||
)
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
) {
|
||||
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
|
||||
if (!AnimeServiceDataSingleton.isServiceRunning) {
|
||||
val intent = Intent(context, AnimeDownloaderService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
AnimeServiceDataSingleton.isServiceRunning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton("No") { _, _ -> }
|
||||
@@ -272,18 +95,6 @@ object Helper {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun getSimpleCache(context: Context): SimpleCache {
|
||||
return if (simpleCache == null) {
|
||||
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
|
||||
val database = Injekt.get<StandaloneDatabaseProvider>()
|
||||
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
|
||||
simpleCache!!
|
||||
} else {
|
||||
simpleCache!!
|
||||
}
|
||||
}
|
||||
|
||||
private fun isNotificationPermissionGranted(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return ActivityCompat.checkSelfPermission(
|
||||
|
||||
@@ -2,7 +2,6 @@ package ani.dantotsu.home
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -28,13 +27,13 @@ import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentAnimeBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.ProgressAdapter
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -50,9 +49,6 @@ class AnimeFragment : Fragment() {
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var animePageAdapter: AnimePageAdapter
|
||||
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
val model: AnilistAnimeViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -211,13 +207,28 @@ class AnimeFragment : Fragment() {
|
||||
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getMovies().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getTopRated().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getMostFav().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
if (animePageAdapter.trendingViewPager != null) {
|
||||
animePageAdapter.updateHeight()
|
||||
model.getTrending().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
animePageAdapter.updateTrending(
|
||||
MediaAdaptor(
|
||||
if (uiSettings.smallView) 3 else 2,
|
||||
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||
it,
|
||||
requireActivity(),
|
||||
viewPager = animePageAdapter.trendingViewPager
|
||||
@@ -267,9 +278,12 @@ class AnimeFragment : Fragment() {
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending(1)
|
||||
model.loadUpdated()
|
||||
model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("popular_list", false))
|
||||
model.loadAll()
|
||||
model.loadPopular(
|
||||
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularAnimeList
|
||||
)
|
||||
)
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.animeRefresh?.isRefreshing = false
|
||||
@@ -284,7 +298,9 @@ class AnimeFragment : Fragment() {
|
||||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
|
||||
if (this::animePageAdapter.isInitialized && _binding != null) {
|
||||
animePageAdapter.updateNotificationCount()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -22,18 +23,20 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemAnimePageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.CalendarActivity
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -41,11 +44,10 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
|
||||
val ready = MutableLiveData(false)
|
||||
lateinit var binding: ItemAnimePageBinding
|
||||
private lateinit var trendingBinding: LayoutTrendingBinding
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
|
||||
val binding =
|
||||
@@ -55,37 +57,33 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
|
||||
override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) {
|
||||
binding = holder.binding
|
||||
trendingViewPager = binding.animeTrendingViewPager
|
||||
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
||||
trendingViewPager = trendingBinding.trendingViewPager
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
updateAvatar()
|
||||
|
||||
binding.animeSearchBar.hint = "ANIME"
|
||||
binding.animeSearchBarText.setOnClickListener {
|
||||
trendingBinding.searchBar.hint = "ANIME"
|
||||
trendingBinding.searchBarText.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context,
|
||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
|
||||
@@ -93,15 +91,28 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.animeSearchBar.setEndIconOnClickListener {
|
||||
binding.animeSearchBarText.performClick()
|
||||
}
|
||||
|
||||
binding.animeUserAvatar.setSafeOnClickListener {
|
||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
view.context,
|
||||
Intent(view.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
trendingBinding.searchBar.setEndIconOnClickListener {
|
||||
trendingBinding.searchBar.performClick()
|
||||
}
|
||||
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
|
||||
listOf(
|
||||
binding.animePreviousSeason,
|
||||
@@ -130,17 +141,14 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.animeIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
binding.animeIncludeList.isVisible = Anilist.userid != null
|
||||
|
||||
binding.animeIncludeList.isChecked = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("popular_list", true) ?: true
|
||||
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
|
||||
|
||||
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
|
||||
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putBoolean("popular_list", isChecked)?.apply()
|
||||
PrefManager.setVal(PrefName.PopularAnimeList, isChecked)
|
||||
}
|
||||
if (ready.value == false)
|
||||
ready.postValue(true)
|
||||
@@ -157,59 +165,115 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
|
||||
}
|
||||
|
||||
fun updateTrending(adaptor: MediaAdaptor) {
|
||||
binding.animeTrendingProgressBar.visibility = View.GONE
|
||||
binding.animeTrendingViewPager.adapter = adaptor
|
||||
binding.animeTrendingViewPager.offscreenPageLimit = 3
|
||||
binding.animeTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||
binding.animeTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
|
||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||
trendingBinding.trendingViewPager.adapter = adaptor
|
||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||
RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.animeTrendingViewPager.currentItem =
|
||||
binding.animeTrendingViewPager.currentItem + 1
|
||||
trendingBinding.trendingViewPager.currentItem += 1
|
||||
}
|
||||
binding.animeTrendingViewPager.registerOnPageChangeCallback(
|
||||
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
trendHandler!!.removeCallbacks(trendRun)
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
trendHandler?.removeCallbacks(trendRun)
|
||||
if (PrefManager.getVal(PrefName.TrendingScroller)) {
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.animeTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
trendingBinding.trendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
||||
binding.animeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
binding.animeSeasonsCont.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateRecent(adaptor: MediaAdaptor) {
|
||||
binding.animeUpdatedProgressBar.visibility = View.GONE
|
||||
binding.animeUpdatedRecyclerView.adapter = adaptor
|
||||
binding.animeUpdatedRecyclerView.layoutManager =
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeUpdatedRecyclerView,
|
||||
animeUpdatedProgressBar,
|
||||
animeRecently
|
||||
)
|
||||
animePopular.visibility = View.VISIBLE
|
||||
animePopular.startAnimation(setSlideUp())
|
||||
if (adaptor.itemCount == 0) {
|
||||
animeRecentlyContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateMovies(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeMoviesRecyclerView,
|
||||
animeMoviesProgressBar,
|
||||
animeMovies
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeTopRatedRecyclerView,
|
||||
animeTopRatedProgressBar,
|
||||
animeTopRated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
animeMostFavRecyclerView,
|
||||
animeMostFavProgressBar,
|
||||
animeMostFav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(
|
||||
binding.animeUpdatedRecyclerView.context,
|
||||
recyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.animeRecently.visibility = View.VISIBLE
|
||||
binding.animeRecently.startAnimation(setSlideUp(uiSettings))
|
||||
binding.animeUpdatedRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.animePopular.visibility = View.VISIBLE
|
||||
binding.animePopular.startAnimation(setSlideUp(uiSettings))
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
if (Anilist.avatar != null && ready.value == true) {
|
||||
binding.animeUserAvatar.loadImage(Anilist.avatar)
|
||||
binding.animeUserAvatar.imageTintList = null
|
||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||
trendingBinding.userAvatar.imageTintList = null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotificationCount() {
|
||||
if (this::binding.isInitialized) {
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,13 @@ import android.content.Intent
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
@@ -21,23 +23,25 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.bottomBar
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.FragmentHomeBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.user.ListActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -70,16 +74,20 @@ class HomeFragment : Fragment() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val scope = lifecycleScope
|
||||
var uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
fun load() {
|
||||
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
|
||||
binding.homeUserName.text = Anilist.username
|
||||
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
|
||||
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
|
||||
binding.homeUserAvatar.loadImage(Anilist.avatar)
|
||||
if (!uiSettings.bannerAnimations) binding.homeUserBg.pause()
|
||||
binding.homeUserBg.loadImage(Anilist.bg)
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
blurImage(
|
||||
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
|
||||
Anilist.bg
|
||||
)
|
||||
binding.homeUserDataProgressBar.visibility = View.GONE
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
|
||||
binding.homeAnimeList.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
@@ -98,14 +106,14 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||
binding.homeUserDataContainer.visibility = View.VISIBLE
|
||||
binding.homeUserDataContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideUp(), 0.25f)
|
||||
binding.homeAnimeList.visibility = View.VISIBLE
|
||||
binding.homeMangaList.visibility = View.VISIBLE
|
||||
binding.homeListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
else {
|
||||
snackString(currContext()?.getString(R.string.please_reload))
|
||||
@@ -119,26 +127,38 @@ class HomeFragment : Fragment() {
|
||||
"dialog"
|
||||
)
|
||||
}
|
||||
binding.homeUserAvatarContainer.setOnLongClickListener {
|
||||
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
|
||||
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.homeTopContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
var reached = false
|
||||
val duration = (uiSettings.animationSpeed * 200).toLong()
|
||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||
.start()
|
||||
} else {
|
||||
if (reached) {
|
||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (!binding.homeScroll.canScrollVertically(1)) {
|
||||
reached = true
|
||||
bottomBar.animate().translationZ(0f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
|
||||
.start()
|
||||
} else {
|
||||
if (reached) {
|
||||
bottomBar.animate().translationZ(12f).setDuration(duration).start()
|
||||
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
|
||||
.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,13 +226,13 @@ class HomeFragment : Fragment() {
|
||||
)
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
|
||||
} else {
|
||||
empty.visibility = View.VISIBLE
|
||||
}
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp(uiSettings))
|
||||
title.startAnimation(setSlideUp())
|
||||
progress.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
@@ -295,25 +315,26 @@ class HomeFragment : Fragment() {
|
||||
binding.homeRecommended
|
||||
)
|
||||
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
|
||||
|
||||
model.empty.observe(viewLifecycleOwner) {
|
||||
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
|
||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||
binding.homeDantotsuContainer.startAnimation(setSlideUp(uiSettings))
|
||||
binding.homeDantotsuContainer.startAnimation(setSlideUp())
|
||||
binding.homeDantotsuIcon.setSafeOnClickListener {
|
||||
(binding.homeDantotsuIcon.drawable as Animatable).start()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val array = arrayOf(
|
||||
Runnable { runBlocking { model.setAnimeContinue() } },
|
||||
Runnable { runBlocking { model.setAnimeFav() } },
|
||||
Runnable { runBlocking { model.setAnimePlanned() } },
|
||||
Runnable { runBlocking { model.setMangaContinue() } },
|
||||
Runnable { runBlocking { model.setMangaFav() } },
|
||||
Runnable { runBlocking { model.setMangaPlanned() } },
|
||||
Runnable { runBlocking { model.setRecommendation() } }
|
||||
"AnimeContinue",
|
||||
"AnimeFav",
|
||||
"AnimePlanned",
|
||||
"MangaContinue",
|
||||
"MangaFav",
|
||||
"MangaPlanned",
|
||||
"Recommendation"
|
||||
)
|
||||
|
||||
val containers = arrayOf(
|
||||
@@ -330,8 +351,6 @@ class HomeFragment : Fragment() {
|
||||
live.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
withContext(Dispatchers.IO) {
|
||||
//Get userData First
|
||||
getUserId(requireContext()) {
|
||||
@@ -340,9 +359,13 @@ class HomeFragment : Fragment() {
|
||||
model.loaded = true
|
||||
model.setListImages()
|
||||
var empty = true
|
||||
val homeLayoutShow: List<Boolean> =
|
||||
PrefManager.getVal(PrefName.HomeLayoutShow)
|
||||
runBlocking {
|
||||
model.initHomePage()
|
||||
}
|
||||
(array.indices).forEach { i ->
|
||||
if (uiSettings.homeLayoutShow[i]) {
|
||||
array[i].run()
|
||||
if (homeLayoutShow.elementAt(i)) {
|
||||
empty = false
|
||||
} else withContext(Dispatchers.Main) {
|
||||
containers[i].visibility = View.GONE
|
||||
@@ -359,6 +382,10 @@ class HomeFragment : Fragment() {
|
||||
|
||||
override fun onResume() {
|
||||
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
|
||||
if (_binding != null) {
|
||||
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.fragment.app.Fragment
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.databinding.FragmentLoginBinding
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
|
||||
import ani.dantotsu.settings.saving.internal.PreferencePackager
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
|
||||
class LoginFragment : Fragment() {
|
||||
|
||||
@@ -29,5 +39,99 @@ class LoginFragment : Fragment() {
|
||||
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
|
||||
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
|
||||
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
|
||||
|
||||
val openDocumentLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val jsonString =
|
||||
requireActivity().contentResolver.openInputStream(uri)?.readBytes()
|
||||
?: throw Exception("Error reading file")
|
||||
val name =
|
||||
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
|
||||
//.sani is encrypted, .ani is not
|
||||
if (name.endsWith(".sani")) {
|
||||
passwordAlertDialog { password ->
|
||||
if (password != null) {
|
||||
val salt = jsonString.copyOfRange(0, 16)
|
||||
val encrypted = jsonString.copyOfRange(16, jsonString.size)
|
||||
val decryptedJson = try {
|
||||
PreferenceKeystore.decryptWithPassword(
|
||||
password,
|
||||
encrypted,
|
||||
salt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
toast("Incorrect password")
|
||||
return@passwordAlertDialog
|
||||
}
|
||||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
} else if (name.endsWith(".ani")) {
|
||||
val decryptedJson = jsonString.toString(Charsets.UTF_8)
|
||||
if (PreferencePackager.unpack(decryptedJson))
|
||||
restartApp()
|
||||
} else {
|
||||
toast("Invalid file type")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log(e)
|
||||
toast("Error importing settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.importSettingsButton.setOnClickListener {
|
||||
openDocumentLauncher.launch(arrayOf("*/*"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
|
||||
val password = CharArray(16).apply { fill('0') }
|
||||
|
||||
// Inflate the dialog layout
|
||||
val dialogView =
|
||||
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null)
|
||||
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
|
||||
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
|
||||
subtitleTextView?.visibility = View.VISIBLE
|
||||
subtitleTextView?.text = "Enter your password to decrypt the file"
|
||||
|
||||
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup)
|
||||
.setTitle("Enter Password")
|
||||
.setView(dialogView)
|
||||
.setPositiveButton("OK", null)
|
||||
.setNegativeButton("Cancel") { dialog, _ ->
|
||||
password.fill('0')
|
||||
dialog.dismiss()
|
||||
callback(null)
|
||||
}
|
||||
.create()
|
||||
|
||||
dialog.window?.setDimAmount(0.8f)
|
||||
dialog.show()
|
||||
|
||||
// Override the positive button here
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
|
||||
if (editText?.text?.isNotBlank() == true) {
|
||||
editText.text?.toString()?.trim()?.toCharArray(password)
|
||||
dialog.dismiss()
|
||||
callback(password)
|
||||
} else {
|
||||
toast("Password cannot be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restartApp() {
|
||||
val intent = Intent(requireActivity(), requireActivity().javaClass)
|
||||
requireActivity().finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package ani.dantotsu.home
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -26,12 +25,12 @@ import ani.dantotsu.connections.anilist.AnilistMangaViewModel
|
||||
import ani.dantotsu.connections.anilist.SearchResults
|
||||
import ani.dantotsu.connections.anilist.getUserId
|
||||
import ani.dantotsu.databinding.FragmentMangaBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.ProgressAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -46,9 +45,6 @@ class MangaFragment : Fragment() {
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var mangaPageAdapter: MangaPageAdapter
|
||||
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
val model: AnilistMangaViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
@@ -164,18 +160,44 @@ class MangaFragment : Fragment() {
|
||||
})
|
||||
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
|
||||
if (i == true) {
|
||||
model.getTrendingNovel().observe(viewLifecycleOwner) {
|
||||
model.getPopularNovel().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getPopularManga().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getPopularManhwa().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrendingManhwa(
|
||||
MediaAdaptor(
|
||||
0,
|
||||
it,
|
||||
requireActivity()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
model.getTopRated().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
model.getMostFav().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
|
||||
}
|
||||
}
|
||||
if (mangaPageAdapter.trendingViewPager != null) {
|
||||
mangaPageAdapter.updateHeight()
|
||||
model.getTrending().observe(viewLifecycleOwner) {
|
||||
if (it != null) {
|
||||
mangaPageAdapter.updateTrending(
|
||||
MediaAdaptor(
|
||||
if (uiSettings.smallView) 3 else 2,
|
||||
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
|
||||
it,
|
||||
requireActivity(),
|
||||
viewPager = mangaPageAdapter.trendingViewPager
|
||||
@@ -241,9 +263,12 @@ class MangaFragment : Fragment() {
|
||||
}
|
||||
model.loaded = true
|
||||
model.loadTrending()
|
||||
model.loadTrendingNovel()
|
||||
model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("popular_list", false) )
|
||||
model.loadAll()
|
||||
model.loadPopular(
|
||||
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
|
||||
PrefName.PopularMangaList
|
||||
)
|
||||
)
|
||||
}
|
||||
live.postValue(false)
|
||||
_binding?.mangaRefresh?.isRefreshing = false
|
||||
@@ -259,6 +284,9 @@ class MangaFragment : Fragment() {
|
||||
binding.root.requestApplyInsets()
|
||||
binding.root.requestLayout()
|
||||
}
|
||||
if (this::mangaPageAdapter.isInitialized && _binding != null) {
|
||||
mangaPageAdapter.updateNotificationCount()
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LayoutAnimationController
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -22,17 +23,19 @@ import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.databinding.ItemMangaPageBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.databinding.LayoutTrendingBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.GenreActivity
|
||||
import ani.dantotsu.media.MediaAdaptor
|
||||
import ani.dantotsu.media.SearchActivity
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.setSlideIn
|
||||
import ani.dantotsu.setSlideUp
|
||||
import ani.dantotsu.settings.SettingsDialogFragment
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
@@ -40,11 +43,10 @@ import com.google.android.material.textfield.TextInputLayout
|
||||
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
|
||||
val ready = MutableLiveData(false)
|
||||
lateinit var binding: ItemMangaPageBinding
|
||||
private lateinit var trendingBinding: LayoutTrendingBinding
|
||||
private var trendHandler: Handler? = null
|
||||
private lateinit var trendRun: Runnable
|
||||
var trendingViewPager: ViewPager2? = null
|
||||
private var uiSettings: UserInterfaceSettings =
|
||||
loadData("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
|
||||
val binding =
|
||||
@@ -54,37 +56,34 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
|
||||
override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) {
|
||||
binding = holder.binding
|
||||
trendingViewPager = binding.mangaTrendingViewPager
|
||||
trendingBinding = LayoutTrendingBinding.bind(binding.root)
|
||||
trendingViewPager = trendingBinding.trendingViewPager
|
||||
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
|
||||
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
|
||||
val currentColor = textInputLayout.boxBackgroundColor
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
|
||||
textInputLayout.boxBackgroundColor = semiTransparentColor
|
||||
val materialCardView =
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
|
||||
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
|
||||
materialCardView.setCardBackgroundColor(semiTransparentColor)
|
||||
val typedValue = TypedValue()
|
||||
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
|
||||
val color = typedValue.data
|
||||
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
|
||||
|
||||
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("colorOverflow", false) ?: false
|
||||
if (!colorOverflow) {
|
||||
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
|
||||
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
|
||||
}
|
||||
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
|
||||
|
||||
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = (-108f).px
|
||||
}
|
||||
|
||||
updateAvatar()
|
||||
|
||||
binding.mangaSearchBar.hint = "MANGA"
|
||||
binding.mangaSearchBarText.setOnClickListener {
|
||||
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
trendingBinding.searchBar.hint = "MANGA"
|
||||
trendingBinding.searchBarText.setOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
it.context,
|
||||
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
|
||||
@@ -92,14 +91,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.mangaUserAvatar.setSafeOnClickListener {
|
||||
trendingBinding.userAvatar.setSafeOnClickListener {
|
||||
val dialogFragment =
|
||||
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
|
||||
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
|
||||
}
|
||||
trendingBinding.userAvatar.setOnLongClickListener { view ->
|
||||
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
ContextCompat.startActivity(
|
||||
view.context,
|
||||
Intent(view.context, ProfileActivity::class.java)
|
||||
.putExtra("userId", Anilist.userid), null
|
||||
)
|
||||
false
|
||||
}
|
||||
|
||||
binding.mangaSearchBar.setEndIconOnClickListener {
|
||||
binding.mangaSearchBarText.performClick()
|
||||
trendingBinding.searchBar.setEndIconOnClickListener {
|
||||
trendingBinding.searchBarText.performClick()
|
||||
}
|
||||
|
||||
binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg")
|
||||
@@ -123,17 +131,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
)
|
||||
}
|
||||
|
||||
binding.mangaIncludeList.visibility =
|
||||
if (Anilist.userid != null) View.VISIBLE else View.GONE
|
||||
binding.mangaIncludeList.isVisible = Anilist.userid != null
|
||||
|
||||
binding.mangaIncludeList.isChecked = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
?.getBoolean("popular_list", true) ?: true
|
||||
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
|
||||
|
||||
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
|
||||
onIncludeListClick.invoke(isChecked)
|
||||
|
||||
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
|
||||
?.putBoolean("popular_list", isChecked)?.apply()
|
||||
PrefManager.setVal(PrefName.PopularMangaList, isChecked)
|
||||
}
|
||||
if (ready.value == false)
|
||||
ready.postValue(true)
|
||||
@@ -148,56 +153,121 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
|
||||
}
|
||||
|
||||
fun updateTrending(adaptor: MediaAdaptor) {
|
||||
binding.mangaTrendingProgressBar.visibility = View.GONE
|
||||
binding.mangaTrendingViewPager.adapter = adaptor
|
||||
binding.mangaTrendingViewPager.offscreenPageLimit = 3
|
||||
binding.mangaTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
|
||||
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendingBinding.trendingProgressBar.visibility = View.GONE
|
||||
trendingBinding.trendingViewPager.adapter = adaptor
|
||||
trendingBinding.trendingViewPager.offscreenPageLimit = 3
|
||||
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
|
||||
RecyclerView.OVER_SCROLL_NEVER
|
||||
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
|
||||
trendHandler = Handler(Looper.getMainLooper())
|
||||
trendRun = Runnable {
|
||||
binding.mangaTrendingViewPager.currentItem =
|
||||
binding.mangaTrendingViewPager.currentItem + 1
|
||||
trendingBinding.trendingViewPager.currentItem += 1
|
||||
}
|
||||
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
|
||||
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
trendHandler!!.removeCallbacks(trendRun)
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
trendHandler?.removeCallbacks(trendRun)
|
||||
if (PrefManager.getVal(PrefName.TrendingScroller))
|
||||
trendHandler!!.postDelayed(trendRun, 4000)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.mangaTrendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
|
||||
trendingBinding.trendingViewPager.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
trendingBinding.titleContainer.startAnimation(setSlideUp())
|
||||
binding.mangaListContainer.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
|
||||
}
|
||||
|
||||
fun updateTrendingManga(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTrendingMangaRecyclerView,
|
||||
mangaTrendingMangaProgressBar,
|
||||
mangaTrendingManga
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTrendingManhwaRecyclerView,
|
||||
mangaTrendingManhwaProgressBar,
|
||||
mangaTrendingManhwa
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNovel(adaptor: MediaAdaptor) {
|
||||
binding.mangaNovelProgressBar.visibility = View.GONE
|
||||
binding.mangaNovelRecyclerView.adapter = adaptor
|
||||
binding.mangaNovelRecyclerView.layoutManager =
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaNovelRecyclerView,
|
||||
mangaNovelProgressBar,
|
||||
mangaNovel
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun updateTopRated(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaTopRatedRecyclerView,
|
||||
mangaTopRatedProgressBar,
|
||||
mangaTopRated
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMostFav(adaptor: MediaAdaptor) {
|
||||
binding.apply {
|
||||
init(
|
||||
adaptor,
|
||||
mangaMostFavRecyclerView,
|
||||
mangaMostFavProgressBar,
|
||||
mangaMostFav
|
||||
)
|
||||
mangaPopular.visibility = View.VISIBLE
|
||||
mangaPopular.startAnimation(setSlideUp())
|
||||
}
|
||||
}
|
||||
|
||||
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
|
||||
progress.visibility = View.GONE
|
||||
recyclerView.adapter = adaptor
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(
|
||||
binding.mangaNovelRecyclerView.context,
|
||||
recyclerView.context,
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
|
||||
|
||||
binding.mangaNovel.visibility = View.VISIBLE
|
||||
binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
|
||||
binding.mangaNovelRecyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
|
||||
binding.mangaPopular.visibility = View.VISIBLE
|
||||
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
|
||||
recyclerView.visibility = View.VISIBLE
|
||||
title.visibility = View.VISIBLE
|
||||
title.startAnimation(setSlideUp())
|
||||
recyclerView.layoutAnimation =
|
||||
LayoutAnimationController(setSlideIn(), 0.25f)
|
||||
}
|
||||
|
||||
fun updateAvatar() {
|
||||
if (Anilist.avatar != null && ready.value == true) {
|
||||
binding.mangaUserAvatar.loadImage(Anilist.avatar)
|
||||
binding.mangaUserAvatar.imageTintList = null
|
||||
trendingBinding.userAvatar.loadImage(Anilist.avatar)
|
||||
trendingBinding.userAvatar.imageTintList = null
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotificationCount() {
|
||||
if (this::binding.isInitialized) {
|
||||
trendingBinding.notificationCount.visibility =
|
||||
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
|
||||
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.home
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -23,43 +22,36 @@ import ani.dantotsu.databinding.ActivityNoInternetBinding
|
||||
import ani.dantotsu.download.anime.OfflineAnimeFragment
|
||||
import ani.dantotsu.download.manga.OfflineMangaFragment
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.offline.OfflineFragment
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.selectedOption
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
|
||||
class NoInternet : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityNoInternetBinding
|
||||
lateinit var bottomBar: AnimatedBottomBar
|
||||
private var uiSettings = UserInterfaceSettings()
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
|
||||
binding = ActivityNoInternetBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
|
||||
val backgroundDrawable = _bottomBar.background as GradientDrawable
|
||||
val backgroundDrawable = bottomBar.background as GradientDrawable
|
||||
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
|
||||
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
|
||||
backgroundDrawable.setColor(semiTransparentColor)
|
||||
_bottomBar.background = backgroundDrawable
|
||||
bottomBar.background = backgroundDrawable
|
||||
}
|
||||
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("colorOverflow", false)
|
||||
if (!colorOverflow) {
|
||||
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
|
||||
|
||||
}
|
||||
|
||||
var doubleBackToExitPressedOnce = false
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
@@ -76,8 +68,7 @@ class NoInternet : AppCompatActivity() {
|
||||
|
||||
binding.root.doOnAttach {
|
||||
initActivity(this)
|
||||
uiSettings = loadData("ui_settings") ?: uiSettings
|
||||
selectedOption = uiSettings.defaultStartUpTab
|
||||
selectedOption = PrefManager.getVal(PrefName.DefaultStartUpTab)
|
||||
|
||||
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
@@ -89,7 +80,7 @@ class NoInternet : AppCompatActivity() {
|
||||
val mainViewPager = binding.viewpager
|
||||
mainViewPager.isUserInputEnabled = false
|
||||
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
navbar.setOnTabSelectListener(object :
|
||||
AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
|
||||
@@ -3,7 +3,10 @@ package ani.dantotsu.media
|
||||
import java.io.Serializable
|
||||
|
||||
data class Author(
|
||||
val id: String,
|
||||
val name: String,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
|
||||
var id: Int,
|
||||
var name: String?,
|
||||
var image: String?,
|
||||
var role: String?,
|
||||
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
|
||||
var character: ArrayList<Character>? = null
|
||||
) : Serializable
|
||||
|
||||
@@ -12,13 +12,13 @@ import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.EmptyAdapter
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityAuthorBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.statusBarHeight
|
||||
@@ -33,10 +33,9 @@ class AuthorActivity : AppCompatActivity() {
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
private var author: Author? = null
|
||||
private var loaded = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityAuthorBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -56,14 +55,15 @@ class AuthorActivity : AppCompatActivity() {
|
||||
binding.studioClose.setOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
model.getAuthor().observe(this) {
|
||||
if (it != null) {
|
||||
author = it
|
||||
loaded = true
|
||||
binding.studioProgressBar.visibility = View.GONE
|
||||
binding.studioRecycler.visibility = View.VISIBLE
|
||||
|
||||
if (author!!.yearMedia.isNullOrEmpty()) {
|
||||
binding.studioRecycler.visibility = View.GONE
|
||||
}
|
||||
val titlePosition = arrayListOf<Int>()
|
||||
val concatAdapter = ConcatAdapter()
|
||||
val map = author!!.yearMedia ?: return@observe
|
||||
@@ -90,9 +90,19 @@ class AuthorActivity : AppCompatActivity() {
|
||||
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
|
||||
concatAdapter.addAdapter(EmptyAdapter(empty))
|
||||
}
|
||||
|
||||
binding.studioRecycler.adapter = concatAdapter
|
||||
binding.studioRecycler.layoutManager = gridLayoutManager
|
||||
|
||||
binding.charactersRecycler.visibility = View.VISIBLE
|
||||
binding.charactersText.visibility = View.VISIBLE
|
||||
binding.charactersRecycler.adapter =
|
||||
CharacterAdapter(author!!.character ?: arrayListOf())
|
||||
binding.charactersRecycler.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
if (author!!.character.isNullOrEmpty()) {
|
||||
binding.charactersRecycler.visibility = View.GONE
|
||||
binding.charactersText.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
||||
|
||||
58
app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt
Normal file
58
app/src/main/java/ani/dantotsu/media/AuthorAdapter.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.setAnimation
|
||||
import java.io.Serializable
|
||||
|
||||
class AuthorAdapter(
|
||||
private val authorList: ArrayList<Author>,
|
||||
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
|
||||
val binding =
|
||||
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return AuthorViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val author = authorList[position]
|
||||
binding.itemCompactRelation.text = author.role
|
||||
binding.itemCompactImage.loadImage(author.image)
|
||||
binding.itemCompactTitle.text = author.name
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = authorList.size
|
||||
inner class AuthorViewHolder(val binding: ItemCharacterBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
init {
|
||||
itemView.setOnClickListener {
|
||||
val author = authorList[bindingAdapterPosition]
|
||||
ContextCompat.startActivity(
|
||||
itemView.context,
|
||||
Intent(
|
||||
itemView.context,
|
||||
AuthorActivity::class.java
|
||||
).putExtra("author", author as Serializable),
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
itemView.context as Activity,
|
||||
Pair.create(
|
||||
binding.itemCompactImage,
|
||||
ViewCompat.getTransitionName(binding.itemCompactImage)!!
|
||||
),
|
||||
).toBundle()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
@@ -16,11 +14,10 @@ import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.databinding.ActivityListBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.hideSystemBarsExtendView
|
||||
import ani.dantotsu.media.user.ListViewPagerAdapter
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
@@ -35,10 +32,9 @@ class CalendarActivity : AppCompatActivity() {
|
||||
private var selectedTabIdx = 1
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityListBinding.inflate(layoutInflater)
|
||||
|
||||
@@ -46,13 +42,6 @@ class CalendarActivity : AppCompatActivity() {
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
|
||||
val primaryColor = typedValue.data
|
||||
val typedValue2 = TypedValue()
|
||||
theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue2,
|
||||
true
|
||||
)
|
||||
val titleTextColor = typedValue2.data
|
||||
val typedValue3 = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
|
||||
val primaryTextColor = typedValue3.data
|
||||
@@ -67,8 +56,7 @@ class CalendarActivity : AppCompatActivity() {
|
||||
binding.listTitle.setTextColor(primaryTextColor)
|
||||
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
|
||||
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
if (!uiSettings.immersiveMode) {
|
||||
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
|
||||
this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg_inv)
|
||||
binding.root.fitsSystemWindows = true
|
||||
@@ -76,20 +64,16 @@ class CalendarActivity : AppCompatActivity() {
|
||||
} else {
|
||||
binding.root.fitsSystemWindows = false
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN
|
||||
)
|
||||
hideSystemBarsExtendView()
|
||||
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = statusBarHeight
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
}
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.listTitle.setText(R.string.release_calendar)
|
||||
binding.listSort.visibility = View.GONE
|
||||
|
||||
binding.random.visibility = View.GONE
|
||||
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {
|
||||
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
|
||||
|
||||
@@ -9,10 +9,11 @@ data class Character(
|
||||
val image: String?,
|
||||
val banner: String?,
|
||||
val role: String,
|
||||
|
||||
var isFav: Boolean,
|
||||
var description: String? = null,
|
||||
var age: String? = null,
|
||||
var gender: String? = null,
|
||||
var dateOfBirth: FuzzyDate? = null,
|
||||
var roles: ArrayList<Media>? = null
|
||||
var roles: ArrayList<Media>? = null,
|
||||
val voiceActor: ArrayList<Author>? = null,
|
||||
) : Serializable
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
@@ -11,10 +10,8 @@ import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.databinding.ItemCharacterBinding
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import java.io.Serializable
|
||||
|
||||
class CharacterAdapter(
|
||||
@@ -26,15 +23,13 @@ class CharacterAdapter(
|
||||
return CharacterViewHolder(binding)
|
||||
}
|
||||
|
||||
private val uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
setAnimation(binding.root.context, holder.binding.root, uiSettings)
|
||||
setAnimation(binding.root.context, holder.binding.root)
|
||||
val character = characterList[position]
|
||||
binding.itemCompactRelation.text = character.role + " "
|
||||
val whitespace = "${character.role} "
|
||||
character.voiceActor
|
||||
binding.itemCompactRelation.text = whitespace
|
||||
binding.itemCompactImage.loadImage(character.image)
|
||||
binding.itemCompactTitle.text = character.name
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -7,6 +8,7 @@ import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.math.MathUtils.clamp
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
@@ -15,21 +17,25 @@ import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.AnilistMutations
|
||||
import ani.dantotsu.databinding.ActivityCharacterBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.abs
|
||||
|
||||
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
@@ -38,22 +44,21 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
private val model: OtherDetailsViewModel by viewModels()
|
||||
private lateinit var character: Character
|
||||
private var loaded = false
|
||||
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityCharacterBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
initActivity(this)
|
||||
screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.transparent)
|
||||
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
|
||||
|
||||
banner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
@@ -77,7 +82,40 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
character.image
|
||||
)
|
||||
}
|
||||
|
||||
val link = "https://anilist.co/character/${character.id}"
|
||||
binding.characterShare.setOnClickListener {
|
||||
val i = Intent(Intent.ACTION_SEND)
|
||||
i.type = "text/plain"
|
||||
i.putExtra(Intent.EXTRA_TEXT, link)
|
||||
startActivity(Intent.createChooser(i, character.name))
|
||||
}
|
||||
binding.characterShare.setOnLongClickListener {
|
||||
openLinkInBrowser(link)
|
||||
true
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
character.isFav =
|
||||
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.characterFav.setImageResource(
|
||||
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.characterFav.setOnClickListener {
|
||||
lifecycleScope.launch {
|
||||
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, character.id)) {
|
||||
character.isFav = !character.isFav
|
||||
binding.characterFav.setImageResource(
|
||||
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
|
||||
)
|
||||
} else {
|
||||
snackString("Failed to toggle favorite")
|
||||
}
|
||||
}
|
||||
}
|
||||
model.getCharacter().observe(this) {
|
||||
if (it != null && !loaded) {
|
||||
character = it
|
||||
@@ -116,7 +154,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.characterProgress.isGone = loaded
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@@ -136,18 +174,16 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
|
||||
|
||||
binding.characterCover.visibility =
|
||||
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
|
||||
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
|
||||
if (percentage >= percent && !isCollapsed) {
|
||||
isCollapsed = true
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.nav_bg)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
|
||||
}
|
||||
if (percentage <= percent && isCollapsed) {
|
||||
isCollapsed = false
|
||||
if (uiSettings.immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.status)
|
||||
binding.characterAppBar.setBackgroundResource(R.color.bg)
|
||||
if (immersiveMode) this.window.statusBarColor =
|
||||
ContextCompat.getColor(this, R.color.transparent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currActivity
|
||||
@@ -20,23 +21,36 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
|
||||
return GenreViewHolder(binding)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
|
||||
val binding = holder.binding
|
||||
val desc =
|
||||
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") +
|
||||
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") +
|
||||
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||
"Male" -> currActivity()!!.getString(R.string.male)
|
||||
"Female" -> currActivity()!!.getString(R.string.female)
|
||||
else -> character.gender
|
||||
} else "") + "\n" + character.description
|
||||
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
|
||||
(if (character.dateOfBirth.toString() != "")
|
||||
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
|
||||
(if (character.gender != "null")
|
||||
currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
|
||||
currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(
|
||||
R.string.male
|
||||
)
|
||||
|
||||
currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(
|
||||
R.string.female
|
||||
)
|
||||
|
||||
else -> character.gender
|
||||
} else "") + "\n" + character.description
|
||||
|
||||
binding.characterDesc.isTextSelectable
|
||||
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SpoilerPlugin()).build()
|
||||
markWon.setMarkdown(binding.characterDesc, desc)
|
||||
|
||||
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
|
||||
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
|
||||
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(
|
||||
activity, LinearLayoutManager.HORIZONTAL, false
|
||||
)
|
||||
if (binding.voiceActorRecycler.adapter!!.itemCount == 0) {
|
||||
binding.voiceActorContainer.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = 1
|
||||
|
||||
@@ -12,9 +12,9 @@ import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.LangSet
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -27,7 +27,7 @@ class GenreActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
LangSet.setLocale(this)
|
||||
|
||||
ThemeManager(this).applyTheme()
|
||||
binding = ActivityGenreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
@@ -54,7 +54,9 @@ class GenreActivity : AppCompatActivity() {
|
||||
GridLayoutManager(this, (screenWidth / 156f).toInt())
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {
|
||||
model.loadGenres(
|
||||
Anilist.genres ?: loadLocalGenres() ?: arrayListOf()
|
||||
) {
|
||||
MainScope().launch {
|
||||
adapter.addGenre(it)
|
||||
}
|
||||
@@ -62,4 +64,15 @@ class GenreActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLocalGenres(): ArrayList<String>? {
|
||||
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
|
||||
.toMutableList()
|
||||
return if (genres.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
//sort alphabetically
|
||||
genres.sort().let { genres as ArrayList<String> }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import ani.dantotsu.connections.anilist.api.MediaList
|
||||
import ani.dantotsu.connections.anilist.api.MediaType
|
||||
import ani.dantotsu.media.anime.Anime
|
||||
import ani.dantotsu.media.manga.Manga
|
||||
import ani.dantotsu.profile.User
|
||||
import java.io.Serializable
|
||||
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
|
||||
|
||||
@@ -25,7 +26,7 @@ data class Media(
|
||||
var cover: String? = null,
|
||||
var banner: String? = null,
|
||||
var relation: String? = null,
|
||||
var popularity: Int? = null,
|
||||
var favourites: Int? = null,
|
||||
|
||||
var isAdult: Boolean,
|
||||
var isFav: Boolean = false,
|
||||
@@ -56,13 +57,17 @@ data class Media(
|
||||
var trailer: String? = null,
|
||||
var startDate: FuzzyDate? = null,
|
||||
var endDate: FuzzyDate? = null,
|
||||
var popularity: Int? = null,
|
||||
|
||||
var timeUntilAiring: Long? = null,
|
||||
|
||||
var characters: ArrayList<Character>? = null,
|
||||
var staff: ArrayList<Author>? = null,
|
||||
var prequel: Media? = null,
|
||||
var sequel: Media? = null,
|
||||
var relations: ArrayList<Media>? = null,
|
||||
var recommendations: ArrayList<Media>? = null,
|
||||
|
||||
var users: ArrayList<User>? = null,
|
||||
var vrvId: String? = null,
|
||||
var crunchySlug: String? = null,
|
||||
|
||||
@@ -82,7 +87,7 @@ data class Media(
|
||||
name = apiMedia.title!!.english,
|
||||
nameRomaji = apiMedia.title!!.romaji,
|
||||
userPreferredName = apiMedia.title!!.userPreferred,
|
||||
cover = apiMedia.coverImage?.large,
|
||||
cover = apiMedia.coverImage?.large ?: apiMedia.coverImage?.medium,
|
||||
banner = apiMedia.bannerImage,
|
||||
status = apiMedia.status.toString(),
|
||||
isFav = apiMedia.isFavourite!!,
|
||||
@@ -94,6 +99,8 @@ data class Media(
|
||||
meanScore = apiMedia.meanScore,
|
||||
startDate = apiMedia.startDate,
|
||||
endDate = apiMedia.endDate,
|
||||
favourites = apiMedia.favourites,
|
||||
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 },
|
||||
anime = if (apiMedia.type == MediaType.ANIME) Anime(
|
||||
totalEpisodes = apiMedia.episodes,
|
||||
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
|
||||
@@ -108,6 +115,8 @@ data class Media(
|
||||
this.userScore = mediaList.score?.toInt() ?: 0
|
||||
this.userStatus = mediaList.status?.toString()
|
||||
this.userUpdatedAt = mediaList.updatedAt?.toLong()
|
||||
this.genres =
|
||||
mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
|
||||
}
|
||||
|
||||
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
@@ -15,24 +13,25 @@ import android.widget.ImageView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.ItemMediaCompactBinding
|
||||
import ani.dantotsu.databinding.ItemMediaLargeBinding
|
||||
import ani.dantotsu.databinding.ItemMediaPageBinding
|
||||
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.setAnimation
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||
import java.io.Serializable
|
||||
|
||||
|
||||
@@ -42,11 +41,9 @@ class MediaAdaptor(
|
||||
private val activity: FragmentActivity,
|
||||
private val matchParent: Boolean = false,
|
||||
private val viewPager: ViewPager2? = null,
|
||||
private val fav: Boolean = false,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val uiSettings =
|
||||
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (type) {
|
||||
0 -> MediaViewHolder(
|
||||
@@ -86,17 +83,17 @@ class MediaAdaptor(
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (type) {
|
||||
0 -> {
|
||||
val b = (holder as MediaViewHolder).binding
|
||||
setAnimation(activity, b.root, uiSettings)
|
||||
setAnimation(activity, b.root)
|
||||
val media = mediaList?.getOrNull(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
@@ -130,18 +127,19 @@ class MediaAdaptor(
|
||||
)
|
||||
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
|
||||
}
|
||||
b.itemCompactProgressContainer.visibility = if (fav) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
1 -> {
|
||||
val b = (holder as MediaLargeViewHolder).binding
|
||||
setAnimation(activity, b.root, uiSettings)
|
||||
setAnimation(activity, b.root)
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
@@ -151,25 +149,30 @@ class MediaAdaptor(
|
||||
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
|
||||
)
|
||||
if (media.anime != null) {
|
||||
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
|
||||
val itemTotal = " " + if ((media.anime.totalEpisodes
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.episode_plural)
|
||||
else currActivity()!!.getString(R.string.episode_singular)
|
||||
) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(
|
||||
R.string.episode_singular
|
||||
)
|
||||
b.itemTotal.text = itemTotal
|
||||
b.itemCompactTotal.text =
|
||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
|
||||
?: "??").toString()) else (media.anime.totalEpisodes
|
||||
?: "??").toString()
|
||||
} else if (media.manga != null) {
|
||||
b.itemTotal.text = " " + if ((media.manga.totalChapters
|
||||
val itemTotal = " " + if ((media.manga.totalChapters
|
||||
?: 0) != 1
|
||||
) currActivity()!!.getString(R.string.chapter_plural)
|
||||
else currActivity()!!.getString(R.string.chapter_singular)
|
||||
) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
|
||||
R.string.chapter_singular
|
||||
)
|
||||
b.itemTotal.text = itemTotal
|
||||
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
|
||||
}
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
|
||||
val start = mediaList.size
|
||||
mediaList.addAll(mediaList)
|
||||
notifyDataSetChanged()
|
||||
val end = mediaList.size - start
|
||||
notifyItemRangeInserted(start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,25 +181,22 @@ class MediaAdaptor(
|
||||
val b = (holder as MediaPageViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (uiSettings.bannerAnimations)
|
||||
if (bannerAnimations)
|
||||
b.itemCompactBanner.setTransitionGenerator(
|
||||
RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed)) as Float)).toLong(),
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
.load(GlideUrl(media.banner ?: media.cover))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.into(banner)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
blurImage(
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
||||
media.banner ?: media.cover
|
||||
)
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
@@ -234,25 +234,21 @@ class MediaAdaptor(
|
||||
val b = (holder as MediaPageSmallViewHolder).binding
|
||||
val media = mediaList?.get(position)
|
||||
if (media != null) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
b.itemCompactImage.loadImage(media.cover)
|
||||
if (uiSettings.bannerAnimations)
|
||||
if (bannerAnimations)
|
||||
b.itemCompactBanner.setTransitionGenerator(
|
||||
RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||
AccelerateDecelerateInterpolator()
|
||||
)
|
||||
)
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
|
||||
val context = b.itemCompactBanner.context
|
||||
if (!(context as Activity).isDestroyed)
|
||||
Glide.with(context as Context)
|
||||
.load(GlideUrl(media.banner ?: media.cover))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
|
||||
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
|
||||
.into(banner)
|
||||
b.itemCompactOngoing.visibility =
|
||||
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
|
||||
blurImage(
|
||||
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
|
||||
media.banner ?: media.cover
|
||||
)
|
||||
b.itemCompactOngoing.isVisible =
|
||||
media.status == currActivity()!!.getString(R.string.status_releasing)
|
||||
b.itemCompactTitle.text = media.userPreferredName
|
||||
b.itemCompactScore.text =
|
||||
((if (media.userScore == 0) (media.meanScore
|
||||
@@ -396,10 +392,8 @@ class MediaAdaptor(
|
||||
if (itemCompactImage != null) {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
activity,
|
||||
Pair.create(
|
||||
itemCompactImage,
|
||||
ViewCompat.getTransitionName(activity.findViewById(R.id.itemCompactImage))!!
|
||||
),
|
||||
itemCompactImage,
|
||||
ViewCompat.getTransitionName(itemCompactImage)!!
|
||||
).toBundle()
|
||||
} else {
|
||||
null
|
||||
|
||||
@@ -2,8 +2,8 @@ package ani.dantotsu.media
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.util.TypedValue
|
||||
@@ -13,72 +13,90 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.bold
|
||||
import androidx.core.text.color
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMargins
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import ani.dantotsu.CustomBottomNavBar
|
||||
import ani.dantotsu.GesturesListener
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.ZoomOutPageTransformer
|
||||
import ani.dantotsu.blurImage
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.databinding.ActivityMediaBinding
|
||||
import ani.dantotsu.initActivity
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||
import ani.dantotsu.media.comments.CommentsFragment
|
||||
import ani.dantotsu.media.manga.MangaReadFragment
|
||||
import ani.dantotsu.media.novel.NovelReadFragment
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.others.AndroidBug5497Workaround
|
||||
import ani.dantotsu.others.ImageViewDialog
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.UserInterfaceSettings
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.statusBarHeight
|
||||
import ani.dantotsu.themes.ThemeManager
|
||||
import ani.dantotsu.util.LauncherWrapper
|
||||
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.navigation.NavigationBarView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import nl.joery.animatedbottombar.AnimatedBottomBar
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
|
||||
|
||||
private lateinit var binding: ActivityMediaBinding
|
||||
lateinit var launcher: LauncherWrapper
|
||||
lateinit var binding: ActivityMediaBinding
|
||||
private val scope = lifecycleScope
|
||||
private val model: MediaDetailsViewModel by viewModels()
|
||||
private lateinit var tabLayout: NavigationBarView
|
||||
private lateinit var uiSettings: UserInterfaceSettings
|
||||
var selected = 0
|
||||
lateinit var navBar: AnimatedBottomBar
|
||||
var anime = true
|
||||
private var adult = false
|
||||
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
|
||||
val id = intent.getIntExtra("mediaId", -1)
|
||||
if (id != -1) {
|
||||
runBlocking {
|
||||
withContext(Dispatchers.IO) {
|
||||
media = Anilist.query.getMedia(id, false) ?: emptyMedia()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (media.name == "No media found") {
|
||||
snackString(media.name)
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return
|
||||
}
|
||||
val contract = ActivityResultContracts.OpenDocumentTree()
|
||||
launcher = LauncherWrapper(this, contract)
|
||||
|
||||
mediaSingleton = null
|
||||
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
|
||||
MediaSingleton.bitmap = null
|
||||
@@ -86,22 +104,44 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
binding = ActivityMediaBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
screenWidth = resources.displayMetrics.widthPixels.toFloat()
|
||||
navBar = binding.mediaBottomBar
|
||||
|
||||
//Ui init
|
||||
// Ui init
|
||||
|
||||
initActivity(this)
|
||||
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
|
||||
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
val oldMargin = binding.mediaViewPager.marginBottom
|
||||
AndroidBug5497Workaround.assistActivity(this) {
|
||||
if (it) {
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = 0
|
||||
}
|
||||
navBar.visibility = View.GONE
|
||||
} else {
|
||||
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = oldMargin
|
||||
}
|
||||
navBar.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
val navBarRightMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) navBarHeight else 0
|
||||
val navBarBottomMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 0 else navBarHeight
|
||||
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
rightMargin = navBarRightMargin
|
||||
bottomMargin = navBarBottomMargin
|
||||
}
|
||||
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
|
||||
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
|
||||
binding.mediaCollapsing.minimumHeight = statusBarHeight
|
||||
|
||||
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = navBarHeight
|
||||
}
|
||||
|
||||
binding.mediaTitle.isSelected = true
|
||||
|
||||
mMaxScrollSize = binding.mediaAppBar.totalScrollRange
|
||||
@@ -111,20 +151,20 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
if (uiSettings.bannerAnimations) {
|
||||
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
|
||||
if (bannerAnimations) {
|
||||
val adi = AccelerateDecelerateInterpolator()
|
||||
val generator = RandomTransitionGenerator(
|
||||
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
|
||||
(10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
|
||||
adi
|
||||
)
|
||||
binding.mediaBanner.setTransitionGenerator(generator)
|
||||
}
|
||||
val banner =
|
||||
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
|
||||
val viewPager = binding.mediaViewPager
|
||||
tabLayout = binding.mediaTab as NavigationBarView
|
||||
viewPager.isUserInputEnabled = false
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
|
||||
viewPager.setPageTransformer(ZoomOutPageTransformer())
|
||||
|
||||
|
||||
val isDownload = intent.getBooleanExtra("download", false)
|
||||
@@ -132,16 +172,18 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
binding.mediaCoverImage.loadImage(media.cover)
|
||||
binding.mediaCoverImage.setOnLongClickListener {
|
||||
val coverTitle = "${media.userPreferredName}[Cover]"
|
||||
ImageViewDialog.newInstance(
|
||||
this,
|
||||
media.userPreferredName + "[Cover]",
|
||||
coverTitle,
|
||||
media.cover
|
||||
)
|
||||
}
|
||||
banner.loadImage(media.banner ?: media.cover, 400)
|
||||
|
||||
blurImage(banner, media.banner ?: media.cover)
|
||||
val gestureDetector = GestureDetector(this, object : GesturesListener() {
|
||||
override fun onDoubleClick(event: MotionEvent) {
|
||||
if (!uiSettings.bannerAnimations)
|
||||
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
|
||||
snackString(getString(R.string.enable_banner_animations))
|
||||
else {
|
||||
binding.mediaBanner.restart()
|
||||
@@ -150,20 +192,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
|
||||
override fun onLongClick(event: MotionEvent) {
|
||||
val bannerTitle = "${media.userPreferredName}[Banner]"
|
||||
ImageViewDialog.newInstance(
|
||||
this@MediaDetailsActivity,
|
||||
media.userPreferredName + "[Banner]",
|
||||
bannerTitle,
|
||||
media.banner ?: media.cover
|
||||
)
|
||||
banner.performClick()
|
||||
}
|
||||
})
|
||||
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
|
||||
if (this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
|
||||
.getBoolean("incognito", false)) {
|
||||
binding.mediaTitle.text = " ${media.userPreferredName}"
|
||||
if (PrefManager.getVal(PrefName.Incognito)) {
|
||||
val mediaTitle = " ${media.userPreferredName}"
|
||||
binding.mediaTitle.text = mediaTitle
|
||||
binding.incognito.visibility = View.VISIBLE
|
||||
}else {
|
||||
} else {
|
||||
binding.mediaTitle.text = media.userPreferredName
|
||||
}
|
||||
binding.mediaTitle.setOnLongClickListener {
|
||||
@@ -185,20 +228,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
R.drawable.ic_round_favorite_24
|
||||
)
|
||||
)
|
||||
val typedValue = TypedValue()
|
||||
this.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSecondary,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
val color = typedValue.data
|
||||
val typedValue2 = TypedValue()
|
||||
this.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSecondary,
|
||||
typedValue2,
|
||||
true
|
||||
)
|
||||
val color2 = typedValue.data
|
||||
|
||||
PopImageButton(
|
||||
scope,
|
||||
@@ -206,7 +235,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
R.drawable.ic_round_favorite_24,
|
||||
R.drawable.ic_round_favorite_border_24,
|
||||
R.color.bg_opp,
|
||||
R.color.violet_400,//TODO: Change to colorSecondary
|
||||
R.color.violet_400,
|
||||
media.isFav
|
||||
) {
|
||||
media.isFav = it
|
||||
@@ -221,13 +250,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
@SuppressLint("ResourceType")
|
||||
fun total() {
|
||||
val text = SpannableStringBuilder().apply {
|
||||
val typedValue = TypedValue()
|
||||
val mediaTypedValue = TypedValue()
|
||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorOnBackground,
|
||||
typedValue,
|
||||
mediaTypedValue,
|
||||
true
|
||||
)
|
||||
val white = typedValue.data
|
||||
val white = mediaTypedValue.data
|
||||
if (media.userStatus != null) {
|
||||
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
|
||||
val typedValue = TypedValue()
|
||||
@@ -284,7 +313,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
} else snackString(getString(R.string.please_login_anilist))
|
||||
}
|
||||
binding.mediaAddToList.setOnLongClickListener {
|
||||
saveData("${media.id}_progressDialog", true)
|
||||
PrefManager.setCustomVal(
|
||||
"${media.id}_progressDialog",
|
||||
true,
|
||||
)
|
||||
snackString(getString(R.string.auto_update_reset))
|
||||
true
|
||||
}
|
||||
@@ -314,49 +346,68 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
progress()
|
||||
}
|
||||
}
|
||||
|
||||
adult = media.isAdult
|
||||
tabLayout.menu.clear()
|
||||
if (media.anime != null) {
|
||||
viewPager.adapter =
|
||||
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
|
||||
tabLayout.inflateMenu(R.menu.anime_menu_detail)
|
||||
ViewPagerAdapter(
|
||||
supportFragmentManager,
|
||||
lifecycle,
|
||||
SupportedMedia.ANIME,
|
||||
media,
|
||||
intent.getIntExtra("commentId", -1)
|
||||
)
|
||||
} else if (media.manga != null) {
|
||||
viewPager.adapter = ViewPagerAdapter(
|
||||
supportFragmentManager,
|
||||
lifecycle,
|
||||
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
|
||||
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA,
|
||||
media,
|
||||
intent.getIntExtra("commentId", -1)
|
||||
)
|
||||
if (media.format == "NOVEL") {
|
||||
tabLayout.inflateMenu(R.menu.novel_menu_detail)
|
||||
} else {
|
||||
tabLayout.inflateMenu(R.menu.manga_menu_detail)
|
||||
}
|
||||
anime = false
|
||||
}
|
||||
|
||||
|
||||
selected = media.selected!!.window
|
||||
binding.mediaTitle.translationX = -screenWidth
|
||||
tabLayout.visibility = View.VISIBLE
|
||||
|
||||
tabLayout.setOnItemSelectedListener { item ->
|
||||
selectFromID(item.itemId)
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel, this)
|
||||
true
|
||||
val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
|
||||
val watchTab = if (anime) {
|
||||
navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
|
||||
} else if (media.format == "NOVEL") {
|
||||
navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
|
||||
} else {
|
||||
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
|
||||
}
|
||||
|
||||
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
|
||||
val commentTab =
|
||||
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
|
||||
navBar.addTab(infoTab)
|
||||
navBar.addTab(watchTab)
|
||||
navBar.addTab(commentTab)
|
||||
if (model.continueMedia == null && media.cameFromContinue) {
|
||||
model.continueMedia = loadData("continue_media") ?: true
|
||||
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
|
||||
selected = 1
|
||||
}
|
||||
if (intent.getStringExtra("FRAGMENT_TO_LOAD") != null) selected = 2
|
||||
if (viewPager.currentItem != selected) viewPager.post {
|
||||
viewPager.setCurrentItem(selected, false)
|
||||
}
|
||||
binding.commentInputLayout.isVisible = selected == 2
|
||||
navBar.selectTabAt(selected)
|
||||
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
|
||||
override fun onTabSelected(
|
||||
lastIndex: Int,
|
||||
lastTab: AnimatedBottomBar.Tab?,
|
||||
newIndex: Int,
|
||||
newTab: AnimatedBottomBar.Tab
|
||||
) {
|
||||
selected = newIndex
|
||||
binding.commentInputLayout.isVisible = selected == 2
|
||||
viewPager.setCurrentItem(selected, true)
|
||||
val sel = model.loadSelected(media, isDownload)
|
||||
sel.window = selected
|
||||
model.saveSelected(media.id, sel)
|
||||
}
|
||||
})
|
||||
|
||||
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
|
||||
live.observe(this) {
|
||||
@@ -369,33 +420,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun selectFromID(id: Int) {
|
||||
when (id) {
|
||||
R.id.info -> {
|
||||
selected = 0
|
||||
}
|
||||
|
||||
R.id.watch, R.id.read -> {
|
||||
selected = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun idFromSelect(): Int {
|
||||
if (anime) when (selected) {
|
||||
0 -> return R.id.info
|
||||
1 -> return R.id.watch
|
||||
}
|
||||
else when (selected) {
|
||||
0 -> return R.id.info
|
||||
1 -> return R.id.read
|
||||
}
|
||||
return R.id.info
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val rightMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) navBarHeight else 0
|
||||
val bottomMargin = if (resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_LANDSCAPE
|
||||
) 0 else navBarHeight
|
||||
val params: ViewGroup.MarginLayoutParams =
|
||||
navBar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
params.updateMargins(right = rightMargin, bottom = bottomMargin)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
tabLayout.selectedItemId = idFromSelect()
|
||||
navBar.selectTabAt(selected)
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
@@ -403,24 +442,36 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
ANIME, MANGA, NOVEL
|
||||
}
|
||||
|
||||
//ViewPager
|
||||
// ViewPager
|
||||
private class ViewPagerAdapter(
|
||||
fragmentManager: FragmentManager,
|
||||
lifecycle: Lifecycle,
|
||||
private val media: SupportedMedia
|
||||
private val mediaType: SupportedMedia,
|
||||
private val media: Media,
|
||||
private val commentId: Int
|
||||
) :
|
||||
FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||
|
||||
override fun getItemCount(): Int = 2
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> MediaInfoFragment()
|
||||
1 -> when (media) {
|
||||
1 -> when (mediaType) {
|
||||
SupportedMedia.ANIME -> AnimeWatchFragment()
|
||||
SupportedMedia.MANGA -> MangaReadFragment()
|
||||
SupportedMedia.NOVEL -> NovelReadFragment()
|
||||
}
|
||||
|
||||
2 -> {
|
||||
val fragment = CommentsFragment()
|
||||
val bundle = Bundle()
|
||||
bundle.putInt("mediaId", media.id)
|
||||
bundle.putString("mediaName", media.mainName())
|
||||
if (commentId != -1) bundle.putInt("commentId", commentId)
|
||||
fragment.arguments = bundle
|
||||
fragment
|
||||
}
|
||||
|
||||
else -> MediaInfoFragment()
|
||||
}
|
||||
}
|
||||
@@ -437,14 +488,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
binding.mediaCover.visibility =
|
||||
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
|
||||
val duration = (200 * uiSettings.animationSpeed).toLong()
|
||||
val typedValue = TypedValue()
|
||||
this@MediaDetailsActivity.theme.resolveAttribute(
|
||||
com.google.android.material.R.attr.colorSecondary,
|
||||
typedValue,
|
||||
true
|
||||
)
|
||||
val color = typedValue.data
|
||||
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
|
||||
if (percentage >= percent && !isCollapsed) {
|
||||
isCollapsed = true
|
||||
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
|
||||
@@ -467,7 +511,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
.start()
|
||||
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
|
||||
.setDuration(duration).start()
|
||||
if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
|
||||
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.mediaBanner.resume()
|
||||
}
|
||||
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
|
||||
false
|
||||
@@ -483,6 +527,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
private val c1: Int,
|
||||
private val c2: Int,
|
||||
var clicked: Boolean,
|
||||
needsInitialClick: Boolean = false,
|
||||
callback: suspend (Boolean) -> (Unit)
|
||||
) {
|
||||
private var disabled = false
|
||||
@@ -491,9 +536,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
|
||||
init {
|
||||
enabled(true)
|
||||
scope.launch {
|
||||
delay(100) //TODO: a listener would be better
|
||||
clicked()
|
||||
if (needsInitialClick) {
|
||||
scope.launch {
|
||||
clicked()
|
||||
}
|
||||
}
|
||||
image.setOnClickListener {
|
||||
if (pressable && !disabled) {
|
||||
@@ -549,5 +595,4 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
|
||||
companion object {
|
||||
var mediaSingleton: Media? = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.FragmentManager
|
||||
@@ -11,8 +9,6 @@ import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.media.anime.Episode
|
||||
import ani.dantotsu.media.anime.SelectorDialogFragment
|
||||
import ani.dantotsu.media.manga.MangaChapter
|
||||
@@ -28,56 +24,51 @@ import ani.dantotsu.parsers.NovelSources
|
||||
import ani.dantotsu.parsers.ShowResponse
|
||||
import ani.dantotsu.parsers.VideoExtractor
|
||||
import ani.dantotsu.parsers.WatchSources
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import ani.dantotsu.util.Logger
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MediaDetailsViewModel : ViewModel() {
|
||||
val scrolledToTop = MutableLiveData(true)
|
||||
|
||||
fun saveSelected(id: Int, data: Selected, activity: Activity? = null) {
|
||||
saveData("$id-select", data, activity)
|
||||
fun saveSelected(id: Int, data: Selected) {
|
||||
PrefManager.setCustomVal("Selected-$id", data)
|
||||
}
|
||||
|
||||
|
||||
fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
|
||||
val sharedPreferences = Injekt.get<SharedPreferences>()
|
||||
val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
|
||||
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
|
||||
true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0)
|
||||
else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0)
|
||||
}
|
||||
it.preferDub = loadData("settings_prefer_dub") ?: false
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
val data =
|
||||
PrefManager.getNullableCustomVal("Selected-${media.id}", null, Selected::class.java)
|
||||
?: Selected().let {
|
||||
it.sourceIndex = 0
|
||||
it.preferDub = PrefManager.getVal(PrefName.SettingsPreferDub)
|
||||
saveSelected(media.id, it)
|
||||
it
|
||||
}
|
||||
if (isDownload) {
|
||||
data.sourceIndex = if (media.anime != null) {
|
||||
AnimeSources.list.size - 1
|
||||
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") {
|
||||
MangaSources.list.size - 1
|
||||
} else {
|
||||
NovelSources.list.size - 1
|
||||
data.sourceIndex = when {
|
||||
media.anime != null -> {
|
||||
AnimeSources.list.size - 1
|
||||
}
|
||||
|
||||
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
|
||||
MangaSources.list.size - 1
|
||||
}
|
||||
|
||||
else -> {
|
||||
NovelSources.list.size - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
fun loadSelectedStringLocation(sourceName: String): Int {
|
||||
//find the location of the source in the list
|
||||
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
|
||||
if (location == -1) {
|
||||
location = 0
|
||||
}
|
||||
return location
|
||||
}
|
||||
|
||||
var continueMedia: Boolean? = null
|
||||
private var loading = false
|
||||
|
||||
@@ -158,10 +149,10 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
watchSources?.get(i)?.apply {
|
||||
if (!post && !allowsPreloading) return@apply
|
||||
ep.sEpisode?.let {
|
||||
loadByVideoServers(link, ep.extra, it) {
|
||||
if (it.videos.isNotEmpty()) {
|
||||
list.add(it)
|
||||
ep.extractorCallback?.invoke(it)
|
||||
loadByVideoServers(link, ep.extra, it) { extractor ->
|
||||
if (extractor.videos.isNotEmpty()) {
|
||||
list.add(extractor)
|
||||
ep.extractorCallback?.invoke(extractor)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +220,7 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun setEpisode(ep: Episode?, who: String) {
|
||||
logger("set episode ${ep?.number} - $who", false)
|
||||
Logger.log("set episode ${ep?.number} - $who")
|
||||
episode.postValue(ep)
|
||||
MainScope().launch(Dispatchers.Main) {
|
||||
episode.value = null
|
||||
@@ -276,7 +267,7 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
mangaChapters
|
||||
|
||||
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
|
||||
logger("Loading Manga Chapters : $mangaLoaded")
|
||||
Logger.log("Loading Manga Chapters : $mangaLoaded")
|
||||
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
|
||||
mangaLoaded[i] =
|
||||
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
|
||||
@@ -297,7 +288,6 @@ class MediaDetailsViewModel : ViewModel() {
|
||||
suspend fun loadMangaChapterImages(
|
||||
chapter: MangaChapter,
|
||||
selected: Selected,
|
||||
series: String,
|
||||
post: Boolean = true
|
||||
): Boolean {
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package ani.dantotsu.media
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -16,16 +15,35 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.GenresViewModel
|
||||
import ani.dantotsu.databinding.*
|
||||
import ani.dantotsu.copyToClipboard
|
||||
import ani.dantotsu.currActivity
|
||||
import ani.dantotsu.databinding.ActivityGenreBinding
|
||||
import ani.dantotsu.databinding.FragmentMediaInfoBinding
|
||||
import ani.dantotsu.databinding.ItemChipBinding
|
||||
import ani.dantotsu.databinding.ItemQuelsBinding
|
||||
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
|
||||
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
|
||||
import ani.dantotsu.databinding.ItemTitleSearchBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTextBinding
|
||||
import ani.dantotsu.databinding.ItemTitleTrailerBinding
|
||||
import ani.dantotsu.displayTimer
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.px
|
||||
import ani.dantotsu.setSafeOnClickListener
|
||||
import ani.dantotsu.settings.saving.PrefManager
|
||||
import ani.dantotsu.settings.saving.PrefName
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -35,7 +53,6 @@ import java.io.Serializable
|
||||
import java.net.URLEncoder
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
class MediaInfoFragment : Fragment() {
|
||||
private var _binding: FragmentMediaInfoBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
@@ -44,6 +61,8 @@ class MediaInfoFragment : Fragment() {
|
||||
private var type = "ANIME"
|
||||
private val genreModel: GenresViewModel by activityViewModels()
|
||||
|
||||
private val tripleTab = "\t\t\t"
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
@@ -60,9 +79,9 @@ class MediaInfoFragment : Fragment() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val model: MediaDetailsViewModel by activityViewModels()
|
||||
val offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("offlineMode", false) || !isOnline(requireContext())
|
||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
|
||||
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
|
||||
binding.mediaInfoProgressBar.isGone = loaded
|
||||
binding.mediaInfoContainer.isVisible = loaded
|
||||
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
|
||||
|
||||
model.scrolledToTop.observe(viewLifecycleOwner) {
|
||||
@@ -72,16 +91,19 @@ class MediaInfoFragment : Fragment() {
|
||||
model.getMedia().observe(viewLifecycleOwner) { media ->
|
||||
if (media != null && !loaded) {
|
||||
loaded = true
|
||||
|
||||
binding.mediaInfoProgressBar.visibility = View.GONE
|
||||
binding.mediaInfoContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
|
||||
val infoName = tripleTab + (media.name ?: media.nameRomaji)
|
||||
binding.mediaInfoName.text = infoName
|
||||
binding.mediaInfoName.setOnLongClickListener {
|
||||
copyToClipboard(media.name ?: media.nameRomaji)
|
||||
true
|
||||
}
|
||||
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
|
||||
View.VISIBLE
|
||||
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
|
||||
val infoNameRomanji = tripleTab + media.nameRomaji
|
||||
binding.mediaInfoNameRomaji.text = infoNameRomanji
|
||||
binding.mediaInfoNameRomaji.setOnLongClickListener {
|
||||
copyToClipboard(media.nameRomaji)
|
||||
true
|
||||
@@ -93,13 +115,40 @@ class MediaInfoFragment : Fragment() {
|
||||
binding.mediaInfoSource.text = media.source
|
||||
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
|
||||
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
|
||||
binding.mediaInfoPopularity.text = media.popularity.toString()
|
||||
binding.mediaInfoFavorites.text = media.favourites.toString()
|
||||
if (media.anime != null) {
|
||||
binding.mediaInfoDuration.text =
|
||||
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
|
||||
val episodeDuration = media.anime.episodeDuration
|
||||
|
||||
binding.mediaInfoDuration.text = when {
|
||||
episodeDuration != null -> {
|
||||
val hours = episodeDuration / 60
|
||||
val minutes = episodeDuration % 60
|
||||
|
||||
val formattedDuration = buildString {
|
||||
if (hours > 0) {
|
||||
append("$hours hour")
|
||||
if (hours > 1) append("s")
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
if (hours > 0) append(", ")
|
||||
append("$minutes min")
|
||||
if (minutes > 1) append("s")
|
||||
}
|
||||
}
|
||||
|
||||
formattedDuration
|
||||
}
|
||||
|
||||
else -> "??"
|
||||
}
|
||||
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoSeason.text =
|
||||
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
|
||||
val seasonInfo =
|
||||
"${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
|
||||
binding.mediaInfoSeason.text = seasonInfo
|
||||
|
||||
if (media.anime.mainStudio != null) {
|
||||
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
|
||||
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
|
||||
@@ -133,9 +182,12 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
|
||||
binding.mediaInfoTotal.text =
|
||||
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
|
||||
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()
|
||||
val infoTotal = if (media.anime.nextAiringEpisode != null)
|
||||
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
|
||||
else
|
||||
(media.anime.totalEpisodes ?: "~").toString()
|
||||
binding.mediaInfoTotal.text = infoTotal
|
||||
|
||||
} else if (media.manga != null) {
|
||||
type = "MANGA"
|
||||
binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
|
||||
@@ -162,8 +214,10 @@ class MediaInfoFragment : Fragment() {
|
||||
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
binding.mediaInfoDescription.text =
|
||||
"\t\t\t" + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
|
||||
val infoDesc =
|
||||
tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
|
||||
binding.mediaInfoDescription.text = infoDesc
|
||||
|
||||
binding.mediaInfoDescription.setOnClickListener {
|
||||
if (binding.mediaInfoDescription.maxLines == 5) {
|
||||
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
|
||||
@@ -173,8 +227,7 @@ class MediaInfoFragment : Fragment() {
|
||||
.setDuration(400).start()
|
||||
}
|
||||
}
|
||||
|
||||
countDown(media, binding.mediaInfoContainer)
|
||||
displayTimer(media, binding.mediaInfoContainer)
|
||||
val parent = _binding?.mediaInfoContainer!!
|
||||
val screenWidth = resources.displayMetrics.run { widthPixels / density }
|
||||
|
||||
@@ -384,101 +437,157 @@ class MediaInfoFragment : Fragment() {
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.characters)
|
||||
bind.itemRecycler.adapter =
|
||||
CharacterAdapter(media.characters!!)
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
if (!media.relations.isNullOrEmpty() && !offline) {
|
||||
if (media.sequel != null || media.prequel != null) {
|
||||
val bind = ItemQuelsBinding.inflate(
|
||||
ItemQuelsBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
|
||||
if (media.sequel != null) {
|
||||
bind.mediaInfoSequel.visibility = View.VISIBLE
|
||||
bind.mediaInfoSequelImage.loadImage(
|
||||
media.sequel!!.banner ?: media.sequel!!.cover
|
||||
)
|
||||
bind.mediaInfoSequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.sequel as Serializable
|
||||
), null
|
||||
if (media.sequel != null) {
|
||||
mediaInfoSequel.visibility = View.VISIBLE
|
||||
mediaInfoSequelImage.loadImage(
|
||||
media.sequel!!.banner ?: media.sequel!!.cover
|
||||
)
|
||||
}
|
||||
}
|
||||
if (media.prequel != null) {
|
||||
bind.mediaInfoPrequel.visibility = View.VISIBLE
|
||||
bind.mediaInfoPrequelImage.loadImage(
|
||||
media.prequel!!.banner ?: media.prequel!!.cover
|
||||
)
|
||||
bind.mediaInfoPrequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
mediaInfoSequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.prequel as Serializable
|
||||
), null
|
||||
)
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.sequel as Serializable
|
||||
), null
|
||||
)
|
||||
}
|
||||
}
|
||||
if (media.prequel != null) {
|
||||
mediaInfoPrequel.visibility = View.VISIBLE
|
||||
mediaInfoPrequelImage.loadImage(
|
||||
media.prequel!!.banner ?: media.prequel!!.cover
|
||||
)
|
||||
mediaInfoPrequel.setSafeOnClickListener {
|
||||
ContextCompat.startActivity(
|
||||
requireContext(),
|
||||
Intent(
|
||||
requireContext(),
|
||||
MediaDetailsActivity::class.java
|
||||
).putExtra(
|
||||
"media",
|
||||
media.prequel as Serializable
|
||||
), null
|
||||
)
|
||||
}
|
||||
}
|
||||
parent.addView(root)
|
||||
}
|
||||
|
||||
ItemTitleSearchBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
|
||||
titleSearchImage.loadImage(media.banner ?: media.cover)
|
||||
titleSearchText.text =
|
||||
getString(R.string.search_title, media.mainName())
|
||||
titleSearchCard.setSafeOnClickListener {
|
||||
val query = Intent(requireContext(), SearchActivity::class.java)
|
||||
.putExtra("type", "ANIME")
|
||||
.putExtra("query", media.mainName())
|
||||
.putExtra("search", true)
|
||||
ContextCompat.startActivity(requireContext(), query, null)
|
||||
}
|
||||
|
||||
parent.addView(root)
|
||||
}
|
||||
parent.addView(bind.root)
|
||||
}
|
||||
|
||||
val bindi = ItemTitleRecyclerBinding.inflate(
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
).apply {
|
||||
|
||||
bindi.itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||
bindi.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(bindi.root)
|
||||
itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.relations!!, requireActivity())
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
|
||||
if (!media.recommendations.isNullOrEmpty() && !offline ) {
|
||||
val bind = ItemTitleRecyclerBinding.inflate(
|
||||
if (!media.characters.isNullOrEmpty() && !offline) {
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
bind.itemTitle.setText(R.string.recommended)
|
||||
bind.itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||
bind.itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
).apply {
|
||||
itemTitle.setText(R.string.characters)
|
||||
itemRecycler.adapter =
|
||||
CharacterAdapter(media.characters!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.staff.isNullOrEmpty() && !offline) {
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
parent.addView(bind.root)
|
||||
).apply {
|
||||
itemTitle.setText(R.string.staff)
|
||||
itemRecycler.adapter =
|
||||
AuthorAdapter(media.staff!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.recommendations.isNullOrEmpty() && !offline) {
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
itemTitle.setText(R.string.recommended)
|
||||
itemRecycler.adapter =
|
||||
MediaAdaptor(0, media.recommendations!!, requireActivity())
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
if (!media.users.isNullOrEmpty() && !offline) {
|
||||
ItemTitleRecyclerBinding.inflate(
|
||||
LayoutInflater.from(context),
|
||||
parent,
|
||||
false
|
||||
).apply {
|
||||
itemTitle.setText(R.string.social)
|
||||
itemRecycler.adapter =
|
||||
MediaSocialAdapter(media.users!!)
|
||||
itemRecycler.layoutManager = LinearLayoutManager(
|
||||
requireContext(),
|
||||
LinearLayoutManager.HORIZONTAL,
|
||||
false
|
||||
)
|
||||
parent.addView(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,11 +612,12 @@ class MediaInfoFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.onViewCreated(view, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
|
||||
binding.mediaInfoProgressBar.isGone = loaded
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter.LengthFilter
|
||||
import android.view.Gravity
|
||||
@@ -11,11 +10,18 @@ import android.widget.ArrayAdapter
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.DatePickerFragment
|
||||
import ani.dantotsu.InputFilterMinMax
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListBinding
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWith
|
||||
import com.google.android.material.materialswitch.MaterialSwitch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -36,7 +42,6 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
var media: Media?
|
||||
@@ -168,9 +173,10 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
val init =
|
||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||
.toInt() else 0
|
||||
if (init < (total
|
||||
?: 5000)
|
||||
) binding.mediaListProgress.setText((init + 1).toString())
|
||||
if (init < (total ?: 5000)) {
|
||||
val progressText = "${init + 1}"
|
||||
binding.mediaListProgress.setText(progressText)
|
||||
}
|
||||
if (init + 1 == (total ?: 5000)) {
|
||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||
onComplete()
|
||||
@@ -254,20 +260,28 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
binding.mediaListDelete.setOnClickListener {
|
||||
val id = media!!.userListId
|
||||
if (id != null) {
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Anilist.mutation.deleteList(id)
|
||||
var id = media!!.userListId
|
||||
scope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (id != null) {
|
||||
Anilist.mutation.deleteList(id!!)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
} else {
|
||||
val profile = Anilist.query.userMediaDetails(media!!)
|
||||
profile.userListId?.let { listId ->
|
||||
id = listId
|
||||
Anilist.mutation.deleteList(listId)
|
||||
MAL.query.deleteList(media?.anime != null, media?.idMAL)
|
||||
}
|
||||
}
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
snackString(getString(R.string.no_list_id))
|
||||
Refresh.all()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter.LengthFilter
|
||||
import android.view.Gravity
|
||||
@@ -10,11 +9,16 @@ import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.BottomSheetDialogFragment
|
||||
import ani.dantotsu.InputFilterMinMax
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
|
||||
import ani.dantotsu.navBarHeight
|
||||
import ani.dantotsu.others.getSerialized
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -54,10 +58,43 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
|
||||
val scope = viewLifecycleOwner.lifecycleScope
|
||||
binding.mediaListDelete.setOnClickListener {
|
||||
var id = media.userListId
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (id != null) {
|
||||
try {
|
||||
Anilist.mutation.deleteList(id!!)
|
||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
snackString(getString(R.string.delete_fail_reason, e.message))
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
} else {
|
||||
val profile = Anilist.query.userMediaDetails(media)
|
||||
profile.userListId?.let { listId ->
|
||||
id = listId
|
||||
Anilist.mutation.deleteList(listId)
|
||||
MAL.query.deleteList(media.anime != null, media.idMAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (id != null) {
|
||||
Refresh.all()
|
||||
snackString(getString(R.string.deleted_from_list))
|
||||
dismissAllowingStateLoss()
|
||||
} else {
|
||||
snackString(getString(R.string.no_list_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.mediaListProgressBar.visibility = View.GONE
|
||||
binding.mediaListLayout.visibility = View.VISIBLE
|
||||
@@ -120,7 +157,10 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
|
||||
val init =
|
||||
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
|
||||
.toInt() else 0
|
||||
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
|
||||
if (init < (total ?: 5000)) {
|
||||
val progressText = "${init + 1}"
|
||||
binding.mediaListProgress.setText(progressText)
|
||||
}
|
||||
if (init + 1 == (total ?: 5000)) {
|
||||
binding.mediaListStatus.setText(statusStrings[2], false)
|
||||
}
|
||||
|
||||
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
146
app/src/main/java/ani/dantotsu/media/MediaNameAdapter.kt
Normal file
@@ -0,0 +1,146 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import java.util.Locale
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object MediaNameAdapter {
|
||||
|
||||
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
|
||||
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
|
||||
private const val REGEX_EPISODE =
|
||||
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
|
||||
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
|
||||
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
|
||||
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
|
||||
|
||||
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
|
||||
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val soft = subdubMatcher.group(1)
|
||||
val subdub = subdubMatcher.group(2)
|
||||
val bed = subdubMatcher.group(3) ?: ""
|
||||
|
||||
val toggled = when (typeToSetTo) {
|
||||
SubDubType.SUB -> "sub"
|
||||
SubDubType.DUB -> "dub"
|
||||
SubDubType.NULL -> ""
|
||||
}
|
||||
val toggledCasePreserved =
|
||||
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
|
||||
?.isUpperCase() == true
|
||||
) toggled.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(
|
||||
Locale.ROOT
|
||||
) else it.toString()
|
||||
} else toggled
|
||||
|
||||
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun getSubDub(text: String): SubDubType {
|
||||
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
|
||||
val subdubMatcher: Matcher = subdubPattern.matcher(text)
|
||||
|
||||
return if (subdubMatcher.find()) {
|
||||
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
|
||||
when (subdub) {
|
||||
"sub" -> SubDubType.SUB
|
||||
"dub" -> SubDubType.DUB
|
||||
else -> SubDubType.NULL
|
||||
}
|
||||
} else {
|
||||
SubDubType.NULL
|
||||
}
|
||||
}
|
||||
|
||||
enum class SubDubType {
|
||||
SUB, DUB, NULL
|
||||
}
|
||||
|
||||
fun findSeasonNumber(text: String): Int? {
|
||||
val seasonPattern: Pattern = Pattern.compile(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
|
||||
val seasonMatcher: Matcher = seasonPattern.matcher(text)
|
||||
|
||||
return if (seasonMatcher.find()) {
|
||||
seasonMatcher.group(2)?.toInt()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun findEpisodeNumber(text: String): Float? {
|
||||
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, Pattern.CASE_INSENSITIVE)
|
||||
val episodeMatcher: Matcher = episodePattern.matcher(text)
|
||||
|
||||
return if (episodeMatcher.find()) {
|
||||
if (episodeMatcher.group(2) != null) {
|
||||
episodeMatcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedEpisodeNumberPattern: Pattern =
|
||||
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||
val failedEpisodeNumberMatcher: Matcher =
|
||||
failedEpisodeNumberPattern.matcher(text)
|
||||
if (failedEpisodeNumberMatcher.find()) {
|
||||
failedEpisodeNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun removeEpisodeNumber(text: String): String {
|
||||
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "").ifEmpty {
|
||||
text
|
||||
}
|
||||
val letterPattern = Regex("[a-zA-Z]")
|
||||
return if (letterPattern.containsMatchIn(removedNumber)) {
|
||||
removedNumber
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeEpisodeNumberCompletely(text: String): String {
|
||||
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
|
||||
val removedNumber = text.replace(regexPattern, "")
|
||||
return if (removedNumber.equals(text, true)) { // if nothing was removed
|
||||
val failedEpisodeNumberPattern =
|
||||
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
|
||||
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
|
||||
mr.value.replaceFirst(mr.groupValues[1], "")
|
||||
}
|
||||
} else {
|
||||
removedNumber
|
||||
}
|
||||
}
|
||||
|
||||
fun findChapterNumber(text: String): Float? {
|
||||
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
|
||||
val matcher: Matcher = pattern.matcher(text)
|
||||
|
||||
return if (matcher.find()) {
|
||||
matcher.group(2)?.toFloat()
|
||||
} else {
|
||||
val failedChapterNumberPattern: Pattern =
|
||||
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
|
||||
val failedChapterNumberMatcher: Matcher =
|
||||
failedChapterNumberPattern.matcher(text)
|
||||
if (failedChapterNumberMatcher.find()) {
|
||||
failedChapterNumberMatcher.group(1)?.toFloat()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
70
app/src/main/java/ani/dantotsu/media/MediaSocialAdapter.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.databinding.ItemFollowerGridBinding
|
||||
import ani.dantotsu.loadImage
|
||||
import ani.dantotsu.profile.ProfileActivity
|
||||
import ani.dantotsu.profile.User
|
||||
import ani.dantotsu.setAnimation
|
||||
|
||||
class MediaSocialAdapter(private val user: ArrayList<User>) :
|
||||
RecyclerView.Adapter<MediaSocialAdapter.DeveloperViewHolder>() {
|
||||
|
||||
inner class DeveloperViewHolder(val binding: ItemFollowerGridBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder {
|
||||
return DeveloperViewHolder(
|
||||
ItemFollowerGridBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onBindViewHolder(holder: DeveloperViewHolder, position: Int) {
|
||||
holder.binding.apply {
|
||||
val user = user[position]
|
||||
val score = user.score?.div(10.0) ?: 0.0
|
||||
setAnimation(root.context, root)
|
||||
profileUserName.text = user.name
|
||||
profileInfo.apply {
|
||||
text = when (user.status) {
|
||||
"CURRENT" -> "WATCHING"
|
||||
else -> user.status ?: ""
|
||||
}
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
profileCompactUserProgress.text = user.progress.toString()
|
||||
profileCompactScore.text = score.toString()
|
||||
profileCompactTotal.text = " | ${user.totalEpisodes ?: "~"}"
|
||||
profileUserAvatar.loadImage(user.pfp)
|
||||
|
||||
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
|
||||
profileCompactScoreBG.apply {
|
||||
visibility = View.VISIBLE
|
||||
background = ContextCompat.getDrawable(root.context, scoreDrawable)
|
||||
}
|
||||
|
||||
profileCompactProgressContainer.visibility = View.VISIBLE
|
||||
|
||||
profileUserAvatar.setOnClickListener {
|
||||
val intent = Intent(root.context, ProfileActivity::class.java).apply {
|
||||
putExtra("userId", user.id)
|
||||
}
|
||||
ContextCompat.startActivity(root.context, intent, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = user.size
|
||||
}
|
||||
56
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
56
app/src/main/java/ani/dantotsu/media/MediaType.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
package ani.dantotsu.media
|
||||
|
||||
interface Type {
|
||||
fun asText(): String
|
||||
}
|
||||
|
||||
enum class MediaType : Type {
|
||||
ANIME,
|
||||
MANGA,
|
||||
NOVEL;
|
||||
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
ANIME -> "Anime"
|
||||
MANGA -> "Manga"
|
||||
NOVEL -> "Novel"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string: String): MediaType? {
|
||||
return when (string) {
|
||||
"Anime" -> ANIME
|
||||
"Manga" -> MANGA
|
||||
"Novel" -> NOVEL
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class AddonType : Type {
|
||||
TORRENT,
|
||||
DOWNLOAD;
|
||||
|
||||
override fun asText(): String {
|
||||
return when (this) {
|
||||
TORRENT -> "Torrent"
|
||||
DOWNLOAD -> "Download"
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromText(string: String): AddonType? {
|
||||
return when (string) {
|
||||
"Torrent" -> TORRENT
|
||||
"Download" -> DOWNLOAD
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user