Compare commits

...

2049 Commits

Author SHA1 Message Date
rebel onion
53807ff68d New translations strings.xml (Portuguese, Brazilian) 2025-03-20 08:29:47 -05:00
rebel onion
902bf96a29 New translations strings.xml (Assamese) 2025-03-07 08:30:44 -06:00
rebel onion
91743dc0b7 New translations strings.xml (Assamese) 2025-03-07 07:08:23 -06:00
rebel onion
384f8603b5 New translations strings.xml (Greek) 2025-01-19 10:46:15 -06:00
rebel onion
6c4edda017 New translations strings.xml (Greek) 2025-01-19 09:50:01 -06:00
rebel onion
2f4f7345e4 New translations strings.xml (Portuguese, Brazilian) 2025-01-06 12:17:24 -06:00
rebel onion
8847d37a7d New translations strings.xml (Portuguese, Brazilian) 2025-01-06 10:50:53 -06:00
rebel onion
4e1f82aada New translations strings.xml (Portuguese, Brazilian) 2025-01-06 07:35:08 -06:00
rebel onion
2395a57271 New translations strings.xml (Assamese) 2025-01-04 05:54:27 -06:00
rebel onion
43cbf35761 New translations strings.xml (Hindi) 2025-01-04 05:54:26 -06:00
rebel onion
d095f87a1a New translations strings.xml (Bengali) 2025-01-04 05:54:25 -06:00
rebel onion
6be5fbd0c8 New translations strings.xml (Spanish, Mexico) 2025-01-04 05:54:23 -06:00
rebel onion
5450d331b8 New translations strings.xml (Portuguese, Brazilian) 2025-01-04 05:54:22 -06:00
rebel onion
d53cdce2e2 New translations strings.xml (Urdu (Pakistan)) 2025-01-04 05:54:21 -06:00
rebel onion
bb8d03f776 New translations strings.xml (Chinese Simplified) 2025-01-04 05:54:20 -06:00
rebel onion
df65eec0d3 New translations strings.xml (Turkish) 2025-01-04 05:54:19 -06:00
rebel onion
b112db7803 New translations strings.xml (Albanian) 2025-01-04 05:54:18 -06:00
rebel onion
178d7f7820 New translations strings.xml (Russian) 2025-01-04 05:54:17 -06:00
rebel onion
afaa162907 New translations strings.xml (Polish) 2025-01-04 05:54:16 -06:00
rebel onion
1c4a7ff8af New translations strings.xml (Dutch) 2025-01-04 05:54:15 -06:00
rebel onion
19c14d81c3 New translations strings.xml (Korean) 2025-01-04 05:54:13 -06:00
rebel onion
a794550969 New translations strings.xml (Japanese) 2025-01-04 05:54:12 -06:00
rebel onion
dfa55741cf New translations strings.xml (Italian) 2025-01-04 05:54:11 -06:00
rebel onion
2a32c92cfc New translations strings.xml (Greek) 2025-01-04 05:54:10 -06:00
rebel onion
eaaf0f355a New translations strings.xml (German) 2025-01-04 05:54:09 -06:00
rebel onion
7262795cb0 New translations strings.xml (Arabic) 2025-01-04 05:54:08 -06:00
rebel onion
4c7a644e0a New translations strings.xml (Spanish) 2025-01-04 05:54:06 -06:00
rebel onion
b802338fdf New translations strings.xml (French) 2025-01-04 05:54:05 -06:00
rebel onion
d4346d292a New translations strings.xml (Assamese) 2025-01-03 09:14:18 -06:00
rebel onion
ba3e5b8ae2 New translations strings.xml (Hindi) 2025-01-03 09:14:16 -06:00
rebel onion
08b9820ff6 New translations strings.xml (Bengali) 2025-01-03 09:14:15 -06:00
rebel onion
65cd62a94a New translations strings.xml (Spanish, Mexico) 2025-01-03 09:14:13 -06:00
rebel onion
0154eeace4 New translations strings.xml (Portuguese, Brazilian) 2025-01-03 09:14:11 -06:00
rebel onion
bb8db3cc67 New translations strings.xml (Urdu (Pakistan)) 2025-01-03 09:14:06 -06:00
rebel onion
b03908ca5c New translations strings.xml (Chinese Simplified) 2025-01-03 09:14:03 -06:00
rebel onion
67b8e51fdb New translations strings.xml (Turkish) 2025-01-03 09:14:01 -06:00
rebel onion
3bf67b1997 New translations strings.xml (Albanian) 2025-01-03 09:14:00 -06:00
rebel onion
486b3fbaa9 New translations strings.xml (Russian) 2025-01-03 09:13:59 -06:00
rebel onion
7d5264760d New translations strings.xml (Polish) 2025-01-03 09:13:58 -06:00
rebel onion
2707e461e7 New translations strings.xml (Dutch) 2025-01-03 09:13:56 -06:00
rebel onion
d82f4b633a New translations strings.xml (Korean) 2025-01-03 09:13:55 -06:00
rebel onion
17ab29dc03 New translations strings.xml (Japanese) 2025-01-03 09:13:54 -06:00
rebel onion
b66e625a53 New translations strings.xml (Italian) 2025-01-03 09:13:52 -06:00
rebel onion
b54384fe33 New translations strings.xml (Greek) 2025-01-03 09:13:50 -06:00
rebel onion
edfa6f0ed0 New translations strings.xml (German) 2025-01-03 09:13:47 -06:00
rebel onion
53e5f86ae2 New translations strings.xml (Arabic) 2025-01-03 09:13:46 -06:00
rebel onion
a6531ac9cc New translations strings.xml (Spanish) 2025-01-03 09:13:44 -06:00
rebel onion
7b9e4d4870 New translations strings.xml (French) 2025-01-03 09:13:39 -06:00
rebel onion
aa2e0cd5e0 New translations strings.xml (Assamese) 2025-01-02 03:17:52 -06:00
rebel onion
4d0e777401 New translations strings.xml (Hindi) 2025-01-02 03:17:50 -06:00
rebel onion
9f6882888f New translations strings.xml (Bengali) 2025-01-02 03:17:49 -06:00
rebel onion
2044681859 New translations strings.xml (Spanish, Mexico) 2025-01-02 03:17:48 -06:00
rebel onion
7b17f9a29c New translations strings.xml (Portuguese, Brazilian) 2025-01-02 03:17:47 -06:00
rebel onion
46e094bdf8 New translations strings.xml (Urdu (Pakistan)) 2025-01-02 03:17:46 -06:00
rebel onion
a60e371b0f New translations strings.xml (Chinese Simplified) 2025-01-02 03:17:44 -06:00
rebel onion
aa5f94cd14 New translations strings.xml (Turkish) 2025-01-02 03:17:43 -06:00
rebel onion
baf6f59f8a New translations strings.xml (Albanian) 2025-01-02 03:17:42 -06:00
rebel onion
fa0c69aecf New translations strings.xml (Russian) 2025-01-02 03:17:41 -06:00
rebel onion
ebd1a22c75 New translations strings.xml (Polish) 2025-01-02 03:17:39 -06:00
rebel onion
16cce05bbd New translations strings.xml (Dutch) 2025-01-02 03:17:38 -06:00
rebel onion
0daf2cf26f New translations strings.xml (Korean) 2025-01-02 03:17:37 -06:00
rebel onion
8bbfa6166b New translations strings.xml (Japanese) 2025-01-02 03:17:35 -06:00
rebel onion
dbc3884b1c New translations strings.xml (Italian) 2025-01-02 03:17:34 -06:00
rebel onion
f2b5b5ef62 New translations strings.xml (Greek) 2025-01-02 03:17:33 -06:00
rebel onion
a5c57ab6c2 New translations strings.xml (German) 2025-01-02 03:17:32 -06:00
rebel onion
6ac7978d7c New translations strings.xml (Arabic) 2025-01-02 03:17:31 -06:00
rebel onion
b26c764999 New translations strings.xml (Spanish) 2025-01-02 03:17:30 -06:00
rebel onion
de3012692f New translations strings.xml (French) 2025-01-02 03:17:28 -06:00
rebel onion
0ca0474920 New translations strings.xml (Assamese) 2024-12-31 09:15:54 -06:00
rebel onion
8328759f95 New translations strings.xml (Arabic) 2024-12-31 09:15:53 -06:00
rebel onion
393afa4159 New translations strings.xml (Arabic) 2024-12-31 07:59:29 -06:00
rebel onion
a25f701e8f New translations strings.xml (Assamese) 2024-12-30 21:39:07 -06:00
rebel onion
903a77bd6d New translations strings.xml (Hindi) 2024-12-30 21:39:05 -06:00
rebel onion
353f7023a8 New translations strings.xml (Bengali) 2024-12-30 21:39:04 -06:00
rebel onion
4b8d96849f New translations strings.xml (Spanish, Mexico) 2024-12-30 21:39:03 -06:00
rebel onion
090fcf8e10 New translations strings.xml (Portuguese, Brazilian) 2024-12-30 21:39:01 -06:00
rebel onion
8df7cb9dd2 New translations strings.xml (Urdu (Pakistan)) 2024-12-30 21:39:00 -06:00
rebel onion
c6188c45dc New translations strings.xml (Chinese Simplified) 2024-12-30 21:38:59 -06:00
rebel onion
25b5ea1474 New translations strings.xml (Turkish) 2024-12-30 21:38:56 -06:00
rebel onion
35bdfe3999 New translations strings.xml (Albanian) 2024-12-30 21:38:55 -06:00
rebel onion
bd70ee1031 New translations strings.xml (Russian) 2024-12-30 21:38:53 -06:00
rebel onion
640fff73f5 New translations strings.xml (Polish) 2024-12-30 21:38:52 -06:00
rebel onion
a271c5740f New translations strings.xml (Dutch) 2024-12-30 21:38:51 -06:00
rebel onion
31b8d7c7f9 New translations strings.xml (Korean) 2024-12-30 21:38:50 -06:00
rebel onion
983afff3f7 New translations strings.xml (Japanese) 2024-12-30 21:38:49 -06:00
rebel onion
e89c180f09 New translations strings.xml (Italian) 2024-12-30 21:38:47 -06:00
rebel onion
aac0246fd6 New translations strings.xml (Greek) 2024-12-30 21:38:46 -06:00
rebel onion
aa10bd8000 New translations strings.xml (German) 2024-12-30 21:38:45 -06:00
rebel onion
ebd0cca6b6 New translations strings.xml (Arabic) 2024-12-30 21:38:44 -06:00
rebel onion
4b21582059 New translations strings.xml (Spanish) 2024-12-30 21:38:43 -06:00
rebel onion
1ca92afb65 New translations strings.xml (French) 2024-12-30 21:38:42 -06:00
rebel onion
49d92e1867 New translations strings.xml (Assamese) 2024-12-30 19:40:50 -06:00
rebel onion
120a91b591 New translations strings.xml (Hindi) 2024-12-30 19:40:48 -06:00
rebel onion
bf74fbfd15 New translations strings.xml (Bengali) 2024-12-30 19:40:47 -06:00
rebel onion
a564a2f48b New translations strings.xml (Spanish, Mexico) 2024-12-30 19:40:46 -06:00
rebel onion
2687769c64 New translations strings.xml (Portuguese, Brazilian) 2024-12-30 19:40:45 -06:00
rebel onion
a17991be84 New translations strings.xml (Urdu (Pakistan)) 2024-12-30 19:40:44 -06:00
rebel onion
700cdf4a93 New translations strings.xml (Chinese Simplified) 2024-12-30 19:40:43 -06:00
rebel onion
773c1d17c4 New translations strings.xml (Turkish) 2024-12-30 19:40:41 -06:00
rebel onion
3222931be6 New translations strings.xml (Albanian) 2024-12-30 19:40:40 -06:00
rebel onion
fd63096466 New translations strings.xml (Russian) 2024-12-30 19:40:39 -06:00
rebel onion
f4a799d514 New translations strings.xml (Polish) 2024-12-30 19:40:38 -06:00
rebel onion
9f65441677 New translations strings.xml (Dutch) 2024-12-30 19:40:37 -06:00
rebel onion
ceb10ba179 New translations strings.xml (Korean) 2024-12-30 19:40:35 -06:00
rebel onion
17547ad7b6 New translations strings.xml (Japanese) 2024-12-30 19:40:34 -06:00
rebel onion
02a6c85575 New translations strings.xml (Italian) 2024-12-30 19:40:33 -06:00
rebel onion
963ab68f83 New translations strings.xml (Greek) 2024-12-30 19:40:32 -06:00
rebel onion
e85425ede8 New translations strings.xml (German) 2024-12-30 19:40:30 -06:00
rebel onion
7835a90aed New translations strings.xml (Arabic) 2024-12-30 19:40:29 -06:00
rebel onion
2ac94487ac New translations strings.xml (Spanish) 2024-12-30 19:40:28 -06:00
rebel onion
7ec12e5ee1 New translations strings.xml (French) 2024-12-30 19:40:26 -06:00
rebel onion
840c5635d2 New translations strings.xml (Assamese) 2024-12-18 22:49:25 -06:00
rebel onion
bc7c46b3e8 New translations strings.xml (Hindi) 2024-12-18 22:49:23 -06:00
rebel onion
2136181d2c New translations strings.xml (Bengali) 2024-12-18 22:49:22 -06:00
rebel onion
783eb588b1 New translations strings.xml (Spanish, Mexico) 2024-12-18 22:49:21 -06:00
rebel onion
9345295270 New translations strings.xml (Portuguese, Brazilian) 2024-12-18 22:49:19 -06:00
rebel onion
e257a8ca99 New translations strings.xml (Urdu (Pakistan)) 2024-12-18 22:49:18 -06:00
rebel onion
c062780519 New translations strings.xml (Chinese Simplified) 2024-12-18 22:49:17 -06:00
rebel onion
8fa3a995db New translations strings.xml (Turkish) 2024-12-18 22:49:15 -06:00
rebel onion
7e0ade4a38 New translations strings.xml (Albanian) 2024-12-18 22:49:14 -06:00
rebel onion
e0fb5530f6 New translations strings.xml (Russian) 2024-12-18 22:49:13 -06:00
rebel onion
d94e8be220 New translations strings.xml (Polish) 2024-12-18 22:49:12 -06:00
rebel onion
472e4a788c New translations strings.xml (Dutch) 2024-12-18 22:49:11 -06:00
rebel onion
300d0600af New translations strings.xml (Korean) 2024-12-18 22:49:09 -06:00
rebel onion
87ae95dffc New translations strings.xml (Japanese) 2024-12-18 22:49:08 -06:00
rebel onion
4873b8b6ee New translations strings.xml (Italian) 2024-12-18 22:49:07 -06:00
rebel onion
26ed3bacc5 New translations strings.xml (Greek) 2024-12-18 22:49:06 -06:00
rebel onion
5fbf9fdb26 New translations strings.xml (German) 2024-12-18 22:49:04 -06:00
rebel onion
f0bcbb5fee New translations strings.xml (Arabic) 2024-12-18 22:49:03 -06:00
rebel onion
b494b60251 New translations strings.xml (Spanish) 2024-12-18 22:49:02 -06:00
rebel onion
e621306f6d New translations strings.xml (French) 2024-12-18 22:48:58 -06:00
rebel onion
94d69d530e New translations strings.xml (Assamese) 2024-12-15 09:25:44 -06:00
rebel onion
5fb343972a New translations strings.xml (Hindi) 2024-12-15 09:25:43 -06:00
rebel onion
edbe16959d New translations strings.xml (Bengali) 2024-12-15 09:25:42 -06:00
rebel onion
8d77450384 New translations strings.xml (Spanish, Mexico) 2024-12-15 09:25:40 -06:00
rebel onion
6a69567cdc New translations strings.xml (Portuguese, Brazilian) 2024-12-15 09:25:39 -06:00
rebel onion
645972c451 New translations strings.xml (Urdu (Pakistan)) 2024-12-15 09:25:38 -06:00
rebel onion
114504cfa2 New translations strings.xml (Chinese Simplified) 2024-12-15 09:25:37 -06:00
rebel onion
0a1d9090c0 New translations strings.xml (Turkish) 2024-12-15 09:25:35 -06:00
rebel onion
12cb670af4 New translations strings.xml (Albanian) 2024-12-15 09:25:34 -06:00
rebel onion
3ff66d7792 New translations strings.xml (Russian) 2024-12-15 09:25:33 -06:00
rebel onion
b34d6c1cb0 New translations strings.xml (Polish) 2024-12-15 09:25:32 -06:00
rebel onion
1fe5e5f0c0 New translations strings.xml (Dutch) 2024-12-15 09:25:31 -06:00
rebel onion
58251d19ef New translations strings.xml (Korean) 2024-12-15 09:25:30 -06:00
rebel onion
1715cebaea New translations strings.xml (Japanese) 2024-12-15 09:25:29 -06:00
rebel onion
ba6c162e1f New translations strings.xml (Italian) 2024-12-15 09:25:28 -06:00
rebel onion
8137ef0acc New translations strings.xml (Greek) 2024-12-15 09:25:26 -06:00
rebel onion
836cdde7fb New translations strings.xml (German) 2024-12-15 09:25:25 -06:00
rebel onion
dc3d48edd6 New translations strings.xml (Arabic) 2024-12-15 09:25:24 -06:00
rebel onion
2dffa1c4ac New translations strings.xml (Spanish) 2024-12-15 09:25:23 -06:00
rebel onion
f829fd29e0 New translations strings.xml (French) 2024-12-15 09:25:21 -06:00
rebel onion
d47e0bf6af New translations strings.xml (Portuguese, Brazilian) 2024-11-30 11:53:04 -06:00
rebel onion
cb495dffbd New translations strings.xml (Portuguese, Brazilian) 2024-11-30 10:50:37 -06:00
rebel onion
3d109b1ff0 New translations strings.xml (Portuguese, Brazilian) 2024-11-30 09:38:57 -06:00
rebel onion
b8d6b097fe New translations strings.xml (Portuguese, Brazilian) 2024-11-30 08:16:50 -06:00
rebel onion
999b6f8f4c New translations strings.xml (Portuguese, Brazilian) 2024-11-30 07:12:23 -06:00
rebel onion
71d85838c7 New translations strings.xml (Portuguese, Brazilian) 2024-11-30 00:30:25 -06:00
rebel onion
9d509c581f New translations strings.xml (Portuguese, Brazilian) 2024-11-29 22:45:22 -06:00
rebel onion
7c8b8b17a2 New translations strings.xml (Portuguese, Brazilian) 2024-11-29 21:07:51 -06:00
rebel onion
126ed7a2fb New translations strings.xml (Assamese) 2024-11-22 12:53:03 -06:00
rebel onion
93ac9b1e81 New translations strings.xml (Hindi) 2024-11-22 12:53:01 -06:00
rebel onion
840190bd1d New translations strings.xml (Bengali) 2024-11-22 12:53:00 -06:00
rebel onion
113e156900 New translations strings.xml (Spanish, Mexico) 2024-11-22 12:52:59 -06:00
rebel onion
e1a07c7c3b New translations strings.xml (Urdu (Pakistan)) 2024-11-22 12:52:58 -06:00
rebel onion
c1e657d63a New translations strings.xml (Chinese Simplified) 2024-11-22 12:52:56 -06:00
rebel onion
019b4e3389 New translations strings.xml (Turkish) 2024-11-22 12:52:55 -06:00
rebel onion
44b904ac81 New translations strings.xml (Albanian) 2024-11-22 12:52:54 -06:00
rebel onion
22223af035 New translations strings.xml (Russian) 2024-11-22 12:52:53 -06:00
rebel onion
90f7256edc New translations strings.xml (Polish) 2024-11-22 12:52:51 -06:00
rebel onion
927435a1c6 New translations strings.xml (Dutch) 2024-11-22 12:52:50 -06:00
rebel onion
47b9e8cc3b New translations strings.xml (Korean) 2024-11-22 12:52:49 -06:00
rebel onion
f5a03146ea New translations strings.xml (Japanese) 2024-11-22 12:52:48 -06:00
rebel onion
2c8bff7b78 New translations strings.xml (Italian) 2024-11-22 12:52:46 -06:00
rebel onion
00084bd87d New translations strings.xml (Greek) 2024-11-22 12:52:45 -06:00
rebel onion
b46de0bd62 New translations strings.xml (German) 2024-11-22 12:52:44 -06:00
rebel onion
d317f9bf36 New translations strings.xml (Portuguese, Brazilian) 2024-11-22 12:52:43 -06:00
rebel onion
6226f933ff New translations strings.xml (Arabic) 2024-11-22 12:52:41 -06:00
rebel onion
bbc0d97d2a New translations strings.xml (Spanish) 2024-11-22 12:52:40 -06:00
rebel onion
7fa74c755b New translations strings.xml (French) 2024-11-22 12:52:39 -06:00
rebel onion
9d0708f2ff New translations strings.xml (Portuguese, Brazilian) 2024-11-22 11:18:29 -06:00
rebel onion
ab9ad65e5b New translations strings.xml (Portuguese, Brazilian) 2024-11-22 09:58:54 -06:00
rebel onion
81bb2586ea New translations strings.xml (Assamese) 2024-11-20 12:02:44 -06:00
rebel onion
51c9d1c3b9 New translations strings.xml (Hindi) 2024-11-20 12:02:42 -06:00
rebel onion
4e568a1d82 New translations strings.xml (Bengali) 2024-11-20 12:02:41 -06:00
rebel onion
f892fab256 New translations strings.xml (Spanish, Mexico) 2024-11-20 12:02:39 -06:00
rebel onion
f8e98da62f New translations strings.xml (Urdu (Pakistan)) 2024-11-20 12:02:37 -06:00
rebel onion
18664fb239 New translations strings.xml (Chinese Simplified) 2024-11-20 12:02:34 -06:00
rebel onion
7f2edc4b63 New translations strings.xml (Turkish) 2024-11-20 12:02:32 -06:00
rebel onion
96fa35f0a9 New translations strings.xml (Albanian) 2024-11-20 12:02:31 -06:00
rebel onion
916c585c38 New translations strings.xml (Russian) 2024-11-20 12:02:29 -06:00
rebel onion
e13f001379 New translations strings.xml (Polish) 2024-11-20 12:02:28 -06:00
rebel onion
f4e5bf8927 New translations strings.xml (Dutch) 2024-11-20 12:02:26 -06:00
rebel onion
327b843f8a New translations strings.xml (Korean) 2024-11-20 12:02:24 -06:00
rebel onion
cab38e8bff New translations strings.xml (Japanese) 2024-11-20 12:02:23 -06:00
rebel onion
bcdf515f95 New translations strings.xml (Italian) 2024-11-20 12:02:21 -06:00
rebel onion
2f1dbceb6c New translations strings.xml (Greek) 2024-11-20 12:02:20 -06:00
rebel onion
652bb76bdf New translations strings.xml (German) 2024-11-20 12:02:18 -06:00
rebel onion
ee9c9e0134 New translations strings.xml (Arabic) 2024-11-20 12:02:17 -06:00
rebel onion
3763363199 New translations strings.xml (Spanish) 2024-11-20 12:02:15 -06:00
rebel onion
9197f163e3 New translations strings.xml (French) 2024-11-20 12:02:14 -06:00
rebel onion
08c8e83d15 New translations strings.xml (Portuguese, Brazilian) 2024-11-20 12:02:13 -06:00
rebel onion
7fab2e8cb5 New translations strings.xml (Assamese) 2024-11-19 11:46:16 -06:00
rebel onion
4f0056e0dc New translations strings.xml (Hindi) 2024-11-19 11:46:14 -06:00
rebel onion
5aa78c739f New translations strings.xml (Bengali) 2024-11-19 11:46:13 -06:00
rebel onion
ac747dfcc1 New translations strings.xml (Spanish, Mexico) 2024-11-19 11:46:11 -06:00
rebel onion
a7a07de396 New translations strings.xml (Urdu (Pakistan)) 2024-11-19 11:46:10 -06:00
rebel onion
526670ff6d New translations strings.xml (Chinese Simplified) 2024-11-19 11:46:09 -06:00
rebel onion
60b6254167 New translations strings.xml (Turkish) 2024-11-19 11:46:08 -06:00
rebel onion
8329ed0ce8 New translations strings.xml (Albanian) 2024-11-19 11:46:07 -06:00
rebel onion
952beb75b3 New translations strings.xml (Russian) 2024-11-19 11:46:05 -06:00
rebel onion
e2729c3274 New translations strings.xml (Polish) 2024-11-19 11:46:04 -06:00
rebel onion
1e2dac074c New translations strings.xml (Dutch) 2024-11-19 11:46:02 -06:00
rebel onion
ee59b5aea1 New translations strings.xml (Korean) 2024-11-19 11:46:01 -06:00
rebel onion
f3ba74f46a New translations strings.xml (Japanese) 2024-11-19 11:46:00 -06:00
rebel onion
f74c32e4b8 New translations strings.xml (Italian) 2024-11-19 11:45:59 -06:00
rebel onion
d1860831e3 New translations strings.xml (Greek) 2024-11-19 11:45:57 -06:00
rebel onion
c89ca91f4a New translations strings.xml (German) 2024-11-19 11:45:56 -06:00
rebel onion
50f375f187 New translations strings.xml (Arabic) 2024-11-19 11:45:54 -06:00
rebel onion
2cb26442d3 New translations strings.xml (Spanish) 2024-11-19 11:45:53 -06:00
rebel onion
07f161baf9 New translations strings.xml (French) 2024-11-19 10:31:13 -06:00
rebel onion
b2ceaa01fa New translations strings.xml (Portuguese, Brazilian) 2024-11-19 10:31:02 -06:00
rebel onion
dfbf849691 New translations strings.xml (Assamese) 2024-11-18 12:31:57 -06:00
rebel onion
ddc8cbf6fa New translations strings.xml (Hindi) 2024-11-18 12:31:56 -06:00
rebel onion
bc2e05c75b New translations strings.xml (Bengali) 2024-11-18 12:31:54 -06:00
rebel onion
2bd1f9affb New translations strings.xml (Spanish, Mexico) 2024-11-18 12:31:53 -06:00
rebel onion
b376ab2f5d New translations strings.xml (Urdu (Pakistan)) 2024-11-18 12:31:52 -06:00
rebel onion
494280b4aa New translations strings.xml (Chinese Simplified) 2024-11-18 12:31:50 -06:00
rebel onion
93154eef9e New translations strings.xml (Turkish) 2024-11-18 12:31:49 -06:00
rebel onion
007bbd3154 New translations strings.xml (Albanian) 2024-11-18 12:31:48 -06:00
rebel onion
a54af8d312 New translations strings.xml (Russian) 2024-11-18 12:31:46 -06:00
rebel onion
294b6b0aa6 New translations strings.xml (Polish) 2024-11-18 12:31:44 -06:00
rebel onion
713fa77a0b New translations strings.xml (Dutch) 2024-11-18 12:31:43 -06:00
rebel onion
8004a5e194 New translations strings.xml (Korean) 2024-11-18 12:31:41 -06:00
rebel onion
ac470df370 New translations strings.xml (Japanese) 2024-11-18 12:31:40 -06:00
rebel onion
0eef242112 New translations strings.xml (Italian) 2024-11-18 12:31:39 -06:00
rebel onion
286db2e1a9 New translations strings.xml (Greek) 2024-11-18 12:31:37 -06:00
rebel onion
aeb00cc790 New translations strings.xml (German) 2024-11-18 12:31:36 -06:00
rebel onion
555554d3fd New translations strings.xml (Arabic) 2024-11-18 12:31:34 -06:00
rebel onion
e148ac132d New translations strings.xml (Spanish) 2024-11-18 12:31:33 -06:00
rebel onion
ae2f19709a New translations strings.xml (French) 2024-11-18 12:31:31 -06:00
rebel onion
f2a0897f1e New translations strings.xml (Portuguese, Brazilian) 2024-11-18 12:31:30 -06:00
rebel onion
97eb752000 New translations strings.xml (Assamese) 2024-11-17 01:12:47 -06:00
rebel onion
a92d41398d New translations strings.xml (Hindi) 2024-11-17 01:12:45 -06:00
rebel onion
4c360d306a New translations strings.xml (Bengali) 2024-11-17 01:12:44 -06:00
rebel onion
aaa4751c17 New translations strings.xml (Spanish, Mexico) 2024-11-17 01:12:43 -06:00
rebel onion
20498bc429 New translations strings.xml (Urdu (Pakistan)) 2024-11-17 01:12:41 -06:00
rebel onion
c4062af91a New translations strings.xml (Chinese Simplified) 2024-11-17 01:12:40 -06:00
rebel onion
2d7fb67ad5 New translations strings.xml (Turkish) 2024-11-17 01:12:38 -06:00
rebel onion
8eae6906d8 New translations strings.xml (Albanian) 2024-11-17 01:12:37 -06:00
rebel onion
54d60c9603 New translations strings.xml (Russian) 2024-11-17 01:12:36 -06:00
rebel onion
a577970705 New translations strings.xml (Polish) 2024-11-17 01:12:35 -06:00
rebel onion
2e97a62365 New translations strings.xml (Dutch) 2024-11-17 01:12:33 -06:00
rebel onion
abe4a1e394 New translations strings.xml (Korean) 2024-11-17 01:12:32 -06:00
rebel onion
b42ec620df New translations strings.xml (Japanese) 2024-11-17 01:12:31 -06:00
rebel onion
58a68271fc New translations strings.xml (Italian) 2024-11-17 01:12:30 -06:00
rebel onion
c58eb14c59 New translations strings.xml (Greek) 2024-11-17 01:12:28 -06:00
rebel onion
7726abcf00 New translations strings.xml (German) 2024-11-17 01:12:27 -06:00
rebel onion
93f9547e3e New translations strings.xml (Arabic) 2024-11-17 01:12:25 -06:00
rebel onion
4f72028284 New translations strings.xml (Spanish) 2024-11-17 01:12:24 -06:00
rebel onion
caa4ff6d7a New translations strings.xml (French) 2024-11-17 01:12:22 -06:00
rebel onion
92655c62c4 New translations strings.xml (Portuguese, Brazilian) 2024-11-17 01:12:21 -06:00
rebel onion
671565b80c New translations strings.xml (Portuguese, Brazilian) 2024-11-11 09:25:33 -06:00
rebel onion
020e0e385e New translations strings.xml (Greek) 2024-11-09 03:57:22 -06:00
rebel onion
aa271aa26d New translations strings.xml (Portuguese, Brazilian) 2024-11-07 14:01:05 -06:00
rebel onion
ccae92a605 New translations strings.xml (Assamese) 2024-11-07 11:28:50 -06:00
rebel onion
ca3057326d New translations strings.xml (Hindi) 2024-11-07 11:28:48 -06:00
rebel onion
417acee7da New translations strings.xml (Bengali) 2024-11-07 11:28:47 -06:00
rebel onion
b95da94f7f New translations strings.xml (Spanish, Mexico) 2024-11-07 11:28:46 -06:00
rebel onion
2cbb78f804 New translations strings.xml (Urdu (Pakistan)) 2024-11-07 11:28:45 -06:00
rebel onion
a2903ec1bf New translations strings.xml (Chinese Simplified) 2024-11-07 11:28:43 -06:00
rebel onion
48fae1cc73 New translations strings.xml (Turkish) 2024-11-07 11:28:42 -06:00
rebel onion
cc5faa8f9a New translations strings.xml (Albanian) 2024-11-07 11:28:41 -06:00
rebel onion
5dfa5001fa New translations strings.xml (Russian) 2024-11-07 11:28:39 -06:00
rebel onion
4859fa2532 New translations strings.xml (Polish) 2024-11-07 11:28:38 -06:00
rebel onion
d6b3cd3e4f New translations strings.xml (Dutch) 2024-11-07 11:28:37 -06:00
rebel onion
9ab29af5af New translations strings.xml (Korean) 2024-11-07 11:28:35 -06:00
rebel onion
a4a82d6a56 New translations strings.xml (Japanese) 2024-11-07 11:28:34 -06:00
rebel onion
2aa742aaf0 New translations strings.xml (Italian) 2024-11-07 11:28:33 -06:00
rebel onion
1e951baeb4 New translations strings.xml (Greek) 2024-11-07 11:28:32 -06:00
rebel onion
1a0d912083 New translations strings.xml (German) 2024-11-07 11:28:30 -06:00
rebel onion
0071286153 New translations strings.xml (Arabic) 2024-11-07 11:28:29 -06:00
rebel onion
6a966e7166 New translations strings.xml (Spanish) 2024-11-07 11:28:28 -06:00
rebel onion
734805458d New translations strings.xml (French) 2024-11-07 11:28:27 -06:00
rebel onion
8de9831954 New translations strings.xml (Portuguese, Brazilian) 2024-11-07 11:28:25 -06:00
rebel onion
04a2cea6fe New translations strings.xml (Portuguese, Brazilian) 2024-10-27 13:40:20 -05:00
rebel onion
2f34669301 New translations strings.xml (Assamese) 2024-10-05 15:05:06 -05:00
rebel onion
4f6ab17ea1 New translations strings.xml (Assamese) 2024-10-05 13:47:55 -05:00
rebel onion
ccbb50bc3a New translations strings.xml (Hindi) 2024-10-05 13:47:54 -05:00
rebel onion
a3bf2bc18e New translations strings.xml (Bengali) 2024-10-05 13:47:53 -05:00
rebel onion
b616850895 New translations strings.xml (Spanish, Mexico) 2024-10-05 13:47:51 -05:00
rebel onion
ed2c8902fd New translations strings.xml (Urdu (Pakistan)) 2024-10-05 13:47:50 -05:00
rebel onion
d59c96fad9 New translations strings.xml (Chinese Simplified) 2024-10-05 13:47:49 -05:00
rebel onion
1ad544d0df New translations strings.xml (Turkish) 2024-10-05 13:47:48 -05:00
rebel onion
942bf83d24 New translations strings.xml (Albanian) 2024-10-05 13:47:47 -05:00
rebel onion
408d1abe16 New translations strings.xml (Russian) 2024-10-05 13:47:45 -05:00
rebel onion
0e198b3423 New translations strings.xml (Polish) 2024-10-05 13:47:44 -05:00
rebel onion
d1b0cf65b0 New translations strings.xml (Dutch) 2024-10-05 13:47:43 -05:00
rebel onion
323ab1d4ab New translations strings.xml (Korean) 2024-10-05 13:47:42 -05:00
rebel onion
7950982bfc New translations strings.xml (Japanese) 2024-10-05 13:47:41 -05:00
rebel onion
d70b3fd541 New translations strings.xml (Italian) 2024-10-05 13:47:39 -05:00
rebel onion
6877c82315 New translations strings.xml (Greek) 2024-10-05 13:47:38 -05:00
rebel onion
860d108f82 New translations strings.xml (German) 2024-10-05 13:47:37 -05:00
rebel onion
bacc7708c0 New translations strings.xml (Arabic) 2024-10-05 13:47:36 -05:00
rebel onion
d4766e27be New translations strings.xml (Spanish) 2024-10-05 13:47:35 -05:00
rebel onion
bf130bfc72 New translations strings.xml (French) 2024-10-05 13:47:34 -05:00
rebel onion
09aa955299 New translations strings.xml (Portuguese, Brazilian) 2024-10-05 13:47:33 -05:00
rebel onion
4efbdd0554 New translations strings.xml (Portuguese, Brazilian) 2024-09-30 19:19:55 -05:00
rebel onion
12f0a83c7d New translations strings.xml (Portuguese, Brazilian) 2024-09-30 18:17:38 -05:00
rebel onion
26c345d130 New translations strings.xml (Portuguese, Brazilian) 2024-09-30 17:11:22 -05:00
rebel onion
660a40ce23 New translations strings.xml (Greek) 2024-09-15 13:26:26 -05:00
rebel onion
2fda4d62b7 New translations strings.xml (Arabic) 2024-08-26 04:32:36 -05:00
rebel onion
e00942001a New translations strings.xml (Arabic) 2024-08-26 02:04:00 -05:00
rebel onion
13d9084456 New translations strings.xml (Arabic) 2024-08-26 00:46:34 -05:00
rebel onion
5ee1d2d5b7 New translations strings.xml (Italian) 2024-08-11 13:10:03 -05:00
rebel onion
87e31ff7f7 New translations strings.xml (Italian) 2024-08-11 12:12:59 -05:00
rebel onion
842f24af91 New translations strings.xml (Italian) 2024-08-11 11:03:30 -05:00
rebel onion
fb2373c385 New translations strings.xml (Italian) 2024-08-10 12:00:52 -05:00
rebel onion
ca50c3c1d4 New translations strings.xml (Italian) 2024-08-10 10:49:34 -05:00
rebel onion
ad7f452b8e New translations strings.xml (Italian) 2024-08-09 17:18:50 -05:00
rebel onion
a7e6773428 New translations strings.xml (Italian) 2024-08-09 16:02:03 -05:00
rebel onion
aa1c46caf3 New translations strings.xml (Italian) 2024-08-09 14:45:57 -05:00
rebel onion
0ef412d5f3 New translations strings.xml (Italian) 2024-08-09 12:38:14 -05:00
rebel onion
ba8772b54d New translations strings.xml (Italian) 2024-08-09 10:36:43 -05:00
rebel onion
13595fe1b9 New translations strings.xml (Italian) 2024-08-09 08:56:12 -05:00
rebel onion
59d3d4f816 New translations strings.xml (Italian) 2024-08-08 16:24:29 -05:00
rebel onion
b0618733a3 New translations strings.xml (Italian) 2024-08-08 14:57:55 -05:00
rebel onion
1d5dc80d43 New translations strings.xml (Italian) 2024-08-08 13:47:06 -05:00
rebel onion
1b4e675c68 New translations strings.xml (Italian) 2024-08-08 12:16:38 -05:00
rebel onion
d883a30a75 New translations strings.xml (Italian) 2024-08-08 10:58:29 -05:00
rebel onion
4bc900c74b New translations strings.xml (Italian) 2024-08-08 09:54:52 -05:00
rebel onion
303965ea97 New translations strings.xml (Italian) 2024-08-08 06:07:12 -05:00
rebel onion
b50241a123 New translations strings.xml (Italian) 2024-08-08 04:19:15 -05:00
rebel onion
728a02a034 New translations strings.xml (Italian) 2024-08-07 18:05:54 -05:00
rebel onion
005601ab53 New translations strings.xml (Assamese) 2024-07-11 05:27:08 -05:00
rebel onion
649cab0bcc New translations strings.xml (Assamese) 2024-07-11 03:31:51 -05:00
rebel onion
00728d1fed New translations strings.xml (Polish) 2024-07-09 05:35:06 -05:00
rebel onion
a398674276 New translations strings.xml (Arabic) 2024-07-09 03:10:46 -05:00
rebel onion
bcf10dd11a New translations strings.xml (Portuguese, Brazilian) 2024-07-08 21:46:54 -05:00
rebel onion
1f2d7899ca New translations strings.xml (Turkish) 2024-07-08 21:46:51 -05:00
rebel onion
c60dc58173 New translations strings.xml (Polish) 2024-07-08 21:46:50 -05:00
rebel onion
5fa1e8d863 New translations strings.xml (Albanian) 2024-07-08 21:46:49 -05:00
rebel onion
441e296afa New translations strings.xml (Urdu (Pakistan)) 2024-07-08 21:46:47 -05:00
rebel onion
e7763b97e3 New translations strings.xml (Dutch) 2024-07-08 21:46:46 -05:00
rebel onion
9f9076844e New translations strings.xml (Italian) 2024-07-08 21:46:45 -05:00
rebel onion
097ab3915e New translations strings.xml (Bengali) 2024-07-08 21:46:44 -05:00
rebel onion
891a5e6264 New translations strings.xml (Korean) 2024-07-08 21:46:43 -05:00
rebel onion
3c32490d38 New translations strings.xml (Greek) 2024-07-08 21:46:42 -05:00
rebel onion
0964d1f531 New translations strings.xml (Assamese) 2024-07-08 21:46:40 -05:00
rebel onion
9ffa3c4176 New translations strings.xml (Hindi) 2024-07-08 21:46:39 -05:00
rebel onion
f8861d7a61 New translations strings.xml (Spanish, Mexico) 2024-07-08 21:46:38 -05:00
rebel onion
368a166b5c New translations strings.xml (Chinese Simplified) 2024-07-08 21:46:37 -05:00
rebel onion
5d1dc0e70a New translations strings.xml (Russian) 2024-07-08 21:46:36 -05:00
rebel onion
75dd72b5d6 New translations strings.xml (Japanese) 2024-07-08 21:46:34 -05:00
rebel onion
12d76ead3c New translations strings.xml (German) 2024-07-08 21:46:33 -05:00
rebel onion
edb24815ff New translations strings.xml (Arabic) 2024-07-08 21:46:32 -05:00
rebel onion
c5bf3886a4 New translations strings.xml (Spanish) 2024-07-08 21:46:31 -05:00
rebel onion
1a692e7779 New translations strings.xml (French) 2024-07-08 21:46:30 -05:00
rebel onion
4f80667943 New translations strings.xml (Portuguese, Brazilian) 2024-07-08 13:30:38 -05:00
rebel onion
e583701755 New translations strings.xml (Turkish) 2024-07-08 13:30:37 -05:00
rebel onion
0f0d3e577a New translations strings.xml (Polish) 2024-07-08 13:30:36 -05:00
rebel onion
c655a04c41 New translations strings.xml (Albanian) 2024-07-08 13:30:34 -05:00
rebel onion
869da4b0d8 New translations strings.xml (Urdu (Pakistan)) 2024-07-08 13:30:33 -05:00
rebel onion
323e725c74 New translations strings.xml (Dutch) 2024-07-08 13:30:31 -05:00
rebel onion
965e8dc1b8 New translations strings.xml (Italian) 2024-07-08 13:30:30 -05:00
rebel onion
917902a695 New translations strings.xml (Bengali) 2024-07-08 13:30:28 -05:00
rebel onion
64fb8b06ee New translations strings.xml (Korean) 2024-07-08 13:30:26 -05:00
rebel onion
7bf2b9cc8f New translations strings.xml (Greek) 2024-07-08 13:30:24 -05:00
rebel onion
f03ed32617 New translations strings.xml (Assamese) 2024-07-08 13:30:22 -05:00
rebel onion
c7d01058e5 New translations strings.xml (Hindi) 2024-07-08 13:30:19 -05:00
rebel onion
a039f73f4e New translations strings.xml (Spanish, Mexico) 2024-07-08 13:30:17 -05:00
rebel onion
5bd6ce8f1a New translations strings.xml (Chinese Simplified) 2024-07-08 13:30:15 -05:00
rebel onion
5c3ad3f7a0 New translations strings.xml (Russian) 2024-07-08 13:30:14 -05:00
rebel onion
d297215db2 New translations strings.xml (Japanese) 2024-07-08 13:30:12 -05:00
rebel onion
cabafc02fd New translations strings.xml (German) 2024-07-08 13:30:10 -05:00
rebel onion
becfda37a2 New translations strings.xml (Arabic) 2024-07-08 13:30:08 -05:00
rebel onion
97882ffea7 New translations strings.xml (Spanish) 2024-07-08 13:30:07 -05:00
rebel onion
ac4a0bb214 New translations strings.xml (French) 2024-07-08 13:30:06 -05:00
rebel onion
677dca710b New translations strings.xml (Portuguese, Brazilian) 2024-07-08 08:17:19 -05:00
rebel onion
1bd821edb8 New translations strings.xml (Turkish) 2024-07-08 08:17:17 -05:00
rebel onion
4714d6b0e7 New translations strings.xml (Polish) 2024-07-08 08:17:16 -05:00
rebel onion
979dd05298 New translations strings.xml (Albanian) 2024-07-08 08:17:15 -05:00
rebel onion
ac39785d4a New translations strings.xml (Urdu (Pakistan)) 2024-07-08 08:17:13 -05:00
rebel onion
15e900d51b New translations strings.xml (Dutch) 2024-07-08 08:17:12 -05:00
rebel onion
2e7da05f0e New translations strings.xml (Italian) 2024-07-08 08:17:10 -05:00
rebel onion
5c882d43c7 New translations strings.xml (Bengali) 2024-07-08 08:17:08 -05:00
rebel onion
5be0c6bd89 New translations strings.xml (Korean) 2024-07-08 08:17:07 -05:00
rebel onion
748d2f8ba1 New translations strings.xml (Greek) 2024-07-08 08:17:05 -05:00
rebel onion
b0ae50eabb New translations strings.xml (Assamese) 2024-07-08 08:17:02 -05:00
rebel onion
bcf3c0f2a1 New translations strings.xml (Hindi) 2024-07-08 08:17:01 -05:00
rebel onion
a6cee5586a New translations strings.xml (Spanish, Mexico) 2024-07-08 08:17:00 -05:00
rebel onion
2222cfe991 New translations strings.xml (Chinese Simplified) 2024-07-08 08:16:58 -05:00
rebel onion
9f9a97998e New translations strings.xml (Russian) 2024-07-08 08:16:57 -05:00
rebel onion
e95181958a New translations strings.xml (Japanese) 2024-07-08 08:16:55 -05:00
rebel onion
7777f87ab4 New translations strings.xml (German) 2024-07-08 08:16:54 -05:00
rebel onion
1b821b1524 New translations strings.xml (Arabic) 2024-07-08 08:16:53 -05:00
rebel onion
6c83fd97c8 New translations strings.xml (Spanish) 2024-07-08 08:16:51 -05:00
rebel onion
4046539610 New translations strings.xml (French) 2024-07-08 08:16:49 -05:00
rebel onion
63d50a73d2 New translations strings.xml (Arabic) 2024-07-07 08:41:51 -05:00
rebel onion
b2a789d882 New translations strings.xml (Arabic) 2024-07-07 07:11:25 -05:00
rebel onion
4222e587c5 New translations strings.xml (Polish) 2024-06-30 16:30:20 -05:00
rebel onion
3cc3618782 New translations strings.xml (Portuguese, Brazilian) 2024-06-29 11:11:35 -05:00
rebel onion
c371639aee New translations strings.xml (Turkish) 2024-06-29 11:11:34 -05:00
rebel onion
c1db16262e New translations strings.xml (Polish) 2024-06-29 11:11:33 -05:00
rebel onion
0d0c5cea05 New translations strings.xml (Albanian) 2024-06-29 11:11:32 -05:00
rebel onion
92c2e40e12 New translations strings.xml (Urdu (Pakistan)) 2024-06-29 11:11:31 -05:00
rebel onion
a64865603f New translations strings.xml (Dutch) 2024-06-29 11:11:30 -05:00
rebel onion
323dc38918 New translations strings.xml (Italian) 2024-06-29 11:11:29 -05:00
rebel onion
6d44ac01e5 New translations strings.xml (Bengali) 2024-06-29 11:11:27 -05:00
rebel onion
01a0288cf8 New translations strings.xml (Korean) 2024-06-29 11:11:26 -05:00
rebel onion
db1d66e3fd New translations strings.xml (Greek) 2024-06-29 11:11:25 -05:00
rebel onion
b1d821451c New translations strings.xml (Assamese) 2024-06-29 11:11:24 -05:00
rebel onion
a0ce7464a5 New translations strings.xml (Hindi) 2024-06-29 11:11:23 -05:00
rebel onion
91f1546d10 New translations strings.xml (Spanish, Mexico) 2024-06-29 11:11:22 -05:00
rebel onion
89a5b7812b New translations strings.xml (Chinese Simplified) 2024-06-29 11:11:21 -05:00
rebel onion
4315c09d2e New translations strings.xml (Russian) 2024-06-29 11:11:19 -05:00
rebel onion
0bb02a50c1 New translations strings.xml (Japanese) 2024-06-29 11:11:18 -05:00
rebel onion
27b3a28c8b New translations strings.xml (German) 2024-06-29 11:11:17 -05:00
rebel onion
97edd35840 New translations strings.xml (Arabic) 2024-06-29 11:11:16 -05:00
rebel onion
3913e4f27a New translations strings.xml (Spanish) 2024-06-29 11:11:15 -05:00
rebel onion
ce4f7f1884 New translations strings.xml (French) 2024-06-29 11:11:14 -05:00
rebel onion
6c68b48e89 New translations strings.xml (Portuguese, Brazilian) 2024-06-29 04:41:00 -05:00
rebel onion
de5f238b64 New translations strings.xml (Italian) 2024-06-29 04:40:59 -05:00
rebel onion
2d698d2a62 New translations strings.xml (Korean) 2024-06-29 04:40:58 -05:00
rebel onion
6db124b4cc New translations strings.xml (Assamese) 2024-06-29 04:40:56 -05:00
rebel onion
549a34e153 New translations strings.xml (Hindi) 2024-06-29 04:40:55 -05:00
rebel onion
9eaf959a5a New translations strings.xml (Spanish, Mexico) 2024-06-29 04:40:54 -05:00
rebel onion
ee62edcc7e New translations strings.xml (Russian) 2024-06-29 04:40:53 -05:00
rebel onion
2968d91535 New translations strings.xml (Japanese) 2024-06-29 04:40:52 -05:00
rebel onion
2fb95c6099 New translations strings.xml (German) 2024-06-29 04:40:51 -05:00
rebel onion
a7825297ed New translations strings.xml (Spanish) 2024-06-29 04:40:49 -05:00
rebel onion
e72c235078 New translations strings.xml (Albanian) 2024-06-29 03:28:15 -05:00
rebel onion
89a4847c9f New translations strings.xml (Dutch) 2024-06-29 03:28:13 -05:00
rebel onion
15719ad29b New translations strings.xml (Bengali) 2024-06-29 03:28:12 -05:00
rebel onion
cc0f7113e9 New translations strings.xml (Chinese Simplified) 2024-06-29 03:28:11 -05:00
rebel onion
004290c5fe New translations strings.xml (French) 2024-06-29 03:28:10 -05:00
rebel onion
83b28e40db New translations strings.xml (Polish) 2024-06-28 05:49:38 -05:00
rebel onion
737fce70fb New translations strings.xml (Greek) 2024-06-26 11:40:05 -05:00
rebel onion
8759c336dc New translations strings.xml (Portuguese, Brazilian) 2024-06-26 09:19:08 -05:00
rebel onion
d85f99e32a New translations strings.xml (Turkish) 2024-06-26 09:19:06 -05:00
rebel onion
b2740a8b21 New translations strings.xml (Polish) 2024-06-26 09:19:04 -05:00
rebel onion
30fc3ff713 New translations strings.xml (Albanian) 2024-06-26 09:19:03 -05:00
rebel onion
406007f4ed New translations strings.xml (Urdu (Pakistan)) 2024-06-26 09:19:01 -05:00
rebel onion
7c5e42f941 New translations strings.xml (Dutch) 2024-06-26 09:19:00 -05:00
rebel onion
e890c417a0 New translations strings.xml (Italian) 2024-06-26 09:18:59 -05:00
rebel onion
0c01f435d8 New translations strings.xml (Bengali) 2024-06-26 09:18:57 -05:00
rebel onion
dbb7759b0f New translations strings.xml (Korean) 2024-06-26 09:18:56 -05:00
rebel onion
905f72b965 New translations strings.xml (Greek) 2024-06-26 09:18:55 -05:00
rebel onion
77ee082809 New translations strings.xml (Assamese) 2024-06-26 09:18:53 -05:00
rebel onion
3a19684dcb New translations strings.xml (Hindi) 2024-06-26 09:18:52 -05:00
rebel onion
2890ca1c4a New translations strings.xml (Spanish, Mexico) 2024-06-26 09:18:51 -05:00
rebel onion
3b00572b91 New translations strings.xml (Chinese Simplified) 2024-06-26 09:18:49 -05:00
rebel onion
ed89ddb2ba New translations strings.xml (Russian) 2024-06-26 09:18:48 -05:00
rebel onion
4de630b08a New translations strings.xml (Japanese) 2024-06-26 09:18:47 -05:00
rebel onion
6bc26e3144 New translations strings.xml (German) 2024-06-26 09:18:45 -05:00
rebel onion
579411aefb New translations strings.xml (Arabic) 2024-06-26 09:18:44 -05:00
rebel onion
7c9f52d986 New translations strings.xml (Spanish) 2024-06-26 09:18:43 -05:00
rebel onion
2a55c7a846 New translations strings.xml (French) 2024-06-26 09:18:42 -05:00
rebel onion
1268763d99 New translations strings.xml (Arabic) 2024-06-25 16:38:46 -05:00
rebel onion
1338bc0ebb New translations strings.xml (Arabic) 2024-06-25 15:15:05 -05:00
rebel onion
423a657da7 New translations strings.xml (Turkish) 2024-06-25 05:36:13 -05:00
rebel onion
d36d818bfa New translations strings.xml (Assamese) 2024-06-25 03:25:20 -05:00
rebel onion
4a851ade4f New translations strings.xml (Portuguese, Brazilian) 2024-06-25 01:30:54 -05:00
rebel onion
8dba9243a4 New translations strings.xml (Albanian) 2024-06-25 01:30:53 -05:00
rebel onion
b4013d1d28 New translations strings.xml (Dutch) 2024-06-25 01:30:51 -05:00
rebel onion
6bc23ba8e1 New translations strings.xml (Italian) 2024-06-25 01:30:50 -05:00
rebel onion
4d6aaf6f36 New translations strings.xml (Bengali) 2024-06-25 01:30:49 -05:00
rebel onion
291f516e6f New translations strings.xml (Korean) 2024-06-25 01:30:47 -05:00
rebel onion
5fb0fb14e7 New translations strings.xml (Greek) 2024-06-25 01:30:46 -05:00
rebel onion
c40d61f471 New translations strings.xml (Hindi) 2024-06-25 01:30:45 -05:00
rebel onion
80da5e3480 New translations strings.xml (Spanish, Mexico) 2024-06-25 01:30:42 -05:00
rebel onion
cfd5790589 New translations strings.xml (Chinese Simplified) 2024-06-25 01:30:41 -05:00
rebel onion
98e700ab11 New translations strings.xml (Russian) 2024-06-25 01:30:40 -05:00
rebel onion
5c55a9a597 New translations strings.xml (Japanese) 2024-06-25 01:30:38 -05:00
rebel onion
2833c58856 New translations strings.xml (German) 2024-06-25 01:30:37 -05:00
rebel onion
38ccb8926d New translations strings.xml (Spanish) 2024-06-25 01:30:36 -05:00
rebel onion
76b5e2f3c2 New translations strings.xml (French) 2024-06-25 01:30:34 -05:00
rebel onion
da02ee2706 New translations strings.xml (Portuguese, Brazilian) 2024-06-25 00:33:03 -05:00
rebel onion
92cae18d53 New translations strings.xml (Turkish) 2024-06-25 00:33:01 -05:00
rebel onion
2e70f13a42 New translations strings.xml (Polish) 2024-06-25 00:33:00 -05:00
rebel onion
0358341e42 New translations strings.xml (Albanian) 2024-06-25 00:32:59 -05:00
rebel onion
5a65d2ef5f New translations strings.xml (Urdu (Pakistan)) 2024-06-25 00:32:58 -05:00
rebel onion
ad713a357a New translations strings.xml (Dutch) 2024-06-25 00:32:57 -05:00
rebel onion
d8776b6ab0 New translations strings.xml (Italian) 2024-06-25 00:32:56 -05:00
rebel onion
d2de96f486 New translations strings.xml (Bengali) 2024-06-25 00:32:54 -05:00
rebel onion
acf1a4ea32 New translations strings.xml (Korean) 2024-06-25 00:32:53 -05:00
rebel onion
2fdafedc2f New translations strings.xml (Greek) 2024-06-25 00:32:52 -05:00
rebel onion
d04fa0eaa7 New translations strings.xml (Assamese) 2024-06-25 00:32:51 -05:00
rebel onion
9d23caab12 New translations strings.xml (Hindi) 2024-06-25 00:32:49 -05:00
rebel onion
2121400e78 New translations strings.xml (Spanish, Mexico) 2024-06-25 00:32:48 -05:00
rebel onion
e02da10e23 New translations strings.xml (Chinese Simplified) 2024-06-25 00:32:47 -05:00
rebel onion
3bada69085 New translations strings.xml (Russian) 2024-06-25 00:32:46 -05:00
rebel onion
7845d3059f New translations strings.xml (Japanese) 2024-06-25 00:32:45 -05:00
rebel onion
5921dd8e9b New translations strings.xml (German) 2024-06-25 00:32:44 -05:00
rebel onion
43e0aff3ab New translations strings.xml (Arabic) 2024-06-25 00:32:42 -05:00
rebel onion
ed5a4cb9be New translations strings.xml (Spanish) 2024-06-25 00:32:41 -05:00
rebel onion
824b13cf6e New translations strings.xml (French) 2024-06-25 00:32:39 -05:00
rebel onion
6e1f5c7993 New translations strings.xml (Albanian) 2024-06-22 07:26:47 -05:00
rebel onion
05eceec6ce New translations strings.xml (Dutch) 2024-06-22 07:26:46 -05:00
rebel onion
35d3e35004 New translations strings.xml (Bengali) 2024-06-22 07:26:44 -05:00
rebel onion
3a4dec04df New translations strings.xml (Korean) 2024-06-22 07:26:43 -05:00
rebel onion
dd3f5e74f8 New translations strings.xml (Hindi) 2024-06-22 07:26:42 -05:00
rebel onion
6c434bbbce New translations strings.xml (Spanish, Mexico) 2024-06-22 07:26:40 -05:00
rebel onion
8f5416080c New translations strings.xml (Chinese Simplified) 2024-06-22 07:26:39 -05:00
rebel onion
c633c62c4c New translations strings.xml (Russian) 2024-06-22 07:26:38 -05:00
rebel onion
7e5622ba0a New translations strings.xml (Italian) 2024-06-22 07:26:36 -05:00
rebel onion
9f1253274c New translations strings.xml (Greek) 2024-06-22 07:26:35 -05:00
rebel onion
2c7a43b32f New translations strings.xml (Japanese) 2024-06-22 07:26:34 -05:00
rebel onion
a1c5d5d818 New translations strings.xml (German) 2024-06-22 07:26:33 -05:00
rebel onion
851f137723 New translations strings.xml (Spanish) 2024-06-22 07:26:31 -05:00
rebel onion
706af34e90 New translations strings.xml (French) 2024-06-22 07:26:30 -05:00
rebel onion
9d650bf3c7 New translations strings.xml (Albanian) 2024-06-22 06:20:25 -05:00
rebel onion
f575cc8a70 New translations strings.xml (Polish) 2024-06-22 05:14:23 -05:00
rebel onion
ad44e77934 New translations strings.xml (Polish) 2024-06-22 04:16:16 -05:00
rebel onion
788bd4dd0b New translations strings.xml (Turkish) 2024-06-22 03:15:40 -05:00
rebel onion
911e7432e4 New translations strings.xml (Turkish) 2024-06-22 02:16:36 -05:00
rebel onion
d62b26813a New translations strings.xml (Portuguese, Brazilian) 2024-06-21 14:15:18 -05:00
rebel onion
32be2cf617 New translations strings.xml (Turkish) 2024-06-21 14:15:17 -05:00
rebel onion
bbd6e7ec45 New translations strings.xml (Polish) 2024-06-21 14:15:15 -05:00
rebel onion
8655a53519 New translations strings.xml (Albanian) 2024-06-21 14:15:14 -05:00
rebel onion
0b9fbe31b9 New translations strings.xml (Urdu (Pakistan)) 2024-06-21 14:15:13 -05:00
rebel onion
c95ee7ce82 New translations strings.xml (Dutch) 2024-06-21 14:15:12 -05:00
rebel onion
a25dff9106 New translations strings.xml (Italian) 2024-06-21 14:15:11 -05:00
rebel onion
0835f879b3 New translations strings.xml (Bengali) 2024-06-21 14:15:09 -05:00
rebel onion
b336ce3915 New translations strings.xml (Korean) 2024-06-21 14:15:08 -05:00
rebel onion
271f71fe21 New translations strings.xml (Greek) 2024-06-21 14:15:07 -05:00
rebel onion
afec155692 New translations strings.xml (Assamese) 2024-06-21 14:15:06 -05:00
rebel onion
b08497d7cb New translations strings.xml (Hindi) 2024-06-21 14:15:05 -05:00
rebel onion
9600fb2efb New translations strings.xml (Spanish, Mexico) 2024-06-21 14:15:03 -05:00
rebel onion
6540fb511b New translations strings.xml (Chinese Simplified) 2024-06-21 14:15:02 -05:00
rebel onion
55689edc6e New translations strings.xml (Russian) 2024-06-21 14:15:01 -05:00
rebel onion
5adae74ad8 New translations strings.xml (Japanese) 2024-06-21 14:15:00 -05:00
rebel onion
7187e681e8 New translations strings.xml (German) 2024-06-21 14:14:58 -05:00
rebel onion
b106f4e66b New translations strings.xml (Arabic) 2024-06-21 14:14:57 -05:00
rebel onion
869d982bfb New translations strings.xml (Spanish) 2024-06-21 14:14:56 -05:00
rebel onion
11b7de55ca New translations strings.xml (French) 2024-06-21 14:14:55 -05:00
rebel onion
11912ddff3 New translations strings.xml (Portuguese, Brazilian) 2024-06-20 21:11:43 -05:00
rebel onion
ba7fc2cf6d New translations strings.xml (Portuguese, Brazilian) 2024-06-20 19:57:17 -05:00
rebel onion
114f34feed New translations strings.xml (Portuguese, Brazilian) 2024-06-20 10:32:49 -05:00
rebel onion
a611265534 New translations strings.xml (Turkish) 2024-06-20 10:32:48 -05:00
rebel onion
ceaa5d6445 New translations strings.xml (Polish) 2024-06-20 10:32:46 -05:00
rebel onion
08b858a28f New translations strings.xml (Albanian) 2024-06-20 10:32:45 -05:00
rebel onion
da24a8e348 New translations strings.xml (Urdu (Pakistan)) 2024-06-20 10:32:44 -05:00
rebel onion
2db010d4b7 New translations strings.xml (Dutch) 2024-06-20 10:32:43 -05:00
rebel onion
35cc7a6374 New translations strings.xml (Italian) 2024-06-20 10:32:41 -05:00
rebel onion
0b2b7f3769 New translations strings.xml (Bengali) 2024-06-20 10:32:40 -05:00
rebel onion
4fa97d8abc New translations strings.xml (Korean) 2024-06-20 10:32:39 -05:00
rebel onion
fc9fe84744 New translations strings.xml (Greek) 2024-06-20 10:32:37 -05:00
rebel onion
fc27b92ef8 New translations strings.xml (Assamese) 2024-06-20 10:32:36 -05:00
rebel onion
7d508ba843 New translations strings.xml (Hindi) 2024-06-20 10:32:35 -05:00
rebel onion
b312535e83 New translations strings.xml (Spanish, Mexico) 2024-06-20 10:32:33 -05:00
rebel onion
319cfc7542 New translations strings.xml (Chinese Simplified) 2024-06-20 10:32:32 -05:00
rebel onion
7a81f941c1 New translations strings.xml (Russian) 2024-06-20 10:32:30 -05:00
rebel onion
b5e2d5c2e9 New translations strings.xml (Japanese) 2024-06-20 10:32:29 -05:00
rebel onion
60872f9095 New translations strings.xml (German) 2024-06-20 10:32:27 -05:00
rebel onion
ded8525d23 New translations strings.xml (Arabic) 2024-06-20 10:32:26 -05:00
rebel onion
1c9799cdab New translations strings.xml (Spanish) 2024-06-20 10:32:25 -05:00
rebel onion
204747fc94 New translations strings.xml (French) 2024-06-20 10:32:23 -05:00
rebel onion
13444a2176 New translations strings.xml (Portuguese, Brazilian) 2024-06-16 08:09:37 -05:00
rebel onion
5f7cc092c7 New translations strings.xml (Portuguese, Brazilian) 2024-06-16 00:21:15 -05:00
rebel onion
b026d8ffef New translations strings.xml (Turkish) 2024-06-16 00:21:14 -05:00
rebel onion
035344677f New translations strings.xml (Polish) 2024-06-16 00:21:13 -05:00
rebel onion
76756bed4a New translations strings.xml (Albanian) 2024-06-16 00:21:12 -05:00
rebel onion
78d49f7ad1 New translations strings.xml (Urdu (Pakistan)) 2024-06-16 00:21:11 -05:00
rebel onion
d35c9d3ea9 New translations strings.xml (Dutch) 2024-06-16 00:21:09 -05:00
rebel onion
dbf25344a9 New translations strings.xml (Italian) 2024-06-16 00:21:08 -05:00
rebel onion
e83edad329 New translations strings.xml (Bengali) 2024-06-16 00:21:07 -05:00
rebel onion
656e03f799 New translations strings.xml (Korean) 2024-06-16 00:21:06 -05:00
rebel onion
72d5f555ca New translations strings.xml (Greek) 2024-06-16 00:21:05 -05:00
rebel onion
36f52612dc New translations strings.xml (Assamese) 2024-06-16 00:21:04 -05:00
rebel onion
dac5ee262a New translations strings.xml (Hindi) 2024-06-16 00:21:03 -05:00
rebel onion
f693c8288d New translations strings.xml (Spanish, Mexico) 2024-06-16 00:21:02 -05:00
rebel onion
40a3a192a7 New translations strings.xml (Chinese Simplified) 2024-06-16 00:21:01 -05:00
rebel onion
814da6172e New translations strings.xml (Russian) 2024-06-16 00:20:59 -05:00
rebel onion
e729b096bb New translations strings.xml (Japanese) 2024-06-16 00:20:58 -05:00
rebel onion
5d888d6dac New translations strings.xml (German) 2024-06-16 00:20:57 -05:00
rebel onion
f7fe4d09f9 New translations strings.xml (Arabic) 2024-06-16 00:20:56 -05:00
rebel onion
ce50b9d241 New translations strings.xml (Spanish) 2024-06-16 00:20:55 -05:00
rebel onion
a4a53a3e0e New translations strings.xml (French) 2024-06-16 00:20:54 -05:00
rebel onion
a20c8477d7 New translations strings.xml (Portuguese, Brazilian) 2024-06-14 06:21:03 -05:00
rebel onion
0d73819d17 New translations strings.xml (Turkish) 2024-06-14 06:21:02 -05:00
rebel onion
ee9e2ad4a3 New translations strings.xml (Polish) 2024-06-14 06:21:01 -05:00
rebel onion
9664cdc72a New translations strings.xml (Albanian) 2024-06-14 06:21:00 -05:00
rebel onion
b2caa71f23 New translations strings.xml (Urdu (Pakistan)) 2024-06-14 06:20:59 -05:00
rebel onion
b31424b529 New translations strings.xml (Dutch) 2024-06-14 06:20:58 -05:00
rebel onion
b1446f87d7 New translations strings.xml (Italian) 2024-06-14 06:20:57 -05:00
rebel onion
fce0b9279b New translations strings.xml (Bengali) 2024-06-14 06:20:55 -05:00
rebel onion
cc7d7cfb22 New translations strings.xml (Korean) 2024-06-14 06:20:54 -05:00
rebel onion
01aa52caa1 New translations strings.xml (Greek) 2024-06-14 06:20:53 -05:00
rebel onion
82cb690138 New translations strings.xml (Assamese) 2024-06-14 06:20:52 -05:00
rebel onion
194b1b1428 New translations strings.xml (Hindi) 2024-06-14 06:20:50 -05:00
rebel onion
55bdf10282 New translations strings.xml (Spanish, Mexico) 2024-06-14 06:20:49 -05:00
rebel onion
c87034ff39 New translations strings.xml (Chinese Simplified) 2024-06-14 06:20:48 -05:00
rebel onion
53e6509db6 New translations strings.xml (Russian) 2024-06-14 06:20:47 -05:00
rebel onion
d81ab5abbe New translations strings.xml (Japanese) 2024-06-14 06:20:46 -05:00
rebel onion
50268e609e New translations strings.xml (German) 2024-06-14 06:20:45 -05:00
rebel onion
46db6182b3 New translations strings.xml (Arabic) 2024-06-14 06:20:44 -05:00
rebel onion
321cc097e8 New translations strings.xml (Spanish) 2024-06-14 06:20:42 -05:00
rebel onion
97fde6a5c1 New translations strings.xml (French) 2024-06-14 06:20:41 -05:00
rebel onion
b92a2999a6 New translations strings.xml (Arabic) 2024-06-04 14:02:27 -05:00
rebel onion
736801ef1f New translations strings.xml (Arabic) 2024-06-04 12:49:39 -05:00
rebel onion
e774678ce8 New translations strings.xml (Portuguese, Brazilian) 2024-06-02 01:20:26 -05:00
rebel onion
28a0e09b64 New translations strings.xml (Polish) 2024-06-02 01:20:25 -05:00
rebel onion
57c5d332b0 New translations strings.xml (Albanian) 2024-06-02 01:20:24 -05:00
rebel onion
96bc7bfc53 New translations strings.xml (Dutch) 2024-06-02 01:20:23 -05:00
rebel onion
0b2082763b New translations strings.xml (Italian) 2024-06-02 01:20:22 -05:00
rebel onion
9975f76f3f New translations strings.xml (Bengali) 2024-06-02 01:20:21 -05:00
rebel onion
8de08bd0e0 New translations strings.xml (Korean) 2024-06-02 01:20:20 -05:00
rebel onion
9289674ff2 New translations strings.xml (Greek) 2024-06-02 01:20:19 -05:00
rebel onion
bde36d85dc New translations strings.xml (Assamese) 2024-06-02 01:20:18 -05:00
rebel onion
15ece3d939 New translations strings.xml (Hindi) 2024-06-02 01:20:17 -05:00
rebel onion
3cd60c004c New translations strings.xml (Spanish, Mexico) 2024-06-02 01:20:16 -05:00
rebel onion
cd9a64f4b7 New translations strings.xml (Chinese Simplified) 2024-06-02 01:20:15 -05:00
rebel onion
989d3dbb77 New translations strings.xml (Russian) 2024-06-02 01:20:13 -05:00
rebel onion
8b9e4eca77 New translations strings.xml (Japanese) 2024-06-02 01:20:12 -05:00
rebel onion
91207627ba New translations strings.xml (German) 2024-06-02 01:20:11 -05:00
rebel onion
d458d30e6e New translations strings.xml (Spanish) 2024-06-02 01:20:10 -05:00
rebel onion
75b382f53c New translations strings.xml (French) 2024-06-02 01:20:09 -05:00
rebel onion
650ab0a9ac New translations strings.xml (Portuguese, Brazilian) 2024-06-01 16:01:35 -05:00
rebel onion
a07bef0613 New translations strings.xml (Turkish) 2024-06-01 16:01:34 -05:00
rebel onion
01b061890c New translations strings.xml (Polish) 2024-06-01 16:01:33 -05:00
rebel onion
a970337ef7 New translations strings.xml (Albanian) 2024-06-01 16:01:32 -05:00
rebel onion
878b8af0b3 New translations strings.xml (Urdu (Pakistan)) 2024-06-01 16:01:30 -05:00
rebel onion
bde4e73cd1 New translations strings.xml (Dutch) 2024-06-01 16:01:29 -05:00
rebel onion
b13a6e0943 New translations strings.xml (Italian) 2024-06-01 16:01:28 -05:00
rebel onion
0be9ee6c49 New translations strings.xml (Bengali) 2024-06-01 16:01:27 -05:00
rebel onion
c19d15dbf6 New translations strings.xml (Korean) 2024-06-01 16:01:26 -05:00
rebel onion
27bab50e57 New translations strings.xml (Greek) 2024-06-01 16:01:25 -05:00
rebel onion
3ea85878c9 New translations strings.xml (Assamese) 2024-06-01 16:01:24 -05:00
rebel onion
1ff415900a New translations strings.xml (Hindi) 2024-06-01 16:01:23 -05:00
rebel onion
19cd0596a5 New translations strings.xml (Spanish, Mexico) 2024-06-01 16:01:22 -05:00
rebel onion
603fbf6254 New translations strings.xml (Chinese Simplified) 2024-06-01 16:01:21 -05:00
rebel onion
317958257c New translations strings.xml (Russian) 2024-06-01 16:01:20 -05:00
rebel onion
51cc16013a New translations strings.xml (Japanese) 2024-06-01 16:01:18 -05:00
rebel onion
6b565ba0ab New translations strings.xml (German) 2024-06-01 16:01:17 -05:00
rebel onion
c1bbd34f51 New translations strings.xml (Arabic) 2024-06-01 16:01:16 -05:00
rebel onion
795ceb9f5b New translations strings.xml (Spanish) 2024-06-01 16:01:15 -05:00
rebel onion
74e73a663b New translations strings.xml (French) 2024-06-01 16:01:14 -05:00
rebel onion
d3f34f0bae New translations strings.xml (Turkish) 2024-06-01 10:27:08 -05:00
rebel onion
1e1cd929c9 New translations strings.xml (Polish) 2024-06-01 10:27:07 -05:00
rebel onion
b19f9326d1 New translations strings.xml (Portuguese, Brazilian) 2024-06-01 08:30:25 -05:00
rebel onion
cbe3e556ca New translations strings.xml (Turkish) 2024-06-01 08:30:24 -05:00
rebel onion
ea2a3fb56d New translations strings.xml (Polish) 2024-06-01 08:30:22 -05:00
rebel onion
3f5eb4b858 New translations strings.xml (Albanian) 2024-06-01 08:30:22 -05:00
rebel onion
898bba3827 New translations strings.xml (Urdu (Pakistan)) 2024-06-01 08:30:20 -05:00
rebel onion
63c3b1b5e8 New translations strings.xml (Dutch) 2024-06-01 08:30:19 -05:00
rebel onion
a19a896da3 New translations strings.xml (Italian) 2024-06-01 08:30:18 -05:00
rebel onion
ac5f810a12 New translations strings.xml (Bengali) 2024-06-01 08:30:17 -05:00
rebel onion
b03ad95e4c New translations strings.xml (Korean) 2024-06-01 08:30:16 -05:00
rebel onion
ed7d49644b New translations strings.xml (Greek) 2024-06-01 08:30:15 -05:00
rebel onion
73a7143ea3 New translations strings.xml (Assamese) 2024-06-01 08:30:14 -05:00
rebel onion
80de313a26 New translations strings.xml (Hindi) 2024-06-01 08:30:12 -05:00
rebel onion
fe2d89f6a3 New translations strings.xml (Spanish, Mexico) 2024-06-01 08:30:11 -05:00
rebel onion
e53ad65049 New translations strings.xml (Chinese Simplified) 2024-06-01 08:30:10 -05:00
rebel onion
fcda0eae62 New translations strings.xml (Russian) 2024-06-01 08:30:09 -05:00
rebel onion
01323c4d2c New translations strings.xml (Japanese) 2024-06-01 08:30:08 -05:00
rebel onion
d62705f509 New translations strings.xml (German) 2024-06-01 08:30:07 -05:00
rebel onion
3f26e78de1 New translations strings.xml (Arabic) 2024-06-01 08:30:06 -05:00
rebel onion
b47f231701 New translations strings.xml (Spanish) 2024-06-01 08:30:05 -05:00
rebel onion
ec8bb5634a New translations strings.xml (French) 2024-06-01 08:30:04 -05:00
rebel onion
bc39551fb1 New translations strings.xml (Portuguese, Brazilian) 2024-06-01 01:37:49 -05:00
rebel onion
a3e84ce41a New translations strings.xml (Bengali) 2024-06-01 01:37:48 -05:00
rebel onion
11ffca8650 New translations strings.xml (Spanish, Mexico) 2024-06-01 00:08:28 -05:00
rebel onion
4584b1b74d New translations strings.xml (Portuguese, Brazilian) 2024-05-31 12:20:30 -05:00
rebel onion
1afee8c9ee New translations strings.xml (Italian) 2024-05-31 12:20:29 -05:00
rebel onion
5012c4efa6 New translations strings.xml (Korean) 2024-05-31 12:20:28 -05:00
rebel onion
83af384e80 New translations strings.xml (Greek) 2024-05-31 12:20:27 -05:00
rebel onion
6b32ea4acb New translations strings.xml (Hindi) 2024-05-31 12:20:25 -05:00
rebel onion
f677121794 New translations strings.xml (Spanish, Mexico) 2024-05-31 12:20:24 -05:00
rebel onion
7118dfc74a New translations strings.xml (Russian) 2024-05-31 12:20:23 -05:00
rebel onion
55f400c205 New translations strings.xml (Japanese) 2024-05-31 12:20:22 -05:00
rebel onion
9ba281f552 New translations strings.xml (Spanish) 2024-05-31 12:20:20 -05:00
rebel onion
beece59947 New translations strings.xml (Albanian) 2024-05-31 11:06:42 -05:00
rebel onion
0d43fc0781 New translations strings.xml (Dutch) 2024-05-31 11:06:41 -05:00
rebel onion
e14142b4f6 New translations strings.xml (Assamese) 2024-05-31 11:06:40 -05:00
rebel onion
765b6d2452 New translations strings.xml (Chinese Simplified) 2024-05-31 11:06:39 -05:00
rebel onion
076d821e8c New translations strings.xml (German) 2024-05-31 11:06:37 -05:00
rebel onion
17985bf61f New translations strings.xml (French) 2024-05-31 11:06:36 -05:00
rebel onion
03dbeddabb New translations strings.xml (Arabic) 2024-05-30 17:50:45 -05:00
rebel onion
ae372998f0 New translations strings.xml (Turkish) 2024-05-29 10:31:36 -05:00
rebel onion
262ce571ba New translations strings.xml (Polish) 2024-05-29 07:34:57 -05:00
rebel onion
c0eb96f3cd New translations strings.xml (Portuguese, Brazilian) 2024-05-28 15:42:22 -05:00
rebel onion
cfbf4e109d New translations strings.xml (Turkish) 2024-05-28 15:42:21 -05:00
rebel onion
e05af0dfcc New translations strings.xml (Polish) 2024-05-28 15:42:20 -05:00
rebel onion
64f078263f New translations strings.xml (Albanian) 2024-05-28 15:42:19 -05:00
rebel onion
c4aa8c91f8 New translations strings.xml (Urdu (Pakistan)) 2024-05-28 15:42:18 -05:00
rebel onion
a91285a40f New translations strings.xml (Dutch) 2024-05-28 15:42:17 -05:00
rebel onion
d56578b8be New translations strings.xml (Italian) 2024-05-28 15:42:16 -05:00
rebel onion
b05c14e880 New translations strings.xml (Bengali) 2024-05-28 15:42:14 -05:00
rebel onion
60644590f4 New translations strings.xml (Korean) 2024-05-28 15:42:13 -05:00
rebel onion
a8b1dc11a5 New translations strings.xml (Greek) 2024-05-28 15:42:12 -05:00
rebel onion
2cbeb28cd7 New translations strings.xml (Assamese) 2024-05-28 15:42:11 -05:00
rebel onion
579c23907f New translations strings.xml (Hindi) 2024-05-28 15:42:10 -05:00
rebel onion
a16ae62fb3 New translations strings.xml (Spanish, Mexico) 2024-05-28 15:42:09 -05:00
rebel onion
0bf623c9d4 New translations strings.xml (Chinese Simplified) 2024-05-28 15:42:08 -05:00
rebel onion
67dd9b8e01 New translations strings.xml (Russian) 2024-05-28 15:42:06 -05:00
rebel onion
7b1824cf36 New translations strings.xml (Japanese) 2024-05-28 15:42:05 -05:00
rebel onion
085adbfdaa New translations strings.xml (German) 2024-05-28 15:42:04 -05:00
rebel onion
7fb70627f9 New translations strings.xml (Arabic) 2024-05-28 15:42:03 -05:00
rebel onion
ea12e5a054 New translations strings.xml (Spanish) 2024-05-28 15:42:02 -05:00
rebel onion
a5b24ac8b7 New translations strings.xml (French) 2024-05-28 15:42:01 -05:00
rebel onion
830ca60883 New translations strings.xml (Arabic) 2024-05-28 04:26:12 -05:00
rebel onion
f0c1bc88a5 New translations strings.xml (Polish) 2024-05-27 13:11:17 -05:00
rebel onion
cea67261d5 New translations strings.xml (Turkish) 2024-05-27 10:23:04 -05:00
rebel onion
c33934c211 New translations strings.xml (Portuguese, Brazilian) 2024-05-27 07:25:36 -05:00
rebel onion
3ec0b9aba2 New translations strings.xml (Turkish) 2024-05-27 07:25:35 -05:00
rebel onion
748b359615 New translations strings.xml (Polish) 2024-05-27 07:25:33 -05:00
rebel onion
e22734dd54 New translations strings.xml (Albanian) 2024-05-27 07:25:32 -05:00
rebel onion
6dc6f801a8 New translations strings.xml (Urdu (Pakistan)) 2024-05-27 07:25:31 -05:00
rebel onion
c6c95cdde7 New translations strings.xml (Dutch) 2024-05-27 07:25:29 -05:00
rebel onion
c0d0d931b0 New translations strings.xml (Italian) 2024-05-27 07:25:28 -05:00
rebel onion
991a040f3f New translations strings.xml (Bengali) 2024-05-27 07:25:27 -05:00
rebel onion
29b9e57bd8 New translations strings.xml (Korean) 2024-05-27 07:25:25 -05:00
rebel onion
f52bd0e51c New translations strings.xml (Greek) 2024-05-27 07:25:24 -05:00
rebel onion
b8c03ecc10 New translations strings.xml (Assamese) 2024-05-27 07:25:22 -05:00
rebel onion
7c09419fae New translations strings.xml (Hindi) 2024-05-27 07:25:21 -05:00
rebel onion
576c99d702 New translations strings.xml (Spanish, Mexico) 2024-05-27 07:25:19 -05:00
rebel onion
a7c2f60307 New translations strings.xml (Chinese Simplified) 2024-05-27 07:25:18 -05:00
rebel onion
2a2101d926 New translations strings.xml (Russian) 2024-05-27 07:25:17 -05:00
rebel onion
1379617f23 New translations strings.xml (Japanese) 2024-05-27 07:25:15 -05:00
rebel onion
85fba27644 New translations strings.xml (German) 2024-05-27 07:25:14 -05:00
rebel onion
17084b4ec3 New translations strings.xml (Arabic) 2024-05-27 07:25:12 -05:00
rebel onion
b77d6076ab New translations strings.xml (Spanish) 2024-05-27 07:25:11 -05:00
rebel onion
2997aa9784 New translations strings.xml (French) 2024-05-27 07:25:09 -05:00
rebel onion
df77ec6ee1 New translations strings.xml (Albanian) 2024-05-26 00:38:08 -05:00
rebel onion
d5409a80a3 New translations strings.xml (Dutch) 2024-05-26 00:38:07 -05:00
rebel onion
c42c0b49f9 New translations strings.xml (Italian) 2024-05-26 00:38:05 -05:00
rebel onion
23893f88d5 New translations strings.xml (Korean) 2024-05-26 00:38:04 -05:00
rebel onion
240d4faf09 New translations strings.xml (Greek) 2024-05-26 00:38:03 -05:00
rebel onion
0e8eb5eb16 New translations strings.xml (Assamese) 2024-05-26 00:38:02 -05:00
rebel onion
827dae53a5 New translations strings.xml (Hindi) 2024-05-26 00:38:01 -05:00
rebel onion
6d49928a11 New translations strings.xml (Chinese Simplified) 2024-05-26 00:38:00 -05:00
rebel onion
f63028c98c New translations strings.xml (Russian) 2024-05-26 00:37:59 -05:00
rebel onion
a4d8100b73 New translations strings.xml (Japanese) 2024-05-26 00:37:58 -05:00
rebel onion
34d328507d New translations strings.xml (German) 2024-05-26 00:37:56 -05:00
rebel onion
245384c720 New translations strings.xml (Spanish) 2024-05-26 00:37:55 -05:00
rebel onion
fb7a6e57b2 New translations strings.xml (French) 2024-05-26 00:37:54 -05:00
rebel onion
5af59582df New translations strings.xml (Polish) 2024-05-25 17:04:53 -05:00
rebel onion
8e7647a3b5 New translations strings.xml (Polish) 2024-05-25 15:59:51 -05:00
rebel onion
c57e1ad0e9 New translations strings.xml (Polish) 2024-05-25 15:04:44 -05:00
rebel onion
953183a956 New translations strings.xml (Turkish) 2024-05-25 14:04:31 -05:00
rebel onion
29ab73e5c9 New translations strings.xml (Polish) 2024-05-25 14:04:30 -05:00
rebel onion
eced8e1ea6 New translations strings.xml (Polish) 2024-05-25 12:51:42 -05:00
rebel onion
600a6898f1 New translations strings.xml (Polish) 2024-05-25 11:55:31 -05:00
rebel onion
9e14c03322 New translations strings.xml (Portuguese, Brazilian) 2024-05-25 10:54:04 -05:00
rebel onion
0125d075b2 New translations strings.xml (Turkish) 2024-05-25 10:54:03 -05:00
rebel onion
5e68906a22 New translations strings.xml (Polish) 2024-05-25 10:54:02 -05:00
rebel onion
a0618d1b5d New translations strings.xml (Albanian) 2024-05-25 10:54:01 -05:00
rebel onion
e5a0a2fa34 New translations strings.xml (Urdu (Pakistan)) 2024-05-25 10:54:00 -05:00
rebel onion
25cc16c408 New translations strings.xml (Dutch) 2024-05-25 10:53:59 -05:00
rebel onion
3f430fc8e0 New translations strings.xml (Italian) 2024-05-25 10:53:58 -05:00
rebel onion
5a3bb27566 New translations strings.xml (Bengali) 2024-05-25 10:53:57 -05:00
rebel onion
31fed55966 New translations strings.xml (Korean) 2024-05-25 10:53:56 -05:00
rebel onion
d056cd2f13 New translations strings.xml (Greek) 2024-05-25 10:53:54 -05:00
rebel onion
06029f2827 New translations strings.xml (Assamese) 2024-05-25 10:53:53 -05:00
rebel onion
c07efc1595 New translations strings.xml (Hindi) 2024-05-25 10:53:52 -05:00
rebel onion
93a320ded2 New translations strings.xml (Spanish, Mexico) 2024-05-25 10:53:51 -05:00
rebel onion
b14778c7ac New translations strings.xml (Chinese Simplified) 2024-05-25 10:53:50 -05:00
rebel onion
73544c3fc1 New translations strings.xml (Russian) 2024-05-25 10:53:49 -05:00
rebel onion
bfaf4f6f96 New translations strings.xml (Japanese) 2024-05-25 10:53:47 -05:00
rebel onion
84c0fe7100 New translations strings.xml (German) 2024-05-25 10:53:46 -05:00
rebel onion
a541161bd0 New translations strings.xml (Arabic) 2024-05-25 10:53:45 -05:00
rebel onion
d6496ba028 New translations strings.xml (Spanish) 2024-05-25 10:53:44 -05:00
rebel onion
3d6d0c70a0 New translations strings.xml (French) 2024-05-25 10:53:43 -05:00
rebel onion
7461de1292 New translations strings.xml (Polish) 2024-05-25 09:40:16 -05:00
rebel onion
4a3deb30b8 New translations strings.xml (Polish) 2024-05-25 07:03:46 -05:00
rebel onion
bba3cbfb47 New translations strings.xml (Polish) 2024-05-25 05:46:04 -05:00
rebel onion
bba915b898 New translations strings.xml (Albanian) 2024-05-25 04:12:28 -05:00
rebel onion
f8934ef315 New translations strings.xml (Dutch) 2024-05-25 04:12:26 -05:00
rebel onion
4e84e5d32c New translations strings.xml (Italian) 2024-05-25 04:12:25 -05:00
rebel onion
536ccfd38f New translations strings.xml (Korean) 2024-05-25 04:12:24 -05:00
rebel onion
514c24fefa New translations strings.xml (Greek) 2024-05-25 04:12:23 -05:00
rebel onion
e3b556bdd0 New translations strings.xml (Assamese) 2024-05-25 04:12:22 -05:00
rebel onion
dea15469c1 New translations strings.xml (Hindi) 2024-05-25 04:12:21 -05:00
rebel onion
442fdec098 New translations strings.xml (Chinese Simplified) 2024-05-25 04:12:19 -05:00
rebel onion
3b65bfa2d5 New translations strings.xml (Russian) 2024-05-25 04:12:18 -05:00
rebel onion
ae5b47db1e New translations strings.xml (Japanese) 2024-05-25 04:12:17 -05:00
rebel onion
cb4accec08 New translations strings.xml (German) 2024-05-25 04:12:16 -05:00
rebel onion
1349640272 New translations strings.xml (Spanish) 2024-05-25 04:12:15 -05:00
rebel onion
f65e1f6e55 New translations strings.xml (French) 2024-05-25 04:12:14 -05:00
rebel onion
6eb7cd0f26 New translations strings.xml (Portuguese, Brazilian) 2024-05-24 17:55:30 -05:00
rebel onion
bff337ac03 New translations strings.xml (Portuguese, Brazilian) 2024-05-24 16:13:53 -05:00
rebel onion
11a6cdd2e5 New translations strings.xml (Portuguese, Brazilian) 2024-05-24 12:41:01 -05:00
rebel onion
d6a72097c0 New translations strings.xml (Polish) 2024-05-24 07:19:31 -05:00
rebel onion
d166294de6 New translations strings.xml (Polish) 2024-05-24 05:30:59 -05:00
rebel onion
35cd54a7d9 New translations strings.xml (Turkish) 2024-05-22 16:35:42 -05:00
rebel onion
4a12d8e9b3 New translations strings.xml (Portuguese, Brazilian) 2024-05-22 05:18:08 -05:00
rebel onion
0a4164431f New translations strings.xml (Turkish) 2024-05-22 05:18:07 -05:00
rebel onion
d04673204e New translations strings.xml (Polish) 2024-05-22 05:18:06 -05:00
rebel onion
8a56ca74a5 New translations strings.xml (Albanian) 2024-05-22 05:18:05 -05:00
rebel onion
b1a2f89693 New translations strings.xml (Urdu (Pakistan)) 2024-05-22 05:18:03 -05:00
rebel onion
3c3eb138e0 New translations strings.xml (Dutch) 2024-05-22 05:18:02 -05:00
rebel onion
01599086f7 New translations strings.xml (Italian) 2024-05-22 05:18:01 -05:00
rebel onion
744dc3082b New translations strings.xml (Bengali) 2024-05-22 05:17:59 -05:00
rebel onion
7363a7c2d4 New translations strings.xml (Korean) 2024-05-22 05:17:58 -05:00
rebel onion
79136ba5b7 New translations strings.xml (Greek) 2024-05-22 05:17:57 -05:00
rebel onion
c951eefdd4 New translations strings.xml (Assamese) 2024-05-22 05:17:56 -05:00
rebel onion
f44cb5e57d New translations strings.xml (Hindi) 2024-05-22 05:17:54 -05:00
rebel onion
e5cbb6afeb New translations strings.xml (Spanish, Mexico) 2024-05-22 05:17:53 -05:00
rebel onion
18722a45ba New translations strings.xml (Chinese Simplified) 2024-05-22 05:17:52 -05:00
rebel onion
88f6fb61c9 New translations strings.xml (Russian) 2024-05-22 05:17:51 -05:00
rebel onion
68802999ce New translations strings.xml (Japanese) 2024-05-22 05:17:49 -05:00
rebel onion
fd8c84d3c0 New translations strings.xml (German) 2024-05-22 05:17:47 -05:00
rebel onion
25e7496927 New translations strings.xml (Arabic) 2024-05-22 05:17:46 -05:00
rebel onion
72a24df665 New translations strings.xml (Spanish) 2024-05-22 05:17:45 -05:00
rebel onion
d832ce6410 New translations strings.xml (French) 2024-05-22 05:17:44 -05:00
rebel onion
5d4d91c737 New translations strings.xml (Polish) 2024-05-21 00:32:46 -05:00
rebel onion
c617b72483 New translations strings.xml (Dutch) 2024-05-21 00:32:45 -05:00
rebel onion
83dc58cc82 New translations strings.xml (Italian) 2024-05-21 00:32:44 -05:00
rebel onion
79b9a4a649 New translations strings.xml (Korean) 2024-05-21 00:32:43 -05:00
rebel onion
4794037b2b New translations strings.xml (Greek) 2024-05-21 00:32:41 -05:00
rebel onion
f7a4853e78 New translations strings.xml (Assamese) 2024-05-21 00:32:40 -05:00
rebel onion
27fef48103 New translations strings.xml (Hindi) 2024-05-21 00:32:39 -05:00
rebel onion
6ec9ab7c81 New translations strings.xml (Chinese Simplified) 2024-05-21 00:32:38 -05:00
rebel onion
332d607f42 New translations strings.xml (Russian) 2024-05-21 00:32:37 -05:00
rebel onion
95da8c4197 New translations strings.xml (Japanese) 2024-05-21 00:32:36 -05:00
rebel onion
a4cb07eb3b New translations strings.xml (Spanish) 2024-05-21 00:32:35 -05:00
rebel onion
9dcc7b7a25 New translations strings.xml (French) 2024-05-21 00:32:34 -05:00
rebel onion
4ea399a6e4 New translations strings.xml (Albanian) 2024-05-20 23:30:00 -05:00
rebel onion
ec3366bcce New translations strings.xml (Turkish) 2024-05-20 18:45:28 -05:00
rebel onion
35a217ae6e New translations strings.xml (German) 2024-05-20 17:35:47 -05:00
rebel onion
3ef634693d New translations strings.xml (Portuguese, Brazilian) 2024-05-20 12:33:53 -05:00
rebel onion
3a925edd63 New translations strings.xml (Turkish) 2024-05-20 12:33:52 -05:00
rebel onion
e7e85e0c2f New translations strings.xml (Polish) 2024-05-20 12:33:51 -05:00
rebel onion
aec6d40af8 New translations strings.xml (Albanian) 2024-05-20 12:33:49 -05:00
rebel onion
128ba28da8 New translations strings.xml (Urdu (Pakistan)) 2024-05-20 12:33:48 -05:00
rebel onion
297f21a9dd New translations strings.xml (Dutch) 2024-05-20 12:33:47 -05:00
rebel onion
77a0371c4b New translations strings.xml (Italian) 2024-05-20 12:33:46 -05:00
rebel onion
7d1528ca1e New translations strings.xml (Bengali) 2024-05-20 12:33:45 -05:00
rebel onion
b82b0c6167 New translations strings.xml (Korean) 2024-05-20 12:33:44 -05:00
rebel onion
bd59707f61 New translations strings.xml (Greek) 2024-05-20 12:33:42 -05:00
rebel onion
4130ea2dd3 New translations strings.xml (Assamese) 2024-05-20 12:33:41 -05:00
rebel onion
86db993344 New translations strings.xml (Hindi) 2024-05-20 12:33:40 -05:00
rebel onion
accf7e117b New translations strings.xml (Spanish, Mexico) 2024-05-20 12:33:39 -05:00
rebel onion
63fe89f064 New translations strings.xml (Chinese Simplified) 2024-05-20 12:33:38 -05:00
rebel onion
8ce0b64fe3 New translations strings.xml (Russian) 2024-05-20 12:33:37 -05:00
rebel onion
5a2610fd9e New translations strings.xml (Japanese) 2024-05-20 12:33:36 -05:00
rebel onion
f3977f54d5 New translations strings.xml (German) 2024-05-20 12:33:35 -05:00
rebel onion
e82e6200b0 New translations strings.xml (Arabic) 2024-05-20 12:33:33 -05:00
rebel onion
4b9036a4ce New translations strings.xml (Spanish) 2024-05-20 12:33:32 -05:00
rebel onion
33c4d9c244 New translations strings.xml (French) 2024-05-20 12:33:31 -05:00
rebel onion
14115678ef New translations strings.xml (Portuguese, Brazilian) 2024-05-20 11:34:11 -05:00
rebel onion
d5fb143f28 New translations strings.xml (Turkish) 2024-05-20 11:34:10 -05:00
rebel onion
6ab5eb31ca New translations strings.xml (Polish) 2024-05-20 11:34:09 -05:00
rebel onion
3e75bc405c New translations strings.xml (Albanian) 2024-05-20 11:34:07 -05:00
rebel onion
8405c38557 New translations strings.xml (Urdu (Pakistan)) 2024-05-20 11:34:06 -05:00
rebel onion
9818eb699e New translations strings.xml (Dutch) 2024-05-20 11:34:05 -05:00
rebel onion
21a378e078 New translations strings.xml (Italian) 2024-05-20 11:34:04 -05:00
rebel onion
9a3a96ed19 New translations strings.xml (Bengali) 2024-05-20 11:34:02 -05:00
rebel onion
bd30cc4b0f New translations strings.xml (Korean) 2024-05-20 11:34:01 -05:00
rebel onion
02d04ecb57 New translations strings.xml (Greek) 2024-05-20 11:34:00 -05:00
rebel onion
e23311e817 New translations strings.xml (Assamese) 2024-05-20 11:33:59 -05:00
rebel onion
60fe855813 New translations strings.xml (Hindi) 2024-05-20 11:33:58 -05:00
rebel onion
5c0bd7bf90 New translations strings.xml (Spanish, Mexico) 2024-05-20 11:33:57 -05:00
rebel onion
d4d6e8c5a0 New translations strings.xml (Chinese Simplified) 2024-05-20 11:33:56 -05:00
rebel onion
e779dfbe9d New translations strings.xml (Russian) 2024-05-20 11:33:54 -05:00
rebel onion
9e39a3cc64 New translations strings.xml (Japanese) 2024-05-20 11:33:53 -05:00
rebel onion
7e02a7d928 New translations strings.xml (German) 2024-05-20 11:33:52 -05:00
rebel onion
8c3fe1bd80 New translations strings.xml (Arabic) 2024-05-20 11:33:51 -05:00
rebel onion
0b72160c93 New translations strings.xml (Spanish) 2024-05-20 11:33:50 -05:00
rebel onion
f529a0cc3f New translations strings.xml (French) 2024-05-20 11:33:49 -05:00
rebel onion
b4520d91b5 New translations strings.xml (Polish) 2024-05-20 01:10:10 -05:00
rebel onion
771985d063 New translations strings.xml (Korean) 2024-05-20 01:10:09 -05:00
rebel onion
feba28f55d New translations strings.xml (Russian) 2024-05-20 01:10:08 -05:00
rebel onion
32d3376d44 New translations strings.xml (Spanish) 2024-05-20 01:10:07 -05:00
rebel onion
7ae51daf39 New translations strings.xml (Albanian) 2024-05-19 23:53:53 -05:00
rebel onion
b5a7e69040 New translations strings.xml (Dutch) 2024-05-19 23:53:52 -05:00
rebel onion
7f431a1e7b New translations strings.xml (Italian) 2024-05-19 23:53:51 -05:00
rebel onion
c2a7aae848 New translations strings.xml (Greek) 2024-05-19 23:53:50 -05:00
rebel onion
9a3fff9c09 New translations strings.xml (Assamese) 2024-05-19 23:53:49 -05:00
rebel onion
90ad8b016e New translations strings.xml (Hindi) 2024-05-19 23:53:47 -05:00
rebel onion
ed6fc25507 New translations strings.xml (Chinese Simplified) 2024-05-19 23:53:46 -05:00
rebel onion
1b13ef9f34 New translations strings.xml (Japanese) 2024-05-19 23:53:45 -05:00
rebel onion
7f7e7e2e82 New translations strings.xml (German) 2024-05-19 23:53:44 -05:00
rebel onion
dfdc866dfc New translations strings.xml (French) 2024-05-19 23:53:43 -05:00
rebel onion
58021c5813 New translations strings.xml (Portuguese, Brazilian) 2024-05-19 15:29:25 -05:00
rebel onion
aa1fb1d921 New translations strings.xml (Turkish) 2024-05-19 15:29:24 -05:00
rebel onion
25cb70e766 New translations strings.xml (Polish) 2024-05-19 15:29:23 -05:00
rebel onion
48dd69fbf6 New translations strings.xml (Albanian) 2024-05-19 15:29:22 -05:00
rebel onion
bfa2bcb3e4 New translations strings.xml (Urdu (Pakistan)) 2024-05-19 15:29:21 -05:00
rebel onion
ebad32f0d3 New translations strings.xml (Dutch) 2024-05-19 15:29:20 -05:00
rebel onion
167dd4dff0 New translations strings.xml (Italian) 2024-05-19 15:29:19 -05:00
rebel onion
f9cf7365fd New translations strings.xml (Bengali) 2024-05-19 15:29:18 -05:00
rebel onion
c03b5100d3 New translations strings.xml (Korean) 2024-05-19 15:29:17 -05:00
rebel onion
745a605052 New translations strings.xml (Greek) 2024-05-19 15:29:16 -05:00
rebel onion
f6e63144fc New translations strings.xml (Assamese) 2024-05-19 15:29:15 -05:00
rebel onion
65b6ff90c3 New translations strings.xml (Hindi) 2024-05-19 15:29:14 -05:00
rebel onion
0261a20572 New translations strings.xml (Spanish, Mexico) 2024-05-19 15:29:12 -05:00
rebel onion
878d0b7e42 New translations strings.xml (Chinese Simplified) 2024-05-19 15:29:11 -05:00
rebel onion
1705f29cbf New translations strings.xml (Russian) 2024-05-19 15:29:10 -05:00
rebel onion
22abc94065 New translations strings.xml (Japanese) 2024-05-19 15:29:09 -05:00
rebel onion
5ad1ee917e New translations strings.xml (German) 2024-05-19 15:29:08 -05:00
rebel onion
37dcf7e9a4 New translations strings.xml (Arabic) 2024-05-19 15:29:07 -05:00
rebel onion
bf270794e9 New translations strings.xml (Spanish) 2024-05-19 15:29:06 -05:00
rebel onion
e8c43da0c9 New translations strings.xml (French) 2024-05-19 15:29:05 -05:00
rebel onion
f910c6da18 New translations strings.xml (Bengali) 2024-05-19 13:51:22 -05:00
rebel onion
8a76fd33a9 New translations strings.xml (Bengali) 2024-05-19 12:35:37 -05:00
rebel onion
5c4153cb6b New translations strings.xml (Turkish) 2024-05-19 06:00:22 -05:00
rebel onion
7f0ee95236 New translations strings.xml (Polish) 2024-05-19 01:52:47 -05:00
rebel onion
c3e47fc417 New translations strings.xml (Dutch) 2024-05-19 01:52:46 -05:00
rebel onion
555490129c New translations strings.xml (Italian) 2024-05-19 01:52:45 -05:00
rebel onion
2dcc49c6a9 New translations strings.xml (Korean) 2024-05-19 01:52:44 -05:00
rebel onion
0d294990c5 New translations strings.xml (Greek) 2024-05-19 01:52:43 -05:00
rebel onion
9924aa4e93 New translations strings.xml (Assamese) 2024-05-19 01:52:42 -05:00
rebel onion
4f44cf6840 New translations strings.xml (Hindi) 2024-05-19 01:52:41 -05:00
rebel onion
d9e4d2da95 New translations strings.xml (Chinese Simplified) 2024-05-19 01:52:40 -05:00
rebel onion
fdc46093c6 New translations strings.xml (Russian) 2024-05-19 01:52:39 -05:00
rebel onion
0cff00c426 New translations strings.xml (Japanese) 2024-05-19 01:52:38 -05:00
rebel onion
5f22941164 New translations strings.xml (German) 2024-05-19 01:52:37 -05:00
rebel onion
4f6c587388 New translations strings.xml (Spanish) 2024-05-19 01:52:36 -05:00
rebel onion
b2349c9757 New translations strings.xml (French) 2024-05-19 01:52:35 -05:00
rebel onion
47cd0648ac New translations strings.xml (Albanian) 2024-05-19 00:14:41 -05:00
rebel onion
70746f8786 New translations strings.xml (Portuguese, Brazilian) 2024-05-18 11:49:24 -05:00
rebel onion
919fd87bbc New translations strings.xml (Turkish) 2024-05-18 11:49:23 -05:00
rebel onion
a80c0a6a3b New translations strings.xml (Polish) 2024-05-18 11:49:22 -05:00
rebel onion
7f78515dd0 New translations strings.xml (Albanian) 2024-05-18 11:49:21 -05:00
rebel onion
d466d47751 New translations strings.xml (Urdu (Pakistan)) 2024-05-18 11:49:20 -05:00
rebel onion
a834620606 New translations strings.xml (Dutch) 2024-05-18 11:49:19 -05:00
rebel onion
b898001fa3 New translations strings.xml (Italian) 2024-05-18 11:49:18 -05:00
rebel onion
c9eb224aaa New translations strings.xml (Bengali) 2024-05-18 11:49:17 -05:00
rebel onion
885c764c87 New translations strings.xml (Korean) 2024-05-18 11:49:16 -05:00
rebel onion
99996bb82f New translations strings.xml (Greek) 2024-05-18 11:49:15 -05:00
rebel onion
9b42b21d44 New translations strings.xml (Assamese) 2024-05-18 11:49:14 -05:00
rebel onion
d1fcd3394c New translations strings.xml (Hindi) 2024-05-18 11:49:12 -05:00
rebel onion
e54cab033b New translations strings.xml (Spanish, Mexico) 2024-05-18 11:49:11 -05:00
rebel onion
9a01ae279c New translations strings.xml (Chinese Simplified) 2024-05-18 11:49:10 -05:00
rebel onion
cfc67e2700 New translations strings.xml (Russian) 2024-05-18 11:49:09 -05:00
rebel onion
996d35408a New translations strings.xml (Japanese) 2024-05-18 11:49:08 -05:00
rebel onion
333af057f4 New translations strings.xml (German) 2024-05-18 11:49:07 -05:00
rebel onion
45d7d06e0f New translations strings.xml (Arabic) 2024-05-18 11:49:06 -05:00
rebel onion
acce63ce24 New translations strings.xml (Spanish) 2024-05-18 11:49:05 -05:00
rebel onion
80c9ce35cb New translations strings.xml (French) 2024-05-18 11:49:04 -05:00
rebel onion
e51a64c242 New translations strings.xml (Albanian) 2024-05-17 12:20:36 -05:00
rebel onion
59cf2e4d79 New translations strings.xml (Arabic) 2024-05-17 12:20:35 -05:00
rebel onion
7edfc676f1 New translations strings.xml (Portuguese, Brazilian) 2024-05-17 10:24:10 -05:00
rebel onion
f197b71950 New translations strings.xml (Turkish) 2024-05-17 10:24:09 -05:00
rebel onion
c61404545c New translations strings.xml (Polish) 2024-05-17 10:24:07 -05:00
rebel onion
9d349dae5b New translations strings.xml (Albanian) 2024-05-17 10:24:06 -05:00
rebel onion
779ee45b20 New translations strings.xml (Urdu (Pakistan)) 2024-05-17 10:24:05 -05:00
rebel onion
f0c5802471 New translations strings.xml (Dutch) 2024-05-17 10:24:04 -05:00
rebel onion
4b7dc3921c New translations strings.xml (Italian) 2024-05-17 10:24:02 -05:00
rebel onion
100e96f843 New translations strings.xml (Bengali) 2024-05-17 10:24:01 -05:00
rebel onion
30391805dc New translations strings.xml (Korean) 2024-05-17 10:24:00 -05:00
rebel onion
49f0424956 New translations strings.xml (Greek) 2024-05-17 10:23:59 -05:00
rebel onion
eea76dde54 New translations strings.xml (Assamese) 2024-05-17 10:23:58 -05:00
rebel onion
74be4a8e7f New translations strings.xml (Hindi) 2024-05-17 10:23:56 -05:00
rebel onion
4452839d76 New translations strings.xml (Spanish, Mexico) 2024-05-17 10:23:55 -05:00
rebel onion
eae3785c21 New translations strings.xml (Chinese Simplified) 2024-05-17 10:23:54 -05:00
rebel onion
d818abc578 New translations strings.xml (Russian) 2024-05-17 10:23:53 -05:00
rebel onion
80f8024ae8 New translations strings.xml (Japanese) 2024-05-17 10:23:52 -05:00
rebel onion
294a3a168a New translations strings.xml (German) 2024-05-17 10:23:51 -05:00
rebel onion
59b1e4203a New translations strings.xml (Arabic) 2024-05-17 10:23:50 -05:00
rebel onion
598613e189 New translations strings.xml (Spanish) 2024-05-17 10:23:48 -05:00
rebel onion
2e69f2f967 New translations strings.xml (French) 2024-05-17 10:23:47 -05:00
rebel onion
ee60f5e59b New translations strings.xml (Arabic) 2024-05-17 05:11:30 -05:00
rebel onion
c652792404 New translations strings.xml (Arabic) 2024-05-17 04:02:12 -05:00
rebel onion
af04890cd9 New translations strings.xml (German) 2024-05-16 18:34:04 -05:00
rebel onion
6bdae9f53e New translations strings.xml (Turkish) 2024-05-16 16:15:52 -05:00
rebel onion
10657bfd78 New translations strings.xml (Portuguese, Brazilian) 2024-05-16 15:19:38 -05:00
rebel onion
400570f4b9 New translations strings.xml (Turkish) 2024-05-16 15:19:37 -05:00
rebel onion
48eacbc5cc New translations strings.xml (Polish) 2024-05-16 15:19:36 -05:00
rebel onion
787d2a4456 New translations strings.xml (Albanian) 2024-05-16 15:19:35 -05:00
rebel onion
a8088cdae7 New translations strings.xml (Urdu (Pakistan)) 2024-05-16 15:19:34 -05:00
rebel onion
29f3ce0278 New translations strings.xml (Dutch) 2024-05-16 15:19:32 -05:00
rebel onion
b787d9a27d New translations strings.xml (Italian) 2024-05-16 15:19:31 -05:00
rebel onion
8f72a4d6eb New translations strings.xml (Bengali) 2024-05-16 15:19:30 -05:00
rebel onion
b4faf8a0a5 New translations strings.xml (Korean) 2024-05-16 15:19:29 -05:00
rebel onion
f8042d2e99 New translations strings.xml (Greek) 2024-05-16 15:19:28 -05:00
rebel onion
f3a3707b95 New translations strings.xml (Assamese) 2024-05-16 15:19:26 -05:00
rebel onion
82e5f6b50f New translations strings.xml (Hindi) 2024-05-16 15:19:25 -05:00
rebel onion
b06784d9c1 New translations strings.xml (Spanish, Mexico) 2024-05-16 15:19:24 -05:00
rebel onion
6910fde9a2 New translations strings.xml (Chinese Simplified) 2024-05-16 15:19:22 -05:00
rebel onion
d70f82b26e New translations strings.xml (Russian) 2024-05-16 15:19:21 -05:00
rebel onion
0279b837ee New translations strings.xml (Japanese) 2024-05-16 15:19:20 -05:00
rebel onion
510d02c7cf New translations strings.xml (German) 2024-05-16 15:19:19 -05:00
rebel onion
cc5cd9f21a New translations strings.xml (Arabic) 2024-05-16 15:19:18 -05:00
rebel onion
8bdf09b71b New translations strings.xml (Spanish) 2024-05-16 15:19:17 -05:00
rebel onion
ffa8a19eca New translations strings.xml (French) 2024-05-16 15:19:16 -05:00
rebel onion
dcfdb48dc2 New translations strings.xml (Portuguese, Brazilian) 2024-05-15 15:34:57 -05:00
rebel onion
a418295ece New translations strings.xml (Portuguese, Brazilian) 2024-05-15 11:36:09 -05:00
rebel onion
fda5cf70fc New translations strings.xml (Portuguese, Brazilian) 2024-05-15 10:40:01 -05:00
rebel onion
40d3602f55 New translations strings.xml (Portuguese, Brazilian) 2024-05-15 03:45:42 -05:00
rebel onion
3e6382a051 New translations strings.xml (Turkish) 2024-05-14 16:37:42 -05:00
rebel onion
3dbd4f9cfc New translations strings.xml (Arabic) 2024-05-14 16:37:41 -05:00
rebel onion
a807c251d1 New translations strings.xml (Turkish) 2024-05-14 15:30:43 -05:00
rebel onion
9ec5532ea6 New translations strings.xml (Portuguese, Brazilian) 2024-05-14 10:01:59 -05:00
rebel onion
eb5f3045f4 New translations strings.xml (Portuguese, Brazilian) 2024-05-14 08:39:46 -05:00
rebel onion
3e72fa3430 New translations strings.xml (Polish) 2024-05-14 04:57:52 -05:00
rebel onion
59c6bc6e96 New translations strings.xml (Albanian) 2024-05-14 04:57:50 -05:00
rebel onion
28fc390837 New translations strings.xml (Dutch) 2024-05-14 04:57:49 -05:00
rebel onion
1a32f5d161 New translations strings.xml (Italian) 2024-05-14 04:57:48 -05:00
rebel onion
05642967a7 New translations strings.xml (Korean) 2024-05-14 04:57:47 -05:00
rebel onion
a6ac55f9f2 New translations strings.xml (Greek) 2024-05-14 04:57:45 -05:00
rebel onion
d83061b2bb New translations strings.xml (Assamese) 2024-05-14 04:57:44 -05:00
rebel onion
cb9a4960d2 New translations strings.xml (Hindi) 2024-05-14 04:57:43 -05:00
rebel onion
96ed8f2867 New translations strings.xml (Chinese Simplified) 2024-05-14 04:57:41 -05:00
rebel onion
743b73f0bf New translations strings.xml (Russian) 2024-05-14 04:57:40 -05:00
rebel onion
7ad26fe339 New translations strings.xml (Japanese) 2024-05-14 04:57:39 -05:00
rebel onion
359b6e8d28 New translations strings.xml (German) 2024-05-14 04:57:38 -05:00
rebel onion
f0051fcc22 New translations strings.xml (Spanish) 2024-05-14 04:57:36 -05:00
rebel onion
2dca6fe24c New translations strings.xml (French) 2024-05-14 04:57:35 -05:00
rebel onion
ce662ee561 New translations strings.xml (Arabic) 2024-05-13 14:18:12 -05:00
rebel onion
fb06244e1d New translations strings.xml (Arabic) 2024-05-13 13:11:33 -05:00
rebel onion
ee2d700298 New translations strings.xml (Turkish) 2024-05-13 03:40:26 -05:00
rebel onion
adcdadd0f1 New translations strings.xml (Portuguese, Brazilian) 2024-05-12 21:55:27 -05:00
rebel onion
1d5b752c42 New translations strings.xml (Turkish) 2024-05-12 21:55:25 -05:00
rebel onion
b70629c592 New translations strings.xml (Polish) 2024-05-12 21:55:24 -05:00
rebel onion
3fb72e39d4 New translations strings.xml (Albanian) 2024-05-12 21:55:22 -05:00
rebel onion
69275c7fc7 New translations strings.xml (Urdu (Pakistan)) 2024-05-12 21:55:20 -05:00
rebel onion
b747f9fc62 New translations strings.xml (Dutch) 2024-05-12 21:55:19 -05:00
rebel onion
a66237d108 New translations strings.xml (Italian) 2024-05-12 21:55:17 -05:00
rebel onion
97d53b7850 New translations strings.xml (Bengali) 2024-05-12 21:55:16 -05:00
rebel onion
c9a2a93908 New translations strings.xml (Korean) 2024-05-12 21:55:15 -05:00
rebel onion
7509e117c7 New translations strings.xml (Greek) 2024-05-12 21:55:14 -05:00
rebel onion
899dd5cb52 New translations strings.xml (Assamese) 2024-05-12 21:55:13 -05:00
rebel onion
751c020935 New translations strings.xml (Hindi) 2024-05-12 21:55:12 -05:00
rebel onion
6a751a66cd New translations strings.xml (Spanish, Mexico) 2024-05-12 21:55:11 -05:00
rebel onion
c21355e1af New translations strings.xml (Chinese Simplified) 2024-05-12 21:55:09 -05:00
rebel onion
ba90993038 New translations strings.xml (Russian) 2024-05-12 21:55:08 -05:00
rebel onion
c3841e14e0 New translations strings.xml (Japanese) 2024-05-12 21:55:07 -05:00
rebel onion
4e20be58a1 New translations strings.xml (German) 2024-05-12 21:55:06 -05:00
rebel onion
418fd2565d New translations strings.xml (Arabic) 2024-05-12 21:55:05 -05:00
rebel onion
464578bdb8 New translations strings.xml (Spanish) 2024-05-12 21:55:04 -05:00
rebel onion
f4712ed3d6 New translations strings.xml (French) 2024-05-12 21:55:03 -05:00
rebel onion
b48b335afb New translations strings.xml (Arabic) 2024-05-12 19:10:23 -05:00
rebel onion
c77710fa95 New translations strings.xml (Turkish) 2024-05-12 11:50:15 -05:00
rebel onion
402116ff4c New translations strings.xml (Portuguese, Brazilian) 2024-05-12 09:29:36 -05:00
rebel onion
c362239d15 New translations strings.xml (Turkish) 2024-05-12 09:29:35 -05:00
rebel onion
bed2816fb8 New translations strings.xml (Polish) 2024-05-12 09:29:34 -05:00
rebel onion
fd5bdecf88 New translations strings.xml (Albanian) 2024-05-12 09:29:33 -05:00
rebel onion
a96a1ec7e2 New translations strings.xml (Urdu (Pakistan)) 2024-05-12 09:29:32 -05:00
rebel onion
4e588cd59a New translations strings.xml (Dutch) 2024-05-12 09:29:30 -05:00
rebel onion
354f223d7e New translations strings.xml (Italian) 2024-05-12 09:29:29 -05:00
rebel onion
eacd16331c New translations strings.xml (Bengali) 2024-05-12 09:29:28 -05:00
rebel onion
5b215fe72a New translations strings.xml (Korean) 2024-05-12 09:29:27 -05:00
rebel onion
7ba70f4d44 New translations strings.xml (Greek) 2024-05-12 09:29:26 -05:00
rebel onion
732b61c160 New translations strings.xml (Assamese) 2024-05-12 09:29:25 -05:00
rebel onion
19f23f4ea1 New translations strings.xml (Hindi) 2024-05-12 09:29:24 -05:00
rebel onion
8c8641c1a8 New translations strings.xml (Spanish, Mexico) 2024-05-12 09:29:23 -05:00
rebel onion
495157a5c3 New translations strings.xml (Chinese Simplified) 2024-05-12 09:29:22 -05:00
rebel onion
d0b1eb44b5 New translations strings.xml (Russian) 2024-05-12 09:29:21 -05:00
rebel onion
aea4081784 New translations strings.xml (Japanese) 2024-05-12 09:29:20 -05:00
rebel onion
a2b70003ec New translations strings.xml (German) 2024-05-12 09:29:19 -05:00
rebel onion
e4b37dde33 New translations strings.xml (Arabic) 2024-05-12 09:29:17 -05:00
rebel onion
8236c18239 New translations strings.xml (Spanish) 2024-05-12 09:29:16 -05:00
rebel onion
aefff852bc New translations strings.xml (French) 2024-05-12 09:29:15 -05:00
rebel onion
f23187ba10 New translations strings.xml (Portuguese, Brazilian) 2024-05-12 08:30:10 -05:00
rebel onion
bf6c35388f New translations strings.xml (Turkish) 2024-05-12 08:30:09 -05:00
rebel onion
99fe7170f8 New translations strings.xml (Polish) 2024-05-12 08:30:08 -05:00
rebel onion
488822e454 New translations strings.xml (Albanian) 2024-05-12 08:30:07 -05:00
rebel onion
a9dc432786 New translations strings.xml (Urdu (Pakistan)) 2024-05-12 08:30:06 -05:00
rebel onion
a9632a9024 New translations strings.xml (Dutch) 2024-05-12 08:30:05 -05:00
rebel onion
e26ada0623 New translations strings.xml (Italian) 2024-05-12 08:30:04 -05:00
rebel onion
05f5548747 New translations strings.xml (Bengali) 2024-05-12 08:30:03 -05:00
rebel onion
cb95978428 New translations strings.xml (Korean) 2024-05-12 08:30:01 -05:00
rebel onion
f3371acb3b New translations strings.xml (Greek) 2024-05-12 08:30:00 -05:00
rebel onion
7f930cf0c9 New translations strings.xml (Assamese) 2024-05-12 08:29:59 -05:00
rebel onion
3136511af3 New translations strings.xml (Hindi) 2024-05-12 08:29:58 -05:00
rebel onion
aff8fd93db New translations strings.xml (Spanish, Mexico) 2024-05-12 08:29:57 -05:00
rebel onion
f6a4181658 New translations strings.xml (Chinese Simplified) 2024-05-12 08:29:56 -05:00
rebel onion
760489f6f9 New translations strings.xml (Russian) 2024-05-12 08:29:55 -05:00
rebel onion
3407301b98 New translations strings.xml (Japanese) 2024-05-12 08:29:54 -05:00
rebel onion
399c6c877c New translations strings.xml (German) 2024-05-12 08:29:53 -05:00
rebel onion
df98acdc9a New translations strings.xml (Arabic) 2024-05-12 08:29:52 -05:00
rebel onion
ca9f976241 New translations strings.xml (Spanish) 2024-05-12 08:29:51 -05:00
rebel onion
1d715cf631 New translations strings.xml (French) 2024-05-12 08:29:50 -05:00
rebel onion
704c604ee4 New translations strings.xml (Portuguese, Brazilian) 2024-05-12 06:35:16 -05:00
rebel onion
8c8d9f15d2 New translations strings.xml (Turkish) 2024-05-12 06:35:15 -05:00
rebel onion
94c53a0d33 New translations strings.xml (Polish) 2024-05-12 06:35:14 -05:00
rebel onion
5e48094d77 New translations strings.xml (Albanian) 2024-05-12 06:35:13 -05:00
rebel onion
57042c593a New translations strings.xml (Urdu (Pakistan)) 2024-05-12 06:35:12 -05:00
rebel onion
e930a24f48 New translations strings.xml (Dutch) 2024-05-12 06:35:11 -05:00
rebel onion
7052c19af4 New translations strings.xml (Italian) 2024-05-12 06:35:10 -05:00
rebel onion
59bb4270fb New translations strings.xml (Bengali) 2024-05-12 06:35:09 -05:00
rebel onion
8906a9b93e New translations strings.xml (Korean) 2024-05-12 06:35:08 -05:00
rebel onion
201007a46a New translations strings.xml (Greek) 2024-05-12 06:35:07 -05:00
rebel onion
41aa4edb91 New translations strings.xml (Assamese) 2024-05-12 06:35:06 -05:00
rebel onion
5635dfd447 New translations strings.xml (Hindi) 2024-05-12 06:35:05 -05:00
rebel onion
f48ca609e5 New translations strings.xml (Spanish, Mexico) 2024-05-12 06:35:04 -05:00
rebel onion
2df8d8690b New translations strings.xml (Chinese Simplified) 2024-05-12 06:35:02 -05:00
rebel onion
2782f89a21 New translations strings.xml (Russian) 2024-05-12 06:35:01 -05:00
rebel onion
6e1a831f69 New translations strings.xml (Japanese) 2024-05-12 06:35:00 -05:00
rebel onion
08f189753a New translations strings.xml (German) 2024-05-12 06:34:59 -05:00
rebel onion
048c1b0cc7 New translations strings.xml (Arabic) 2024-05-12 06:34:58 -05:00
rebel onion
946ce1873b New translations strings.xml (Spanish) 2024-05-12 06:34:57 -05:00
rebel onion
01d4dc7c2e New translations strings.xml (French) 2024-05-12 06:34:56 -05:00
rebel onion
705abf68be New translations strings.xml (Greek) 2024-05-12 05:33:59 -05:00
rebel onion
13eeaf5321 New translations strings.xml (Portuguese, Brazilian) 2024-05-12 04:37:50 -05:00
rebel onion
0a160bfba5 New translations strings.xml (Turkish) 2024-05-12 04:37:49 -05:00
rebel onion
ee70ab63b6 New translations strings.xml (Polish) 2024-05-12 04:37:48 -05:00
rebel onion
6b6e49efd3 New translations strings.xml (Albanian) 2024-05-12 04:37:47 -05:00
rebel onion
949d88736c New translations strings.xml (Urdu (Pakistan)) 2024-05-12 04:37:46 -05:00
rebel onion
22c2bf33c0 New translations strings.xml (Dutch) 2024-05-12 04:37:45 -05:00
rebel onion
39a62977fe New translations strings.xml (Italian) 2024-05-12 04:37:44 -05:00
rebel onion
6aea176253 New translations strings.xml (Bengali) 2024-05-12 04:37:43 -05:00
rebel onion
45e088a635 New translations strings.xml (Korean) 2024-05-12 04:37:42 -05:00
rebel onion
113db63e2d New translations strings.xml (Greek) 2024-05-12 04:37:41 -05:00
rebel onion
8fa9d696b1 New translations strings.xml (Assamese) 2024-05-12 04:37:40 -05:00
rebel onion
f64e68beb7 New translations strings.xml (Hindi) 2024-05-12 04:37:39 -05:00
rebel onion
a19ee36bf8 New translations strings.xml (Spanish, Mexico) 2024-05-12 04:37:38 -05:00
rebel onion
f6cab9cc54 New translations strings.xml (Chinese Simplified) 2024-05-12 04:37:36 -05:00
rebel onion
8beebf67e2 New translations strings.xml (Russian) 2024-05-12 04:37:35 -05:00
rebel onion
ab9a7cfa5a New translations strings.xml (Japanese) 2024-05-12 04:37:35 -05:00
rebel onion
45277ecfef New translations strings.xml (German) 2024-05-12 04:37:33 -05:00
rebel onion
dcb08b00f9 New translations strings.xml (Arabic) 2024-05-12 04:37:32 -05:00
rebel onion
126f526f9f New translations strings.xml (Spanish) 2024-05-12 04:37:31 -05:00
rebel onion
c750cf78a4 New translations strings.xml (French) 2024-05-12 04:37:30 -05:00
rebel onion
4e369e372b New translations strings.xml (Arabic) 2024-05-12 03:35:12 -05:00
rebel onion
150010839d New translations strings.xml (Arabic) 2024-05-12 02:39:24 -05:00
rebel onion
1dc4dfdef8 New translations strings.xml (Turkish) 2024-05-11 13:23:35 -05:00
rebel onion
2175796c6d New translations strings.xml (Portuguese, Brazilian) 2024-05-11 12:19:36 -05:00
rebel onion
42ad2e5a3d New translations strings.xml (Polish) 2024-05-11 11:21:14 -05:00
rebel onion
ebe4ad6b96 New translations strings.xml (Dutch) 2024-05-11 11:21:13 -05:00
rebel onion
9f70c63544 New translations strings.xml (Korean) 2024-05-11 11:21:12 -05:00
rebel onion
afb7c6cd7f New translations strings.xml (Greek) 2024-05-11 11:21:11 -05:00
rebel onion
9114c2a8c2 New translations strings.xml (Hindi) 2024-05-11 11:21:10 -05:00
rebel onion
33e29746b7 New translations strings.xml (Russian) 2024-05-11 11:21:09 -05:00
rebel onion
b2900c5751 New translations strings.xml (Japanese) 2024-05-11 11:21:08 -05:00
rebel onion
f368c066d6 New translations strings.xml (German) 2024-05-11 11:21:07 -05:00
rebel onion
5129d6a698 New translations strings.xml (Spanish) 2024-05-11 11:21:06 -05:00
rebel onion
65f7b25668 New translations strings.xml (French) 2024-05-11 11:21:05 -05:00
rebel onion
b7b05be5af New translations strings.xml (Dutch) 2024-05-11 10:13:00 -05:00
rebel onion
863de3bfa4 New translations strings.xml (Italian) 2024-05-11 10:12:59 -05:00
rebel onion
e4a925969d New translations strings.xml (Assamese) 2024-05-11 10:12:58 -05:00
rebel onion
6be81dfbea New translations strings.xml (Chinese Simplified) 2024-05-11 10:12:57 -05:00
rebel onion
72709618b0 New translations strings.xml (Arabic) 2024-05-11 10:12:56 -05:00
rebel onion
28e968cad4 New translations strings.xml (Portuguese, Brazilian) 2024-05-11 08:29:53 -05:00
rebel onion
3ba5e319a5 New translations strings.xml (Turkish) 2024-05-11 08:29:51 -05:00
rebel onion
45ce1ce649 New translations strings.xml (Polish) 2024-05-11 08:29:50 -05:00
rebel onion
67998c5535 New translations strings.xml (Albanian) 2024-05-11 08:29:49 -05:00
rebel onion
3dfe673b93 New translations strings.xml (Urdu (Pakistan)) 2024-05-11 08:29:48 -05:00
rebel onion
1ebfea677b New translations strings.xml (Dutch) 2024-05-11 08:29:47 -05:00
rebel onion
ac8bb3a9cf New translations strings.xml (Italian) 2024-05-11 08:29:46 -05:00
rebel onion
30b0c06eb3 New translations strings.xml (Bengali) 2024-05-11 08:29:45 -05:00
rebel onion
1e3a5892e9 New translations strings.xml (Korean) 2024-05-11 08:29:44 -05:00
rebel onion
db94c1e5ee New translations strings.xml (Greek) 2024-05-11 08:29:43 -05:00
rebel onion
7a043abc64 New translations strings.xml (Assamese) 2024-05-11 08:29:42 -05:00
rebel onion
651684c8e4 New translations strings.xml (Hindi) 2024-05-11 08:29:40 -05:00
rebel onion
b50eebca22 New translations strings.xml (Spanish, Mexico) 2024-05-11 08:29:39 -05:00
rebel onion
8f8d293439 New translations strings.xml (Chinese Simplified) 2024-05-11 08:29:38 -05:00
rebel onion
3a6b97aed1 New translations strings.xml (Russian) 2024-05-11 08:29:37 -05:00
rebel onion
35bd0ab3bc New translations strings.xml (Japanese) 2024-05-11 08:29:36 -05:00
rebel onion
0a85ffb366 New translations strings.xml (German) 2024-05-11 08:29:35 -05:00
rebel onion
7bfbad11a0 New translations strings.xml (Arabic) 2024-05-11 08:29:34 -05:00
rebel onion
3b1339b60a New translations strings.xml (Spanish) 2024-05-11 08:29:33 -05:00
rebel onion
726db46420 New translations strings.xml (French) 2024-05-11 08:29:32 -05:00
rebel onion
45c9d3495f New translations strings.xml (Arabic) 2024-05-10 13:59:30 -05:00
rebel onion
0258c235d5 New translations strings.xml (Arabic) 2024-05-10 12:08:36 -05:00
rebel onion
c2f9d7abfe New translations strings.xml (Portuguese, Brazilian) 2024-05-09 08:01:30 -05:00
rebel onion
e53758abd9 New translations strings.xml (Portuguese, Brazilian) 2024-05-08 12:48:25 -05:00
rebel onion
4e6e61b736 New translations strings.xml (Portuguese, Brazilian) 2024-05-08 11:45:34 -05:00
rebel onion
7ac82a64c1 New translations strings.xml (Portuguese, Brazilian) 2024-05-08 10:31:20 -05:00
rebel onion
aef0d31d98 New translations strings.xml (Portuguese, Brazilian) 2024-05-07 20:28:58 -05:00
rebel onion
e521032291 New translations strings.xml (Portuguese, Brazilian) 2024-05-07 19:13:57 -05:00
rebel onion
6d34e1f594 New translations strings.xml (Portuguese, Brazilian) 2024-05-07 17:57:53 -05:00
rebel onion
a8bcc48530 New translations strings.xml (Portuguese, Brazilian) 2024-05-07 16:53:31 -05:00
rebel onion
52332fe578 New translations strings.xml (Portuguese, Brazilian) 2024-05-07 15:21:57 -05:00
rebel onion
9b42bdba7b New translations strings.xml (Portuguese, Brazilian) 2024-05-07 12:00:09 -05:00
rebel onion
73e073b5fe New translations strings.xml (Portuguese, Brazilian) 2024-05-07 11:04:47 -05:00
rebel onion
506b6383e2 New translations strings.xml (Portuguese, Brazilian) 2024-05-07 09:57:52 -05:00
rebel onion
fd4f675615 New translations strings.xml (Portuguese, Brazilian) 2024-05-07 06:31:31 -05:00
rebel onion
280e9cce25 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 20:55:35 -05:00
rebel onion
498e857d07 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 19:17:10 -05:00
rebel onion
46c80e8a37 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 18:16:57 -05:00
rebel onion
8923a9dcc1 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 17:19:03 -05:00
rebel onion
167a56c962 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 16:16:08 -05:00
rebel onion
bd6bca881d New translations strings.xml (Portuguese, Brazilian) 2024-05-06 15:07:01 -05:00
rebel onion
60109beab1 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 14:04:05 -05:00
rebel onion
b3026581d8 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 13:00:46 -05:00
rebel onion
4705e9fd0f New translations strings.xml (Portuguese, Brazilian) 2024-05-06 12:02:32 -05:00
rebel onion
6a6514365d New translations strings.xml (Portuguese, Brazilian) 2024-05-06 10:49:07 -05:00
rebel onion
7679753b51 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 09:50:30 -05:00
rebel onion
c27a009d02 New translations strings.xml (Portuguese, Brazilian) 2024-05-06 07:47:02 -05:00
rebel onion
3974dac2ee New translations strings.xml (Polish) 2024-05-06 03:12:06 -05:00
rebel onion
c8bb46178a New translations strings.xml (Albanian) 2024-05-06 03:12:05 -05:00
rebel onion
2c92d4afec New translations strings.xml (Dutch) 2024-05-06 03:12:04 -05:00
rebel onion
356c83eb5a New translations strings.xml (Italian) 2024-05-06 03:12:03 -05:00
rebel onion
425f8e4758 New translations strings.xml (Korean) 2024-05-06 03:12:02 -05:00
rebel onion
d389b54a40 New translations strings.xml (Greek) 2024-05-06 03:12:01 -05:00
rebel onion
9ec9867bf5 New translations strings.xml (Chinese Simplified) 2024-05-06 03:11:59 -05:00
rebel onion
73f7552f28 New translations strings.xml (Russian) 2024-05-06 03:11:58 -05:00
rebel onion
8349936447 New translations strings.xml (Japanese) 2024-05-06 03:11:57 -05:00
rebel onion
4b98ddcf21 New translations strings.xml (French) 2024-05-06 03:11:56 -05:00
rebel onion
f1b041dd47 New translations strings.xml (Spanish) 2024-05-06 01:39:29 -05:00
rebel onion
766c77f32b New translations strings.xml (Portuguese, Brazilian) 2024-05-05 20:12:47 -05:00
rebel onion
1f051b2ef0 New translations strings.xml (Turkish) 2024-05-05 16:37:46 -05:00
rebel onion
3676e52899 New translations strings.xml (Assamese) 2024-05-05 14:08:45 -05:00
rebel onion
4ad70054ae New translations strings.xml (Hindi) 2024-05-05 14:08:44 -05:00
rebel onion
dbd1ef9ff8 New translations strings.xml (German) 2024-05-05 12:11:59 -05:00
rebel onion
803a9843ea New translations strings.xml (Turkish) 2024-05-04 16:35:23 -05:00
rebel onion
ae1a98211a New translations strings.xml (Turkish) 2024-05-04 15:39:13 -05:00
rebel onion
6a3e0dabbd New translations strings.xml (Polish) 2024-05-04 15:39:12 -05:00
rebel onion
7900b7b44e New translations strings.xml (Albanian) 2024-05-04 15:39:10 -05:00
rebel onion
16a8a37bd4 New translations strings.xml (Urdu (Pakistan)) 2024-05-04 15:39:09 -05:00
rebel onion
dee2b68a8a New translations strings.xml (Dutch) 2024-05-04 15:39:08 -05:00
rebel onion
4182f7ca04 New translations strings.xml (Italian) 2024-05-04 15:39:07 -05:00
rebel onion
d29d3c6e2a New translations strings.xml (Bengali) 2024-05-04 15:39:06 -05:00
rebel onion
3522f3e7fd New translations strings.xml (Korean) 2024-05-04 15:39:05 -05:00
rebel onion
1f46ac6e32 New translations strings.xml (Greek) 2024-05-04 15:39:04 -05:00
rebel onion
7de46bd778 New translations strings.xml (Assamese) 2024-05-04 15:39:03 -05:00
rebel onion
40a4c23839 New translations strings.xml (Hindi) 2024-05-04 15:39:02 -05:00
rebel onion
c32b3312ea New translations strings.xml (Spanish, Mexico) 2024-05-04 15:39:01 -05:00
rebel onion
8fa0a10103 New translations strings.xml (Chinese Simplified) 2024-05-04 15:39:00 -05:00
rebel onion
6b4028a456 New translations strings.xml (Russian) 2024-05-04 15:38:59 -05:00
rebel onion
2f780447b6 New translations strings.xml (Japanese) 2024-05-04 15:38:58 -05:00
rebel onion
ed4164d03e New translations strings.xml (German) 2024-05-04 15:38:57 -05:00
rebel onion
fef05061b4 New translations strings.xml (Arabic) 2024-05-04 15:38:56 -05:00
rebel onion
8fb0d9ffa1 New translations strings.xml (Spanish) 2024-05-04 15:38:55 -05:00
rebel onion
2c17e80367 New translations strings.xml (French) 2024-05-04 15:38:54 -05:00
rebel onion
dcdcc3432b New translations strings.xml (Turkish) 2024-05-04 14:33:40 -05:00
rebel onion
42d719d3b5 New translations strings.xml (Polish) 2024-05-04 14:33:39 -05:00
rebel onion
8e019e8ff9 New translations strings.xml (Albanian) 2024-05-04 14:33:38 -05:00
rebel onion
2720887870 New translations strings.xml (Urdu (Pakistan)) 2024-05-04 14:33:37 -05:00
rebel onion
69fe1670c3 New translations strings.xml (Dutch) 2024-05-04 14:33:36 -05:00
rebel onion
1fcf0bf3e6 New translations strings.xml (Italian) 2024-05-04 14:33:35 -05:00
rebel onion
86c0d671e4 New translations strings.xml (Bengali) 2024-05-04 14:33:34 -05:00
rebel onion
338a450746 New translations strings.xml (Korean) 2024-05-04 14:33:33 -05:00
rebel onion
c84b1effba New translations strings.xml (Greek) 2024-05-04 14:33:32 -05:00
rebel onion
8be2d6a7d0 New translations strings.xml (Assamese) 2024-05-04 14:33:31 -05:00
rebel onion
8830b2b08a New translations strings.xml (Hindi) 2024-05-04 14:33:29 -05:00
rebel onion
f4cd9b78b4 New translations strings.xml (Spanish, Mexico) 2024-05-04 14:33:28 -05:00
rebel onion
179c77f09c New translations strings.xml (Chinese Simplified) 2024-05-04 14:33:27 -05:00
rebel onion
911fd058a6 New translations strings.xml (Russian) 2024-05-04 14:33:26 -05:00
rebel onion
8d8def030a New translations strings.xml (Japanese) 2024-05-04 14:33:25 -05:00
rebel onion
ac89630e11 New translations strings.xml (German) 2024-05-04 14:33:24 -05:00
rebel onion
f05e1f1693 New translations strings.xml (Arabic) 2024-05-04 14:33:23 -05:00
rebel onion
e59ab24424 New translations strings.xml (Spanish) 2024-05-04 14:33:22 -05:00
rebel onion
4254320fc9 New translations strings.xml (French) 2024-05-04 14:33:21 -05:00
rebel onion
f5ff73d87d New translations strings.xml (Polish) 2024-05-04 04:48:07 -05:00
rebel onion
ed5ffbd813 New translations strings.xml (Turkish) 2024-05-04 03:45:22 -05:00
rebel onion
afc8a2f76d New translations strings.xml (Albanian) 2024-05-04 03:45:20 -05:00
rebel onion
8f8561d175 New translations strings.xml (Dutch) 2024-05-04 03:45:19 -05:00
rebel onion
36b54ad384 New translations strings.xml (Korean) 2024-05-04 03:45:18 -05:00
rebel onion
668b7ca1b2 New translations strings.xml (Greek) 2024-05-04 03:45:17 -05:00
rebel onion
9bc48c16c7 New translations strings.xml (Chinese Simplified) 2024-05-04 03:45:16 -05:00
rebel onion
a980793630 New translations strings.xml (Russian) 2024-05-04 03:45:15 -05:00
rebel onion
94a2f544d5 New translations strings.xml (Japanese) 2024-05-04 03:45:14 -05:00
rebel onion
3ad3c163ed New translations strings.xml (German) 2024-05-04 03:45:13 -05:00
rebel onion
714a92af06 New translations strings.xml (French) 2024-05-04 03:45:11 -05:00
rebel onion
c7b1879ba9 New translations strings.xml (Assamese) 2024-05-04 02:11:30 -05:00
rebel onion
64a53dccd1 New translations strings.xml (Hindi) 2024-05-04 02:11:29 -05:00
rebel onion
15d0e3d702 New translations strings.xml (Turkish) 2024-05-03 15:03:19 -05:00
rebel onion
d3ded0c897 New translations strings.xml (Polish) 2024-05-03 15:03:18 -05:00
rebel onion
d473d3fe95 New translations strings.xml (Albanian) 2024-05-03 15:03:17 -05:00
rebel onion
9688633de9 New translations strings.xml (Urdu (Pakistan)) 2024-05-03 15:03:16 -05:00
rebel onion
b4f860990d New translations strings.xml (Dutch) 2024-05-03 15:03:14 -05:00
rebel onion
e7771046d3 New translations strings.xml (Italian) 2024-05-03 15:03:13 -05:00
rebel onion
ce86b82eed New translations strings.xml (Bengali) 2024-05-03 15:03:12 -05:00
rebel onion
3a3e470ac8 New translations strings.xml (Korean) 2024-05-03 15:03:11 -05:00
rebel onion
029807ec6b New translations strings.xml (Greek) 2024-05-03 15:03:10 -05:00
rebel onion
affdd0454a New translations strings.xml (Assamese) 2024-05-03 15:03:08 -05:00
rebel onion
fd3172e5a9 New translations strings.xml (Hindi) 2024-05-03 15:03:07 -05:00
rebel onion
2db8f8f3ff New translations strings.xml (Spanish, Mexico) 2024-05-03 15:03:06 -05:00
rebel onion
78c2a92949 New translations strings.xml (Chinese Simplified) 2024-05-03 15:03:05 -05:00
rebel onion
75cd2e1422 New translations strings.xml (Russian) 2024-05-03 15:03:04 -05:00
rebel onion
1231089989 New translations strings.xml (Japanese) 2024-05-03 15:03:03 -05:00
rebel onion
86f14fc006 New translations strings.xml (German) 2024-05-03 15:03:01 -05:00
rebel onion
f9966694b8 New translations strings.xml (Arabic) 2024-05-03 15:03:00 -05:00
rebel onion
737012db0f New translations strings.xml (Spanish) 2024-05-03 15:02:59 -05:00
rebel onion
f3d4bdcc3a New translations strings.xml (French) 2024-05-03 15:02:58 -05:00
rebel onion
5863d697be New translations strings.xml (Turkish) 2024-05-03 07:15:26 -05:00
rebel onion
29627a396b New translations strings.xml (Turkish) 2024-05-03 05:27:06 -05:00
rebel onion
ab72d6c3ef New translations strings.xml (Korean) 2024-05-03 05:27:05 -05:00
rebel onion
77f2e7f756 New translations strings.xml (Assamese) 2024-05-03 05:27:04 -05:00
rebel onion
83cbc23e71 New translations strings.xml (Hindi) 2024-05-03 05:27:02 -05:00
rebel onion
01d1453453 New translations strings.xml (Chinese Simplified) 2024-05-03 05:27:01 -05:00
rebel onion
0b733a7743 New translations strings.xml (Russian) 2024-05-03 05:27:00 -05:00
rebel onion
afa99d3034 New translations strings.xml (Japanese) 2024-05-03 05:26:59 -05:00
rebel onion
67f63f6afe New translations strings.xml (Turkish) 2024-05-03 03:35:25 -05:00
rebel onion
257a3ecb40 New translations strings.xml (Turkish) 2024-05-03 02:05:39 -05:00
rebel onion
69283ec1e5 New translations strings.xml (Turkish) 2024-05-03 00:55:28 -05:00
rebel onion
3fa5e0c105 New translations strings.xml (Turkish) 2024-05-02 16:41:14 -05:00
rebel onion
45fa72005b New translations strings.xml (French) 2024-05-02 15:30:51 -05:00
rebel onion
7f1681944e New translations strings.xml (Turkish) 2024-05-02 11:34:49 -05:00
rebel onion
7a8c0ba119 New translations strings.xml (Turkish) 2024-05-02 09:14:45 -05:00
rebel onion
bc0b86652f New translations strings.xml (Turkish) 2024-05-02 07:34:20 -05:00
rebel onion
6174cdabde New translations strings.xml (Turkish) 2024-05-02 06:31:48 -05:00
rebel onion
83b002cb91 New translations strings.xml (Turkish) 2024-05-02 05:28:48 -05:00
rebel onion
80f06334aa New translations strings.xml (Turkish) 2024-05-02 03:25:50 -05:00
rebel onion
9125cc3af1 New translations strings.xml (Assamese) 2024-05-02 00:29:49 -05:00
rebel onion
308bba8ca2 New translations strings.xml (Turkish) 2024-05-01 22:07:13 -05:00
rebel onion
5659a33a7c New translations strings.xml (Polish) 2024-05-01 22:07:12 -05:00
rebel onion
7d8144a1c3 New translations strings.xml (Albanian) 2024-05-01 22:07:11 -05:00
rebel onion
fbec5ec038 New translations strings.xml (Urdu (Pakistan)) 2024-05-01 22:07:10 -05:00
rebel onion
bc2f283ea8 New translations strings.xml (Dutch) 2024-05-01 22:07:08 -05:00
rebel onion
3f822ae3eb New translations strings.xml (Italian) 2024-05-01 22:07:07 -05:00
rebel onion
bc9ab7c639 New translations strings.xml (Bengali) 2024-05-01 22:07:05 -05:00
rebel onion
fa153e57ce New translations strings.xml (Korean) 2024-05-01 22:07:04 -05:00
rebel onion
0516c4fb9a New translations strings.xml (Greek) 2024-05-01 22:07:03 -05:00
rebel onion
3e9253a78e New translations strings.xml (Assamese) 2024-05-01 22:07:01 -05:00
rebel onion
9dbb221082 New translations strings.xml (Hindi) 2024-05-01 22:07:00 -05:00
rebel onion
fd3f0a22e2 New translations strings.xml (Spanish, Mexico) 2024-05-01 22:06:59 -05:00
rebel onion
2d05133150 New translations strings.xml (Chinese Simplified) 2024-05-01 22:06:58 -05:00
rebel onion
4fb65c7090 New translations strings.xml (Russian) 2024-05-01 22:06:57 -05:00
rebel onion
e7c8d17097 New translations strings.xml (Japanese) 2024-05-01 22:06:55 -05:00
rebel onion
ddacc4ff4b New translations strings.xml (German) 2024-05-01 22:06:54 -05:00
rebel onion
011ccf495e New translations strings.xml (Arabic) 2024-05-01 22:06:53 -05:00
rebel onion
460b430fa3 New translations strings.xml (Spanish) 2024-05-01 22:06:52 -05:00
rebel onion
fef54c8599 New translations strings.xml (French) 2024-05-01 22:06:51 -05:00
rebel onion
f2535a4b0c New translations strings.xml (Turkish) 2024-05-01 21:03:49 -05:00
rebel onion
a876d967b7 New translations strings.xml (Turkish) 2024-05-01 19:35:04 -05:00
rebel onion
8a3c448712 New translations strings.xml (Turkish) 2024-05-01 18:34:26 -05:00
rebel onion
ac199cf65a New translations strings.xml (Turkish) 2024-05-01 17:35:43 -05:00
rebel onion
e272c8b7f3 New translations strings.xml (Turkish) 2024-05-01 16:23:14 -05:00
rebel onion
b86b4ff67f New translations strings.xml (Turkish) 2024-05-01 15:15:24 -05:00
rebel onion
15233b73b6 New translations strings.xml (Polish) 2024-05-01 15:15:23 -05:00
rebel onion
75deddff97 New translations strings.xml (Albanian) 2024-05-01 15:15:22 -05:00
rebel onion
7817706d73 New translations strings.xml (Urdu (Pakistan)) 2024-05-01 15:15:21 -05:00
rebel onion
15baf7f5dc New translations strings.xml (Dutch) 2024-05-01 15:15:20 -05:00
rebel onion
b4b5228d00 New translations strings.xml (Italian) 2024-05-01 15:15:18 -05:00
rebel onion
52312c5450 New translations strings.xml (Bengali) 2024-05-01 15:15:17 -05:00
rebel onion
ec837e3877 New translations strings.xml (Korean) 2024-05-01 15:15:16 -05:00
rebel onion
b5bfd0831d New translations strings.xml (Greek) 2024-05-01 15:15:14 -05:00
rebel onion
3bc80163e3 New translations strings.xml (Assamese) 2024-05-01 15:15:13 -05:00
rebel onion
6879a16b8c New translations strings.xml (Hindi) 2024-05-01 15:15:12 -05:00
rebel onion
ea18684ccd New translations strings.xml (Spanish, Mexico) 2024-05-01 15:15:11 -05:00
rebel onion
9ccaf2c7e5 New translations strings.xml (Chinese Simplified) 2024-05-01 15:15:10 -05:00
rebel onion
329c3b1320 New translations strings.xml (Russian) 2024-05-01 15:15:08 -05:00
rebel onion
1ca8af3c4a New translations strings.xml (Japanese) 2024-05-01 15:15:07 -05:00
rebel onion
a9e5167ccd New translations strings.xml (German) 2024-05-01 15:15:05 -05:00
rebel onion
46b3567fc9 New translations strings.xml (Arabic) 2024-05-01 15:15:04 -05:00
rebel onion
724ce1e509 New translations strings.xml (Spanish) 2024-05-01 15:15:03 -05:00
rebel onion
afc91edbb2 New translations strings.xml (French) 2024-05-01 15:15:02 -05:00
rebel onion
850921b289 New translations strings.xml (Albanian) 2024-05-01 13:29:55 -05:00
rebel onion
133af98ad1 New translations strings.xml (Urdu (Pakistan)) 2024-05-01 13:29:54 -05:00
rebel onion
4b6a88ac32 New translations strings.xml (Dutch) 2024-05-01 13:29:53 -05:00
rebel onion
22afe4d49f New translations strings.xml (Italian) 2024-05-01 13:29:52 -05:00
rebel onion
21b1e0d7f5 New translations strings.xml (Bengali) 2024-05-01 13:29:50 -05:00
rebel onion
6ae813378c New translations strings.xml (Korean) 2024-05-01 13:29:49 -05:00
rebel onion
99c1902c1e New translations strings.xml (Greek) 2024-05-01 13:29:48 -05:00
rebel onion
883d8efe9b New translations strings.xml (Assamese) 2024-05-01 13:29:47 -05:00
rebel onion
dc72e1d18f New translations strings.xml (Hindi) 2024-05-01 13:29:46 -05:00
rebel onion
55a9290c44 New translations strings.xml (Spanish, Mexico) 2024-05-01 13:29:45 -05:00
rebel onion
b25d072dab New translations strings.xml (Chinese Simplified) 2024-05-01 13:29:44 -05:00
rebel onion
72af12b41e New translations strings.xml (Russian) 2024-05-01 13:29:42 -05:00
rebel onion
66919726ca New translations strings.xml (Japanese) 2024-05-01 13:29:41 -05:00
rebel onion
f91ea98d7e New translations strings.xml (German) 2024-05-01 13:29:40 -05:00
rebel onion
f37444d50a New translations strings.xml (Arabic) 2024-05-01 13:29:39 -05:00
rebel onion
43580be7e8 New translations strings.xml (Spanish) 2024-05-01 13:29:38 -05:00
rebel onion
a6dd46d129 New translations strings.xml (French) 2024-05-01 13:29:36 -05:00
rebel onion
757bd5aa9f New translations strings.xml (Arabic) 2024-05-01 01:00:37 -05:00
rebel onion
e16ea086a6 New translations strings.xml (Albanian) 2024-04-30 10:23:47 -05:00
rebel onion
4c876ac6c7 New translations strings.xml (Urdu (Pakistan)) 2024-04-30 10:23:46 -05:00
rebel onion
cbff202864 New translations strings.xml (Dutch) 2024-04-30 10:23:44 -05:00
rebel onion
3ab3f9bb1b New translations strings.xml (Italian) 2024-04-30 10:23:43 -05:00
rebel onion
7a382c24af New translations strings.xml (Bengali) 2024-04-30 10:23:42 -05:00
rebel onion
94ac5f6689 New translations strings.xml (Korean) 2024-04-30 10:23:41 -05:00
rebel onion
f35fa44c8b New translations strings.xml (Greek) 2024-04-30 10:23:40 -05:00
rebel onion
2acb388bdd New translations strings.xml (Assamese) 2024-04-30 10:23:39 -05:00
rebel onion
4f79cf0b8c New translations strings.xml (Hindi) 2024-04-30 10:23:38 -05:00
rebel onion
e593341696 New translations strings.xml (Spanish, Mexico) 2024-04-30 10:23:36 -05:00
rebel onion
a48e5a6ea0 New translations strings.xml (Chinese Simplified) 2024-04-30 10:23:35 -05:00
rebel onion
f8a78d1604 New translations strings.xml (Russian) 2024-04-30 10:23:34 -05:00
rebel onion
2f889a8704 New translations strings.xml (Japanese) 2024-04-30 10:23:33 -05:00
rebel onion
4e4766497d New translations strings.xml (German) 2024-04-30 10:23:32 -05:00
rebel onion
032bf88d55 New translations strings.xml (Arabic) 2024-04-30 10:23:30 -05:00
rebel onion
8ea84f7d96 New translations strings.xml (Spanish) 2024-04-30 10:23:29 -05:00
rebel onion
a00cf15fa4 New translations strings.xml (French) 2024-04-30 10:23:28 -05:00
rebel onion
155f570f17 New translations strings.xml (Assamese) 2024-04-29 03:35:57 -05:00
rebel onion
d7485623be New translations strings.xml (Hindi) 2024-04-29 03:35:56 -05:00
rebel onion
c4ef64472d New translations strings.xml (Spanish, Mexico) 2024-04-28 22:59:58 -05:00
rebel onion
8b986f76fa New translations strings.xml (Arabic) 2024-04-28 11:21:50 -05:00
rebel onion
66ea2ff9d7 New translations strings.xml (Arabic) 2024-04-28 09:57:13 -05:00
rebel onion
34f6de4efd New translations strings.xml (Albanian) 2024-04-27 15:59:43 -05:00
rebel onion
cf58724481 New translations strings.xml (Urdu (Pakistan)) 2024-04-27 15:59:42 -05:00
rebel onion
1db981ae20 New translations strings.xml (Dutch) 2024-04-27 15:59:41 -05:00
rebel onion
22d98b8991 New translations strings.xml (Italian) 2024-04-27 15:59:40 -05:00
rebel onion
ce77a63ed8 New translations strings.xml (Bengali) 2024-04-27 15:59:39 -05:00
rebel onion
b307f0aa00 New translations strings.xml (Korean) 2024-04-27 15:59:38 -05:00
rebel onion
94ac7c8bf2 New translations strings.xml (Greek) 2024-04-27 15:59:37 -05:00
rebel onion
3de880dd3a New translations strings.xml (Assamese) 2024-04-27 15:59:36 -05:00
rebel onion
5443d7c61a New translations strings.xml (Hindi) 2024-04-27 15:59:35 -05:00
rebel onion
8ec7a8139f New translations strings.xml (Spanish, Mexico) 2024-04-27 15:59:34 -05:00
rebel onion
efa6d077c1 New translations strings.xml (Chinese Simplified) 2024-04-27 15:59:33 -05:00
rebel onion
fa71c538c5 New translations strings.xml (Russian) 2024-04-27 15:59:32 -05:00
rebel onion
a983b3c318 New translations strings.xml (Japanese) 2024-04-27 15:59:31 -05:00
rebel onion
1e9922a62e New translations strings.xml (German) 2024-04-27 15:59:30 -05:00
rebel onion
3e24113e1b New translations strings.xml (Arabic) 2024-04-27 15:59:29 -05:00
rebel onion
d02b9cfab5 New translations strings.xml (Spanish) 2024-04-27 15:59:28 -05:00
rebel onion
8c33ac660d New translations strings.xml (French) 2024-04-27 15:59:27 -05:00
rebel onion
22f6f2d7b5 New translations strings.xml (Italian) 2024-04-27 14:56:24 -05:00
rebel onion
bf1e35bba0 New translations strings.xml (Arabic) 2024-04-27 13:52:43 -05:00
rebel onion
df48a08fe8 New translations strings.xml (Arabic) 2024-04-27 12:54:29 -05:00
rebel onion
c6258a6174 New translations strings.xml (Arabic) 2024-04-26 21:26:04 -05:00
rebel onion
a38c2086d0 New translations strings.xml (Arabic) 2024-04-26 19:49:18 -05:00
rebel onion
4db3876129 New translations strings.xml (French) 2024-04-26 15:51:41 -05:00
rebel onion
1c5782ebec New translations strings.xml (French) 2024-04-26 14:56:12 -05:00
rebel onion
03856c1827 New translations strings.xml (Assamese) 2024-04-26 12:27:53 -05:00
rebel onion
bcda9d7a9b New translations strings.xml (Hindi) 2024-04-26 12:27:52 -05:00
rebel onion
c401b1bde7 New translations strings.xml (Albanian) 2024-04-26 10:46:05 -05:00
rebel onion
31b0cfe1b6 New translations strings.xml (Urdu (Pakistan)) 2024-04-26 10:46:04 -05:00
rebel onion
f4f411ab75 New translations strings.xml (Dutch) 2024-04-26 10:46:03 -05:00
rebel onion
7856855de5 New translations strings.xml (Italian) 2024-04-26 10:46:02 -05:00
rebel onion
ee55ca3e37 New translations strings.xml (Bengali) 2024-04-26 10:46:00 -05:00
rebel onion
a40a9e9be6 New translations strings.xml (Korean) 2024-04-26 10:45:59 -05:00
rebel onion
e45f0ab83f New translations strings.xml (Greek) 2024-04-26 10:45:58 -05:00
rebel onion
fff90c3904 New translations strings.xml (Assamese) 2024-04-26 10:45:57 -05:00
rebel onion
d06effc960 New translations strings.xml (Hindi) 2024-04-26 10:45:56 -05:00
rebel onion
8d08743360 New translations strings.xml (Spanish, Mexico) 2024-04-26 10:45:54 -05:00
rebel onion
7c77c8a229 New translations strings.xml (Chinese Simplified) 2024-04-26 10:45:53 -05:00
rebel onion
9b683dbb6a New translations strings.xml (Russian) 2024-04-26 10:45:52 -05:00
rebel onion
4328f0f58e New translations strings.xml (Japanese) 2024-04-26 10:45:51 -05:00
rebel onion
062da007a8 New translations strings.xml (German) 2024-04-26 10:45:50 -05:00
rebel onion
e31b4a603d New translations strings.xml (Arabic) 2024-04-26 10:45:48 -05:00
rebel onion
196b7497e4 New translations strings.xml (Spanish) 2024-04-26 10:45:47 -05:00
rebel onion
8cfdce6e1b New translations strings.xml (French) 2024-04-26 10:45:46 -05:00
rebel onion
8ed6d69da1 New translations strings.xml (Korean) 2024-04-26 09:16:37 -05:00
rebel onion
1d62aa80a0 New translations strings.xml (Japanese) 2024-04-26 09:16:36 -05:00
rebel onion
b81dea93ab New translations strings.xml (German) 2024-04-26 09:16:34 -05:00
rebel onion
364a1588a2 New translations strings.xml (Hindi) 2024-04-26 02:27:53 -05:00
rebel onion
9a88f0b09a New translations strings.xml (Arabic) 2024-04-25 16:16:13 -05:00
rebel onion
92a614b4b5 New translations strings.xml (Italian) 2024-04-25 13:32:56 -05:00
rebel onion
f4262f39c4 New translations strings.xml (Italian) 2024-04-25 12:01:03 -05:00
rebel onion
f17af3ca50 New translations strings.xml (Arabic) 2024-04-25 10:48:07 -05:00
rebel onion
261ef768de New translations strings.xml (Arabic) 2024-04-25 08:55:46 -05:00
rebel onion
7fba2bdcba New translations strings.xml (German) 2024-04-24 18:37:14 -05:00
rebel onion
7d760b9f42 New translations strings.xml (German) 2024-04-24 17:22:24 -05:00
rebel onion
a3a65d7a3b New translations strings.xml (Hindi) 2024-04-24 14:21:37 -05:00
rebel onion
24fc2c2ce1 New translations strings.xml (Bengali) 2024-04-24 11:17:10 -05:00
rebel onion
796003063f New translations strings.xml (Arabic) 2024-04-24 07:51:27 -05:00
rebel onion
53412c31b6 New translations strings.xml (Arabic) 2024-04-24 06:19:17 -05:00
rebel onion
023c19ee7c New translations strings.xml (Spanish, Mexico) 2024-04-23 19:53:01 -05:00
rebel onion
532da69263 New translations strings.xml (Chinese Simplified) 2024-04-23 19:53:00 -05:00
rebel onion
ec63af57e6 New translations strings.xml (Chinese Simplified) 2024-04-23 18:11:35 -05:00
rebel onion
56ca158f29 New translations strings.xml (Urdu (Pakistan)) 2024-04-23 13:13:46 -05:00
rebel onion
e143c87bfd New translations strings.xml (Urdu (Pakistan)) 2024-04-23 11:32:35 -05:00
rebel onion
3a03e5040e New translations strings.xml (Assamese) 2024-04-23 03:53:59 -05:00
rebel onion
c147658497 New translations strings.xml (Spanish, Mexico) 2024-04-22 18:45:56 -05:00
rebel onion
bc4b354421 New translations strings.xml (French) 2024-04-22 16:33:04 -05:00
rebel onion
dd60a6ea88 New translations strings.xml (Greek) 2024-04-22 15:22:44 -05:00
rebel onion
cab18a8347 New translations strings.xml (Albanian) 2024-04-22 14:10:31 -05:00
rebel onion
7a7aaba633 New translations strings.xml (Urdu (Pakistan)) 2024-04-22 14:10:30 -05:00
rebel onion
2cfa16f328 New translations strings.xml (Dutch) 2024-04-22 14:10:29 -05:00
rebel onion
a73db6b36d New translations strings.xml (Italian) 2024-04-22 14:10:27 -05:00
rebel onion
2c859734a3 New translations strings.xml (Bengali) 2024-04-22 14:10:26 -05:00
rebel onion
4dac190cbd New translations strings.xml (Korean) 2024-04-22 14:10:25 -05:00
rebel onion
8136f8eff6 New translations strings.xml (Greek) 2024-04-22 14:10:24 -05:00
rebel onion
13959496e8 New translations strings.xml (Assamese) 2024-04-22 14:10:23 -05:00
rebel onion
e079a06796 New translations strings.xml (Hindi) 2024-04-22 14:10:22 -05:00
rebel onion
1ece2bd8e2 New translations strings.xml (Spanish, Mexico) 2024-04-22 14:10:20 -05:00
rebel onion
93e9f0cc1c New translations strings.xml (Chinese Simplified) 2024-04-22 14:10:19 -05:00
rebel onion
962f065410 New translations strings.xml (Russian) 2024-04-22 14:10:18 -05:00
rebel onion
ffd609ae8e New translations strings.xml (Japanese) 2024-04-22 14:10:17 -05:00
rebel onion
f047ae115c New translations strings.xml (German) 2024-04-22 14:10:16 -05:00
rebel onion
036dae1f5f New translations strings.xml (Arabic) 2024-04-22 14:10:15 -05:00
rebel onion
cff63e6469 New translations strings.xml (Spanish) 2024-04-22 14:10:14 -05:00
rebel onion
f9be2d1bec New translations strings.xml (French) 2024-04-22 14:10:13 -05:00
rebel onion
7046766386 New translations strings.xml (Arabic) 2024-04-22 10:46:37 -05:00
rebel onion
8b4c5498b9 New translations strings.xml (Assamese) 2024-04-22 02:12:57 -05:00
rebel onion
af62fab676 New translations strings.xml (Hindi) 2024-04-22 02:12:56 -05:00
rebel onion
defe4749f0 New translations strings.xml (Spanish, Mexico) 2024-04-22 00:35:15 -05:00
rebel onion
ff2086e4c9 New translations strings.xml (Spanish, Mexico) 2024-04-21 22:16:46 -05:00
rebel onion
a3ccf78bf7 New translations strings.xml (Arabic) 2024-04-21 20:16:54 -05:00
rebel onion
b1285b61ef New translations strings.xml (Arabic) 2024-04-21 19:13:33 -05:00
rebel onion
9cc80f0dda New translations strings.xml (French) 2024-04-21 16:50:29 -05:00
rebel onion
1f38eed471 New translations strings.xml (Italian) 2024-04-21 15:52:31 -05:00
rebel onion
d8c7856966 New translations strings.xml (Spanish, Mexico) 2024-04-21 15:52:30 -05:00
rebel onion
420c16ad8c New translations strings.xml (French) 2024-04-21 15:52:29 -05:00
rebel onion
a07e5eeddb New translations strings.xml (Spanish, Mexico) 2024-04-21 14:14:24 -05:00
rebel onion
250179ee59 New translations strings.xml (French) 2024-04-21 14:14:23 -05:00
rebel onion
d2279aa36b New translations strings.xml (Spanish, Mexico) 2024-04-21 13:17:21 -05:00
rebel onion
fbd98588f2 New translations strings.xml (Arabic) 2024-04-21 13:17:20 -05:00
rebel onion
877c321664 New translations strings.xml (French) 2024-04-21 13:17:19 -05:00
rebel onion
c6efa74d23 New translations strings.xml (Greek) 2024-04-21 12:13:02 -05:00
rebel onion
9f196828ef New translations strings.xml (Arabic) 2024-04-21 12:13:01 -05:00
rebel onion
2eb72ad756 New translations strings.xml (French) 2024-04-21 12:13:00 -05:00
rebel onion
590be6b406 New translations strings.xml (Greek) 2024-04-21 11:02:32 -05:00
rebel onion
5201752c8e New translations strings.xml (Arabic) 2024-04-21 11:02:31 -05:00
rebel onion
92691578dd New translations strings.xml (Greek) 2024-04-21 08:37:19 -05:00
rebel onion
cbc3172627 New translations strings.xml (Albanian) 2024-04-21 07:39:35 -05:00
rebel onion
fac4729dfe New translations strings.xml (Urdu (Pakistan)) 2024-04-21 07:39:34 -05:00
rebel onion
ca6fcb5441 New translations strings.xml (Dutch) 2024-04-21 07:39:33 -05:00
rebel onion
3b12d31f1b New translations strings.xml (Italian) 2024-04-21 07:39:32 -05:00
rebel onion
d93daf50fd New translations strings.xml (Bengali) 2024-04-21 07:39:31 -05:00
rebel onion
8ec35f5d5f New translations strings.xml (Korean) 2024-04-21 07:39:30 -05:00
rebel onion
d61e1ca97e New translations strings.xml (Greek) 2024-04-21 07:39:29 -05:00
rebel onion
b284d9cb74 New translations strings.xml (Assamese) 2024-04-21 07:39:28 -05:00
rebel onion
ef9e6e9e6c New translations strings.xml (Hindi) 2024-04-21 07:39:27 -05:00
rebel onion
f558dc5a5b New translations strings.xml (Spanish, Mexico) 2024-04-21 07:39:26 -05:00
rebel onion
544e5cd964 New translations strings.xml (Chinese Simplified) 2024-04-21 07:39:25 -05:00
rebel onion
3dfb48c7d7 New translations strings.xml (Russian) 2024-04-21 07:39:24 -05:00
rebel onion
ecd7f2fefd New translations strings.xml (Japanese) 2024-04-21 07:39:23 -05:00
rebel onion
73a7fe09f3 New translations strings.xml (German) 2024-04-21 07:39:22 -05:00
rebel onion
fec8eb3ae2 New translations strings.xml (Arabic) 2024-04-21 07:39:20 -05:00
rebel onion
db84a65931 New translations strings.xml (Spanish) 2024-04-21 07:39:19 -05:00
rebel onion
c9a8625199 New translations strings.xml (French) 2024-04-21 07:39:18 -05:00
rebel onion
0c316a7ea3 New translations strings.xml (Albanian) 2024-04-21 06:42:05 -05:00
rebel onion
0ff1db33bd New translations strings.xml (Urdu (Pakistan)) 2024-04-21 06:42:04 -05:00
rebel onion
b2283db2dc New translations strings.xml (Dutch) 2024-04-21 06:42:03 -05:00
rebel onion
1d90e47ef2 New translations strings.xml (Italian) 2024-04-21 06:42:02 -05:00
rebel onion
461ee0b583 New translations strings.xml (Bengali) 2024-04-21 06:42:01 -05:00
rebel onion
4b47ad45d4 New translations strings.xml (Korean) 2024-04-21 06:42:00 -05:00
rebel onion
8a15f32b00 New translations strings.xml (Greek) 2024-04-21 06:41:59 -05:00
rebel onion
cc1ee25cd3 New translations strings.xml (Assamese) 2024-04-21 06:41:58 -05:00
rebel onion
f482b18147 New translations strings.xml (Hindi) 2024-04-21 06:41:57 -05:00
rebel onion
c1329578f9 New translations strings.xml (Spanish, Mexico) 2024-04-21 06:41:56 -05:00
rebel onion
4d92dafb4f New translations strings.xml (Chinese Simplified) 2024-04-21 06:41:55 -05:00
rebel onion
2516a1c3f3 New translations strings.xml (Russian) 2024-04-21 06:41:54 -05:00
rebel onion
dc01193951 New translations strings.xml (Japanese) 2024-04-21 06:41:53 -05:00
rebel onion
12eae5a462 New translations strings.xml (German) 2024-04-21 06:41:52 -05:00
rebel onion
078cfba980 New translations strings.xml (Arabic) 2024-04-21 06:41:51 -05:00
rebel onion
34d1f12ae2 New translations strings.xml (Spanish) 2024-04-21 06:41:50 -05:00
rebel onion
96c205bf0d New translations strings.xml (French) 2024-04-21 06:41:49 -05:00
rebel onion
5005d77666 New translations strings.xml (Albanian) 2024-04-21 05:21:36 -05:00
rebel onion
f4d0274899 New translations strings.xml (Arabic) 2024-04-21 05:21:35 -05:00
rebel onion
c9f23aeee1 New translations strings.xml (Urdu (Pakistan)) 2024-04-21 04:16:38 -05:00
rebel onion
589b28532b New translations strings.xml (Hindi) 2024-04-21 04:16:37 -05:00
rebel onion
9e462acb32 New translations strings.xml (Arabic) 2024-04-21 04:16:36 -05:00
rebel onion
7c80270673 New translations strings.xml (Urdu (Pakistan)) 2024-04-21 02:33:26 -05:00
rebel onion
59c2ed2459 New translations strings.xml (Assamese) 2024-04-21 02:33:25 -05:00
rebel onion
d14d0734e8 New translations strings.xml (Arabic) 2024-04-21 02:33:24 -05:00
rebel onion
e10a09fc61 New translations strings.xml (Arabic) 2024-04-21 01:33:49 -05:00
rebel onion
8ed44ada20 New translations strings.xml (Arabic) 2024-04-20 23:12:58 -05:00
rebel onion
54e83ed342 New translations strings.xml (Arabic) 2024-04-20 22:51:27 -05:00
rebel onion
fe423d1e68 New translations strings.xml (Arabic) 2024-04-20 21:53:41 -05:00
rebel onion
559ae7592c New translations strings.xml (Arabic) 2024-04-20 20:43:48 -05:00
rebel onion
ab5f6817cf New translations strings.xml (Arabic) 2024-04-20 19:46:47 -05:00
rebel onion
54b080695e New translations strings.xml (French) 2024-04-20 19:46:46 -05:00
rebel onion
4e66e4e8e1 New translations strings.xml (Arabic) 2024-04-20 18:42:05 -05:00
rebel onion
ab29f07c13 New translations strings.xml (Italian) 2024-04-20 17:42:47 -05:00
rebel onion
82150eca62 New translations strings.xml (Dutch) 2024-04-20 16:11:39 -05:00
rebel onion
64c0045ad6 New translations strings.xml (Italian) 2024-04-20 16:11:38 -05:00
rebel onion
04ee4c5a0f New translations strings.xml (Greek) 2024-04-20 13:29:22 -05:00
rebel onion
6afeab4fad New translations strings.xml (Greek) 2024-04-20 12:32:52 -05:00
rebel onion
e49e1a1eea New translations strings.xml (Arabic) 2024-04-20 09:17:49 -05:00
rebel onion
105d7f9e1d New translations strings.xml (Greek) 2024-04-20 07:36:35 -05:00
rebel onion
a4c6ae7fe4 New translations strings.xml (Bengali) 2024-04-20 06:22:16 -05:00
rebel onion
2279ab984e New translations strings.xml (Assamese) 2024-04-20 06:22:15 -05:00
rebel onion
0b8e0fa179 New translations strings.xml (Arabic) 2024-04-20 06:22:14 -05:00
rebel onion
4c34730904 New translations strings.xml (Arabic) 2024-04-20 05:07:29 -05:00
rebel onion
0e0b991fa2 New translations strings.xml (Bengali) 2024-04-20 04:11:47 -05:00
rebel onion
9d6950d80c New translations strings.xml (Arabic) 2024-04-20 04:11:45 -05:00
rebel onion
07db1871a6 New translations strings.xml (Hindi) 2024-04-20 03:07:38 -05:00
rebel onion
f87f2f7058 New translations strings.xml (Assamese) 2024-04-20 01:54:22 -05:00
rebel onion
3e253a0978 New translations strings.xml (Hindi) 2024-04-20 01:54:21 -05:00
rebel onion
d44bb97e2b New translations strings.xml (Assamese) 2024-04-20 00:44:41 -05:00
rebel onion
1c52a960c7 New translations strings.xml (Hindi) 2024-04-20 00:44:40 -05:00
rebel onion
bfa4e6941a New translations strings.xml (Spanish, Mexico) 2024-04-19 22:29:13 -05:00
rebel onion
7452eee4a3 New translations strings.xml (Japanese) 2024-04-19 22:29:12 -05:00
rebel onion
119a701d60 New translations strings.xml (German) 2024-04-19 22:29:11 -05:00
rebel onion
f2de9a4b3b New translations strings.xml (Spanish) 2024-04-19 22:29:10 -05:00
rebel onion
896304e6fb New translations strings.xml (Spanish) 2024-04-19 21:21:58 -05:00
rebel onion
71c5e456e3 New translations strings.xml (Spanish) 2024-04-19 18:27:52 -05:00
rebel onion
a5047813f0 New translations strings.xml (Bengali) 2024-04-19 14:45:57 -05:00
rebel onion
8dd78abe91 New translations strings.xml (Bengali) 2024-04-19 13:33:58 -05:00
rebel onion
7e5f7389a5 New translations strings.xml (Hindi) 2024-04-19 13:33:57 -05:00
rebel onion
414a98b879 New translations strings.xml (Russian) 2024-04-19 13:33:56 -05:00
rebel onion
ca2343a2f4 New translations strings.xml (Arabic) 2024-04-19 13:33:55 -05:00
rebel onion
ac836e2011 New translations strings.xml (Bengali) 2024-04-19 11:55:28 -05:00
rebel onion
f7c47e3a8b New translations strings.xml (Korean) 2024-04-19 11:55:27 -05:00
rebel onion
92dc364364 New translations strings.xml (Greek) 2024-04-19 11:55:26 -05:00
rebel onion
1a1919971a New translations strings.xml (Assamese) 2024-04-19 11:55:24 -05:00
rebel onion
595055073c New translations strings.xml (Hindi) 2024-04-19 11:55:23 -05:00
rebel onion
832befc9a7 New translations strings.xml (Spanish, Mexico) 2024-04-19 11:55:22 -05:00
rebel onion
3ef4a6388d New translations strings.xml (Chinese Simplified) 2024-04-19 11:55:21 -05:00
rebel onion
63a5800025 New translations strings.xml (Russian) 2024-04-19 11:55:19 -05:00
rebel onion
11c875c4a3 New translations strings.xml (Japanese) 2024-04-19 11:55:18 -05:00
rebel onion
0f57f83093 New translations strings.xml (German) 2024-04-19 11:55:16 -05:00
rebel onion
33276a1724 New translations strings.xml (Arabic) 2024-04-19 11:55:15 -05:00
rebel onion
adeb627556 New translations strings.xml (Spanish) 2024-04-19 11:55:14 -05:00
rebel onion
d45a5c0b46 New translations strings.xml (French) 2024-04-19 11:55:12 -05:00
rebelonion
3c46c21a25 feat: downloading extensions 2024-04-19 11:24:03 -05:00
Sadwhy
44178b2de2 Why use decapitated actions? (#373) 2024-04-19 10:52:12 -05:00
rebel onion
13f5d0978d Update Crowdin configuration file 2024-04-19 10:22:05 -05:00
rebelonion
70a50ece43 chore: cleanup pt2 2024-04-19 06:13:14 -05:00
rebelonion
24147e746a chore: code cleanup 2024-04-19 06:03:40 -05:00
rebelonion
386e02a564 fix: exoplayer initialization 2024-04-19 05:40:43 -05:00
rebelonion
865b96a219 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-19 05:25:01 -05:00
rebel onion
dd38bb156b Update Crowdin configuration file 2024-04-19 05:07:19 -05:00
rebelonion
72c07b7d7a fix: app updater single apk 2024-04-19 04:33:36 -05:00
rebelonion
3f19cadffc fix: workflow universal 2024-04-19 04:16:54 -05:00
rebel onion
670d16bd8e Addons (#368)
* feat: (wip) torrent

credit to kuukiyomi

* fix: extensions -> addons

* fix: unified loader

* feat: (wip) modularity

* fix: addon ui

* feat: addon install/uninstall

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-19 04:08:20 -05:00
aayush262
3d1040b280 [skip ci] feat: theme crash fix 2024-04-18 01:55:15 +05:30
aayush262
cd3bb20afd [skip ci] feat: new settings UI 2024-04-17 14:35:53 +05:30
aayush262
91d1d2cf1d feat: WIP new settings UI 2024-04-17 01:51:56 +05:30
Sadwhy
f8a6fad513 fix(settings): Center icons (again) 😔 (#361) 2024-04-16 00:28:41 -05:00
aayush262
9d3d394c7d feat: hide a12 theme settings for unsupported devices 2024-04-15 22:10:38 +05:30
aayush262
820a09b28f feat: settings to recycler view 2024-04-15 22:04:15 +05:30
aayush262
108285021e fix: custom theme dialog not working 2024-04-15 21:54:01 +05:30
aayush262
1d005585c8 feat: sort forks by stars 2024-04-15 21:52:45 +05:30
rebelonion
714591dd2e fix: infinite loop 2024-04-15 01:51:33 -05:00
rebelonion
6e399b32e1 feat: embedded tracks
modified #338
2024-04-15 01:02:28 -05:00
rebelonion
4b413b78fe fix: alpha update message 2024-04-14 23:30:52 -05:00
rebelonion
126bc6134e chore: update extension api 2024-04-14 23:30:37 -05:00
rebelonion
bf33f5d9c8 fix: activity opening twice 2024-04-14 22:39:30 -05:00
rebelonion
a8ff4fdc26 feat: nomedia file 2024-04-14 22:35:11 -05:00
rebelonion
7ca44480a9 fix: offline mode failing 2024-04-14 22:24:58 -05:00
Sadwhy
ea29449413 fix(settings): Centre icons 2024-04-14 23:54:12 +05:30
Sadwhy
9ec448e503 Feet(Settings): Revamped UI (#352)
Feet(Settings): Revamped UI
2024-04-13 01:22:10 +05:30
aayush262
70be4e92fb fix: Disclaimer dialog crash 2024-04-12 13:40:28 +05:30
Sadwhy
c0e3243ee6 Feet(Settings): UI changes (#351)
* Account

* Theme

* Extension, common, notification and anime

* manga and about

* fix(Settings): icon colours
2024-04-12 12:45:53 +05:30
aayush262
b961701189 fix: settings scrolling 2024-04-11 21:37:25 +05:30
aayush262
3619355cb4 fix: idr 2024-04-11 17:26:20 +05:30
aayush262
674a512630 feat: split all settings 2024-04-11 17:25:41 +05:30
aayush262
5e5277404e feat: toggle for icon in rpc 2024-04-10 14:05:24 +05:30
aayush262
c242d9dd99 fix: no more 13 arifs 2024-04-08 22:52:13 +05:30
aayush262
a5a94e5003 fix: no 13 arifs 2024-04-08 21:08:49 +05:30
aayush262
9b6dc1318d fix: idr 2024-04-08 17:45:18 +05:30
TwistedUmbrellaX
87535a9239 fix: got lost in the cherry-picking (#337) 2024-04-07 21:48:26 -05:00
tutel
6be589618c Added Skip Recap Feature (#336)
* Added Skip Recap Feature

* Reverted gradle.properties to default
2024-04-07 21:30:49 -05:00
TwistedUmbrellaX
a51e025c03 fix: address possible format issues (#331)
* fix: address possible format issued

* fix: improve results and logging

* fix: not everything is a title

Fixed the book title style of capitalization

Toast and Snackbar messages appear for less than 3 seconds. Why are they paragraphs?

* Fix: the other half of the file

Probably missed a few, but this fixes the rest of the obvious ones (including a double negative)
2024-04-07 21:28:34 -05:00
TwistedUmbrellaX
29e115ce41 feat: repo editor in extension window (#332)
* fix: error checking in repo editor

* feat: edit repos from extension page
2024-04-07 21:27:27 -05:00
TwistedUmbrellaX
f96d2ffaa5 feat: add per-widget configuration (#333)
* feat: add per-widget configuration

* fix: no need to overengineer it

* feat: add cache to bitmap download

dfgdfg

* fix: elvis has left the operation
2024-04-07 21:21:24 -05:00
TwistedUmbrellaX
47d05e737d fix: exo / subtitle improvements (#335) 2024-04-07 21:19:50 -05:00
aayush262
3666758e6e feat(socials): ratings and progress 2024-04-07 21:31:15 +05:30
aayush262
e49f0dbf32 feat: socials in media 2024-04-07 00:51:50 +05:30
aayush262
abe3f883ae fix: telegram changelogs (again) 2024-04-06 16:47:31 +05:30
aayush262
e5cb7c7fdf fix: Voice artists not showing media 2024-04-06 16:23:58 +05:30
rebelonion
79337b5e7f Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-06 00:58:16 -05:00
rebelonion
ae5907e6b3 fix: correct updater for alpha 2024-04-06 00:57:33 -05:00
aayush262
04fb31eff9 fix: changelogs for telegram 2024-04-06 11:12:00 +05:30
rebelonion
9f7e01a1fb fix: only show count on releasing/hiatus manga 2024-04-05 21:59:44 -05:00
TwistedUmbrellaX
9ace8e5235 fix: it was only an int for convenience (#330)
Probably would have saved a lot of elaborate attempts to fix the issue by simply going the other way.
2024-04-05 21:53:25 -05:00
rebelonion
771cdcc163 fix: MangaExtensionRepos where AnimeExtensionRepos should be 2024-04-05 21:43:32 -05:00
rebelonion
58d5b5bc41 fix: fix the fix for the fix for MangaUpdates
null is better than a crash
2024-04-05 21:21:33 -05:00
TwistedUmbrellaX
04538c52f2 fix: fix the fix for MangaUpdates (#327)
Further filtering prevents the bad records from cancelling the whole operation
2024-04-05 20:04:03 -05:00
rebelonion
dd994dcfab fix: request storage permission for novels 2024-04-05 20:02:36 -05:00
rebelonion
594b71dc16 fix: cache upcoming widget data 2024-04-05 19:50:59 -05:00
rebelonion
cf7ccaebd1 feat: AppUpdater can handle splits 2024-04-05 19:21:00 -05:00
rebelonion
8bde831794 fix: default SearchSources to false
many people thought this was a bug. defaulting off max users more aware of what's going on
2024-04-05 18:54:56 -05:00
rebelonion
2f30bdb6a8 fix: case for empty headers 2024-04-05 18:34:25 -05:00
rebelonion
4d28ae2e3e fix: handle last manga chapter check being null 2024-04-05 18:00:24 -05:00
rebelonion
5fcbfeb3db fix: download name comparison 2024-04-05 17:44:59 -05:00
TwistedUmbrellaX
f6c7b09d9b fix: remove duplicate extension code (#322)
* fix: remove duplicate extension code

* fix: allow Material You icons to load

* fix: remove unused preference item

* fix: load mono on square setups
2024-04-05 16:50:40 -05:00
TwistedUmbrellaX
72c69e7c79 ExoPlayer improvements (#325)
* fix: add declarations for BT headsets

* fix: stop overriding user settings

* fix: offload cache to external storage
2024-04-05 16:49:15 -05:00
aayush262
13a65c2bfa Merge remote-tracking branch 'origin/dev' into dev 2024-04-06 01:21:12 +05:30
aayush262
d2f118a86c feat: more media in recommendations 2024-04-06 01:20:29 +05:30
Sadwhy
ce11c71e95 Fixed artifact upload (#326) 2024-04-05 13:43:22 -05:00
rebel onion
e4574d6c03 feat: send all apks to telegram 2024-04-05 11:40:11 -05:00
Sadwhy
d8c311fbd7 Include all Splits for discord (#324) 2024-04-05 11:36:27 -05:00
aayush262
d6e6c6f8fb Merge remote-tracking branch 'origin/dev' into dev 2024-04-05 13:20:57 +05:30
aayush262
63c3058f5b feat: voiceActor's characters info 2024-04-05 13:20:24 +05:30
aayush262
0d5815d3c9 fix: workflow 2024-04-05 01:22:33 +05:30
aayush262
dec4996760 feat: voiceActors (not info for now) 2024-04-05 01:09:56 +05:30
aayush262
e0df092a70 fix: some tweaks in settings 2024-04-04 22:59:58 +05:30
rebelonion
da56aecd5e fix: return jsdeliver 2024-04-04 05:23:11 -05:00
rebelonion
7688ffa39f Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-04 04:49:08 -05:00
rebelonion
d08e89bb63 Merge branch 'pr/299' into dev 2024-04-04 04:48:51 -05:00
TwistedUmbrellaX
5979479619 Two intents enter, one intent leaves (#317)
* feat: add a profile exit button

* fix: prevent leaving landscape behind

This prevents landscape being left out of changes and potentially causing a crash by adding a shared item for the identical portion of the views.
2024-04-04 04:46:23 -05:00
TwistedUmbrellaX
e1b968bfe0 feat: add a time since chapter item (#316)
* feat: add a time since chapter item

* fix: this is the song that never ends
2024-04-04 04:45:03 -05:00
rebelonion
36c476bc36 fix: remove armeabi 2024-04-04 04:40:46 -05:00
rebel onion
6bfadfa962 Update beta.yml 2024-04-04 04:11:52 -05:00
rebel onion
720b40afa7 feat: custom downloader and downloader location (#313)
* feat: custom downloader (novel broken)

* fix: send headers to ffmpeg

ffmpeg can be a real bitch to work with

* fix: offline page for new download system

* feat: novel to new system | load freezing

* chore: clean manifest

* fix: notification incrementing

* feat: changing the downloads dir
2024-04-04 04:03:45 -05:00
TwistedUmbrellaX
75e90541c9 fix: make bottom to top work properly (#320)
* fix: make bottom to top work properly

Fixes navigating to the wrong chapter, despite the UI being correct. Makes bottom to top its own setting that functions as expected, not just top to bottom with a RTL slider bar.

* fix: allow inversion to pick sides
2024-04-04 03:26:21 -05:00
aayush262
47b1940ace feat: Some comment design tweaks 2024-04-03 22:21:27 +05:30
TwistedUmbrellaX
012b1cd79d fix: make settings great again
auto and time stamps are intertwined already and the dividers should serve a purpose
2024-04-03 10:49:50 -04:00
TwistedUmbrellaX
ff3131d988 feat: manual repository entries
Closes Dantotsu#298
2024-04-03 10:49:50 -04:00
TwistedUmbrellaX
ba1725224a fix: automate getting contributions (#314)
* fix: automate getting contributions

It shouldn't need to be a conscious decision. It would be nice if the site_admin  flag worked for the repo owner, but it's a known value

* fix: also populate the forks page

This hardcodes this repo, since downstream builds should still display the upstream forks
2024-04-02 18:08:44 -05:00
Sadwhy
55bc2add85 Updated icons (#311)
* Branch

* Updated Icons (#13)

* update

* update beta.yml

* nicer icons

* update view_list_24.xml

* revert changes

* Missed one
2024-04-02 18:04:18 -05:00
ibo
9e96fd1e20 feat(accounts): redirect on avatar click (#310)
* feat(discord): custom buttons

* feat(discord): added haptics

* fine...

* fix(strings): my genius is frightening

* feat: add option to only show the first button

* feat: discord rpc menu

* feat(link): add button preview back

* feat(accounts): redirect on avatar click

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-02 18:04:08 -05:00
aayush262
79d20b0b63 feat: ibo happy now? 2024-04-02 19:11:20 +05:30
aayush262
b2a44cfe09 Merge remote-tracking branch 'origin/dev' into dev 2024-04-02 17:02:56 +05:30
ibo
146805af49 feat(filter): revamping search for anime and manga (#272)
* feat: revamping search filter part1

* fix: sortBy dropdown now also calls search instead of only updating image

* feat: added longclick listener to reset and apply + cleaned up code

* feat: status filter fully functional

* chore: upgrade AGP to 8.3.1

* fix: splitted status list and cleaned up

* fix(search): underscore

* feat: attempt to add backend for countryOfOrigin filter

* fix: countryOfOrigin query and gradle

* feat: source filter fully functional

* fix(source): underscore

* feat: swap source with status

* fix: add searchSource to reset fun

* fix: clear underline after reopening bottom sheet

* chore: remove unnecessary declaration

* feat: add global to countryOfOrigin dropdown

* feat: floating cancel and apply button

* fix: added searchStatus and searchYear back to manga filter

* feat: desperate attempt for manga year filter

* feat(sortBy): added new releases item

* fix: year filter

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-01 22:13:41 -05:00
ibo
aabbe9198a feat(discord): custom buttons (#295)
* feat(discord): custom buttons

* feat(discord): added haptics

* fine...

* fix(strings): my genius is frightening

* feat: add option to only show the first button

* feat: discord rpc menu

* feat(link): add button preview back

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-01 22:09:52 -05:00
aayush262
a815bac15d feat: 18+ media on infinite scroll too 2024-04-01 19:31:32 +05:30
Sadwhy
86427a4c3c Add CommitHash to Version Name :prayge: (#307) 2024-04-01 00:15:24 -05:00
aayush262
0d8a82568a feat: Download subs 2024-03-31 18:23:29 +05:30
aayush262
95b2939532 fix: hide recent if its empty 2024-03-31 16:44:27 +05:30
aayush262
76e11e5a3e fix: removed unused banners 2024-03-31 16:43:55 +05:30
aayush262
2d5d02fd67 fix: adult only in recent too 2024-03-31 16:18:38 +05:30
aayush262
f30e6b7809 fix: banner animation 2024-03-31 12:23:17 +05:30
aayush262
04f2034dd1 fix: duplicate media 2024-03-31 09:08:54 +05:30
aayush262
99b3bbaaad feat: adult only media option 2024-03-30 15:44:29 +05:30
aayush262
c0bccc027f feat: combined queries 2024-03-30 15:43:37 +05:30
TwistedUmbrellaX
51beac2d03 Revert (some of) "Just some quality of life garbage (#304)" (#306)
This reverts (some of) commit c29147a681.
2024-03-29 21:53:49 -05:00
rebelonion
63a5150cea fix: home screen number spacing 2024-03-29 18:10:41 -05:00
rebelonion
e34a20bce6 fix: comment scrolling freezing 2024-03-29 18:04:19 -05:00
rebelonion
ca482ea9d4 fix: navbar breaking on return to comments fragment 2024-03-29 17:53:50 -05:00
rebelonion
e31d2ada4f fix: logout of comments when log out of anilist 2024-03-29 17:18:20 -05:00
TwistedUmbrellaX
c29147a681 Just some quality of life garbage (#304)
* fix: statistics widget min sizes

* fix: offset for split TextView values

Due to format and color changes, the text is split between two separate items and this space avoids multiple insertions in code

* feat: extension launch from notice

* fix: wait for the UI to post stuff to it
2024-03-29 17:11:37 -05:00
aayush262
92be9bf626 fix: removed onlist for now 2024-03-30 03:11:05 +05:30
aayush262
a02b8b7b0a fix: text in manga side not disappearing 2024-03-30 03:04:27 +05:30
aayush262
1c1d14fff1 fix: "popular manga" text missing 2024-03-30 02:54:44 +05:30
aayush262
eff0a34c54 Merge remote-tracking branch 'origin/dev' into dev 2024-03-30 02:40:04 +05:30
aayush262
2dc3035a7c feat: more options in anime and manga side 2024-03-30 02:39:47 +05:30
TwistedUmbrellaX
78f6ec27b3 feat: add watch title search button (#303) 2024-03-28 17:46:13 -05:00
TwistedUmbrellaX
6b868fa824 fix: not meant to be quoted (#300)
* fix not meant to be quoted

* fix: thought he was slick

hiding in plain sight

* fix: it's not THAT important

* fix: flexible day / night borders
2024-03-28 17:39:04 -05:00
rebelonion
7951c2cf37 fix: some widget sting newlines 2024-03-27 19:02:42 -05:00
rebelonion
ea678ef55e feat: visual representation of selected widget colors 2024-03-27 18:41:48 -05:00
rebelonion
fbbbf41595 feat: custom theming for stats widget 2024-03-27 18:23:13 -05:00
TwistedUmbrellaX
f83d1d8d84 Profile Stats Widget (#292)
* feat: create a statistics widget

* feat: mirror app color option

* fix: the minimum size cut off

* feat: make the stat widget decent

* fix: prevent bleeding edges

* fix: PREVENT BLEEDING EDGES!

* fix: we didn't really need an overlay
2024-03-27 17:45:26 -05:00
TwistedUmbrellaX
7bcc01b94e Merging stuff. Cleaning up code. The usual (#297)
* chore: merge core extension view

* fix: clean up a sloppy fix

* chore: merge name adapters

* fix: offset the indentation of example
2024-03-27 17:45:01 -05:00
aayush262
ff72f9dbdf fix: activity crash 2024-03-27 14:22:04 +05:30
aayush262
b1210570d1 Merge remote-tracking branch 'origin/dev' into dev 2024-03-27 13:52:13 +05:30
aayush262
ef97b5679e feat(widget): use app color 2024-03-27 13:52:00 +05:30
TwistedUmbrellaX
6dfe0269bf Manga reader quirks (#294)
* fix: resolve showing next on previous

* fix: make your last words succinct
2024-03-26 16:10:27 -05:00
TwistedUmbrellaX
77c57846ed fix: add padding to last item in recycler (#293)
* fix: add padding to last item in recycler

Stop guessing numbers to compensate for a view we can measure. by adding a method to measure them.

* fix: avoid scrolling artifacts in nested
2024-03-26 16:10:02 -05:00
aayush262
19b5b11b07 fix(profile): something 2024-03-26 16:06:53 +05:30
aayush262
27d4ce3c5b fix(profile): info card padding 2024-03-26 15:46:38 +05:30
aayush262
859aa01ec2 fix(profile): info is hidden 2024-03-26 15:11:48 +05:30
aayush262
6d102f7be3 fix(media): comment bar padding 2024-03-26 14:48:18 +05:30
rebelonion
5ae1ead2c9 fix: default bitmap width/height 2024-03-26 00:07:48 -05:00
rebelonion
b1982013dc fix: auto curve edges on resize 2024-03-25 23:36:51 -05:00
rebelonion
954fdde1c4 feat: rounded corners compat 2024-03-25 23:07:45 -05:00
rebelonion
f177e2cf7c chore: clean package location 2024-03-25 22:48:26 -05:00
rebelonion
845ebb4868 feat: widget transparency 2024-03-25 22:18:49 -05:00
rebelonion
b43171bb31 fix: remove unnecessary v26 file 2024-03-25 22:12:23 -05:00
rebelonion
be07fad8f1 fix: layout tweaks for upcoming widget 2024-03-25 22:08:51 -05:00
rebelonion
3375496ef2 fix: auto scale title font size 2024-03-25 21:26:25 -05:00
rebelonion
df23b2f62f feat: currently airing widget 2024-03-25 21:20:17 -05:00
TwistedUmbrellaX
95cddbd409 feat: suppress ime for search in progress (#287) 2024-03-25 15:25:58 -05:00
TwistedUmbrellaX
d46f1b25eb feat: option to disable trending scroll (#288) 2024-03-25 15:24:55 -05:00
TwistedUmbrellaX
378abe73c9 feat: add haptics to long click event (#290)
Please, someone. Anyone. Tell me it's OK to let go....
2024-03-25 15:23:44 -05:00
rebelonion
b5eda797b5 fix: context being lost in settings 2024-03-24 18:47:53 -05:00
TwistedUmbrellaX
f704e322af fix: data loading glitches (#284)
* fix: the obnoxious loading glitch

* chore: some quick build warnings
2024-03-24 16:09:26 -05:00
rebelonion
dc21d28b83 Merge branch 'pr/282' into dev 2024-03-23 22:04:55 -05:00
rebelonion
eb17862177 fix: remove unnecessary InefficientWeight 2024-03-23 22:03:09 -05:00
TwistedUmbrellaX
fc023f307a fix: weights reflect other views (#285) 2024-03-23 22:01:44 -05:00
rebelonion
ad1905c8fe fix: adapter continuous loading on media page 2024-03-23 21:50:24 -05:00
rebelonion
85d54e8f5e Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-23 21:12:57 -05:00
rebelonion
ba09f7533c fix: anime page searching manga 2024-03-23 21:12:55 -05:00
TwistedUmbrellaX
fa6e3a34b5 fix: prefer caching the final version (#283)
While caching the original and the final seems like an ideal way to reduce overhead, you cache an original copy of the image and the modified copy of the image to only ever load the modified copy. The size is set, meaning you are not reusing the original image. There is no reason to cache it.
2024-03-23 20:07:02 -05:00
TwistedUmbrellaX
85ef4b3c12 Add transparency options to subtitle (#281)
* feat: add state llistener to Xpandable

* feat: improve app restart process

* feat: support subtitle transparency
2024-03-23 19:12:22 -05:00
rebel onion
89e18b0e2f Merge pull request #280 from RepoDevil/semi-auto
Automatically search through sources
2024-03-23 19:06:56 -05:00
rebelonion
1b50ffcf11 fix: clean up some warnings 2024-03-23 18:05:43 -05:00
TwistedUmbrellaX
b3f83816c5 feat: support exporting magnets 2024-03-23 18:45:23 -04:00
TwistedUmbrellaX
75b78886ae fix: clarify deceptive descriptions 2024-03-23 18:30:54 -04:00
TwistedUmbrellaX
26d97da066 feat: automatically check sources 2024-03-23 18:30:31 -04:00
rebelonion
ab7bc15573 fix: missing string/imports 2024-03-23 17:07:02 -05:00
rebel onion
d43d643bbd Merge pull request #271 from RepoDevil/cleanup
The motherload
2024-03-23 16:56:18 -05:00
TwistedUmbrellaX
3ca5efc177 chore: update androidx.mediarouter
No additional code changes required
2024-03-23 09:39:36 -04:00
TwistedUmbrellaX
04c858e6fd chore: kotlinOptions is deprecated 2024-03-23 09:32:27 -04:00
TwistedUmbrellaX
25046e4c11 chore: add notes for context view
This will allow the details to be seen when highlighting these items in Android Studio
2024-03-23 08:56:15 -04:00
rebelonion
5134776e2f fix: remove unnecessary setExpedited 2024-03-22 23:08:20 -05:00
rebelonion
cc29ebd75b fix: subscription default importance 2024-03-22 22:57:26 -05:00
TwistedUmbrellaX
2233f1ce44 fix: restore a workaround?
The layout this originally used no longer exists and the new layout is a different type, but maybe this will still work.
2024-03-22 23:47:40 -04:00
rebelonion
a189802061 fix: notification check on app launch 2024-03-22 22:34:21 -05:00
rebelonion
dca6ffdbbe Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-22 21:48:01 -05:00
rebelonion
859946a751 chore: version bump 2024-03-22 21:47:58 -05:00
TwistedUmbrellaX
08bf1a2336 chore: eliminate overlap layouts 2024-03-22 19:12:58 -04:00
TwistedUmbrellaX
27743e3427 fix: padding / margin optimization 2024-03-22 14:35:07 -04:00
TwistedUmbrellaX
52b0cc4129 fix: hardcoded text in profile page 2024-03-22 14:26:12 -04:00
TwistedUmbrellaX
22abc2e21d fix: expanded views while editing
This sets the Editor state of the expanded windows. This is NOT reflected at runtime.
2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
fc8425b12a fix: padding / margin optimization 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
60fc1fa74b fix: tone down the logging a bit 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
190e3ce7bb fix: someone liked the paste hotkey 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
012024ab77 fix: disable auto with time stamps 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
529bdd74c8 chore: flag ites for no translation 2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
6e349b84c0 chore: extract strings from settings 2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
ab9b92035e fix: merge bindings by category 2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
37ec165319 chore: lint performance optimization
This includes shadowed variables, unnecessary parameters, layouts with string literals, items that cause performance bottlenecks, and the merge of extension types into only the necessary separate classes.
2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
958aa634b1 feat: commit to the prank... 2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
125a95285d chore: addressing SetTextI18n 2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
bbaae2e776 fix: settings has over 80 views
One really long layout is bad for performance, but this design also requires being aware of where an item is being placed.
2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
f9090f59b7 fix: support for round vertical 2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
1d740d33a0 fix: putting out 100 little fires
... before they become an inferno
2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
633ec19c90 fix: don't load selected until intent 2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
9b2015f4cf fix: simplify boolean view logic
This is a pretty basic conversion from `if (true) View.VISIBLE else View.GONE` to `isVisible` which is exactly that, but easier to track.
2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
e65e7a79a5 feat: vertical navigation for profile 2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
0996639cac fix: vertical AnimatedBottomBar 2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
e5f58f20c7 fix: undo all of the margin hacks
Using 72dp as the height appears to have been a bit of a hack to appear beyond the navigation bar. In cases where the bar is not present, such as landscape, this left a gap between the bottom of the screen and bar. On API 23, the result was the opposite. All of this can be addressed by simply relying on the actual measurements and not compensating for compensation.
2024-03-22 14:18:13 -04:00
aayush262
d1e03b8237 feat(media): fav and popularity count 2024-03-22 23:44:51 +05:30
aayush262
917ffe644f feat: something 2024-03-22 20:38:34 +05:30
aayush262
02efc01a10 feat(profile): round chips 2024-03-22 12:50:04 +05:30
aayush262
3016792f95 fix(activity): blur banner 2024-03-22 11:24:36 +05:30
Sadwhy
e1b50c86f3 feet(watch): Fixed one inconsistent switch (#273)
* feet(watch): Fixed one inconsistent switch
2024-03-21 11:41:16 +05:30
aayush262
42f23e4345 dix: many small changes 2024-03-21 01:18:36 +05:30
rebelonion
adb304f138 fix: manga/anime page noti icon updating 2024-03-20 14:29:52 -05:00
rebelonion
3bbf9efe63 dix: comment scroll deadspace 2024-03-20 13:54:25 -05:00
rebelonion
b454a2e3d9 fix: comment notification at bottom 2024-03-20 13:08:46 -05:00
rebelonion
23e6323f92 fix: comment reply dead scrolling space 2024-03-20 04:29:14 -05:00
rebelonion
b0dbd7a348 fix: add a check for minimum poll time 2024-03-20 04:10:12 -05:00
rebelonion
f707f8cc33 fix: activity color tweaks 2024-03-20 01:23:48 -05:00
aayush262
fa7126d80d fix: better gradiant color 2024-03-20 11:13:09 +05:30
aayush262
7d5f69888a fix(profile): double usernames 2024-03-20 10:57:50 +05:30
rebelonion
51841cf05f feat: error message snack -> toast 2024-03-20 00:23:42 -05:00
rebelonion
6d2c01ff2b chore: version bump 2024-03-20 00:19:39 -05:00
rebelonion
0bd4755814 fix: remove snack spam 2024-03-20 00:17:27 -05:00
rebelonion
927ba5ac86 fix: AAChartCore library not found tempfix 2024-03-19 20:18:39 -05:00
rebelonion
808d4e6bf5 feat: move subscriptions to new notification method 2024-03-19 19:30:12 -05:00
rebelonion
a39db5ea93 fix: cleaner spoiler text in comments 2024-03-19 17:09:34 -05:00
rebelonion
ca2409ef91 fix: fav workaround for broken anilist api 2024-03-19 16:50:52 -05:00
rebelonion
7b1f1a1357 fix: more robust notification loading 2024-03-19 16:02:52 -05:00
rebelonion
9471683501 feat: AlarmManager option for notifications 2024-03-18 23:51:00 -05:00
rebelonion
deeefb8e35 fix: don't show 500 error code 2024-03-18 17:55:12 -05:00
rebelonion
c777888fdb Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-18 17:45:36 -05:00
rebelonion
ce50627989 fix: add missing ui SharedPreference 2024-03-18 17:45:17 -05:00
TwistedUmbrellaX
9f84845ada fix: login and navigation < API 23 (#258)
* fix: compensate for old nav (48dp)

* fix: allow login to complete < API 23
2024-03-18 17:42:33 -05:00
rebelonion
6a8e422a30 fix: webview loading crash 2024-03-18 17:27:32 -05:00
rebelonion
39d6f0fbd6 fix: don't open links in webview 2024-03-18 10:40:39 -05:00
rebelonion
c240664fda fix: links in apps always open externally 2024-03-18 10:38:19 -05:00
rebelonion
a0f6320eee fix: what file links dantotsu opens 2024-03-18 10:12:32 -05:00
rebelonion
3434aa9744 fix: sticky profile fragment 2024-03-18 09:53:54 -05:00
rebelonion
3e84cfe09a fix: profile fragment scrolling 2024-03-18 00:23:40 -05:00
aayush262
22dccaa24b Merge remote-tracking branch 'origin/dev' into dev 2024-03-18 10:39:42 +05:30
rebelonion
ffe921a223 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-17 23:14:07 -05:00
rebelonion
1fd91b9ec6 Merge branch 'pr/257' into dev 2024-03-17 23:12:40 -05:00
TwistedUmbrellaX
cf10229574 fix: address deprecated code (#256)
* fix: address deprecated code

Build.RADIO has been deprecated since API 15, which means it hasn't worked since before the lowest target API of the app, and versioncode is deprecated in API 28.

* fix: use the convenience method

This takes the unused convenience method and the individually declared uses and merges them.

* fix: simplify compat switch
2024-03-17 23:10:38 -05:00
rebelonion
5c2ae57d77 feat: open user links in dantotsu 2024-03-17 23:05:05 -05:00
rebelonion
353452dd21 feat: open settings files directly 2024-03-17 22:15:20 -05:00
rebelonion
92fa0c117d fix: stats hardware acceleration glitch 2024-03-17 21:59:21 -05:00
rebelonion
9f4cd0ba0d fix: z fighting in viewpager2 potential? 2024-03-17 21:21:17 -05:00
rebelonion
385198e69a fix: text | reply bar not hiding 2024-03-17 21:07:54 -05:00
TwistedUmbrellaX
89fe3b82a3 fix: excess scope and redundancy 2024-03-17 22:05:14 -04:00
rebelonion
af1bc944d8 feat: sort comments in api 2024-03-17 20:52:39 -05:00
rebelonion
a0b22e8d56 fix: setdub out of bounds 2024-03-17 20:15:21 -05:00
rebelonion
c47d1afa1a feat: comment notifications in notification section 2024-03-17 20:05:38 -05:00
TwistedUmbrellaX
12a5b602e9 feat: getColor compatibility changes 2024-03-17 20:03:21 -04:00
rebelonion
25b85569fe fix: fragment IllegalStateException 2024-03-17 18:25:38 -05:00
rebelonion
b373a52218 fix: search for Spanish "episode" 2024-03-17 18:14:00 -05:00
rebelonion
b0e46cd904 fix: all notifications going to the same activity 2024-03-17 17:56:53 -05:00
rebelonion
89a54b4509 fix: recycled stat item 2024-03-17 17:35:59 -05:00
TwistedUmbrellaX
56aefef693 feat: move theme to API 23 res 2024-03-17 18:06:28 -04:00
TwistedUmbrellaX
a8ad018c44 feat: support API 21 with compat 2024-03-17 14:36:07 -04:00
aayush262
726f461ff6 fix(profile): Buggy animation 2024-03-17 21:06:20 +05:30
rebelonion
9c0861a8e4 feat: character fav 2024-03-17 01:39:21 -05:00
rebelonion
ca0162fb9c Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-17 01:01:09 -05:00
rebelonion
cfb8c3c733 feat: toggle fav 2024-03-17 01:00:08 -05:00
aayush262
fea448f850 feat: fav character (WIP) 2024-03-17 11:29:58 +05:30
rebelonion
c033bb0445 fix: use hardware acceleration for bio 2024-03-17 00:35:51 -05:00
rebelonion
bb110be9ab fix: bio color cleanup 2024-03-17 00:10:39 -05:00
rebelonion
fd39c4f391 fix: some color logging 2024-03-16 23:22:37 -05:00
rebelonion
9a3f9c6de2 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-16 23:11:42 -05:00
rebelonion
cf9799da7c chore: kotlin version bump 2024-03-16 23:11:30 -05:00
TwistedUmbrellaX
c054e2f2ac feat: progress for starting manga (#245)
The caveat is that the user must have disabled updating each title individually, along with the other standard checks. This will only apply when a chapter has not been completed.
2024-03-16 23:00:58 -05:00
TwistedUmbrellaX
8177dfdcef feat: delete item from context menu (#251)
* feat: delete item from context menu

* fix: follow the naming convention
2024-03-16 22:56:38 -05:00
rebelonion
813b64980d Merge branch 'pr/247' into dev 2024-03-16 22:53:25 -05:00
rebelonion
fda809bc8a fix: more strategic refresh in comments 2024-03-16 22:44:45 -05:00
rebelonion
5d1b220105 version bump 2024-03-16 19:58:17 -05:00
rebelonion
de21365c90 feat: tell user if not logged in 2024-03-16 19:51:40 -05:00
rebelonion
a24d1515b3 fix: more descriptive string 2024-03-16 19:24:38 -05:00
rebelonion
a3d6f841c6 feat: use markwon builder 2024-03-16 19:10:37 -05:00
rebelonion
b770bca6ba fix: RPC image 2024-03-16 18:56:56 -05:00
rebelonion
eaefbc13f9 feat: logout check 2024-03-16 17:04:39 -05:00
rebelonion
7fcc23c5bf fix: refresh stats every page load 2024-03-16 16:56:24 -05:00
rebelonion
29364bf30a fix: chart load | background of bio? 2024-03-16 16:50:58 -05:00
rebelonion
a9b4916dd8 fix: combine profile query 2024-03-16 15:43:09 -05:00
TwistedUmbrellaX
d4ab0ad57d fix: hide the skip button if hidden (#252)
If using the option to hide the skip button after a delay, setting 0 results in a generic +85 button with no click action.
2024-03-16 11:54:29 -05:00
aayush262
e3f8096749 feat: better profile page 2024-03-16 22:04:57 +05:30
rebelonion
94aae33d10 feat: animations for comment/activity/notification 2024-03-15 21:21:14 -05:00
rebelonion
96e29a8c59 fix: dismiss after extractors loaded 2024-03-15 21:13:03 -05:00
rebelonion
34a9a55d4f fix: comment bar not visible (solution is so cursed) 2024-03-15 20:57:36 -05:00
rebelonion
cf93f6d657 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-15 18:05:31 -05:00
rebelonion
91bcacc978 feat: swipe refresh activity/notifications 2024-03-15 18:05:23 -05:00
TwistedUmbrellaX
e79a824a04 fix: remove landscape buffer (#250)
Portrait compensates for system navigation, which bleeds into landscape (where system nav is on the side) and places the bar 1/3 into the screen.
2024-03-15 17:53:52 -05:00
rebelonion
e00bbb2d8e fix: notification list blank on click 2024-03-15 17:38:02 -05:00
rebelonion
12c77604f1 fix: typo 2024-03-15 17:16:27 -05:00
TwistedUmbrellaX
9a1ec8567c fix: error when streams are empty (#249)
The current design simply presents an empty server list and leaves the user to click away. No action can be taken without leaving the dialog.
2024-03-15 17:15:43 -05:00
aayush262
5dbc01dba3 feat: long tap like button to see users 2024-03-15 18:32:51 +05:30
rebelonion
c5abfa15e0 feat: activity clicking 2024-03-15 05:55:46 -05:00
rebelonion
b69e466853 feat: notification to activity click 2024-03-14 16:25:59 -05:00
rebelonion
9e371778b7 feat: filter lists by genre 2024-03-14 15:51:40 -05:00
rebelonion
ff036165df fix: don't delete global 2024-03-14 15:09:07 -05:00
rebelonion
4ed74b664b feat: notification clicking 2024-03-14 14:40:48 -05:00
rebelonion
ddd59643c5 fix: stop re-sending anilist notifications 2024-03-14 14:17:15 -05:00
rebelonion
6122eb3669 feat: global notification 2024-03-14 14:16:25 -05:00
TwistedUmbrellaX
1b5149f143 fix: update gradle dependancies 2024-03-14 10:35:20 -04:00
rebelonion
b654824eb7 fix: home page not loading 2024-03-14 06:30:04 -05:00
rebelonion
4d2a08c258 feat: anilist notifications (real) 2024-03-14 06:00:48 -05:00
rebelonion
19697f4f39 feat: view profile on anilist 2024-03-14 02:59:59 -05:00
TwistedUmbrellaX
41eea667e5 fix: forgotten uncle onRestart (#244)
* fix: forgotten uncle onRestart

It functions a lot like onResume, but assumes that onCreate ran and the user navigated away from the activity completely.

* fix: don't change to the current tab
2024-03-14 02:46:37 -05:00
TwistedUmbrellaX
f0040b8392 feat: add an option to revert bar hide (#242)
* feat: add an option to revert bar hide

* fix: clarify the bars being hidden
2024-03-13 08:15:13 -05:00
ibo
291f61551a feat: hide scrollBar toggle (#238) 2024-03-13 07:57:40 -05:00
꧁𝓜𝓸𝓱𝓪𝓶𝓶𝓮𝓭 𝓞𝓽𝓪𝓴𝓾꧂
6e8bd08828 Update ExoplayerView.kt (#237)
* feat (player): added portrait mode

Co-authored-by: MohammedOtaku <121404638+MohammedOtaku@users.noreply.github.com>
2024-03-13 07:56:33 -05:00
tutel
e915dd619d Made the skip button dissappear after 5 seconds with a setting to turn it off (#224)
* Made the skip button dissappear after 5 seconds with a setting to turn it off

* Resolved Merge Conflicts and Removed Unnecessary Imports

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved problems

* Fixed a little mistake

* Made Requested Changes

* Removed println I forgot
2024-03-13 07:56:00 -05:00
aayush262
8fb6357fb5 feat: Blur toggle 2024-03-12 20:43:20 +05:30
aayush262
07662a91f4 Merge remote-tracking branch 'origin/dev' into dev 2024-03-12 10:23:12 +05:30
aayush262
37c618cb28 feat: blur function 2024-03-12 10:22:25 +05:30
rebelonion
5536f3b994 fix: logging home page 2024-03-11 11:53:35 -05:00
rebelonion
bdbbe62570 fix: genre sorting 2024-03-11 04:36:51 -05:00
rebelonion
4838e69aea Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-11 04:28:15 -05:00
rebelonion
408737d510 feat: activity replies 2024-03-11 04:28:13 -05:00
TwistedUmbrellaX
a0f05928e0 fix: reapply theme to each init call (#235) 2024-03-11 03:51:55 -05:00
rebelonion
a35887d4ac fix: tiny ui changes 2024-03-11 03:38:32 -05:00
rebelonion
dbce7c5b29 feat: logging to file 2024-03-11 03:01:08 -05:00
rebelonion
1028ac66cb fix: some anilist markdown 2024-03-11 00:05:07 -05:00
rebelonion
eb5e2623a0 feat: combine profile queries 2024-03-10 05:00:23 -05:00
rebelonion
867a4f36b3 fix: popup spam 2024-03-10 03:59:24 -05:00
rebelonion
913d74b285 feat: message activities 2024-03-10 03:59:15 -05:00
rebelonion
eb2eae7d6c fix: broken function name 2024-03-10 03:42:56 -05:00
rebelonion
8e5e548e16 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-10 03:21:22 -05:00
TwistedUmbrellaX
af1a481bdb Cleaning up navigation (#234)
* fix: align bottom to top with RTL

* fix: clean up the overlapping decor

* feat: match theme color with navbar

* fix: measure view on return to view
2024-03-10 03:21:14 -05:00
rebelonion
92089067f1 fix: activity pagination 2024-03-10 03:20:05 -05:00
rebelonion
d04ced94ea fix: comment pagination 2024-03-10 00:02:40 -06:00
TwistedUmbrellaX
14115ada4c A few build and navigation bar improvements (#231)
* fix: match project root to repo name

* feat: hide navigation bar until swiped

* fix: limit announcements to official

* feat: keep navigation visible for back

* fix: remove a duplicate permission
2024-03-09 15:06:48 -06:00
ibo
7f36eba709 feat: longclicklistener for AL profile now accessible everywhere (#228)
* feat: added longclicklistener for AL profile in AnimePageAdapter and MangaPageAdapter

* feat: add delete to smaller media bottom sheet
2024-03-09 15:02:23 -06:00
TwistedUmbrellaX
7504bb9081 fix: optimize querying download uri (#232) 2024-03-09 15:00:00 -06:00
TwistedUmbrellaX
64df08f91c fix: swap chapter names and nav on RTL (#230)
* fix: swap chapter names and nav on RTL

* fix: swipe RTL no longer needs invert
2024-03-09 14:58:15 -06:00
rebelonion
98f4d4f30b feat: global/personal feed | like posts | pagination 2024-03-09 04:33:06 -06:00
rebelonion
a9b03c45c6 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-08 22:05:17 -06:00
rebelonion
3af7926d20 feat: text activity 2024-03-08 22:04:25 -06:00
aayush262
e0cd43c63c fix: some UI changes pt:2 2024-03-08 15:24:35 +05:30
ibo
2742f58af5 feat(): fixed the UI changes 🦍 + notificationIcon logic and long press userAvatar 🐒 (#226) 2024-03-08 14:04:32 +05:30
rebelonion
49175a962a feat: (wip) user activities 2024-03-08 00:30:11 -06:00
rebelonion
46d8248ffd fix: profile recyclerViews 2024-03-07 18:45:02 -06:00
rebelonion
4ba1408f0f fix: notification size 2024-03-07 18:13:07 -06:00
Sadwhy
95fa5dcd9b Feet(profiles): update textviews (#221)
* nothing

* feet: Attached strings

* feet(fix)

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2024-03-07 17:36:18 -06:00
aayush262
a2ca16355a fix: some UI changes (for better or worse) 2024-03-08 00:45:13 +05:30
rebelonion
7ac679f927 feat: anilist notifications 2024-03-07 02:51:04 -06:00
aayush262
e2eae6250b feat: WIP activity and notification page 2024-03-07 01:02:27 +05:30
ibo
2855093f5f feat: Inbox WIP(#222) 2024-03-06 20:36:56 +05:30
rebelonion
e50a65571f fix: follow activity crash 2024-03-06 08:38:55 -06:00
Finnley Somdahl
acef7c3d5e fix: headerAdaptor crash 2024-03-06 08:11:13 -06:00
aayush262
18778f3c5a fix(profile): remove progress in fav media 2024-03-06 15:58:55 +05:30
aayush262
03dae8c1b0 Merge remote-tracking branch 'origin/dev' into dev 2024-03-06 14:15:04 +05:30
aayush262
c862c072b5 feat: author and staff stuff 2024-03-06 13:43:37 +05:30
rebelonion
251f1e89cf Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-05 23:53:40 -06:00
rebelonion
3632055081 fix: correctly set banner 2024-03-05 23:53:38 -06:00
aayush262
bd64454c15 fix(profile): padding and stuff 2024-03-06 10:33:43 +05:30
rebelonion
31afbd547e feat: following / followers page 2024-03-05 19:33:42 -06:00
aayush262
8da0092561 WTF: rebel pls fix it 2024-03-06 02:19:30 +05:30
aayush262
36c64951c7 fix: someshit 2024-03-06 00:19:36 +05:30
aayush262
120e63ea8a fix(comments): hide "search by image" when chips are active 2024-03-05 20:07:50 +05:30
aayush262
ad82faba3f fix(comments): fix comment bar color 2024-03-05 20:03:53 +05:30
aayush262
4c4bbe3214 feat(YT): moved back to watch section 2024-03-05 20:03:18 +05:30
aayush262
ecbc7efebc feat(search): moved search by image 2024-03-05 17:22:40 +05:30
aayush262
89b6f28b9f feat(profile): added fav characters and staff 2024-03-05 17:10:04 +05:30
aayush262
8a1097cd35 Merge remote-tracking branch 'origin/dev' into dev 2024-03-05 14:36:46 +05:30
rebelonion
47d74de7ce fix: normal link colors 2024-03-05 02:33:39 -06:00
rebelonion
f3c89b3ac5 chore: remove debug webview 2024-03-05 02:30:56 -06:00
rebelonion
a2ecc5e30e fix: most profiles 2024-03-05 02:29:00 -06:00
rebelonion
db50975174 fix: navbar color in media details 2024-03-05 01:11:49 -06:00
rebelonion
ab14c4815f fix: home list sorting 2024-03-05 00:53:56 -06:00
rebelonion
7ad586c994 feat: brighten pink theme 2024-03-05 00:29:48 -06:00
rebelonion
db979de829 feat: normalize genres 2024-03-05 00:25:40 -06:00
rebelonion
5218d5cd28 fix: for different count types 2024-03-05 00:00:10 -06:00
aayush262
9e4684e61c Merge remote-tracking branch 'origin/dev' into dev 2024-03-05 11:13:38 +05:30
rebelonion
9b408e7520 feat: follow button 2024-03-04 23:38:05 -06:00
aayush262
10bd7d0918 fix(font): replaced Century Gothic Bold with Levenim MT Bold 2024-03-05 11:02:32 +05:30
rebelonion
5279b0cd65 feat: compare user stats 2024-03-04 22:55:29 -06:00
rebelonion
d181dcf837 feat: open stat in new window 2024-03-04 18:26:12 -06:00
rebelonion
49dc9d55b5 fix: cast color 2024-03-04 17:49:28 -06:00
rebelonion
852e9d0d29 fix: vote indent | serialize error 2024-03-04 00:56:35 -06:00
rebelonion
7a1ed4f83e feat: more charts | code cleanup 2024-03-04 00:02:41 -06:00
aayush262
2673b7d9bc fix(profile):formatting 2024-03-04 01:22:02 +05:30
aayush262
1587aff433 fix(list): bottom padding 2024-03-04 01:12:57 +05:30
aayush262
26b3f50fe0 fix(comments): force scroll 2024-03-03 23:05:19 +05:30
aayush262
286297aa38 Merge remote-tracking branch 'origin/dev' into dev 2024-03-03 22:41:17 +05:30
aayush262
99a805826d fix(profile page): Fav manga not showing 2024-03-03 22:41:02 +05:30
ibo
be1711b51e feat: scanlation bulk ticker (#218)
* feat: scanlation mass tick (WIP)

* feat: scanlation mass tick

* fix: togglebutton on scanlation scrollview

* fix: fix ImageButton padding + overlay

* fix: minor padding adjustment
2024-03-03 11:09:07 -06:00
aayush262
51beea1dc8 fix(profile page): Full bio not showing 2024-03-03 20:49:01 +05:30
aayush262
297e9dd756 feat(profile page): Better charts view ig 2024-03-03 20:36:24 +05:30
aayush262
03b8e7dab6 feat(profile page): added fav anime and manga 2024-03-03 16:24:36 +05:30
rebelonion
dbe837be28 feat: switch some graph styles 2024-03-03 01:29:43 -06:00
rebelonion
945d5886ea feat: genre graph 2024-03-03 01:25:45 -06:00
rebelonion
93fa29829f feat: length / year graph 2024-03-03 00:01:56 -06:00
rebelonion
a9f8d223e9 feat: score graph 2024-03-02 22:32:40 -06:00
Finnley Somdahl
d2876d04f5 feat: (wip) graph theming 2024-03-02 20:12:50 -06:00
rebelonion
fcd5c621de Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-02 14:57:22 -06:00
rebelonion
5fb0204376 feat: multidimensional charts 2024-03-02 14:56:43 -06:00
aayush262
790ab1a343 feat(profile page): something 2024-03-02 23:30:32 +05:30
aayush262
86ed721796 feat(profile page): Stats, Banner animation 2024-03-02 23:01:58 +05:30
rebel onion
2837cad762 feat: break markdown 2024-03-02 05:27:44 -06:00
rebelonion
500de4e45e feat: statistics (wip) 2024-03-02 04:54:02 -06:00
rebelonion
533148069f Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-02 01:15:54 -06:00
rebelonion
42b0a3b62b feat: statistics page (wip) 2024-03-02 01:15:46 -06:00
aayush262
c720aed4fc feat(profile page): WIP 2024-03-02 12:41:45 +05:30
rebelonion
00dad2ad48 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-01 22:38:26 -06:00
rebelonion
ce62a9c645 chore: gradle update 2024-03-01 22:38:25 -06:00
Sadwhy
1e4e2fd701 Remove Arca (#212)
* Removed from gradle

* Removed arca from BasePreferences.kt
2024-03-01 19:46:56 -06:00
rebelonion
103be31a43 fix: separate nullable statistics class 2024-03-01 18:15:09 -06:00
rebelonion
63fa3c829e fix: missing bracket 2024-03-01 18:08:56 -06:00
rebelonion
ab5c623e53 feat: send media ids in profile query 2024-03-01 18:07:46 -06:00
rebelonion
5e307bb796 fix: discord status code cleanup 2024-03-01 17:47:41 -06:00
ibo
a5567ef909 feat: discord status switcher (#211) 2024-03-01 17:40:13 -06:00
rebelonion
da22347267 feat: user profile data 2024-03-01 17:35:52 -06:00
rebelonion
a401ab89f3 feat: warnings 2024-02-29 23:03:57 -06:00
rebelonion
6e6429db82 fix: keep user data up to date 2024-02-29 18:52:27 -06:00
rebelonion
05fc97a933 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-29 15:13:34 -06:00
rebelonion
b9eb9d82f1 fix: navbar squish after loading settings 2024-02-29 15:13:31 -06:00
aayush262
976acd4af2 feat(manga dates): Better time formatting 2024-03-01 00:23:25 +05:30
ibo
c5cbe408c1 Update UserInterfaceSettingsActivity.kt (#207)
feat(UI setting): restart option after changing default tabs
2024-03-01 00:12:40 +05:30
aayush262
1316d5a698 feat(manga): Date and Scanlator in description 2024-02-29 20:43:30 +05:30
rebelonion
89aaef8355 feat: smooth navbar indicator 2024-02-29 03:54:56 -06:00
rebelonion
94e3dff909 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-29 03:29:22 -06:00
rebelonion
6240c61a11 fix: comment item spacing 2024-02-29 03:29:14 -06:00
Sadwhy
f226614980 Using different colours values on ep watched count (#205) 2024-02-29 03:21:08 -06:00
rebelonion
0ab283b254 fix: navbar behind system navbar 2024-02-29 03:20:08 -06:00
rebelonion
449485f06a fix: search bars 2024-02-29 03:10:21 -06:00
rebelonion
60752e83ed fix: no external links 2024-02-28 12:41:41 -06:00
rebelonion
dfd8b597cd fix: limit comment size 2024-02-28 12:41:31 -06:00
rebelonion
e256fb1560 feat: better notification 2024-02-28 01:27:48 -06:00
aayush262
2f7c6e734e feat(comments): UI tweaks
fix(comments): top padding
fix: removed self report
feat: better colors in color picker
2024-02-27 23:24:59 +05:30
rebelonion
efe5f546a2 feat: reply notifications 2024-02-27 02:13:06 -06:00
rebelonion
a8bd9ef97b fix: reply bar not showing 2024-02-26 19:35:12 -06:00
rebelonion
e93ca5d86e fix: failed to parse comment when commenting 2024-02-26 03:10:52 -06:00
rebelonion
7f943d34ac feat: comment placement | tagging 2024-02-26 03:01:11 -06:00
rebelonion
8a922bd083 feat: token lifetime stored 2024-02-25 18:35:45 -06:00
rebelonion
d5c87c46aa fix: replying message hide correctly 2024-02-25 01:18:44 -06:00
rebelonion
f128dee3e4 fix: clear status bar for custom themes 2024-02-25 00:36:53 -06:00
rebelonion
9de129a35b fix: block some tags 2024-02-25 00:29:06 -06:00
rebelonion
6d6b0b975a feat: alert dialog for deleting 2024-02-24 23:54:54 -06:00
rebelonion
bff8983b23 fix: comment replies visibility 2024-02-24 23:50:20 -06:00
rebelonion
55e156579b feat: comment reporting 2024-02-24 22:43:55 -06:00
rebelonion
a251dd4ffb feat: limit comment depth to 4 2024-02-24 19:05:17 -06:00
rebelonion
526098f2bf feat: (wip) limit comment depth to 4 2024-02-23 19:24:17 -06:00
rebelonion
6ccdc10208 feat: add user level to comments 2024-02-23 18:55:53 -06:00
rebelonion
ce355c108e fix: right-side padding on nested comments 2024-02-23 18:43:01 -06:00
rebelonion
612936476d Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-23 18:13:29 -06:00
rebelonion
7ba117ec25 feat: set comment name color above WCAG Guidelines 2024-02-23 18:13:00 -06:00
aayush262
8ea6bf85b8 feat(comments): "reply to" above text input 2024-02-23 20:47:59 +05:30
Sadwhy
70c87b4067 A few ui changes (#204) 2024-02-22 21:32:46 -06:00
rebelonion
4628282715 fix: crash on comments api connection failure 2024-02-22 21:13:12 -06:00
rebelonion
6c14a2eccf feat: colored names in comments 2024-02-22 20:49:36 -06:00
rebelonion
8944941d80 fix: comments api accepts total votes 2024-02-22 19:13:22 -06:00
rebelonion
78da98bd1d fix: keep text state when off screen (commentItem) 2024-02-22 17:36:59 -06:00
rebelonion
57833be7df fix: tag sort 2024-02-21 23:47:48 -06:00
rebelonion
506a0576df fix: subscription icon 2024-02-21 23:40:41 -06:00
rebelonion
458f4d1ff9 fix: most recent watch at beginning of list 2024-02-21 23:32:53 -06:00
rebelonion
21b9d51a35 fix: image saving on api > Q 2024-02-21 23:09:31 -06:00
rebelonion
82922b9792 fix re-add anilist link 2024-02-21 23:01:50 -06:00
rebelonion
160f783c6d Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-21 23:01:11 -06:00
rebelonion
ba7c665a9d fix: code cleanup | reply/edit stability 2024-02-21 22:59:45 -06:00
aayush262
b2f01a24b2 feat(comments): re-added Divider for reply 2024-02-20 14:11:38 +05:30
aayush262
84ae520f93 feat(comments): click on username to open in anilist 2024-02-20 13:59:50 +05:30
aayush262
4f61f4cf2c fix: Github Actions 2024-02-20 10:53:57 +05:30
Yutatsu
eba774618e added default load control (#202) 2024-02-19 22:09:21 -06:00
rebelonion
a74b9e985d fix: remove properties file dependency 2024-02-19 01:31:15 -06:00
rebelonion
0c2e2db1dc feat: basic replying 2024-02-19 01:28:11 -06:00
ibo
98b227876b New Theme :) preview in discord (#200)
* Update ThemeManager.kt

added oriax in picker + applier

* Update colors.xml

Added new color seed

* Update themes.xml

Added Oriax Lightmode

* Update themes.xml

Added Oriax Darkmode + OLED
2024-02-17 22:46:54 -06:00
aayush262
1fe50d2cca feat: reply in comments(WIP) 2024-02-17 12:29:39 +05:30
aayush262
420c0348f9 fix: mod cant ban themself now 2024-02-17 12:25:18 +05:30
aayush262
9be81aa4a9 fix: paddings of badges 2024-02-16 19:49:19 +05:30
aayush262
64c8f4225c fix: paddings in comments 2024-02-16 16:37:31 +05:30
rebelonion
a7c9604c43 fix: first comment message appear every time 2024-02-15 18:45:57 -06:00
rebelonion
68cc81e56c fix: correct config import 2024-02-15 18:39:46 -06:00
rebelonion
c9a64b1638 feat: server-side auth 2024-02-15 18:28:03 -06:00
rebelonion
ee7cff0fea feat: remember comment sort order 2024-02-15 14:17:45 -06:00
rebelonion
4c35f9a0cf fix: don't show 404 if no comments 2024-02-15 13:32:05 -06:00
rebelonion
9d9c4f026d fix: better error message 2024-02-15 13:24:34 -06:00
rebelonion
b4c7ea5f26 fix: round image on comment bar 2024-02-15 13:01:04 -06:00
rebelonion
093bee94c6 fix: update timestamp without reloading the page 2024-02-15 13:00:08 -06:00
rebelonion
fb99429dd7 fix: better attempt to get anilist username 2024-02-15 12:53:06 -06:00
rebelonion
a73c4cd678 feat: comments targeted at database 2024-02-15 12:44:52 -06:00
aayush262
1694a1cb24 feat: comments sorter popup 2024-02-15 15:25:18 +05:30
rebelonion
aaf9bdd00c feat: creating, deleting comments | markdown, spoiler comments 2024-02-14 06:41:24 -06:00
aayush262
129adc5825 chore: upgrade download-artifact-v3 -> v4 2024-02-13 22:39:04 +05:30
aayush262
07e7456ed8 Merge remote-tracking branch 'origin/dev' into dev 2024-02-13 13:36:31 +05:30
aayush262
7168e08587 feat: something idr 2024-02-13 13:35:46 +05:30
rebel onion
efb3b27317 Merge pull request #195 from Sadwhy/patch-6
A well recognized font
2024-02-12 17:34:33 -06:00
Sadwhy
2c3247c194 fixed unfortunate licence issues 2024-02-12 22:24:18 +06:00
aayush262
d37ebf8cdd fix: typo 2024-02-12 21:44:54 +05:30
aayush262
a73b049fd4 feat: moved "Play on youtube" to info page 2024-02-12 21:44:22 +05:30
aayush262
b3de2f805f wip: "send comments" interface 2024-02-12 21:43:29 +05:30
Sadwhy
0bec4f4d61 Mojangles font 2024-02-12 14:38:27 +06:00
aayush262
4be4a0968d Merge remote-tracking branch 'origin/dev' into dev 2024-02-12 01:45:16 +05:30
aayush262
97b957a0ab wip: UI for comments 2024-02-12 01:44:36 +05:30
Sadwhy
a22083dfcd fix: Updated discord links 2024-02-11 23:00:16 +05:30
rebel onion
9dbc3db1b8 Merge pull request #188 from Sadwhy/patch-6
Offline padding and faq update
2024-02-11 05:16:27 -06:00
Sadwhy
7af71ba217 Faq Rewrite P.1 2024-02-11 16:59:48 +06:00
rebelonion
80f3523f2e Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-11 04:16:23 -06:00
rebelonion
0afad1d9ae feat: comment authorization 2024-02-11 04:16:22 -06:00
aayush262
915c6c1dfe feat: better format for change logs 2024-02-10 23:11:20 +05:30
aayush262
e319aeb342 feat: monet icon for alpha 2024-02-10 23:08:13 +05:30
Sadwhy
ac20426689 Added proper padding to offline text/buttons 2024-02-09 09:02:53 +06:00
rebelonion
83c07467a9 fix: disable some buttons on fdroid build 2024-02-08 08:54:10 -06:00
rebelonion
0225b28fea fix: hitting enter on password input continues 2024-02-08 08:38:41 -06:00
rebelonion
1e2a740dae Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-08 08:15:53 -06:00
rebelonion
22e687b9d8 feat: add more helpers/developers 2024-02-08 08:15:51 -06:00
rebelonion
f088b90964 fix: ignore fdroid builds in beta 2024-02-08 08:15:29 -06:00
506 changed files with 49497 additions and 10357 deletions

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -39,7 +39,7 @@ jobs:
fi
echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"%h - %s")
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'}"
@@ -48,9 +48,11 @@ jobs:
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
@@ -64,7 +66,7 @@ jobs:
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
@@ -75,7 +77,7 @@ jobs:
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
@@ -83,28 +85,31 @@ jobs:
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
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: |
#Discord
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g')
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: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
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}] Change logs :${commit_messages}" \
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
env:
@@ -112,18 +117,13 @@ jobs:
VERSION: ${{ env.VERSION }}
- name: Upload Current SHA as Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: last-sha
path: last_sha.txt
- name: Delete Old Pre-Releases
id: delete-pre-releases
uses: sgpublic/delete-release-action@master
- name: Upload Commit log as Artifact
uses: actions/upload-artifact@v4
with:
pre-release-drop: true
pre-release-keep-count: 3
pre-release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: commit-log
path: commit_log.txt

6
.gitignore vendored
View File

@@ -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

View File

@@ -15,15 +15,16 @@ android {
defaultConfig {
applicationId "ani.dantotsu"
minSdk 23
minSdk 21
targetSdk 34
versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "2.2.0"
versionCode 220000000
versionName "3.0.0"
versionCode 300000000
signingConfig signingConfigs.debug
}
flavorDimensions "store"
flavorDimensions += "store"
productFlavors {
fdroid {
// F-Droid specific configuration
@@ -42,19 +43,22 @@ android {
buildTypes {
alpha {
applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"]
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"
versionNameSuffix "-beta01"
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'
}
@@ -76,13 +80,13 @@ android {
dependencies {
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1'
// 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'
@@ -91,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.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"
@@ -105,33 +110,48 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.2.1'
// 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'
@@ -141,11 +161,10 @@ dependencies {
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 'com.squareup.okio:okio:3.8.0'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
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 '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'

View File

@@ -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

View File

@@ -5,12 +5,12 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.app
class FirebaseCrashlytics : CrashlyticsInterface {
override fun initialize(context: Context) {
FirebaseApp.initializeApp(context)
}
override fun logException(e: Throwable) {
FirebaseCrashlytics.getInstance().recordException(e)
}

View File

@@ -11,13 +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 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 io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
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
@@ -25,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) {
@@ -39,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 {
@@ -50,7 +58,7 @@ object AppUpdater {
res to res.substringAfter("# ").substringBefore("\n")
}
logger("Git Version : $version")
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 {
@@ -61,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)
}
)
@@ -78,13 +85,18 @@ object AppUpdater {
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)
}
@@ -104,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
}
}
@@ -161,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)
@@ -190,16 +190,11 @@ object AppUpdater {
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)
}

View File

@@ -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" />
@@ -16,10 +18,10 @@
<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 -->
@@ -38,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" />
@@ -49,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"
@@ -57,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" />
@@ -67,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"
@@ -102,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"
@@ -117,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"
@@ -127,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
@@ -239,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"
@@ -254,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"
@@ -289,25 +407,11 @@
</provider>
<service
android:name=".widgets.CurrentlyAiringRemoteViewsService"
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
@@ -330,6 +434,11 @@
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"

View File

@@ -6,9 +6,13 @@ 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
@@ -17,6 +21,8 @@ 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 eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
@@ -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)
@@ -79,9 +87,11 @@ class App : MultiDexApplication() {
}
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Logger.init(this)
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log("App: Logging started")
initializeNetwork(baseContext)
initializeNetwork()
setupNotificationChannels()
if (!LogcatLogger.isInstalled) {
@@ -91,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()}")
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()}")
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)
}
}
@@ -140,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

View File

@@ -1,13 +1,16 @@
package ani.dantotsu
import android.Manifest
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DatePickerDialog
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@@ -16,32 +19,68 @@ import android.content.res.Configuration
import android.content.res.Resources.getSystem
import android.graphics.Bitmap
import android.graphics.Color
import android.Manifest
import android.graphics.drawable.Drawable
import android.media.MediaScannerConnection
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.*
import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_LOWPAN
import android.net.NetworkCapabilities.TRANSPORT_USB
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
import android.net.Uri
import android.os.*
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.provider.Settings
import android.telephony.TelephonyManager
import android.text.InputFilter
import android.text.Spanned
import android.util.AttributeSet
import android.util.TypedValue
import android.view.*
import android.view.GestureDetector
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.*
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.DatePicker
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp
import androidx.core.view.*
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
@@ -49,18 +88,31 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.subcriptions.NotificationClickReceiver
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -68,15 +120,40 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.*
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.*
import java.lang.Runnable
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.util.*
import kotlin.math.*
import java.util.Calendar
import java.util.TimeZone
import java.util.Timer
import java.util.TimerTask
import kotlin.collections.set
import kotlin.math.log2
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
var statusBarHeight = 0
@@ -108,11 +185,10 @@ fun currActivity(): Activity? {
var loadMedia: Int? = null
var loadIsMAL = false
fun logger(e: Any?, print: Boolean = true) {
if (print)
println(e)
}
val Int.toPx
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
).toInt()
fun initActivity(a: Activity) {
val window = a.window
@@ -132,11 +208,17 @@ fun initActivity(a: Activity) {
if (navBarHeight == 0) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
?.apply {
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
}
}
a.hideStatusBar()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
WindowInsetsControllerCompat(
window,
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
) {
window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) {
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
@@ -148,47 +230,124 @@ fun initActivity(a: Activity) {
val windowInsets =
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
if (windowInsets != null) {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
statusBarHeight = insets.top
navBarHeight = insets.bottom
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
navBarHeight =
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
}
}
if (a !is MainActivity) a.setNavigationTheme()
}
@Suppress("DEPRECATION")
fun Activity.hideSystemBars() {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
}
}
@Suppress("DEPRECATION")
fun Activity.hideStatusBar() {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
fun Activity.hideSystemBarsExtendView() {
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars()
}
fun Activity.showSystemBars() {
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
controller.show(WindowInsetsCompat.Type.systemBars())
}
}
fun Activity.showSystemBarsRetractView() {
WindowCompat.setDecorFitsSystemWindows(window, true)
showSystemBars()
}
fun Activity.setNavigationTheme() {
val tv = TypedValue()
theme.resolveAttribute(android.R.attr.colorBackground, tv, true)
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && tv.isColorType)
|| (tv.type >= TypedValue.TYPE_FIRST_COLOR_INT && tv.type <= TypedValue.TYPE_LAST_COLOR_INT)
) {
window.navigationBarColor = tv.data
}
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight)
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(
paddingLeft,
paddingTop,
paddingRight,
navBarHeight + navBar.measuredHeight + overlayView.measuredHeight
)
}
fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
initActivity(this)
}
fun Activity.restartApp() {
val mainIntent = Intent.makeRestartActivityTask(
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
)
val component =
ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
try {
startActivity(Intent().setComponent(component))
} catch (e: Exception) {
startActivity(mainIntent)
}
finishAndRemoveTask()
PrefManager.setCustomVal("reload", true)
}
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() {
super.onStart()
val window = dialog?.window
val decorView: View = window?.decorView ?: return
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog?.window?.let { window ->
WindowCompat.setDecorFitsSystemWindows(window, false)
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (immersiveMode) {
WindowInsetsControllerCompat(
window, window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
}
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValue,
true
)
window.navigationBarColor = typedValue.data
}
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValue,
true
)
window.navigationBarColor = typedValue.data
}
override fun show(manager: FragmentManager, tag: String?) {
@@ -202,21 +361,35 @@ fun isOnline(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false
}
} else false
else -> false
}
} else false
} else {
@Suppress("DEPRECATION")
return@tryWith connectivityManager.activeNetworkInfo?.run {
type == ConnectivityManager.TYPE_BLUETOOTH ||
type == ConnectivityManager.TYPE_ETHERNET ||
type == ConnectivityManager.TYPE_MOBILE ||
type == ConnectivityManager.TYPE_MOBILE_DUN ||
type == ConnectivityManager.TYPE_MOBILE_HIPRI ||
type == ConnectivityManager.TYPE_WIFI ||
type == ConnectivityManager.TYPE_WIMAX ||
type == ConnectivityManager.TYPE_VPN
} ?: false
}
} ?: false
}
@@ -248,7 +421,7 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
dialog.setButton(
DialogInterface.BUTTON_NEUTRAL,
activity.getString(R.string.remove)
) { dialog, which ->
) { _, which ->
if (which == DialogInterface.BUTTON_NEUTRAL) {
date = FuzzyDate()
}
@@ -278,12 +451,11 @@ class InputFilterMinMax(
val input = (dest.toString() + source.toString()).toDouble()
if (isInRange(min, max, input)) return null
} catch (nfe: NumberFormatException) {
logger(nfe.stackTraceToString())
Logger.log(nfe)
}
return ""
}
@SuppressLint("SetTextI18n")
private fun isInRange(a: Double, b: Double, c: Double): Boolean {
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
@@ -296,7 +468,7 @@ class InputFilterMinMax(
}
class ZoomOutPageTransformer() :
class ZoomOutPageTransformer :
ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
@@ -449,11 +621,17 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
}
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(size).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
}
}
}
@@ -579,9 +757,41 @@ fun View.circularReveal(ex: Int, ey: Int, subX: Boolean, time: Long) {
}
fun openLinkInBrowser(link: String?) {
tryWith {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
currContext()?.startActivity(intent)
link?.let {
try {
val emptyBrowserIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.fromParts("http", "", null)
}
val sendIntent = Intent().apply {
action = Intent.ACTION_VIEW
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
selector = emptyBrowserIntent
}
currContext()!!.startActivity(sendIntent)
} catch (e: ActivityNotFoundException) {
snackString("No browser found")
} catch (e: Exception) {
Logger.log(e)
}
}
}
fun openLinkInYouTube(link: String?) {
link?.let {
try {
val videoIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
setPackage("com.google.android.youtube")
}
currContext()!!.startActivity(videoIntent)
} catch (e: ActivityNotFoundException) {
openLinkInBrowser(link)
} catch (e: Exception) {
Logger.log(e)
}
}
}
@@ -676,26 +886,6 @@ fun savePrefs(
}
}
fun downloadsPermission(activity: AppCompatActivity): Boolean {
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val requiredPermissions = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE)
false
} else {
true
}
}
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
val contentUri = FileProvider.getUriForFile(
@@ -728,7 +918,7 @@ fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
private fun scanFile(path: String, context: Context) {
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
logger("Finished scanning $p")
Logger.log("Finished scanning $p")
}
}
@@ -760,12 +950,15 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
val clipboard = getSystemService(activity, ClipboardManager::class.java)
val clip = ClipData.newPlainText("label", string)
clipboard?.setPrimaryClip(clip)
if (toast) snackString(activity.getString(R.string.copied_text, string))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
if (toast) snackString(activity.getString(R.string.copied_text, string))
}
}
@SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()
) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
@@ -797,6 +990,50 @@ fun countDown(media: Media, view: ViewGroup) {
}
}
fun sinceWhen(media: Media, view: ViewGroup) {
if (media.status != "RELEASING" && media.status != "HIATUS") return
CoroutineScope(Dispatchers.IO).launch {
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
val timeSince = (System.currentTimeMillis() -
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
withContext(Dispatchers.Main) {
val v =
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
object : CountUpTimer(86400000) {
override fun onTick(second: Int) {
val a = second + timeSince
v.mediaCountdown.text = currActivity()?.getString(
R.string.time_format,
a / 86400,
a % 86400 / 3600,
a % 86400 % 3600 / 60,
a % 86400 % 3600 % 60
)
}
override fun onFinish() {
// The legend will never die.
}
}.start()
}
}
}
}
fun displayTimer(media: Media, view: ViewGroup) {
when {
media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> {} // No timer yet
}
}
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
this.forEach {
if (it.value.id == id) {
@@ -866,9 +1103,13 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
}
fun getAppString(res: Int): String {
return currContext()?.getString(res) ?: ""
}
fun toast(string: String?) {
if (string != null) {
logger(string)
Logger.log(string)
MainScope().launch {
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
.show()
@@ -876,16 +1117,20 @@ fun toast(string: String?) {
}
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
fun toast(res: Int) {
toast(getAppString(res))
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
try { //I have no idea why this sometimes crashes for some people...
if (s != null) {
(activity ?: currActivity())?.apply {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
runOnUiThread {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
@@ -905,13 +1150,19 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
}
snackBar.show()
}
return snackBar
}
logger(s)
Logger.log(s)
}
} catch (e: Exception) {
logger(e.stackTraceToString())
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
}
return null
}
fun snackString(r: Int, activity: Activity? = null, clipboard: String? = null): Snackbar? {
return snackString(getAppString(r), activity, clipboard)
}
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
@@ -1037,7 +1288,7 @@ fun incognitoNotification(context: Context) {
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if (incognito) {
val intent = Intent(context, NotificationClickReceiver::class.java)
val intent = Intent(context, IncognitoNotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE
@@ -1055,6 +1306,28 @@ fun incognitoNotification(context: Context) {
}
}
fun hasNotificationPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
fun openSettings(context: Context, channelId: String?): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(
if (channelId != null) Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
else Settings.ACTION_APP_NOTIFICATION_SETTINGS
).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
}
context.startActivity(intent)
true
} else false
}
suspend fun View.pop() {
currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
@@ -1067,3 +1340,104 @@ suspend fun View.pop() {
}
delay(100)
}
fun blurImage(imageView: ImageView, banner: String?) {
if (banner != null) {
val radius = PrefManager.getVal<Float>(PrefName.BlurRadius).toInt()
val sampling = PrefManager.getVal<Float>(PrefName.BlurSampling).toInt()
if (PrefManager.getVal(PrefName.BlurBanners)) {
val context = imageView.context
if (!(context as Activity).isDestroyed) {
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
Glide.with(context as Context)
.load(
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
url
) else File(url)
)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView)
}
} else {
imageView.loadImage(banner)
}
} else {
imageView.setImageResource(R.drawable.linear_gradient_bg)
}
}
/**
* Builds the markwon instance with all the plugins
* @return the markwon instance
*/
fun buildMarkwon(
activity: Context,
userInputContent: Boolean = true,
fragment: Fragment? = null
): Markwon {
val glideContext = fragment?.let { Glide.with(it) } ?: Glide.with(activity)
val markwon = Markwon.builder(activity)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { _, link ->
copyToClipboard(link, true)
}
}
})
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(activity))
.usePlugin(TaskListPlugin.create(activity))
.usePlugin(SpoilerPlugin())
.usePlugin(HtmlPlugin.create { plugin ->
if (userInputContent) {
plugin.addHandler(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
)
}
})
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
private val requestManager: RequestManager = glideContext.apply {
addDefaultRequestListener(object : RequestListener<Any> {
override fun onResourceReady(
resource: Any,
model: Any,
target: Target<Any>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
if (resource is GifDrawable) {
resource.start()
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Any>,
isFirstResource: Boolean
): Boolean {
Logger.log("Image failed to load: $model")
Logger.log(e as Exception)
return false
}
})
}
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
Logger.log("Loading image: ${drawable.destination}")
return requestManager.load(drawable.destination)
}
override fun cancel(target: Target<*>) {
Logger.log("Cancelling image load")
requestManager.clear(target)
}
}))
.build()
return markwon
}

View File

@@ -2,7 +2,9 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
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
@@ -11,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
@@ -25,40 +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.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.subcriptions.Subscription.Companion.startSubscription
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 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
@@ -71,6 +87,7 @@ class MainActivity : AppCompatActivity() {
private var load = false
@kotlin.OptIn(DelicateCoroutinesApi::class)
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -84,17 +101,77 @@ class MainActivity : AppCompatActivity() {
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
bottomNavBar.background = backgroundDrawable
}
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
@@ -144,22 +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
@@ -205,6 +274,17 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
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
@@ -217,7 +297,40 @@ class MainActivity : AppCompatActivity() {
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
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)
@@ -230,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
@@ -255,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
@@ -268,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)
@@ -288,8 +403,21 @@ 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
}
@@ -326,27 +454,78 @@ class MainActivity : AppCompatActivity() {
}
}
}
//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)
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) :

View File

@@ -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 {

View 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()
}

View 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
}
}
}

View 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
}
}

View 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)
}
}
}

View 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)
}
}

View File

@@ -0,0 +1,8 @@
package ani.dantotsu.addons
abstract class LoadResult {
abstract class Success : LoadResult()
}

View File

@@ -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 -> {}
}
}
}
}
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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()
}

View 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
}
}
}

View File

@@ -6,6 +6,8 @@ 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
@@ -18,6 +20,7 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
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
@@ -29,6 +32,7 @@ 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)
@@ -36,10 +40,13 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadsManager(app) }
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()) }

View File

@@ -19,7 +19,7 @@ fun updateProgress(media: Media, number: String) {
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,

View File

@@ -3,16 +3,17 @@ package ani.dantotsu.connections.anilist
import android.content.ActivityNotFoundException
import android.content.Context
import android.net.Uri
import android.util.Log
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.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import java.util.Calendar
object Anilist {
@@ -27,6 +28,7 @@ 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
@@ -37,20 +39,54 @@ object Anilist {
"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"
)
@@ -114,6 +150,9 @@ object Anilist {
episodesWatched = null
chapterRead = null
PrefManager.removeVal(PrefName.AnilistToken)
//logout from comments api
CommentsAPI.logout()
}
suspend inline fun <reified T : Any> executeQuery(
@@ -124,7 +163,8 @@ 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")
@@ -148,7 +188,7 @@ object Anilist {
cacheTime = cache ?: 10
)
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Log.d("AnilistQuery", "Remaining requests: $remaining")
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
@@ -159,10 +199,16 @@ object Anilist {
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) println("Response : ${json.text}")
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
}
}
}

View File

@@ -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,

View File

@@ -6,9 +6,12 @@ import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId
import ani.dantotsu.connections.anilist.Anilist.authorRoles
import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FeedResponse
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.NotificationResponse
import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext
import ani.dantotsu.isOnline
import ani.dantotsu.logError
@@ -17,9 +20,11 @@ import ani.dantotsu.media.Character
import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.others.MalScraper
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
@@ -31,23 +36,27 @@ import java.io.Serializable
import kotlin.system.measureTimeMillis
class AnilistQueries {
suspend fun getUserData(): Boolean {
val response: Query.Viewer?
measureTimeMillis {
response =
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}}}""")
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""")
}.also { println("time : $it") }
val user = response?.data?.user ?: return false
PrefManager.setVal(PrefName.AnilistUserName, user.name)
Anilist.userid = user.id
PrefManager.setVal(PrefName.AnilistUserId, user.id.toString())
Anilist.username = user.name
Anilist.bg = user.bannerImage
Anilist.avatar = user.avatar?.medium
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
Anilist.chapterRead = user.statistics?.manga?.chaptersRead
Anilist.adult = user.options?.displayAdultContent ?: false
Anilist.unreadNotificationCount = user.unreadNotificationCount ?: 0
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
Anilist.unreadNotificationCount += unread
return true
}
@@ -64,18 +73,19 @@ class AnilistQueries {
media.cameFromContinue = false
val query =
"""{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100) progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer { site id } synonyms tags { name rank isMediaSpoiler } characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100) status} episodes chapters nextAiringEpisode{episode} popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) {edges{role node{id name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100) status} episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
"""{Media(id:${media.id}){id favourites popularity episodes chapters mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true)
if (response != null) {
fun parse() {
val fetchedMedia = response?.data?.media ?: return
val user = response?.data?.page
media.source = fetchedMedia.source?.toString()
media.countryOfOrigin = fetchedMedia.countryOfOrigin
media.format = fetchedMedia.format?.toString()
media.favourites = fetchedMedia.favourites
media.popularity = fetchedMedia.popularity
media.startDate = fetchedMedia.startDate
media.endDate = fetchedMedia.endDate
@@ -121,6 +131,38 @@ class AnilistQueries {
name = i.node?.name?.userPreferred,
image = i.node?.image?.medium,
banner = media.banner ?: media.cover,
isFav = i.node?.isFavourite ?: false,
role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN"
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
?: "SUPPORTING"
else -> i.role.toString()
},
voiceActor = i.voiceActors?.map {
Author(
id = it.id,
name = it.name?.userPreferred,
image = it.image?.large,
role = it.languageV2
)
} as ArrayList<Author>
)
)
}
}
}
if (fetchedMedia.staff != null) {
media.staff = arrayListOf()
fetchedMedia.staff?.edges?.forEach { i ->
i.node?.apply {
media.staff?.add(
Author(
id = id,
name = i.node?.name?.userPreferred,
image = i.node?.image?.large,
role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN"
@@ -167,7 +209,24 @@ class AnilistQueries {
}
}
}
if (user?.mediaList?.isNotEmpty() == true) {
media.users = user.mediaList?.mapNotNull {
it.user?.let { user ->
if (user.id != Anilist.userid) {
User(
user.id,
user.name ?: "Unknown",
user.avatar?.large,
"",
it.status?.toString(),
it.score,
it.progress,
fetchedMedia.episodes ?: fetchedMedia.chapters,
)
} else null
}
}?.toCollection(arrayListOf()) ?: arrayListOf()
}
if (fetchedMedia.mediaListEntry != null) {
fetchedMedia.mediaListEntry?.apply {
media.userProgress = progress
@@ -211,8 +270,10 @@ class AnilistQueries {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.anime.author = Author(
it.id.toString(),
it.name?.userPreferred ?: "N/A"
it.id,
it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
)
}
@@ -231,8 +292,10 @@ class AnilistQueries {
} else if (media.manga != null) {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.manga.author = Author(
it.id.toString(),
it.name?.userPreferred ?: "N/A"
it.id,
it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
)
}
}
@@ -249,6 +312,7 @@ class AnilistQueries {
} else {
if (currContext()?.let { isOnline(it) } == true) {
snackString(currContext()?.getString(R.string.error_getting_data))
} else {
}
}
}
@@ -262,6 +326,52 @@ class AnilistQueries {
return media
}
fun userMediaDetails(media: Media): Media {
val query =
"""{Media(id:${media.id}){id mediaListEntry{id status progress private repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite idMal}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true)
if (response != null) {
fun parse() {
val fetchedMedia = response?.data?.media ?: return
if (fetchedMedia.mediaListEntry != null) {
fetchedMedia.mediaListEntry?.apply {
media.userProgress = progress
media.isListPrivate = private ?: false
media.userListId = id
media.userStatus = status?.toString()
media.inCustomListsOf = customLists?.toMutableMap()
media.userRepeat = repeat ?: 0
media.userUpdatedAt = updatedAt?.toString()?.toLong()?.times(1000)
media.userCompletedAt = completedAt ?: FuzzyDate()
media.userStartedAt = startedAt ?: FuzzyDate()
}
} else {
media.isListPrivate = false
media.userStatus = null
media.userListId = null
media.userProgress = null
media.userRepeat = 0
media.userUpdatedAt = null
media.userCompletedAt = FuzzyDate()
media.userStartedAt = FuzzyDate()
}
}
if (response.data?.media != null) parse()
else {
response = executeQuery(query, force = true, useToken = false)
if (response?.data?.media != null) parse()
}
}
}
awaitAll(anilist)
}
return media
}
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
val returnArray = arrayListOf<Media>()
val map = mutableMapOf<Int, Media>()
@@ -299,9 +409,18 @@ class AnilistQueries {
}
}
}
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
if (set.isNotEmpty()) {
set.reversed().forEach {
if (type != "ANIME") {
returnArray.addAll(map.values)
return returnArray
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (map.containsKey(it)) returnArray.add(map[it]!!)
}
for (i in map) {
@@ -315,12 +434,12 @@ class AnilistQueries {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
}
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
var hasNextPage = true
var page = 0
suspend fun getNextPage(page: Int): List<Media> {
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page)}}""")
val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page, id)}}""")
val favourites = response?.data?.user?.favourites
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
@@ -339,8 +458,8 @@ class AnilistQueries {
return responseArray
}
private fun favMediaQuery(anime: Boolean, page: Int): String {
return """User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
suspend fun recommendations(): ArrayList<Media> {
@@ -383,7 +502,7 @@ class AnilistQueries {
}
private fun recommendationPlannedQuery(type: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
}
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
@@ -423,7 +542,8 @@ class AnilistQueries {
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
query += """}""".trimEnd(',')
val response = executeQuery<Query.HomePageMedia>(query)
val response = executeQuery<Query.HomePageMedia>(query, show = true)
Logger.log(response.toString())
val returnMap = mutableMapOf<String, ArrayList<Media>>()
fun current(type: String) {
val subMap = mutableMapOf<Int, Media>()
@@ -446,10 +566,19 @@ class AnilistQueries {
subMap[m.id] = m
}
}
val set = PrefManager.getCustomVal<Set<Int>>("continue_${type.uppercase()}", setOf())
.toMutableSet()
if (set.isNotEmpty()) {
set.reversed().forEach {
if (type != "Anime") {
returnArray.addAll(subMap.values)
returnMap["current$type"] = returnArray
return
}
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
}
for (i in subMap) {
@@ -472,9 +601,14 @@ class AnilistQueries {
subMap[m.id] = m
}
}
val set = PrefManager.getCustomVal<Set<Int>>("continue_$type", setOf()).toMutableSet()
if (set.isNotEmpty()) {
set.reversed().forEach {
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
}
for (i in subMap) {
@@ -558,12 +692,11 @@ class AnilistQueries {
private suspend fun bannerImage(type: String): String? {
//var image = loadData<BannerImage>("banner_$type")
val image: BannerImage? = BannerImage(
PrefManager.getCustomVal("banner_${type}_url", null),
val image = BannerImage(
PrefManager.getCustomVal("banner_${type}_url", ""),
PrefManager.getCustomVal("banner_${type}_time", 0L)
)
if (image == null || image.checkTime()) {
if (image.url.isNullOrEmpty() || image.checkTime()) {
val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
@@ -592,7 +725,7 @@ class AnilistQueries {
sortOrder: String? = null
): MutableMap<String, ArrayList<Media>> {
val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage genres meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
val sorted = mutableMapOf<String, ArrayList<Media>>()
val unsorted = mutableMapOf<String, ArrayList<Media>>()
val all = arrayListOf<Media>()
@@ -620,7 +753,7 @@ class AnilistQueries {
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
}
sorted["Favourites"] = favMedia(anime)
sorted["Favourites"] = favMedia(anime, userId)
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
sorted["Favourites"]?.forEach { fav ->
@@ -630,7 +763,7 @@ class AnilistQueries {
}
sorted["All"] = all
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
val listSort: String? = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
else PrefManager.getVal(PrefName.MangaListSortOrder)
val sort = listSort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) {
@@ -661,8 +794,8 @@ class AnilistQueries {
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
mapOf(
true to adultTags,
false to nonAdultTags
true to adultTags.sortedBy { it },
false to nonAdultTags.sortedBy { it }
)
if (genres.isNullOrEmpty()) {
@@ -698,7 +831,7 @@ class AnilistQueries {
}
}
return if (!genres.isNullOrEmpty() && tags != null) {
Anilist.genres = genres
Anilist.genres = genres?.sortedBy { it }?.toMutableList() as ArrayList<String>
Anilist.tags = tags
true
} else false
@@ -778,18 +911,23 @@ class AnilistQueries {
sort: String? = null,
genres: MutableList<String>? = null,
tags: MutableList<String>? = null,
status: String? = null,
source: String? = null,
format: String? = null,
countryOfOrigin: String? = null,
isAdult: Boolean = false,
onList: Boolean? = null,
excludedGenres: MutableList<String>? = null,
excludedTags: MutableList<String>? = null,
startYear: Int? = null,
seasonYear: Int? = null,
season: String? = null,
id: Int? = null,
hd: Boolean = false,
adultOnly: Boolean = false
): SearchResults? {
val query = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
pageInfo {
total
@@ -834,14 +972,19 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
}
""".replace("\n", " ").replace(""" """, "")
val variables = """{"type":"$type","isAdult":$isAdult
${if (adultOnly) ""","isAdult":true""" else ""}
${if (onList != null) ""","onList":$onList""" else ""}
${if (page != null) ""","page":"$page"""" else ""}
${if (id != null) ""","id":"$id"""" else ""}
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
${if (season != null) ""","season":"$season"""" else ""}
${if (search != null) ""","search":"$search"""" else ""}
${if (source != null) ""","source":"$source"""" else ""}
${if (sort != null) ""","sort":"$sort"""" else ""}
${if (status != null) ""","status":"$status"""" else ""}
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
${
if (excludedGenres?.isNotEmpty() == true)
@@ -873,7 +1016,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
else ""
}
}""".replace("\n", " ").replace(""" """, "")
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
if (response?.media != null) {
val responseArray = arrayListOf<Media>()
@@ -905,7 +1047,11 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
excludedGenres = excludedGenres,
tags = tags,
excludedTags = excludedTags,
status = status,
source = source,
format = format,
countryOfOrigin = countryOfOrigin,
startYear = startYear,
seasonYear = seasonYear,
season = season,
results = responseArray,
@@ -916,11 +1062,156 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
return null
}
private val onListAnime =
(if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace(
"\"",
""
)
private val isAdult =
(if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
private fun recentAnimeUpdates(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
}
private fun trendingMovies(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedAnime(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavAnime(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadAnimeList(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{
recentUpdates:${recentAnimeUpdates(1)}
recentUpdates2:${recentAnimeUpdates(2)}
trendingMovies:${trendingMovies(1)}
trendingMovies2:${trendingMovies(2)}
topRated:${topRatedAnime(1)}
topRated2:${topRatedAnime(2)}
mostFav:${mostFavAnime(1)}
mostFav2:${mostFavAnime(2)}
}""".trimIndent()
}
executeQuery<Query.AnimeList>(query(), force = true)?.data?.apply {
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
val idArr = mutableListOf<Int>()
list["recentUpdates"] = recentUpdates?.airingSchedules?.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
idArr.add(it.id)
Media(it)
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
idArr.add(it.id)
Media(it)
} else if ((listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
}
} as ArrayList<Media>
list["trendingMovies"] = trendingMovies?.media?.map { Media(it) } as ArrayList<Media>
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
list["recentUpdates"]?.addAll(recentUpdates2?.airingSchedules?.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
idArr.add(it.id)
Media(it)
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
idArr.add(it.id)
Media(it)
} else if ((listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
}
} as ArrayList<Media>)
list["trendingMovies"]?.addAll(trendingMovies2?.media?.map { Media(it) } as ArrayList<Media>)
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
}
return list
}
private val onListManga =
(if (PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace(
"\"",
""
)
private fun trendingManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingManhwa(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingNovel(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadMangaList(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{
trendingManga:${trendingManga(1)}
trendingManga2:${trendingManga(2)}
trendingManhwa:${trendingManhwa(1)}
trendingManhwa2:${trendingManhwa(2)}
trendingNovel:${trendingNovel(1)}
trendingNovel2:${trendingNovel(2)}
topRated:${topRatedManga(1)}
topRated2:${topRatedManga(2)}
mostFav:${mostFavManga(1)}
mostFav2:${mostFavManga(2)}
}""".trimIndent()
}
executeQuery<Query.MangaList>(query(), force = true)?.data?.apply {
list["trendingManga"] = trendingManga?.media?.map { Media(it) } as ArrayList<Media>
list["trendingManhwa"] = trendingManhwa?.media?.map { Media(it) } as ArrayList<Media>
list["trendingNovel"] = trendingNovel?.media?.map { Media(it) } as ArrayList<Media>
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
list["trendingManga"]?.addAll(trendingManga2?.media?.map { Media(it) } as ArrayList<Media>)
list["trendingManhwa"]?.addAll(trendingManhwa2?.media?.map { Media(it) } as ArrayList<Media>)
list["trendingNovel"]?.addAll(trendingNovel2?.media?.map { Media(it) } as ArrayList<Media>)
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
}
return list
}
suspend fun recentlyUpdated(
smaller: Boolean = true,
greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000
): MutableList<Media>? {
): MutableList<Media> {
suspend fun execute(page: Int = 1): Page? {
val query = """{
Page(page:$page,perPage:50) {
@@ -967,41 +1258,26 @@ Page(page:$page,perPage:50) {
}""".replace("\n", " ").replace(""" """, "")
return executeQuery<Query.Page>(query, force = true)?.data?.page
}
if (smaller) {
val response = execute()?.airingSchedules ?: return null
val idArr = mutableListOf<Int>()
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
return response.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
var i = 1
val list = mutableListOf<Media>()
var res: Page? = null
suspend fun next() {
res = execute(i)
list.addAll(res?.airingSchedules?.mapNotNull { j ->
j.media?.let {
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
} else null
}
}.toMutableList()
} else {
var i = 1
val list = mutableListOf<Media>()
var res: Page? = null
suspend fun next() {
res = execute(i)
list.addAll(res?.airingSchedules?.mapNotNull { j ->
j.media?.let {
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
} else null
}
} ?: listOf())
}
next()
while (res?.pageInfo?.hasNextPage == true) {
next()
i++
}
return list.reversed().toMutableList()
} ?: listOf())
}
next()
while (res?.pageInfo?.hasNextPage == true) {
next()
i++
}
return list.reversed().toMutableList()
}
suspend fun getCharacterDetails(character: Character): Character {
@@ -1187,19 +1463,39 @@ Page(page:$page,perPage:50) {
}
}
}
characters(page: $page,sort:FAVOURITES_DESC) {
pageInfo{
hasNextPage
}
nodes{
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
}
}
}
}""".replace("\n", " ").replace(""" """, "")
var hasNextPage = true
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
var page = 0
val characters = arrayListOf<Character>()
while (hasNextPage) {
page++
hasNextPage = executeQuery<Query.Author>(
query(page),
force = true
)?.data?.author?.staffMedia?.let {
val query = executeQuery<Query.Author>(
query(page), force = true
)?.data?.author
hasNextPage = query?.staffMedia?.let {
it.edges?.forEach { i ->
i.node?.apply {
val status = status.toString()
@@ -1214,6 +1510,20 @@ Page(page:$page,perPage:50) {
}
it.pageInfo?.hasNextPage == true
} ?: false
query?.characters?.let {
it.nodes?.forEach { i ->
characters.add(
Character(
i.id,
i.name?.userPreferred,
i.image?.large,
i.image?.medium,
"",
false
)
)
}
}
}
if (yearMedia.contains("CANCELLED")) {
@@ -1221,8 +1531,155 @@ Page(page:$page,perPage:50) {
yearMedia.remove("CANCELLED")
yearMedia["CANCELLED"] = a
}
author.character = characters
author.yearMedia = yearMedia
return author
}
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
)
}
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
return executeQuery<ToggleLike>(
"""mutation Like{ToggleLikeV2(id:$id,type:$type){__typename}}"""
)
}
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
return executeQuery<Query.UserProfileResponse>(
"""{followerPage:Page{followers(userId:$id){id}pageInfo{total}}followingPage:Page{following(userId:$id){id}pageInfo{total}}user:User(id:$id){id name about(asHtml:true)avatar{medium large}bannerImage isFollowing isFollower isBlocked favourites{anime{nodes{id coverImage{extraLarge large medium color}}}manga{nodes{id coverImage{extraLarge large medium color}}}characters{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""",
force = true
)
}
suspend fun getUserProfile(username: String): Query.UserProfileResponse? {
val id = getUserId(username) ?: return null
return getUserProfile(id)
}
suspend fun getUserId(username: String): Int? {
return executeQuery<Query.User>(
"""{User(name:"$username"){id}}""",
force = true
)?.data?.user?.id
}
suspend fun getUserStatistics(id: Int, sort: String = "ID"): Query.StatisticsResponse? {
return executeQuery<Query.StatisticsResponse>(
"""{User(id:$id){id name mediaListOptions{scoreFormat}statistics{anime{...UserStatistics}manga{...UserStatistics}}}}fragment UserStatistics on UserStatistics{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead formats(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds format}statuses(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds status}scores(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds score}lengths(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds length}releaseYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds releaseYear}startYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds startYear}genres(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds genre}tags(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds tag{id name}}countries(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds country}voiceActors(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds voiceActor{id name{first middle last full native alternative userPreferred}}characterIds}staff(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds staff{id name{first middle last full native alternative userPreferred}}}studios(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds studio{id name isAnimationStudio}}}""",
force = true,
show = true
)
}
private fun userFavMediaQuery(anime: Boolean, page: Int, id: Int): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
suspend fun userFollowing(id: Int): Query.Following? {
return executeQuery<Query.Following>(
"""{Page {following(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
force = true
)
}
suspend fun userFollowers(id: Int): Query.Follower? {
return executeQuery<Query.Follower>(
"""{Page {followers(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
force = true
)
}
suspend fun initProfilePage(id: Int): Query.ProfilePageMedia? {
return executeQuery<Query.ProfilePageMedia>(
"""{
favoriteAnime:${userFavMediaQuery(true, 1, id)}
favoriteManga:${userFavMediaQuery(false, 1, id)}
}""".trimIndent(), force = true
)
}
suspend fun getNotifications(
id: Int,
page: Int = 1,
resetNotification: Boolean = true
): NotificationResponse? {
val reset = if (resetNotification) "true" else "false"
val res = executeQuery<NotificationResponse>(
"""{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""",
force = true
)
if (res != null && resetNotification) {
val commentNotifications = PrefManager.getVal(PrefName.UnreadCommentNotifications, 0)
res.data.user.unreadNotificationCount += commentNotifications
PrefManager.setVal(PrefName.UnreadCommentNotifications, 0)
Anilist.unreadNotificationCount = 0
}
return res
}
suspend fun getFeed(
userId: Int?,
global: Boolean = false,
page: Int = 1,
activityId: Int? = null
): FeedResponse? {
val filter = if (activityId != null) "id:$activityId,"
else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
else "isFollowing:true,type_not:MESSAGE,"
return executeQuery<FeedResponse>(
"""{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id recipientId messengerId type replyCount likeCount message(asHtml:true)isLocked isSubscribed isLiked isPrivate siteUrl createdAt recipient{id name bannerImage avatar{medium large}}messenger{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}}}}""",
force = true
)
}
suspend fun getUpcomingAnime(id: String): List<Media> {
val res = executeQuery<Query.MediaListCollection>(
"""{MediaListCollection(userId:$id,type:ANIME){lists{name entries{media{id,isFavourite,title{userPreferred,romaji}coverImage{medium}nextAiringEpisode{timeUntilAiring}}}}}}""",
force = true
)
val list = mutableListOf<Media>()
res?.data?.mediaListCollection?.lists?.forEach { listEntry ->
listEntry.entries?.forEach { entry ->
entry.media?.nextAiringEpisode?.timeUntilAiring?.let {
list.add(Media(entry.media!!))
}
}
}
return list.sortedBy { it.timeUntilAiring }
.distinctBy { it.id }
.filter { it.timeUntilAiring != null }
}
suspend fun isUserFav(
favType: AnilistMutations.FavType,
id: Int
): Boolean { //anilist isFavourite is broken, so we need to check it manually
val res = getUserProfile(Anilist.userid ?: return false)
return when (favType) {
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id }
?: false
AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id }
?: false
}
}
companion object {
const val ITEMS_PER_PAGE = 25
}
}

View File

@@ -15,6 +15,7 @@ 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
@@ -57,48 +58,40 @@ 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) }
@@ -110,8 +103,8 @@ class AnilistHomeViewModel : ViewModel() {
suspend fun loadMain(context: FragmentActivity) {
Anilist.getSavedToken()
MAL.getSavedToken(context)
Discord.getSavedToken(context)
MAL.getSavedToken()
Discord.getSavedToken()
if (!BuildConfig.FLAVOR.contains("fdroid")) {
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
}
@@ -142,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,
@@ -165,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)
)
)
}
@@ -183,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() {
@@ -207,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,
@@ -237,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)
)
)
}
@@ -255,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() {
@@ -284,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,
)
)
@@ -303,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
)
@@ -331,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)
}
}

View File

@@ -4,7 +4,6 @@ import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.logError
import ani.dantotsu.logger
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.startMainActivity
@@ -16,7 +15,6 @@ class Login : AppCompatActivity() {
ThemeManager(this).applyTheme()
val data: Uri? = intent?.data
logger(data.toString())
try {
Anilist.token =
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value

View File

@@ -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)

View File

@@ -11,20 +11,25 @@ import ani.dantotsu.themes.ThemeManager
class UrlMedia : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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))
}
}
}

View File

@@ -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

View File

@@ -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?
)
}
@@ -139,41 +141,586 @@ class Query {
)
}
@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(
@@ -203,7 +750,7 @@ class Query {
// // Activity reply query
// val ActivityReply: ActivityReply?,
// // Comment query
// // CommentNotificationWorker query
// val ThreadComment: List<ThreadComment>?,
// // Notification query

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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?,

View File

@@ -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?,

View File

@@ -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
)
}
}
}
}
}

View File

@@ -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
}
}

View File

@@ -1,17 +1,19 @@
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) {
//no-op
Logger.log(e)
}
override fun log(message: String) {
//no-op
Logger.log(message)
}
override fun setUserId(id: String) {

View File

@@ -20,14 +20,14 @@ object Discord {
var avatar: String? = null
fun getSavedToken(context: Context): Boolean {
fun getSavedToken(): Boolean {
token = PrefManager.getVal(
PrefName.DiscordToken, null as String?
)
return token != null
}
fun saveToken(context: Context, token: String) {
fun saveToken(token: String) {
PrefManager.setVal(PrefName.DiscordToken, token)
}
@@ -70,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"
}

View File

@@ -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
@@ -26,6 +21,7 @@ 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
@@ -37,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
@@ -49,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
@@ -67,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(
@@ -162,8 +158,8 @@ class DiscordService : Service() {
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
@@ -232,7 +228,7 @@ class DiscordService : Service() {
resume()
resume = false
} else {
identify(webSocket, baseContext)
identify(webSocket)
log("WebSocket: Identified")
}
}
@@ -245,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()
@@ -270,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(
@@ -289,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()
}
@@ -297,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()
@@ -311,7 +307,7 @@ class DiscordService : Service() {
}
}
fun getToken(context: Context): String {
fun getToken(): String {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
return if (token == null) {
log("WebSocket: Token not found")
@@ -349,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
@@ -375,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()
@@ -449,7 +402,7 @@ class DiscordService : Service() {
Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent")
} catch (e: InterruptedException) {
} catch (ignored: InterruptedException) {
}
}
}

View File

@@ -75,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)
}

View File

@@ -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)
)
))
}

View File

@@ -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
)
}

View 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
)
}
}

View File

@@ -5,7 +5,6 @@ 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
@@ -64,7 +63,7 @@ object MAL {
}
suspend fun getSavedToken(context: FragmentActivity): Boolean {
suspend fun getSavedToken(): Boolean {
return tryWithSuspend(false) {
var res: ResponseToken =
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
@@ -77,7 +76,7 @@ object MAL {
} ?: false
}
fun removeSavedToken(context: Context) {
fun removeSavedToken() {
token = null
username = null
userid = null

View File

@@ -1,13 +1,25 @@
package ani.dantotsu.download
import android.content.Context
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) {
@@ -15,11 +27,11 @@ class DownloadsManager(private val context: Context) {
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)
@@ -41,84 +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)
}
}
}
@@ -126,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 {
@@ -161,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 }
@@ -249,62 +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()
}

View File

@@ -9,32 +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.addons.download.DownloadAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
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 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
@@ -45,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
@@ -56,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
@@ -73,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.
@@ -81,6 +82,11 @@ 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 {
@@ -88,6 +94,7 @@ class AnimeDownloaderService : Service() {
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(
@@ -156,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()
@@ -209,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(
@@ -220,18 +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())
}
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
)
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
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 {
@@ -241,94 +279,124 @@ 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
)
)
Injekt.get<CrashlyticsInterface>().logException(
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")
PrefManager.getAnimeDownloadPreferences().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()
Injekt.get<CrashlyticsInterface>().logException(e)
@@ -337,36 +405,24 @@ class AnimeDownloaderService : Service() {
}
}
@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
@@ -400,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")
@@ -418,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) {
@@ -491,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("/", "")}"
}
}
}
@@ -512,7 +583,6 @@ class AnimeDownloaderService : Service() {
object AnimeServiceDataSingleton {
var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

@@ -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
@@ -38,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) {
@@ -51,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)

View File

@@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -22,8 +21,10 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
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
@@ -32,16 +33,19 @@ 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.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.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
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
@@ -53,9 +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
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@@ -64,6 +72,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -110,10 +119,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
})
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
@@ -134,7 +143,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
grid()
}
layoutcompact.setOnClickListener {
layoutCompact.setOnClickListener {
selected(it as ImageView)
style = 1
PrefManager.setVal(PrefName.OfflineView, style)
@@ -154,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"
@@ -166,20 +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
lifecycleScope.launch {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@launch
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
} ?: run {
snackString("no media found")
}
@@ -187,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 =
@@ -203,13 +213,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
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
@@ -237,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(
@@ -250,7 +253,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val visibility = first != null && first.top < 0
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
scrollTop.isVisible = visibility
}
})
initActivity(requireActivity())
@@ -260,7 +263,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -280,29 +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 tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.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 = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
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 = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -314,37 +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())
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 = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
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 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
@@ -374,8 +391,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel(
"unknown",

View File

@@ -10,19 +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
@@ -30,6 +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 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
@@ -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,14 +254,14 @@ 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}")
Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.chapter)
@@ -262,24 +272,18 @@ 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}")
@@ -287,15 +291,15 @@ class MangaDownloaderService : Service() {
}
}
@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) {

View File

@@ -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
@@ -37,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) {
@@ -50,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)
@@ -60,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

View File

@@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
@@ -20,8 +19,10 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
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
@@ -29,16 +30,20 @@ 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.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.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
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
@@ -46,9 +51,13 @@ 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
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
@@ -57,6 +66,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -146,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 =
@@ -159,17 +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),
null
)
lifecycleScope.launch {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
}
} ?: run {
snackString("no media found")
}
@@ -178,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 =
@@ -192,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
@@ -223,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(
@@ -234,7 +241,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < 0
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
scrollTop.isVisible = visibility
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
}
@@ -246,7 +253,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -266,75 +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 tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = tDownloads.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 tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.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 = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
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())
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 = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
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 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
@@ -342,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,
@@ -358,8 +376,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",

View File

@@ -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 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.
@@ -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}")
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

View File

@@ -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
)
}

View File

@@ -7,187 +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.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
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 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 ani.dantotsu.settings.saving.PrefManager
import eu.kanade.tachiyomi.network.NetworkHelper
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,
@@ -219,22 +58,13 @@ 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,
PrefManager.getAnimeDownloadPreferences().getString(
animeDownloadTask.getTaskName(),
""
) ?: "",
false
)
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
@@ -242,14 +72,15 @@ object Helper {
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") { _, _ -> }
@@ -264,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(

View File

@@ -207,6 +207,21 @@ 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) {
@@ -263,7 +278,7 @@ class AnimeFragment : Fragment() {
}
model.loaded = true
model.loadTrending(1)
model.loadUpdated()
model.loadAll()
model.loadPopular(
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularAnimeList
@@ -283,7 +298,9 @@ class AnimeFragment : Fragment() {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::animePageAdapter.isInitialized && _binding != null) {
animePageAdapter.updateNotificationCount()
}
super.onResume()
}
}

View File

@@ -4,12 +4,14 @@ 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
@@ -21,11 +23,13 @@ import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding
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
@@ -40,6 +44,7 @@ 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
@@ -52,14 +57,15 @@ 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)
@@ -68,16 +74,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.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"),
@@ -85,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,
@@ -122,8 +141,7 @@ 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 = PrefManager.getVal(PrefName.PopularAnimeList)
@@ -147,30 +165,31 @@ 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 =
trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeTitleContainer.startAnimation(setSlideUp())
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.animeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeSeasonsCont.layoutAnimation =
@@ -178,28 +197,83 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
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())
binding.animeUpdatedRecyclerView.layoutAnimation =
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp())
}
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()
}
}

View File

@@ -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,6 +23,7 @@ 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
@@ -32,6 +35,7 @@ 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
@@ -76,9 +80,14 @@ class HomeFragment : Fragment() {
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
binding.homeUserAvatar.loadImage(Anilist.avatar)
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) 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(
@@ -118,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 = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 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)
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()
}
}
}
}
@@ -305,6 +326,7 @@ class HomeFragment : Fragment() {
}
}
val array = arrayOf(
"AnimeContinue",
"AnimeFav",
@@ -360,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()
}
}

View File

@@ -17,6 +17,7 @@ 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() {
@@ -50,7 +51,7 @@ class LoginFragment : Fragment() {
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog() { password ->
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
@@ -78,7 +79,7 @@ class LoginFragment : Fragment() {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
Logger.log(e)
toast("Error importing settings")
}
}

View File

@@ -160,11 +160,37 @@ 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) {
@@ -237,7 +263,7 @@ class MangaFragment : Fragment() {
}
model.loaded = true
model.loadTrending()
model.loadTrendingNovel()
model.loadAll()
model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularMangaList
@@ -258,6 +284,9 @@ class MangaFragment : Fragment() {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::mangaPageAdapter.isInitialized && _binding != null) {
mangaPageAdapter.updateNotificationCount()
}
super.onResume()
}

View File

@@ -4,12 +4,14 @@ 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
@@ -21,10 +23,12 @@ import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemMangaPageBinding
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
@@ -39,6 +43,7 @@ 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
@@ -51,32 +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.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.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"),
@@ -84,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")
@@ -115,8 +131,7 @@ 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 = PrefManager.getVal(PrefName.PopularMangaList)
@@ -138,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 =
trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaTitleContainer.startAnimation(setSlideUp())
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.mangaListContainer.layoutAnimation =
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())
binding.mangaNovelRecyclerView.layoutAnimation =
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp())
}
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()
}
}

View File

@@ -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

View File

@@ -12,6 +12,7 @@ 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
@@ -32,7 +33,6 @@ 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)
@@ -55,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
@@ -89,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) }

View 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()
)
}
}
}
}

View File

@@ -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,8 +14,8 @@ import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
@@ -34,7 +32,6 @@ class CalendarActivity : AppCompatActivity() {
private var selectedTabIdx = 1
private val model: OtherDetailsViewModel by viewModels()
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -45,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
@@ -74,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

View File

@@ -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

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
@@ -24,12 +23,13 @@ class CharacterAdapter(
return CharacterViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding
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
}

View File

@@ -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,20 +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.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
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 {
@@ -48,7 +55,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density }
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status)
ContextCompat.getColor(this, R.color.transparent)
val banner =
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
@@ -75,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
@@ -114,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()
}
@@ -139,13 +179,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
isCollapsed = true
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 (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status)
binding.characterAppBar.setBackgroundResource(R.color.bg)
ContextCompat.getColor(this, R.color.transparent)
}
}
}

View File

@@ -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

View File

@@ -67,11 +67,12 @@ class GenreActivity : AppCompatActivity() {
private fun loadLocalGenres(): ArrayList<String>? {
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
.toMutableList() as ArrayList<String>?
return if (genres.isNullOrEmpty()) {
.toMutableList()
return if (genres.isEmpty()) {
null
} else {
genres
//sort alphabetically
genres.sort().let { genres as ArrayList<String> }
}
}
}

View File

@@ -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!!) {

View File

@@ -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,25 +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.loadImage
import ani.dantotsu.setAnimation
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
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 com.flaviofaria.kenburnsview.RandomTransitionGenerator
import jp.wasabeef.glide.transformations.BlurTransformation
import java.io.Serializable
@@ -43,6 +41,7 @@ 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>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@@ -84,7 +83,7 @@ class MediaAdaptor(
}
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (type) {
0 -> {
@@ -93,8 +92,8 @@ class MediaAdaptor(
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
@@ -128,6 +127,7 @@ class MediaAdaptor(
)
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
}
b.itemCompactProgressContainer.visibility = if (fav) View.GONE else View.VISIBLE
}
}
@@ -137,9 +137,9 @@ class MediaAdaptor(
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
@@ -149,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)
}
}
}
@@ -176,6 +181,7 @@ 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 (bannerAnimations)
@@ -185,17 +191,12 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
val banner =
if (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
@@ -242,17 +243,12 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
val banner =
if (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

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.util.TypedValue
@@ -12,34 +13,40 @@ 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.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.settings.saving.PrefManager
@@ -47,35 +54,49 @@ 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
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
@@ -83,21 +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)
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
@@ -119,7 +163,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val banner =
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager
tabLayout = binding.mediaTab as NavigationBarView
viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer())
@@ -129,13 +172,15 @@ 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 (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
@@ -147,9 +192,10 @@ 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()
@@ -157,7 +203,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
})
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
if (PrefManager.getVal(PrefName.Incognito)) {
binding.mediaTitle.text = " ${media.userPreferredName}"
val mediaTitle = " ${media.userPreferredName}"
binding.mediaTitle.text = mediaTitle
binding.incognito.visibility = View.VISIBLE
} else {
binding.mediaTitle.text = media.userPreferredName
@@ -181,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,
@@ -202,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
@@ -217,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()
@@ -313,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)
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 = 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) {
@@ -368,35 +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() {
if (this::tabLayout.isInitialized) {
tabLayout.selectedItemId = idFromSelect()
}
navBar.selectTabAt(selected)
super.onResume()
}
@@ -404,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()
}
}
@@ -439,13 +489,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCover.visibility =
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
@@ -484,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
@@ -492,6 +536,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
init {
enabled(true)
if (needsInitialClick) {
scope.launch {
clicked()
}
}
image.setOnClickListener {
if (pressable && !disabled) {
pressable = false
@@ -546,5 +595,4 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
companion object {
var mediaSingleton: Media? = null
}
}
}

View File

@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter
@@ -29,6 +28,7 @@ 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
@@ -52,26 +52,23 @@ class MediaDetailsViewModel : ViewModel() {
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
@@ -152,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)
}
}
}
@@ -223,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
@@ -270,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
@@ -291,7 +288,6 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages(
chapter: MangaChapter,
selected: Selected,
series: String,
post: Boolean = true
): Boolean {

View File

@@ -15,16 +15,33 @@ 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
@@ -36,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!!
@@ -45,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?,
@@ -62,8 +80,8 @@ class MediaInfoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoContainer.isVisible = loaded
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
model.scrolledToTop.observe(viewLifecycleOwner) {
@@ -73,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
@@ -94,6 +115,8 @@ 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) {
val episodeDuration = media.anime.episodeDuration
@@ -122,8 +145,10 @@ class MediaInfoFragment : Fragment() {
}
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
@@ -157,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)
@@ -186,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)
@@ -197,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 }
@@ -408,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)
}
}
}
}
@@ -527,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()
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}

View 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
}
}
}
}

View 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
}

View 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
}
}
}
}
}

View File

@@ -30,7 +30,7 @@ class OtherDetailsViewModel : ViewModel() {
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() {
val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6))
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL)
val map = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String, MutableList<Int>>()

View File

@@ -27,7 +27,7 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
return ProgressViewHolder(binding)
}
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) {
val progressBar = holder.binding.root
bar = progressBar

View File

@@ -4,24 +4,30 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
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
import kotlinx.coroutines.launch
import java.util.*
import java.util.Timer
import java.util.TimerTask
class SearchActivity : AppCompatActivity() {
private lateinit var binding: ActivitySearchBinding
@@ -64,11 +70,18 @@ class SearchActivity : AppCompatActivity() {
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
?.toIntOrNull() else null,
results = mutableListOf(),
hasNextPage = false
)
@@ -127,8 +140,12 @@ class SearchActivity : AppCompatActivity() {
excludedTags = it.excludedTags
tags = it.tags
season = it.season
startYear = it.startYear
seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
}
@@ -137,7 +154,7 @@ class SearchActivity : AppCompatActivity() {
model.searchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
@@ -151,7 +168,10 @@ class SearchActivity : AppCompatActivity() {
} else
headerAdaptor.requestFocus?.run()
if (intent.getBooleanExtra("search", false)) search()
if (intent.getBooleanExtra("search", false)) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED)
search()
}
}
}
}
@@ -199,7 +219,9 @@ class SearchActivity : AppCompatActivity() {
var state: Parcelable? = null
override fun onPause() {
headerAdaptor.addHistory()
if (this::headerAdaptor.isInitialized) {
headerAdaptor.addHistory()
}
super.onPause()
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
}

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
@@ -12,6 +13,7 @@ import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
@@ -23,9 +25,12 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.checkbox.MaterialCheckBox.*
import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
import com.google.android.material.checkbox.MaterialCheckBox.STATE_INDETERMINATE
import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -41,6 +46,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -89,16 +108,78 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
activity.updateChips = { it.update() }
}
binding.searchChipRecycler.layoutManager =
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
}
binding.searchFilter.setOnLongClickListener {
val popupMenu = PopupMenu(activity, binding.searchFilter)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
}
true
}
popupMenu.show()
true
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
fun searchTitle() {
activity.result.apply {
search =
@@ -208,13 +289,16 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else {
if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation())
}
binding.searchResultLayout.visibility = View.VISIBLE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
@@ -247,7 +331,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
}
class SearchChipAdapter(val activity: SearchActivity) :
class SearchChipAdapter(
val activity: SearchActivity,
private val searchAdapter: SearchAdapter
) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
@@ -264,11 +351,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position]
holder.binding.root.apply {
text = chip.text
text = chip.text.replace("_", " ")
setOnClickListener {
activity.result.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
}
}
}
@@ -277,6 +365,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
fun update() {
chips = activity.result.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
override fun getItemCount(): Int = chips.size

View File

@@ -1,11 +1,15 @@
package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnimationUtils
import android.widget.ArrayAdapter
import android.widget.PopupMenu
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -17,6 +21,9 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
import ani.dantotsu.databinding.ItemChipBinding
import com.google.android.material.chip.Chip
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Calendar
class SearchFilterBottomDialog : BottomSheetDialogFragment() {
@@ -38,6 +45,54 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var exGenres = mutableListOf<String>()
private var selectedTags = mutableListOf<String>()
private var exTags = mutableListOf<String>()
private fun updateChips() {
binding.searchFilterGenres.adapter?.notifyDataSetChanged()
binding.searchFilterTags.adapter?.notifyDataSetChanged()
}
private fun startBounceZoomAnimation(view: View? = null) {
val targetView = view ?: binding.sortByFilter
val bounceZoomAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
targetView.startAnimation(bounceZoomAnimation)
}
private fun setSortByFilterImage() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.sortByFilter.setImageResource(filterDrawable)
}
private fun resetSearchFilter() {
activity.result.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
selectedGenres.clear()
exGenres.clear()
selectedTags.clear()
exTags.clear()
binding.searchStatus.setText("")
binding.searchSource.setText("")
binding.searchFormat.setText("")
binding.searchSeason.setText("")
binding.searchYear.setText("")
binding.searchStatus.clearFocus()
binding.searchFormat.clearFocus()
binding.searchSeason.clearFocus()
binding.searchYear.clearFocus()
updateChips()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -47,14 +102,157 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf()
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
val rotateAnimation =
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
resetSearchFilter()
}
binding.resetSearchFilter.setOnLongClickListener {
val rotateAnimation =
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
val bounceAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
binding.resetSearchFilter.startAnimation(bounceAnimation)
binding.resetSearchFilter.postDelayed({
resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch {
activity.result.apply {
status =
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source =
binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
excludedTags = exTags
}
activity.updateChips.invoke()
activity.search()
dismiss()
}
}, 500)
true
}
binding.sortByFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
startBounceZoomAnimation()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation()
}
}
true
}
popupMenu.show()
}
binding.countryFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.country_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.country_global -> {
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_china -> {
activity.result.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_japan -> {
activity.result.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
}
true
}
popupMenu.show()
}
binding.searchFilterApply.setOnClickListener {
activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
sort = binding.searchSortBy.text.toString().ifBlank { null }
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
season = binding.searchSeason.text.toString().ifBlank { null }
seasonYear = binding.searchYear.text.toString().toIntOrNull()
if (activity.result.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else {
startYear = binding.searchYear.text.toString().toIntOrNull()
}
sort = activity.result.sort
countryOfOrigin = activity.result.countryOfOrigin
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@@ -67,15 +265,23 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchFilterCancel.setOnClickListener {
dismiss()
}
binding.searchSortBy.setText(activity.result.sort?.let {
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]
})
binding.searchSortBy.setAdapter(
val format =
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
binding.searchStatus.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
resources.getStringArray(R.array.sort_by)
format
)
)
binding.searchSource.setText(activity.result.source?.replace("_", " "))
binding.searchSource.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
Anilist.source.toTypedArray()
)
)
@@ -84,11 +290,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray()
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE
if (activity.result.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString())
} else {
binding.searchYear.setText(activity.result.startYear?.toString())
}
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
else {
binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setAdapter(
@@ -98,16 +318,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
Anilist.seasons.toTypedArray()
)
)
binding.searchYear.setText(activity.result.seasonYear?.toString())
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
}
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
@@ -22,7 +21,6 @@ abstract class SourceAdapter(
return SourceViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
val binding = holder.binding
val character = sources[position]

View File

@@ -65,7 +65,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
i = media!!.selected!!.sourceIndex
val source = if (media!!.anime != null) {
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!]
(if (media!!.isAdult) HAnimeSources else AnimeSources)[i!!]
} else {
anime = false
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!]

View File

@@ -6,6 +6,7 @@ import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -114,7 +115,7 @@ class StudioActivity : AppCompatActivity() {
}
override fun onResume() {
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.studioProgressBar.isGone = loaded
super.onResume()
}
}

View File

@@ -5,19 +5,19 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString
import com.anggrayudi.storage.file.openOutputStream
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class SubtitleDownloader {
companion object {
//doesn't really download the subtitles -\_(o_o)_/-
suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
suspend fun loadSubtitleType(url: String): SubtitleType =
withContext(Dispatchers.IO) {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val networkHelper = Injekt.get<NetworkHelper>()
@@ -51,21 +51,17 @@ class SubtitleDownloader {
downloadedType: DownloadedType
) {
try {
val directory = DownloadsManager.getDirectory(
val directory = DownloadsManager.getSubDirectory(
context,
downloadedType.type,
false,
downloadedType.title,
downloadedType.chapter
)
if (!directory.exists()) { //just in case
directory.mkdirs()
}
val type = loadSubtitleType(context, url)
val subtiteFile = File(directory, "subtitle.${type}")
if (subtiteFile.exists()) {
subtiteFile.delete()
}
subtiteFile.createNewFile()
) ?: throw Exception("Could not create directory")
val type = loadSubtitleType(url)
directory.findFile("subtitle.${type}")?.delete()
val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
?: throw Exception("Could not create subtitle file")
val client = Injekt.get<NetworkHelper>().client
val request = Request.Builder().url(url).build()
@@ -77,7 +73,8 @@ class SubtitleDownloader {
}
reponse.body.byteStream().use { input ->
subtiteFile.outputStream().use { output ->
subtitleFile.openOutputStream(context, false).use { output ->
if (output == null) throw Exception("Could not open output stream")
input.copyTo(output)
}
}

View File

@@ -1,127 +0,0 @@
package ani.dantotsu.media.anime
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
class AnimeNameAdapter {
companion object {
const val episodeRegex =
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
const val failedEpisodeNumberRegex =
"(?<!part\\s)\\b(\\d+)\\b"
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(subdubRegex, 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(subdubRegex, 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(seasonRegex, 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(episodeRegex, 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(failedEpisodeNumberRegex, 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(episodeRegex, 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(episodeRegex, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern: Regex =
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
}
}

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
@@ -12,26 +11,37 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.displayTimer
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.openSettings
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.toast
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@@ -42,7 +52,7 @@ class AnimeWatchAdapter(
private val fragment: AnimeWatchFragment,
private val watchSources: WatchSources
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
@@ -54,20 +64,25 @@ class AnimeWatchAdapter(
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
binding.faqbutton.setOnClickListener {
startActivity(
fragment.requireContext(),
Intent(fragment.requireContext(), FAQActivity::class.java),
null
)
}
//Youtube
if (media.anime!!.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
fragment.requireContext().startActivity(intent)
}
}
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
binding.animeSourceDubbedText.text =
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
@@ -91,15 +106,12 @@ class AnimeWatchAdapter(
null
)
}
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
PrefName.OfflineMode
)
) View.GONE else View.VISIBLE
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
binding.animeSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline
//Source Selection
var source =
@@ -111,8 +123,7 @@ class AnimeWatchAdapter(
this.selectDub = media.selected!!.preferDub
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
}
}
@@ -131,8 +142,7 @@ class AnimeWatchAdapter(
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
source = i
setLanguageList(0, i)
}
@@ -152,14 +162,12 @@ class AnimeWatchAdapter(
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(i, source)
}
subscribeButton(false)
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
} ?: run {
}
} ?: run { }
}
//settings
@@ -179,7 +187,8 @@ class AnimeWatchAdapter(
R.drawable.ic_round_notifications_none_24,
R.color.bg_opp,
R.color.violet_400,
fragment.subscribed
fragment.subscribed,
true
) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
}
@@ -187,7 +196,7 @@ class AnimeWatchAdapter(
subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), getChannelId(true, media.id))
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
}
//Nested Button
@@ -216,9 +225,9 @@ class AnimeWatchAdapter(
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Grid"
2 -> dialogBinding.layoutText.text = "Compact"
0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.grid)
2 -> dialogBinding.layoutText.setText(R.string.compact)
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
@@ -230,24 +239,24 @@ class AnimeWatchAdapter(
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.text = "List"
dialogBinding.layoutText.setText(R.string.list)
run = true
}
dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.text = "Grid"
dialogBinding.layoutText.setText(R.string.grid)
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
dialogBinding.layoutText.text = "Compact"
dialogBinding.layoutText.setText(R.string.compact)
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast("WebView not installed")
toast(R.string.webview_not_installed)
}
//start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
@@ -300,7 +309,6 @@ class AnimeWatchAdapter(
}
//Chips
@SuppressLint("SetTextI18n")
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
@@ -322,7 +330,9 @@ class AnimeWatchAdapter(
0
)
}
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
chip.text = chipText
chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
@@ -356,7 +366,6 @@ class AnimeWatchAdapter(
_binding?.animeSourceChipGroup?.removeAllViews()
}
@SuppressLint("SetTextI18n")
fun handleEpisodes() {
val binding = _binding
if (binding != null) {
@@ -364,9 +373,9 @@ class AnimeWatchAdapter(
val episodes = media.anime.episodes!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp =
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
?: 1
val appEp = PrefManager.getCustomVal<String?>(
"${media.id}_current_ep", ""
)?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) {
@@ -396,21 +405,27 @@ class AnimeWatchAdapter(
}
val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
currActivity()!!.getString(
R.string.continue_episode, ep.number, if (ep.filler)
currActivity()!!.getString(R.string.filler_tag)
else
"", cleanedTitle
)
binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
if (fragment.continueEp) {
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
PrefName.WatchPercentage
)
if (
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
) {
binding.animeSourceContinue.performClick()
fragment.continueEp = false
@@ -421,13 +436,35 @@ class AnimeWatchAdapter(
}
binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE
else
binding.animeSourceNotFound.visibility = View.VISIBLE
val sourceFound = media.anime.episodes!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText(
binding.animeSource.adapter
.getItem(nextIndex).toString(), false
)
fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex)
}
subscribeButton(false)
fragment.loadEpisodes(nextIndex, false)
}
}
binding.animeSource.setOnClickListener { autoSelect = false }
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE
}
@@ -469,8 +506,7 @@ class AnimeWatchAdapter(
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
//Timer
countDown(media, binding.animeSourceContainer)
displayTimer(media, binding.animeSourceContainer)
}
}
}

View File

@@ -17,39 +17,46 @@ import androidx.annotation.OptIn
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.dp
import ani.dantotsu.isOnline
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.setNavigationTheme
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers
@@ -192,10 +199,16 @@ class AnimeWatchFragment : Fragment() {
ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) {
awaitAll(
async { model.loadKitsuEpisodes(media) },
async { model.loadFillerEpisodes(media) }
)
val offline =
!isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
if (offline) {
media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
} else {
awaitAll(
async { model.loadKitsuEpisodes(media) },
async { model.loadFillerEpisodes(media) }
)
}
model.loadEpisodes(media, media.selected!!.sourceIndex)
}
loaded = true
@@ -220,7 +233,7 @@ class AnimeWatchFragment : Fragment() {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc =
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title
@@ -333,16 +346,7 @@ class AnimeWatchFragment : Fragment() {
var subscribed = false
fun onNotificationPressed(subscribed: Boolean, source: String) {
this.subscribed = subscribed
saveSubscription(requireContext(), media, subscribed)
if (!subscribed)
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
else
Notifications.createChannel(
requireContext(),
ANIME_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
saveSubscription(media, subscribed)
snackString(
if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification)
@@ -353,18 +357,12 @@ class AnimeWatchFragment : Fragment() {
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = activity
if (activity is MediaDetailsActivity && isAdded) {
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
} catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
activity.navBar.isVisible = show
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
}
}
var itemSelected = false
@@ -432,7 +430,29 @@ class AnimeWatchFragment : Fragment() {
}
fun onAnimeEpisodeDownloadClick(i: String) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
activity?.let {
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
} else {
snackString(getString(R.string.download_permission_required))
}
}
} else {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
}
}
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
@@ -450,10 +470,11 @@ class AnimeWatchFragment : Fragment() {
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
MediaType.ANIME
)
)
episodeAdapter.purgeDownload(i)
) {
episodeAdapter.purgeDownload(i)
}
}
@OptIn(UnstableApi::class)
@@ -462,22 +483,13 @@ class AnimeWatchFragment : Fragment() {
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
MediaType.ANIME
)
)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
episodeAdapter.deleteDownload(i)
}
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
@@ -541,7 +553,7 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName()) {
if (media.compareName(download.title)) {
episodeAdapter.stopDownload(download.chapter)
}
}
@@ -561,6 +573,8 @@ class AnimeWatchFragment : Fragment() {
super.onResume()
binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme()
}
override fun onPause() {

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