Compare commits

..

1560 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
384 changed files with 27685 additions and 21588 deletions

View File

@@ -1,9 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 🧑‍💻 Dantotsu Help on Discord
url: https://discord.com/invite/4HPZ5nAWwM
about: Get support, ask questions, and join the community discussions.
- name: 📱 Dantotsu Help on Telegram
url: https://t.me/dantotsuapp
about: Connect with the community, ask questions, and get help directly on Telegram.

View File

@@ -1,35 +0,0 @@
name: ❓ Question
description: Submit a question or query related to Dantotsu
labels: [question]
body:
- type: textarea
id: question-details
attributes:
label: Question Details
description: Provide a detailed explanation of your question or query.
placeholder: |
Example:
"How do I customize the settings in Dartotsu to optimize performance?"
validations:
required: true
- type: input
id: related-features
attributes:
label: Related Features (if applicable)
description: Mention any specific feature or section of Dantotsu related to your question.
placeholder: |
Example: "Settings > Performance"
- type: checkboxes
id: submission-checklist
attributes:
label: Submission Checklist
description: Review the following items before submitting your question.
options:
- label: I have searched existing issues to see if this question has already been answered.
required: true
- label: I have provided a clear and concise question title.
required: true
- label: I have provided all relevant details to understand my question fully.
required: true

View File

@@ -1,101 +0,0 @@
name: 🐛 Issue Report
description: Report a bug or problem in Dantotsu
labels: [bug]
body:
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Outline the steps needed to trigger the issue.
placeholder: |
Example:
1. Navigate to the home screen.
2. Click on "Start."
3. Observe the error message.
validations:
required: true
- type: textarea
id: expected-outcome
attributes:
label: Expected Outcome
description: Describe what you expected to happen.
placeholder: |
Example:
"The application should have successfully loaded the dashboard..."
validations:
required: true
- type: textarea
id: actual-outcome
attributes:
label: Actual Outcome
description: Detail what actually occurred when following the steps.
placeholder: |
Example:
"The app crashed and displayed an error message instead..."
validations:
required: true
- type: textarea
id: error-logs
attributes:
label: Error Logs (if applicable)
description: |
If the issue involves crashes, please attach relevant logs. Access them via **Settings → About → Log to file → Share**.
placeholder: |
Paste the logs here or upload as an attachment.
- type: input
id: dartotsu-version
attributes:
label: Dartotsu Version
description: Specify the version of Dartotsu in which the issue occurred.
placeholder: |
Example: "1.2.3"
validations:
required: true
- type: input
id: os-version
attributes:
label: Operating System Version
description: Mention the OS version you are using.
placeholder: |
Example: "Android 12"
validations:
required: true
- type: input
id: device-info
attributes:
label: Device Information
description: Provide your device name and model.
placeholder: |
Example: "Samsung Galaxy S21"
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional Information
placeholder: |
Include any other relevant details or attachments that may help diagnose the issue.
- type: checkboxes
id: submission-checklist
attributes:
label: Submission Checklist
description: Ensure you've reviewed these items before submitting your report.
options:
- label: I have searched existing issues to confirm this is not a duplicate.
required: true
- label: I have provided a clear and descriptive title.
required: true
- label: I am using the **[latest](https://github.com/rebelonion/Dantotsu/latest)** version of Dantotsu. If not, I have provided a reason for not updating.
required: true
- label: I have updated all relevant extensions or dependencies.
required: true
- label: I have filled out all the requested information accurately.
required: true

View File

@@ -1,54 +0,0 @@
name: 🚀 Feature Request
description: Propose a new feature to enhance Dantotsu
labels: [enhancement]
body:
- type: textarea
id: feature-summary
attributes:
label: Feature Summary
description: Provide a concise summary of the feature you'd like to see.
placeholder: |
Example:
"Add support for dark mode..."
validations:
required: true
- type: textarea
id: detailed-description
attributes:
label: Detailed Description
description: Elaborate on how this feature should function and its potential impact.
placeholder: |
Example:
"The dark mode should automatically activate based on system settings..."
value: |
### Current Behavior
- Describe the current functionality or lack thereof.
### Proposed Solution
- Detail how the feature should work and any potential benefits.
### Considerations
- Mention any potential challenges or alternatives.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Provide any other information, relevant screenshots, or references.
placeholder: "Include links to relevant resources, external tools, or related issues."
- type: checkboxes
id: checklist
attributes:
label: Submission Checklist
description: Ensure you've completed these before submitting.
options:
- label: I have searched the existing issues and confirm that this feature has not been requested before.
required: true
- label: I have provided a clear and descriptive title.
required: true
- label: I am using the **[latest](https://github.com/rebelonion/Dantotsu/releases/latest)** version of Dantotsu. If not, I have provided a reason for using an older version.
required: true
- label: I understand that not all feature requests will be accepted, and if declined, I won't resubmit the same request.
required: true

View File

@@ -1,25 +1,17 @@
name: Build APK and Notify Discord
on:
push:
branches-ignore:
- main
- l10n_dev_crowdin
- custom-download-location
branches:
- dev
paths-ignore:
- '**/README.md'
tags:
- "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
env:
CI: true
SKIP_BUILD: false
steps:
- name: Checkout repo
@@ -27,12 +19,14 @@ jobs:
with:
fetch-depth: 0
- name: Download last SHA artifact
uses: dawidd6/action-download-artifact@v6
uses: dawidd6/action-download-artifact@v3
with:
workflow: beta.yml
name: last-sha
path: .
continue-on-error: true
- name: Get Commits Since Last Run
@@ -45,9 +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:"● %s ~%an [֍](https://github.com/${{ github.repository }}/commit/%H)" --max-count=10)
# Replace commit messages with pull request links
COMMIT_LOGS=$(echo "$COMMIT_LOGS" | sed -E 's/#([0-9]+)/[#\1](https:\/\/github.com\/rebelonion\/Dantotsu\/pull\/\1)/g')
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'}"
@@ -57,10 +49,6 @@ jobs:
# Debugging: Print the variable to check its content
echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt
# Extract branch name from github.ref
BRANCH=${{ github.ref }}
BRANCH=${BRANCH#refs/heads/}
echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
shell: /usr/bin/bash -e {0}
env:
CI: true
@@ -77,278 +65,53 @@ jobs:
echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: List files in the directory
run: ls -l
- name: Setup JDK 17
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
cache: gradle
- name: Decode Keystore File
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
if: ${{ env.SKIP_BUILD != 'true' }}
run: chmod +x ./gradlew
- name: Build with Gradle
if: ${{ env.SKIP_BUILD != 'true' }}
run: |
if [ "${{ github.repository }}" == "rebelonion/Dantotsu" ]; then
./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 }};
else
./gradlew assembleGoogleAlpha;
fi
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
if: ${{ env.SKIP_BUILD != 'true' }}
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: |
# Prepare Discord embed
fetch_user_details() {
local login=$1
user_details=$(curl -s "https://api.github.com/users/$login")
name=$(echo "$user_details" | jq -r '.name // .login')
login=$(echo "$user_details" | jq -r '.login')
avatar_url=$(echo "$user_details" | jq -r '.avatar_url')
echo "$name|$login|$avatar_url"
}
# Additional information for the goats
declare -A additional_info
additional_info["ibo"]="\n Discord: <@951737931159187457>\n AniList: [takarealist112](<https://anilist.co/user/5790266/>)"
additional_info["aayush262"]="\n Discord: <@918825160654598224>\n AniList: [aayush262](<https://anilist.co/user/5144645/>)"
additional_info["rebel onion"]="\n Discord: <@714249925248024617>\n AniList: [rebelonion](<https://anilist.co/user/6077251/>)\n PornHub: [rebelonion](<https://www.cornhub.com/model/rebelonion>)"
additional_info["Ankit Grai"]="\n Discord: <@1125628254330560623>\n AniList: [bheshnarayan](<https://anilist.co/user/6417303/>)"
# Decimal color codes for contributors
declare -A contributor_colors
default_color="#bf2cc8"
contributor_colors["ibo"]="#ff9b46"
contributor_colors["aayush262"]="#5d689d"
contributor_colors["Sadwhy"]="#ff7e95"
contributor_colors["grayankit"]="#c51aa1"
contributor_colors["rebelonion"]="#d4e5ed"
hex_to_decimal() { printf '%d' "0x${1#"#"}"; }
# Count recent commits and create an associative array Okay
declare -A recent_commit_counts
echo "Debug: Processing COMMIT_LOG:"
echo "$COMMIT_LOG"
while read -r count name; do
recent_commit_counts["$name"]=$count
echo "Debug: Commit count for $name: $count"
done < <(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | grep -oP '(?<=~)[^[]*' | sort | uniq -c | sort -rn)
echo "Debug: Fetching contributors from GitHub"
# Fetch contributors from GitHub
contributors=$(curl -s "https://api.github.com/repos/${{ github.repository }}/contributors")
echo "Debug: Contributors response:"
echo "$contributors"
# Create a sorted list of contributors based on recent commit counts
sorted_contributors=$(for login in $(echo "$contributors" | jq -r '.[].login'); do
user_info=$(fetch_user_details "$login")
name=$(echo "$user_info" | cut -d'|' -f1)
count=${recent_commit_counts["$name"]:-0}
echo "$count|$login"
done | sort -rn | cut -d'|' -f2)
# Initialize needed variables
developers=""
committers_count=0
max_commits=0
top_contributor=""
top_contributor_count=0
top_contributor_avatar=""
embed_color=$(hex_to_decimal "$default_color")
# Process contributors in the new order
while read -r login; do
user_info=$(fetch_user_details "$login")
name=$(echo "$user_info" | cut -d'|' -f1)
login=$(echo "$user_info" | cut -d'|' -f2)
avatar_url=$(echo "$user_info" | cut -d'|' -f3)
# Only process if they have recent commits
commit_count=${recent_commit_counts["$name"]:-0}
if [ $commit_count -gt 0 ]; then
# Update top contributor information
if [ $commit_count -gt $max_commits ]; then
max_commits=$commit_count
top_contributors=("$login")
top_contributor_count=1
top_contributor_avatar="$avatar_url"
embed_color=$(hex_to_decimal "${contributor_colors[$name]:-$default_color}")
elif [ $commit_count -eq $max_commits ]; then
top_contributors+=("$login")
top_contributor_count=$((top_contributor_count + 1))
embed_color=$(hex_to_decimal "$default_color")
fi
echo "Debug top contributors:"
echo "$top_contributors"
# Get commit count for this contributor on the dev branch
branch_commit_count=$(git log --author="$login" --author="$name" --oneline | awk '!seen[$0]++' | wc -l)
# Debug: Print recent_commit_counts
echo "Debug: recent_commit_counts contents:"
for key in "${!recent_commit_counts[@]}"; do
echo "$key: ${recent_commit_counts[$key]}"
done
extra_info="${additional_info[$name]}"
if [ -n "$extra_info" ]; then
extra_info=$(echo "$extra_info" | sed 's/\\n/\n- /g')
fi
# Construct the developer entry
developer_entry="◗ **${name}** ${extra_info}
- Github: [${login}](https://github.com/${login})
- Commits: ${branch_commit_count}"
# Add the entry to developers, with a newline if it's not the first entry
if [ -n "$developers" ]; then
developers="${developers}
${developer_entry}"
else
developers="${developer_entry}"
fi
committers_count=$((committers_count + 1))
fi
done <<< "$sorted_contributors"
# Set the thumbnail URL and color based on top contributor(s)
if [ $top_contributor_count -eq 1 ]; then
thumbnail_url="$top_contributor_avatar"
else
thumbnail_url="https://i.imgur.com/5o3Y9Jb.gif"
embed_color=$(hex_to_decimal "$default_color")
fi
# Truncate field values
max_length=1000
#Discord
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
if [ ${#developers} -gt $max_length ]; then
developers="${developers:0:$max_length}... (truncated)"
fi
# Truncate commit messages if they are too long
max_length=1900 # Adjust this value as needed
if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
#Telegram
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
# Construct Discord payload
discord_data=$(jq -nc \
--arg field_value "$commit_messages" \
--arg author_value "$developers" \
--arg footer_text "Version $VERSION" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
--arg thumbnail_url "$thumbnail_url" \
--arg embed_color "$embed_color" \
'{
"content": "<@&1225347048321191996>",
"embeds": [
{
"title": "New Alpha-Build dropped",
"color": $embed_color,
"fields": [
{
"name": "Commits:",
"value": $field_value,
"inline": true
},
{
"name": "Developers:",
"value": $author_value,
"inline": false
}
],
"footer": {
"text": $footer_text
},
"timestamp": $timestamp,
"thumbnail": {
"url": $thumbnail_url
}
}
],
"attachments": []
}')
echo "Debug: Final Discord payload:"
echo "$discord_data"
# Send Discord message
curl -H "Content-Type: application/json" \
-d "$discord_data" \
${{ secrets.DISCORD_WEBHOOK }}
echo "You have only send an embed to discord due to SKIP_BUILD being set to true"
# Upload APK to Discord
if [ "$SKIP_BUILD" != "true" ]; then
curl -F "payload_json=${contentbody}" \
-F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
${{ secrets.DISCORD_WEBHOOK }}
else
echo "Skipping APK upload to Discord due to SKIP_BUILD being set to true"
fi
# Format commit messages for Telegram
telegram_commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | while read -r line; do
message=$(echo "$line" | sed -E 's/● (.*) ~(.*) \[֍\]\((.*)\)/● \1 ~\2 <a href="\3">֍<\/a>/')
message=$(echo "$message" | sed -E 's/\[#([0-9]+)\]\((https:\/\/github\.com\/[^)]+)\)/<a href="\2">#\1<\/a>/g')
echo "$message"
done)
telegram_commit_messages="<blockquote>${telegram_commit_messages}</blockquote>"
# Configuring dev info
echo "$developers" > dev_info.txt
echo "$developers"
# making the file executable
chmod +x workflowscripts/tel_parser.sed
./workflowscripts/tel_parser.sed dev_info.txt >> output.txt
dev_info_tel=$(< output.txt)
telegram_dev_info="<blockquote>${dev_info_tel}</blockquote>"
echo "$telegram_dev_info"
# Upload APK to Telegram
if [ "$SKIP_BUILD" != "true" ]; then
APK_PATH="app/build/outputs/apk/google/alpha/app-google-alpha.apk"
response=$(curl -sS -f -X POST \
"https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument" \
-F "chat_id=-1002117798698" \
-F "message_thread_id=7044" \
-F "document=@$APK_PATH" \
-F "caption=New Alpha-Build dropped 🔥
Commits:
${telegram_commit_messages}
Dev:
${telegram_dev_info}
version: ${VERSION}" \
-F "parse_mode=HTML")
else
echo "skipping because skip build set to true"
fi
env:
COMMIT_LOG: ${{ env.COMMIT_LOG }}
VERSION: ${{ env.VERSION }}

View File

@@ -1,84 +0,0 @@
name: Bug Report Greeting
on:
issues:
types: [opened]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Check if the issue is labeled as a Bug Report
id: check_bug_label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
LABELS=$(gh issue view $ISSUE_NUMBER --json labels --jq '.labels[].name')
if echo "$LABELS" | grep -q 'bug'; then
echo "This issue is labeled as a bug report. Checking if the issue creator is the repository owner."
echo "skip_label_check=false" >> $GITHUB_ENV
else
echo "This issue is not labeled as a bug report. Skipping greeting message."
echo "skip_label_check=true" >> $GITHUB_ENV
fi
- name: Check if the issue creator is the repo owner
if: env.skip_label_check == 'false'
id: check_owner
run: |
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
if [ "$ISSUE_AUTHOR" = "$REPO_OWNER" ]; then
echo "The issue creator is the repository owner. Skipping greeting message."
echo "skip=true" >> $GITHUB_ENV
else
echo "The issue creator is not the repository owner. Checking for previous bug reports..."
echo "skip=false" >> $GITHUB_ENV
fi
- name: Check if the user has submitted a bug report before
if: env.skip == 'false'
id: check_first_bug_report
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
# Get all issues (both open and closed) by the author except the current one
PREVIOUS_REPORTS=$(gh issue list --author "$ISSUE_AUTHOR" --label "Bug" --state all --json number --jq '. | map(select(.number != '$ISSUE_NUMBER')) | length')
echo "User $ISSUE_AUTHOR has submitted $PREVIOUS_REPORTS bug report(s) previously"
if [ "$PREVIOUS_REPORTS" -eq 0 ]; then
echo "This is the user's first bug report. Sending greeting message."
echo "skip_first_report=false" >> $GITHUB_ENV
else
echo "User has previous bug reports. Skipping greeting message."
echo "skip_first_report=true" >> $GITHUB_ENV
fi
- name: Send Greeting Message
if: env.skip_label_check == 'false' && env.skip != 'true' && env.skip_first_report != 'true'
uses: actions/github-script@v6
with:
script: |
const issueNumber = context.payload.issue.number;
const message = `
**🛠️ Thank you for reporting a bug!**
Your issue has been successfully submitted and is now awaiting review. We appreciate your help in making Dantotsu better.
**🔍 What Happens Next**
- Our team will investigate the issue and provide updates as soon as possible.
- You may be asked for additional details or clarification if needed.
- Once resolved, we'll notify you of the fix or provide a workaround.
**👥 Connect with Us**
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Engage with our community and ask questions.
- **[Telegram](https://t.me/dantotsuapp)**: Reach out for real-time discussions and updates.
We're working hard to resolve the issue and appreciate your patience!
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: message
});

View File

@@ -1,112 +0,0 @@
name: Extension Issue Handling
on:
issues:
types: [opened, labeled]
jobs:
handle-extension-issues:
runs-on: ubuntu-latest
steps:
- name: Check Issue Content
id: check-issue
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
# Regex patterns for extension-related issues
EXTENSION_REGEX_PATTERNS=(
# Extension not working (more flexible match)
".*(\w+)\s*(extension)?\s*(not working|doesn't work|does not work|cant work|can't work).*"
# No extension available
".*(no|can't find|cannot find|missing).*extension.*"
# No repo or repositories available
".*(no|can't find|cannot find|missing).*repo(s)?\s*(available|found|accessible).*"
# Specific server/stream issues
".*(no streams|server).*(available|working).*"
# Variants of extension problems
".*{.*}.*not working.*"
".*{.*}.*extension.*(issue|problem).*"
)
# Convert to lowercase for case-insensitive matching
LOWER_TITLE=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]')
LOWER_BODY=$(echo "$ISSUE_BODY" | tr '[:upper:]' '[:lower:]')
# Flag to track issue type
IS_EXTENSION_ISSUE=false
IS_NO_EXTENSION_ISSUE=false
# Check title and body against regex patterns
for pattern in "${EXTENSION_REGEX_PATTERNS[@]}"; do
if [[ "$LOWER_TITLE" =~ $pattern ]] || [[ "$LOWER_BODY" =~ $pattern ]]; then
IS_EXTENSION_ISSUE=true
# Special check for no extensions available
if [[ "$LOWER_TITLE" =~ "no extension" ]] || [[ "$LOWER_TITLE" =~ "can't find extension" ]]; then
IS_NO_EXTENSION_ISSUE=true
fi
break
fi
done
# Explicitly output boolean values
if [ "$IS_EXTENSION_ISSUE" = true ]; then
echo "is_extension_issue=true" >> $GITHUB_OUTPUT
else
echo "is_extension_issue=false" >> $GITHUB_OUTPUT
fi
if [ "$IS_NO_EXTENSION_ISSUE" = true ]; then
echo "is_no_extension_issue=true" >> $GITHUB_OUTPUT
else
echo "is_no_extension_issue=false" >> $GITHUB_OUTPUT
fi
- name: Comment and Close Extension Issue
if: steps.check-issue.outputs.is_extension_issue == 'true'
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueNumber = context.issue.number;
// Check if it's a "No Extension" issue
if (${{ steps.check-issue.outputs.is_no_extension_issue }}) {
// DMCA notice message
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: "# Automated Message\n" +
"On 13 June 2024, the official Aniyomi repository got a DMCA notice and had to remove all of their extensions. Because of this, we will not be providing anyone with any links or extensions to avoid legal problems.\n" +
"# How to add repos?\n" +
"Although we do not give or maintain any repositories, we support adding custom repository links to your Dantotsu. \n" +
"Go to `Profile > Settings > Extensions` then paste your anime or manga links there.\n" +
"# How to find repos?\n" +
"It's very easy. Search on Google. But remember that the URL must end with <u><b>index.min.json</b></u> or else it won't work.\n" +
"`TLDR: We will not give repo links.`"
});
} else {
// Standard extension issue message
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `Dantotsu doesn't maintain extensions.
If the extension doesn't work we cannot help you.
Contact the owner of Respective Repo for extension-related problems`
});
}
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed'
});

View File

@@ -1,86 +0,0 @@
name: Feature Request Greeting
on:
issues:
types: [opened]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Check if the issue is labeled as a Feature Request
id: check_feature_label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
LABELS=$(gh issue view $ISSUE_NUMBER --json labels --jq '.labels[].name')
if echo "$LABELS" | grep -q 'enhancement'; then
echo "This issue is labeled as a feature request. Checking if the issue creator is the repository owner."
echo "skip_label_check=false" >> $GITHUB_ENV
else
echo "This issue is not labeled as a feature request. Skipping greeting message."
echo "skip_label_check=true" >> $GITHUB_ENV
fi
- name: Check if the user has submitted a feature request before
if: env.skip_label_check == 'false'
id: check_first_request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
if [ "$ISSUE_AUTHOR" = "$REPO_OWNER" ]; then
echo "The issue creator is the repository owner. Skipping greeting message."
echo "skip_first_request=true" >> $GITHUB_ENV
else
echo "Checking for previous feature requests..."
# Get all issues (both open and closed) by the author except the current one
PREVIOUS_REQUESTS=$(gh issue list --author "$ISSUE_AUTHOR" --label "New Feature" --state all --json number --jq '. | map(select(.number != '$ISSUE_NUMBER')) | length')
echo "User $ISSUE_AUTHOR has submitted $PREVIOUS_REQUESTS feature request(s) previously"
if [ "$PREVIOUS_REQUESTS" -eq 0 ]; then
echo "This is the user's first feature request. Sending greeting message."
echo "skip_first_request=false" >> $GITHUB_ENV
else
echo "User has previous feature requests. Skipping greeting message."
echo "skip_first_request=true" >> $GITHUB_ENV
fi
fi
- name: Send Greeting Message
if: env.skip_label_check == 'false' && env.skip_first_request == 'false'
uses: actions/github-script@v6
with:
script: |
const issueNumber = context.payload.issue.number;
const message = `
**💡 Thank you for your feature request!**
Your request has been successfully submitted and is now under consideration. We value your input in shaping the future of Dantotsu.
**📈 What to Expect Next**
- Our team will review your request and assess its feasibility.
- We may reach out for additional details or clarification.
- Updates on the request will be provided, and it may be scheduled for future development.
**👥 Stay Connected**
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Join our community to discuss ideas and stay updated.
- **[Telegram](https://t.me/dantotsuapp)**: Connect with us directly for real-time updates.
We appreciate your suggestion and look forward to potentially implementing it!
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: message
});

View File

@@ -1,80 +0,0 @@
name: PR Greetings
on:
pull_request:
types: [opened]
pull_request_target:
types: [opened]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Check if the PR creator is the repo owner or Weblate
id: check_owner
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
if [ "$PR_AUTHOR" = "$REPO_OWNER" ] || [ "$PR_AUTHOR" = "weblate" ]; then
echo "The PR creator is the repository owner or Weblate. Skipping greeting message."
echo "skip=true" >> $GITHUB_ENV
else
echo "The PR creator is not the repository owner or Weblate. Checking for previous PRs..."
# Check for both open and closed pull requests by the author
OPEN_PRS=$(gh pr list --author "$PR_AUTHOR" --state open --json number --jq '. | length')
CLOSED_PRS=$(gh pr list --author "$PR_AUTHOR" --state closed --json number --jq '. | length')
TOTAL_PRS=$((OPEN_PRS + CLOSED_PRS))
echo "User $PR_AUTHOR has created $TOTAL_PRS pull request(s) in total"
echo "Open PRs: $OPEN_PRS"
echo "Closed PRs: $CLOSED_PRS"
if [ "$TOTAL_PRS" -eq 1 ]; then
echo "This is the user's first pull request. Sending greeting message."
echo "skip=false" >> $GITHUB_ENV
else
echo "User has previous pull requests. Skipping greeting message."
echo "skip=true" >> $GITHUB_ENV
fi
fi
- name: Send Greeting Message
if: env.skip != 'true'
uses: actions/github-script@v6
with:
script: |
const prNumber = context.payload.pull_request.number;
const message = `
**🎉 Thank you for your contribution!**
Your Pull Request has been successfully submitted and is now awaiting review. We truly appreciate your efforts to improve Dantotsu.
**👥 Connect with the Community**
While you're here, why not join our communities to stay engaged?
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Chat with fellow developers, ask questions, and get the latest updates.
- **[Telegram](https://t.me/dantotsuapp)**: Connect directly with us for real-time discussions and updates.
**📋 What to Expect Next**
- Our team will review your pull request as soon as possible.
- You'll receive notifications if further information or changes are needed.
- Once approved, your changes will be merged into the main project.
We're excited to collaborate with you. Stay tuned for updates!
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: message
});

8
.gitignore vendored
View File

@@ -2,9 +2,6 @@
.gradle/
build/
#kotlin
.kotlin/
# Local configuration file (sdk path, etc)
local.properties
@@ -36,7 +33,4 @@ output.json
scripts/
#crowdin
crowdin.yml
#vscode
.vscode
crowdin.yml

View File

@@ -1,17 +1,674 @@
## Unabandon Public License (UPL)
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
**Preamble**
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This Unabandon Public License (UPL) is designed to ensure the continued development and public availability of source code based on works released under the GNU General Public License Version 3 (GPLv3) while upholding the core principles of GPLv3. This license extends GPLv3 by mandating public accessibility of source code for any derivative works.
Preamble
**Body**
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
1. **Incorporation of GPLv3:** This UPL incorporates all terms and conditions of the GNU General Public License Version 3 (GPLv3) as published by the Free Software Foundation. You can find the complete text of GPLv3 at [https://www.gnu.org/licenses/licenses.en.html](https://www.gnu.org/licenses/licenses.en.html).
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
2. **Public Source Requirement:** In addition to the terms of GPLv3, the source code for any software distributed under this license, including modifications and derivative works, must be publicly available. Public availability means the source code must be accessible to anyone through a publicly accessible repository or download link without any access restrictions or fees.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
3. **Source Code Availability:** The source code must be made publicly available using a recognized open-source hosting platform (e.g., GitHub, GitLab) or be downloadable from a publicly accessible website. The chosen method must clearly identify the source code and its corresponding licensed work.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
**Termination**
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
This UPL terminates automatically if the terms and conditions are not followed by the licensee.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -16,25 +16,6 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
## Terms of Use
By downloading, installing, or using this application, you agree to:
- Use the application in compliance with all applicable laws
- Not use the application to infringe on copyrighted content
- Take full responsibility for any extensions you install or use
- Understand that the developer(s) are not responsible for third-party extensions or user actions
This application is designed for anime tracking and legal streaming service integration. The developers do not provide, maintain, or endorse any extensions that enable access to unauthorized content.
## Important Notice
This application is an anime tracking and management tool. The extension system is designed to integrate with legal streaming services like Jellyfin.
We do not:
- Provide or maintain any streaming extensions
- Host or distribute copyrighted content
- Endorse or encourage copyright infringement
Users are responsible for ensuring their use of this software complies with local laws and regulations.
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
## WANT TO CONTRIBUTE? 🤝
@@ -57,4 +38,4 @@ You can come hang out with our awesome community, request new features, and repo
## LICENSE 📜
Dantotsu is licensed under the Unabandon Public License (UPL). More info can be found [here.](LICENSE.md)
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)

View File

@@ -11,15 +11,15 @@ def gitCommitHash = providers.exec {
}.standardOutput.asText.get().trim()
android {
compileSdk 35
compileSdk 34
defaultConfig {
applicationId "ani.dantotsu"
minSdk 21
targetSdk 35
targetSdk 34
versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.2.0"
versionCode 300200000
versionName "3.0.0"
versionCode 300000000
signingConfig signingConfigs.debug
}
@@ -51,7 +51,7 @@ android {
}
debug {
applicationIdSuffix ".beta"
versionNameSuffix "-beta01"
versionNameSuffix "-beta02"
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
debuggable false
@@ -81,13 +81,13 @@ android {
dependencies {
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:33.0.0')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0'
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.8.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@@ -99,10 +99,8 @@ dependencies {
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.11.0'
implementation 'androidx.webkit:webkit:1.10.0'
implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
// Glide
ext.glide_version = '4.16.0'
@@ -113,7 +111,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.5.0'
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"
@@ -123,11 +121,9 @@ dependencies {
// Media3 Casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0"
// Media3 extension
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
// UI
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.android.material:material:1.11.0'
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'
@@ -135,7 +131,7 @@ dependencies {
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.3'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
// Markwon
ext.markwon_version = '4.6.2'
@@ -161,7 +157,7 @@ dependencies {
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1'
implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
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'

View File

@@ -1,40 +1,9 @@
package ani.dantotsu.others
import androidx.fragment.app.FragmentActivity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
// no-op
//no-op
}
@Serializable
data class GithubResponse(
@SerialName("html_url")
val htmlUrl: String,
@SerialName("tag_name")
val tagName: String,
val prerelease: Boolean,
@SerialName("created_at")
val createdAt: String,
val body: String? = null,
val assets: List<Asset>? = null
) {
@Serializable
data class Asset(
@SerialName("browser_download_url")
val browserDownloadURL: String
)
fun timeStamp(): Long {
return dateFormat.parse(createdAt)!!.time
}
companion object {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
}
}
}
}

View File

@@ -18,9 +18,7 @@ import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.client
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext
import ani.dantotsu.decodeBase64ToString
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
@@ -39,88 +37,26 @@ import java.text.SimpleDateFormat
import java.util.Locale
object AppUpdater {
private val fallbackStableUrl: String
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvc3RhYmxl".decodeBase64ToString()
private val fallbackBetaUrl: String
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvYmV0YQ==".decodeBase64ToString()
@Serializable
data class FallbackResponse(
val version: String,
val changelog: String,
val downloadUrl: String? = null
)
private suspend fun fetchUpdateInfo(repo: String, isDebug: Boolean): Pair<String, String>? {
return try {
fetchFromGithub(repo, isDebug)
} catch (e: Exception) {
Logger.log("Github fetch failed, trying fallback: ${e.message}")
try {
fetchFromFallback(isDebug)
} catch (e: Exception) {
Logger.log("Fallback fetch failed: ${e.message}")
null
}
}
}
private suspend fun fetchFromGithub(repo: String, isDebug: Boolean): Pair<String, String> {
return if (isDebug) {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
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 {
val res = client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
res to res.substringAfter("# ").substringBefore("\n")
}
}
private suspend fun fetchFromFallback(isDebug: Boolean): Pair<String, String> {
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
val response = CommentsAPI.requestBuilder().get(url).parsed<FallbackResponse>()
return response.changelog to response.version
}
private suspend fun fetchApkUrl(repo: String, version: String, isDebug: Boolean): String? {
return try {
fetchApkUrlFromGithub(repo, version)
} catch (e: Exception) {
Logger.log("Github APK fetch failed, trying fallback: ${e.message}")
try {
fetchApkUrlFromFallback(version, isDebug)
} catch (e: Exception) {
Logger.log("Fallback APK fetch failed: ${e.message}")
null
}
}
}
private suspend fun fetchApkUrlFromGithub(repo: String, version: String): String? {
val apks = client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(".apk")
}
return apks?.firstOrNull()?.browserDownloadURL
}
private suspend fun fetchApkUrlFromFallback(version: String, isDebug: Boolean): String? {
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
return CommentsAPI.requestBuilder().get("$url/$version").parsed<FallbackResponse>().downloadUrl
}
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
if (post) snackString(currContext()?.getString(R.string.checking_for_update))
val repo = activity.getString(R.string.repo)
tryWithSuspend {
val (md, version) = fetchUpdateInfo(repo, BuildConfig.DEBUG) ?: return@tryWithSuspend
val (md, version) = if (BuildConfig.DEBUG) {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
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 {
val res =
client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
res to res.substringAfter("# ").substringBefore("\n")
}
Logger.log("Git Version : $version")
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
@@ -133,11 +69,7 @@ object AppUpdater {
)
addView(
TextView(activity).apply {
val markWon = try {
buildMarkwon(activity, false)
} catch (e: IllegalArgumentException) {
return@runOnUiThread
}
val markWon = buildMarkwon(activity, false)
markWon.setMarkdown(this, md)
}
)
@@ -153,11 +85,17 @@ object AppUpdater {
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
MainScope().launch(Dispatchers.IO) {
try {
val apkUrl = fetchApkUrl(repo, version, BuildConfig.DEBUG)
if (apkUrl != null) {
activity.downloadUpdate(version, apkUrl)
} 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)
@@ -170,7 +108,8 @@ object AppUpdater {
}
show(activity.supportFragmentManager, "dialog")
}
} else {
}
else {
if (post) snackString(currContext()?.getString(R.string.no_update_found))
}
}
@@ -201,7 +140,8 @@ object AppUpdater {
//Blatantly kanged from https://github.com/LagradOst/CloudStream-3/blob/master/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
private fun Activity.downloadUpdate(version: String, url: String) {
private fun Activity.downloadUpdate(version: String, url: String): Boolean {
toast(getString(R.string.downloading_update, version))
val downloadManager = this.getSystemService<DownloadManager>()!!
@@ -223,7 +163,7 @@ object AppUpdater {
logError(e)
-1
}
if (id == -1L) return
if (id == -1L) return true
ContextCompat.registerReceiver(
this,
object : BroadcastReceiver() {
@@ -244,6 +184,7 @@ object AppUpdater {
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
return true
}
private fun openApk(context: Context, uri: Uri) {

View File

@@ -1,4 +1,4 @@
`<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
@@ -19,7 +19,6 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
@@ -113,9 +112,10 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip"/>
<data android:mimeType="application/epub+zip" />
<data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" />
@@ -129,22 +129,16 @@
<data android:scheme="file" />
</intent-filter>
</activity>
<activity android:name=".others.calc.CalcActivity"
android:parentActivityName=".MainActivity" />
<activity android:name=".settings.AnilistSettingsActivity"/>
<activity android:name=".settings.FAQActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.UserInterfaceSettingsActivity" />
<activity android:name=".settings.PlayerSettingsActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.FAQActivity" />
<activity
android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAboutActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".home.status.StatusActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAccountActivity"
android:parentActivityName=".MainActivity" />
@@ -156,8 +150,7 @@
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsExtensionsActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustPan"/>
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAddonActivity"
android:parentActivityName=".MainActivity" />
@@ -196,22 +189,12 @@
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.notification.NotificationActivity"
android:name=".profile.activity.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".util.ActivityMarkdownCreator"
android:windowSoftInputMode="adjustResize|stateVisible" />
<activity android:name=".parsers.ParserTestActivity" />
<activity
android:name=".media.ReviewActivity"
android:parentActivityName=".media.MediaDetailsActivity" />
<activity
android:name=".media.ReviewViewActivity"
android:parentActivityName=".media.ReviewActivity" />
<activity
android:name=".media.SearchActivity"
android:parentActivityName=".MainActivity" />
@@ -231,9 +214,6 @@
android:label="@string/manga"
android:launchMode="singleTask" />
<activity android:name=".media.GenreActivity" />
<activity
android:name=".media.MediaListViewActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".media.MediaDetailsActivity"
android:parentActivityName=".MainActivity"
@@ -241,11 +221,6 @@
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" />
<activity android:name=".others.CrashActivity"
android:excludeFromRecents="true"
android:exported="true"
android:process=":error_process"
android:launchMode="singleTask" />
<activity
android:name=".media.anime.ExoplayerView"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
@@ -373,35 +348,30 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.Main" />
<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:scheme="file" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="add-repo"/>
<data android:scheme="tachiyomi"/>
<data android:scheme="aniyomi"/>
<data android:scheme="novelyomi"/>
<data android:host="*" />
</intent-filter>
</activity>
<activity
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat" />
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver
android:name=".notifications.AlarmPermissionStateReceiver"
@@ -465,7 +435,7 @@
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".addons.torrent.TorrentServerService"
android:name=".addons.torrent.ServerService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="true" />

View File

@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication
import ani.dantotsu.addons.download.DownloadAddonManager
@@ -29,16 +28,13 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
@@ -61,22 +57,15 @@ class App : MultiDexApplication() {
val mFTActivityLifecycleCallbacks = FTActivityLifecycleCallbacks()
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate() {
super.onCreate()
PrefManager.init(this)
val crashlytics =
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
Injekt.addSingletonFactory<CrashlyticsInterface> { crashlytics }
crashlytics.initialize(this)
Logger.init(this)
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log(Log.WARN, "App: Logging started")
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
val crashlytics = Injekt.get<CrashlyticsInterface>()
crashlytics.initialize(this)
val useMaterialYou: Boolean = PrefManager.getVal(PrefName.UseMaterialYou)
if (useMaterialYou) {
@@ -98,6 +87,10 @@ class App : MultiDexApplication() {
}
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Logger.init(this)
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log("App: Logging started")
initializeNetwork()
setupNotificationChannels()
@@ -105,50 +98,44 @@ class App : MultiDexApplication() {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 0) {
if (BuildConfig.FLAVOR.contains("fdroid")) {
PrefManager.setVal(PrefName.CommentsEnabled, 2)
} else {
PrefManager.setVal(PrefName.CommentsEnabled, 1)
}
}
animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get()
novelExtensionManager = Injekt.get()
torrentAddonManager = Injekt.get()
downloadAddonManager = Injekt.get()
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager = Injekt.get()
val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch {
animeExtensionManager.findAvailableExtensions()
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager = Injekt.get()
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
CoroutineScope(Dispatchers.IO).launch {
novelExtensionManager = Injekt.get()
val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch {
novelExtensionManager.findAvailableExtensions()
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}
GlobalScope.launch {
torrentAddonManager = Injekt.get()
downloadAddonManager = Injekt.get()
val addonScope = CoroutineScope(Dispatchers.Default)
addonScope.launch {
torrentAddonManager.init()
downloadAddonManager.init()
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
CommentsAPI.fetchAuthToken(this@App)
}
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
val scheduler = TaskScheduler.create(this@App, useAlarmManager)
try {
scheduler.scheduleAllTasks(this@App)
} catch (e: IllegalStateException) {
Logger.log("Failed to schedule tasks")
Logger.log(e)
}
}
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() {
@@ -162,11 +149,7 @@ class App : MultiDexApplication() {
inner class FTActivityLifecycleCallbacks : ActivityLifecycleCallbacks {
var currentActivity: Activity? = null
var lastActivity: String? = null
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
lastActivity = p0.javaClass.simpleName
}
override fun onActivityCreated(p0: Activity, p1: Bundle?) {}
override fun onActivityStarted(p0: Activity) {
currentActivity = p0
}
@@ -182,7 +165,7 @@ class App : MultiDexApplication() {
}
companion object {
var instance: App? = null
private var instance: App? = null
/** Reference to the application context.
*

View File

@@ -67,11 +67,9 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp
@@ -83,7 +81,6 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
@@ -91,20 +88,18 @@ 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.media.MediaDetailsActivity
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.AlignTagHandler
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.profile.ProfileActivity
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.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
@@ -137,9 +132,12 @@ 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
@@ -148,13 +146,10 @@ import java.io.FileOutputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import java.util.Timer
import java.util.TimerTask
import kotlin.collections.set
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.log2
import kotlin.math.max
import kotlin.math.min
@@ -311,7 +306,6 @@ fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
initActivity(this)
}
@@ -345,8 +339,14 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
window.navigationBarColor =
requireContext().getThemeColor(com.google.android.material.R.attr.colorSurface)
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValue,
true
)
window.navigationBarColor = typedValue.data
}
}
@@ -636,24 +636,6 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
}
}
fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(width, height).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade())
.override(width, height)
.into(this)
}
}
}
}
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
if (file?.exists() == true) {
tryWith {
@@ -853,7 +835,6 @@ fun savePrefsToDownloads(
)
}
@SuppressLint("StringFormatMatches")
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
var file = File(path, "$title.ani")
var counter = 1
@@ -873,7 +854,6 @@ fun savePrefs(serialized: String, path: String, title: String, context: Context)
}
}
@SuppressLint("StringFormatMatches")
fun savePrefs(
serialized: String,
path: String,
@@ -921,7 +901,6 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
context.startActivity(Intent.createChooser(intent, "Share $title"))
}
@SuppressLint("StringFormatMatches")
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png")
return try {
@@ -1011,10 +990,47 @@ 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)
else -> {}
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> {} // No timer yet
}
}
@@ -1329,10 +1345,10 @@ 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()
val context = imageView.context
if (!(context as Activity).isDestroyed) {
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
if (PrefManager.getVal(PrefName.BlurBanners)) {
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(
@@ -1342,82 +1358,15 @@ fun blurImage(imageView: ImageView, banner: String?) {
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView)
} else {
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)
.into(imageView)
}
} else {
imageView.loadImage(banner)
}
} else {
imageView.setImageResource(R.drawable.linear_gradient_bg)
}
}
fun Context.getThemeColor(@AttrRes attribute: Int): Int {
val typedValue = TypedValue()
theme.resolveAttribute(attribute, typedValue, true)
return typedValue.data
}
fun ImageView.openImage(title: String, image: String) {
setOnLongClickListener {
ImageViewDialog.newInstance(
context as FragmentActivity, title, image
)
}
}
/**
* Attempts to open the link in the app, otherwise copies it to the clipboard
* @param link the link to open
*/
fun openOrCopyAnilistLink(link: String) {
if (link.startsWith("https://anilist.co/anime/") || link.startsWith("https://anilist.co/manga/")) {
val mangaAnime = link.substringAfter("https://anilist.co/").substringBefore("/")
val id =
link.substringAfter("https://anilist.co/$mangaAnime/").substringBefore("/")
.toIntOrNull()
if (id != null && currContext() != null) {
ContextCompat.startActivity(
currContext()!!,
Intent(currContext()!!, MediaDetailsActivity::class.java)
.putExtra("mediaId", id),
null
)
} else {
copyToClipboard(link, true)
}
} else if (link.startsWith("https://anilist.co/user/")) {
val username = link.substringAfter("https://anilist.co/user/").substringBefore("/")
val id = username.toIntOrNull()
if (currContext() != null) {
val intent = Intent(currContext()!!, ProfileActivity::class.java)
if (id != null) {
intent.putExtra("userId", id)
} else {
intent.putExtra("username", username)
}
ContextCompat.startActivity(
currContext()!!,
intent,
null
)
} else {
copyToClipboard(link, true)
}
} else if (getYoutubeId(link).isNotEmpty()) {
openLinkInYouTube(link)
} else {
copyToClipboard(link, true)
}
}
/**
* Builds the markwon instance with all the plugins
* @return the markwon instance
@@ -1425,15 +1374,14 @@ fun openOrCopyAnilistLink(link: String) {
fun buildMarkwon(
activity: Context,
userInputContent: Boolean = true,
fragment: Fragment? = null,
anilist: Boolean = false
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 ->
openOrCopyAnilistLink(link)
copyToClipboard(link, true)
}
}
})
@@ -1442,14 +1390,13 @@ fun buildMarkwon(
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(activity))
.usePlugin(TaskListPlugin.create(activity))
.usePlugin(SpoilerPlugin(anilist))
.usePlugin(SpoilerPlugin())
.usePlugin(HtmlPlugin.create { plugin ->
if (userInputContent) {
plugin.addHandler(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
)
}
plugin.addHandler(AlignTagHandler())
})
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
@@ -1494,44 +1441,3 @@ fun buildMarkwon(
.build()
return markwon
}
fun getYoutubeId(url: String): String {
val regex =
"""(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|(?:youtu\.be|youtube\.com)/)([^"&?/\s]{11})|youtube\.com/""".toRegex()
val matchResult = regex.find(url)
return matchResult?.groupValues?.getOrNull(1) ?: ""
}
fun getLanguageCode(language: String): CharSequence {
val locales = Locale.getAvailableLocales()
for (locale in locales) {
if (locale.displayLanguage.equals(language, ignoreCase = true)) {
val lang: CharSequence = locale.language
return lang
}
}
val out: CharSequence = "null"
return out
}
fun getLanguageName(language: String): String? {
val locales = Locale.getAvailableLocales()
for (locale in locales) {
if (locale.language.equals(language, ignoreCase = true)) {
return locale.displayLanguage
}
}
return null
}
@OptIn(ExperimentalEncodingApi::class)
fun String.decodeBase64ToString(): String {
return try {
String(Base64.decode(this), Charsets.UTF_8)
} catch (e: Exception) {
Logger.log(e)
""
}
}

View File

@@ -2,6 +2,7 @@ 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
@@ -12,6 +13,7 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
@@ -32,12 +34,12 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.work.OneTimeWorkRequest
import ani.dantotsu.addons.torrent.ServerService
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.addons.torrent.TorrentServerService
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
@@ -45,13 +47,12 @@ import ani.dantotsu.home.LoginFragment
import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.calc.CalcActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.notification.NotificationActivity
import ani.dantotsu.settings.AddRepositoryBottomSheet
import ani.dantotsu.profile.activity.NotificationActivity
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
@@ -60,11 +61,10 @@ import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.AudioHelper
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
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
@@ -101,24 +101,65 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
TaskScheduler.scheduleSingleWork(this)
if (!CalcActivity.hasPermission) {
val pin: String = PrefManager.getVal(PrefName.AppPassword)
if (pin.isNotEmpty()) {
ContextCompat.startActivity(
this@MainActivity,
Intent(this@MainActivity, CalcActivity::class.java)
.putExtra("code", pin)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
null
)
finish()
return
}
}
if (Intent.ACTION_VIEW == intent.action) {
handleViewIntent(intent)
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)
@@ -238,7 +279,7 @@ class MainActivity : AppCompatActivity() {
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_SHORT)
?.setDuration(Snackbar.LENGTH_LONG)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
@@ -254,37 +295,6 @@ class MainActivity : AppCompatActivity() {
} else {
PrefManager.getVal(PrefName.DefaultStartUpTab)
}
val navbar = binding.includedNavbar.navbar
bottomBar = navbar
navbar.visibility = View.VISIBLE
binding.mainProgressBar.visibility = View.GONE
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
navbar.selectTabAt(selectedOption)
navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
navbar.animate().translationZ(12f).setDuration(200).start()
selectedOption = newIndex
mainViewPager.setCurrentItem(newIndex, false)
}
})
if (mainViewPager.currentItem != selectedOption) {
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
@@ -316,6 +326,7 @@ class MainActivity : AppCompatActivity() {
} 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
@@ -332,7 +343,45 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, NoInternet::class.java))
} else {
val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) {
if (it != null) {
if (it) {
val navbar = binding.includedNavbar.navbar
bottomBar = navbar
navbar.visibility = View.VISIBLE
binding.mainProgressBar.visibility = View.GONE
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
navbar.animate().translationZ(12f).setDuration(200).start()
selectedOption = newIndex
mainViewPager.setCurrentItem(newIndex, false)
}
})
if (mainViewPager.currentItem != selectedOption) {
navbar.selectTabAt(selectedOption)
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
}
} else {
binding.mainProgressBar.visibility = View.GONE
}
}
}
//Load Data
if (!load && !launched) {
scope.launch(Dispatchers.IO) {
@@ -405,16 +454,13 @@ class MainActivity : AppCompatActivity() {
}
}
}
if (PrefManager.getVal(PrefName.OC)) {
AudioHelper.run(this, R.raw.audio)
PrefManager.setVal(PrefName.OC, false)
}
val torrentManager = Injekt.get<TorrentAddonManager>()
fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
launchIO {
if (!TorrentServerService.isRunning()) {
TorrentServerService.start()
if (!ServerService.isRunning()) {
ServerService.start()
}
}
}
@@ -443,102 +489,41 @@ class MainActivity : AppCompatActivity() {
params.updateMargins(bottom = margin.toPx)
}
private fun handleViewIntent(intent: Intent) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi" || uri.scheme == "novelyomi") && uri.host == "add-repo") {
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
val (prefName, name) = when (uri.scheme) {
"tachiyomi" -> PrefName.MangaExtensionRepos to "Manga"
"aniyomi" -> PrefName.AnimeExtensionRepos to "Anime"
"novelyomi" -> PrefName.NovelExtensionRepos to "Novel"
else -> throw Exception("Invalid scheme")
}
val savedRepos: Set<String> = PrefManager.getVal(prefName)
val newRepos = savedRepos.toMutableSet()
AddRepositoryBottomSheet.addRepoWarning(this) {
newRepos.add(url)
PrefManager.setVal(prefName, newRepos)
toast("$name Extension Repo added")
}
return
}
if (intent.type == null) return
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 newIntent = Intent(this, this.javaClass)
this.finish()
startActivity(newIntent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val newIntent = Intent(this, this.javaClass)
this.finish()
startActivity(newIntent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
userAgentTextBox.hint = "Password"
subtitle.visibility = View.VISIBLE
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
}
customAlertDialog().apply {
setTitle("Enter Password")
setCustomView(dialogView.root)
setPosButton(R.string.yes) {
val editText = dialogView.userAgentTextBox
if (editText.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
callback(password)
} else {
toast("Password cannot be empty")
}
}
setNegButton(R.string.cancel) {
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)
}
show()
.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")
}
}
}

View File

@@ -137,14 +137,12 @@ suspend fun <T> tryWithSuspend(
* **/
data class FileUrl(
var url: String,
var headers: Map<String, String> = mapOf()
val headers: Map<String, String> = mapOf()
) : Serializable {
companion object {
operator fun get(url: String?, headers: Map<String, String> = mapOf()): FileUrl? {
return FileUrl(url ?: return null, headers)
}
private const val serialVersionUID = 1L
}
}

View File

@@ -35,7 +35,7 @@ class AddonDownloader {
val md = r.body ?: ""
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
Logger.log("Git Version for $repo: $version")
Logger.log("Git Version : $version")
Pair(md, version)
} catch (e: Exception) {
Logger.log("Error checking for update")
@@ -79,6 +79,7 @@ class AddonDownloader {
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) {} },

View File

@@ -6,7 +6,7 @@ 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.DownloadAddonApiV2
import ani.dantotsu.addons.download.DownloadAddonApi
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.download.DownloadLoadResult
import ani.dantotsu.addons.torrent.TorrentAddon
@@ -21,20 +21,6 @@ import eu.kanade.tachiyomi.util.system.getApplicationIcon
class AddonLoader {
companion object {
/**
* Load an extension from a package name with a specific class name
* @param context the context
* @param packageName the package name of the extension
* @param type the type of extension
* @return the loaded extension
* @throws IllegalStateException if the extension is not of the correct type
* @throws ClassNotFoundException if the extension class is not found
* @throws NoClassDefFoundError if the extension class is not found
* @throws Exception if any other error occurs
* @throws PackageManager.NameNotFoundException if the package is not found
* @throws IllegalStateException if the extension is not found
*/
fun loadExtension(
context: Context,
packageName: String,
@@ -84,11 +70,11 @@ class AddonLoader {
val loadedClass = try {
Class.forName(className, false, classLoader)
} catch (e: ClassNotFoundException) {
Logger.log("ClassNotFoundException load error: $extName ($className)")
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: NoClassDefFoundError) {
Logger.log("NoClassDefFoundError load error: $extName ($className)")
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: Exception) {
@@ -115,8 +101,8 @@ class AddonLoader {
}
AddonType.DOWNLOAD -> {
val extension = instance as? DownloadAddonApiV2
?: throw IllegalStateException("Extension is not a DownloadAddonApiV2")
val extension = instance as? DownloadAddonApi
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
DownloadLoadResult.Success(
DownloadAddon.Installed(
name = extName,
@@ -131,43 +117,24 @@ class AddonLoader {
}
}
/**
* Load an extension from a package name (class is determined by type)
* @param context the context
* @param packageName the package name of the extension
* @param type the type of extension
* @return the loaded extension
*/
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
return try {
when (type) {
AddonType.TORRENT -> loadExtension(
context,
packageName,
TorrentAddonManager.TORRENT_CLASS,
type
)
return when (type) {
AddonType.TORRENT -> loadExtension(
context,
packageName,
TorrentAddonManager.TORRENT_CLASS,
type
)
AddonType.DOWNLOAD -> loadExtension(
context,
packageName,
DownloadAddonManager.DOWNLOAD_CLASS,
type
)
}
} catch (e: Exception) {
Logger.log("Error loading extension from package name: $packageName")
Logger.log(e)
null
AddonType.DOWNLOAD -> loadExtension(
context,
packageName,
DownloadAddonManager.DOWNLOAD_CLASS,
type
)
}
}
/**
* Check if a package is an extension by comparing the package name
* @param type the type of extension
* @param pkgInfo the package info
* @return true if the package is an extension
*/
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
return pkgInfo.packageName.equals(type)
}

View File

@@ -19,7 +19,7 @@ abstract class AddonManager<T : Addon.Installed>(
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
abstract suspend fun init()
abstract fun isAvailable(andEnabled: Boolean = true): Boolean
abstract fun isAvailable(): Boolean
abstract fun getVersion(): String?
abstract fun getPackageName(): String?
abstract fun hadError(context: Context): String?

View File

@@ -1,10 +1,11 @@
package ani.dantotsu.addons
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.download.DownloadAddonManager
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
@@ -75,6 +76,7 @@ internal class AddonInstallReceiver : BroadcastReceiver() {
}
Intent.ACTION_PACKAGE_REPLACED -> {
if (ExtensionInstallReceiver.isReplacing(intent)) return
launchNow {
when (type) {
AddonType.DOWNLOAD -> {

View File

@@ -10,7 +10,7 @@ sealed class DownloadAddon : Addon() {
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
val extension: DownloadAddonApiV2,
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

@@ -1,48 +0,0 @@
package ani.dantotsu.addons.download
import android.content.Context
import android.net.Uri
interface DownloadAddonApiV2 {
fun cancelDownload(sessionId: Long)
fun setDownloadPath(context: Context, uri: Uri): String
fun getReadPath(context: Context, uri: Uri): String
suspend fun executeFFProbe(
videoUrl: String,
headers: Map<String, String> = emptyMap(),
logCallback: (String) -> Unit
)
suspend fun executeFFMpeg(
videoUrl: String,
downloadPath: String,
headers: Map<String, String> = emptyMap(),
subtitleUrls: List<Pair<String, String>> = emptyList(),
audioUrls: List<Pair<String, String>> = emptyList(),
statCallback: (Double) -> Unit
): Long
suspend fun customFFMpeg(
command: String,
videoUrls: List<String>,
logCallback: (String) -> Unit
): Long
suspend fun customFFProbe(
command: String,
videoUrls: List<String>,
logCallback: (String) -> Unit
)
fun getState(sessionId: Long): String
fun getStackTrace(sessionId: Long): String?
fun hadError(sessionId: Long): Boolean
fun getFileExtension(): Pair<String, String> = Pair("mkv", "video/x-matroska")
}

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import ani.dantotsu.R
import ani.dantotsu.addons.AddonDownloader
import ani.dantotsu.addons.AddonInstallReceiver
import ani.dantotsu.addons.AddonListener
import ani.dantotsu.addons.AddonLoader
import ani.dantotsu.addons.AddonManager
@@ -24,7 +23,7 @@ class DownloadAddonManager(
override var name: String = "Download Addon"
override var type = AddonType.DOWNLOAD
private val _isInitialized = MutableLiveData(false)
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
val isInitialized: LiveData<Boolean> = _isInitialized
private var error: String? = null
@@ -53,18 +52,17 @@ class DownloadAddonManager(
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
}
}
Logger.log("Download addon initialized successfully")
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing Download addon")
Logger.log("Error initializing Download extension")
Logger.log(e)
error = e.message
}
}
override fun isAvailable(andEnabled: Boolean): Boolean {
override fun isAvailable(): Boolean {
return extension?.extension != null
}

View File

@@ -6,14 +6,12 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import ani.dantotsu.R
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
import ani.dantotsu.addons.AddonInstallReceiver
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.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep
import kotlinx.coroutines.Dispatchers
@@ -27,7 +25,7 @@ class TorrentAddonManager(
override var type: AddonType = AddonType.TORRENT
var torrentHash: String? = null
private val _isInitialized = MutableLiveData(false)
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
val isInitialized: LiveData<Boolean> = _isInitialized
private var error: String? = null
@@ -61,21 +59,18 @@ class TorrentAddonManager(
hasUpdate = hasUpdate(REPO, it.extension.versionName)
}
}
Logger.log("Torrent addon initialized successfully")
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing torrent addon")
Logger.log("Error initializing torrent extension")
Logger.log(e)
error = e.message
}
}
override fun isAvailable(andEnabled: Boolean): Boolean {
return extension?.extension != null && if (andEnabled) {
PrefManager.getVal(PrefName.TorrentEnabled)
} else true
override fun isAvailable(): Boolean {
return extension?.extension != null
}
override fun getVersion(): String? {

View File

@@ -1,10 +1,3 @@
/**
* modified source from
* https://github.com/rebelonion/Dantotsu/pull/305
* and https://github.com/LuftVerbot/kuukiyomi
* all credits to the original authors
*/
package ani.dantotsu.addons.torrent
import android.app.ActivityManager
@@ -29,10 +22,10 @@ import uy.kohesive.injekt.api.get
import kotlin.coroutines.EmptyCoroutineContext
class TorrentServerService : Service() {
class ServerService : Service() {
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
private val applicationContext = Injekt.get<Application>()
private lateinit var extension: TorrentAddonApi
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
override fun onBind(intent: Intent?): IBinder? = null
@@ -41,8 +34,6 @@ class TorrentServerService : Service() {
flags: Int,
startId: Int,
): Int {
extension =
Injekt.get<TorrentAddonManager>().extension?.extension ?: return START_NOT_STICKY
intent?.let {
if (it.action != null) {
when (it.action) {
@@ -84,7 +75,7 @@ class TorrentServerService : Service() {
PendingIntent.getService(
applicationContext,
0,
Intent(applicationContext, TorrentServerService::class.java).apply {
Intent(applicationContext, ServerService::class.java).apply {
action = ACTION_STOP
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
@@ -122,7 +113,7 @@ class TorrentServerService : Service() {
with(Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
@Suppress("DEPRECATION") // We only need our services
getRunningServices(Int.MAX_VALUE).forEach {
if (TorrentServerService::class.java.name.equals(it.service.className)) {
if (ServerService::class.java.name.equals(it.service.className)) {
return true
}
}
@@ -131,12 +122,9 @@ class TorrentServerService : Service() {
}
fun start() {
if (Injekt.get<TorrentAddonManager>().extension?.extension == null) {
return
}
try {
val intent =
Intent(Injekt.get<Application>(), TorrentServerService::class.java).apply {
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
action = ACTION_START
}
Injekt.get<Application>().startService(intent)
@@ -149,7 +137,7 @@ class TorrentServerService : Service() {
fun stop() {
try {
val intent =
Intent(Injekt.get<Application>(), TorrentServerService::class.java).apply {
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
action = ACTION_STOP
}
Injekt.get<Application>().startService(intent)

View File

@@ -8,6 +8,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.parsers.novel.NovelExtensionManager
@@ -59,6 +60,10 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { StandaloneDatabaseProvider(app) }
addSingletonFactory<CrashlyticsInterface> {
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
}
addSingletonFactory { MangaCache() }
ContextCompat.getMainExecutor(app).execute {

View File

@@ -15,8 +15,6 @@ import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import java.util.Calendar
import java.util.Locale
import kotlin.math.abs
object Anilist {
val query: AnilistQueries = AnilistQueries()
@@ -24,7 +22,7 @@ object Anilist {
var token: String? = null
var username: String? = null
var adult: Boolean = false
var userid: Int? = null
var avatar: String? = null
var bg: String? = null
@@ -37,19 +35,6 @@ object Anilist {
var rateLimitReset: Long = 0
var initialized = false
var adult: Boolean = false
var titleLanguage: String? = null
var staffNameLanguage: String? = null
var airingNotifications: Boolean = false
var restrictMessagesToFollowing: Boolean = false
var scoreFormat: String? = null
var rowOrder: String? = null
var activityMergeTime: Int? = null
var timezone: String? = null
var animeCustomLists: List<String>? = null
var mangaCustomLists: List<String>? = null
val sortBy = listOf(
"SCORE_DESC",
"POPULARITY_DESC",
@@ -109,86 +94,6 @@ object Anilist {
"Original Creator", "Story & Art", "Story"
)
val timeZone = listOf(
"(GMT-11:00) Pago Pago",
"(GMT-10:00) Hawaii Time",
"(GMT-09:00) Alaska Time",
"(GMT-08:00) Pacific Time",
"(GMT-07:00) Mountain Time",
"(GMT-06:00) Central Time",
"(GMT-05:00) Eastern Time",
"(GMT-04:00) Atlantic Time - Halifax",
"(GMT-03:00) Sao Paulo",
"(GMT-02:00) Mid-Atlantic",
"(GMT-01:00) Azores",
"(GMT+00:00) London",
"(GMT+01:00) Berlin",
"(GMT+02:00) Helsinki",
"(GMT+03:00) Istanbul",
"(GMT+04:00) Dubai",
"(GMT+04:30) Kabul",
"(GMT+05:00) Maldives",
"(GMT+05:30) India Standard Time",
"(GMT+05:45) Kathmandu",
"(GMT+06:00) Dhaka",
"(GMT+06:30) Cocos",
"(GMT+07:00) Bangkok",
"(GMT+08:00) Hong Kong",
"(GMT+08:30) Pyongyang",
"(GMT+09:00) Tokyo",
"(GMT+09:30) Central Time - Darwin",
"(GMT+10:00) Eastern Time - Brisbane",
"(GMT+10:30) Central Time - Adelaide",
"(GMT+11:00) Eastern Time - Melbourne, Sydney",
"(GMT+12:00) Nauru",
"(GMT+13:00) Auckland",
"(GMT+14:00) Kiritimati",
)
val titleLang = listOf(
"English (Attack on Titan)",
"Romaji (Shingeki no Kyojin)",
"Native (進撃の巨人)"
)
val staffNameLang = listOf(
"Romaji, Western Order (Killua Zoldyck)",
"Romaji (Zoldyck Killua)",
"Native (キルア=ゾルディック)"
)
val scoreFormats = listOf(
"100 Point (55/100)",
"10 Point Decimal (5.5/10)",
"10 Point (5/10)",
"5 Star (3/5)",
"3 Point Smiley :)"
)
val rowOrderMap = mapOf(
"Score" to "score",
"Title" to "title",
"Last Updated" to "updatedAt",
"Last Added" to "id"
)
val activityMergeTimeMap = mapOf(
"Never" to 0,
"30 mins" to 30,
"69 mins" to 69,
"1 hour" to 60,
"2 hours" to 120,
"3 hours" to 180,
"6 hours" to 360,
"12 hours" to 720,
"1 day" to 1440,
"2 days" to 2880,
"3 days" to 4320,
"1 week" to 10080,
"2 weeks" to 20160,
"Always" to 29160
)
private val cal: Calendar = Calendar.getInstance()
private val currentYear = cal.get(Calendar.YEAR)
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
@@ -199,33 +104,6 @@ object Anilist {
else -> 0
}
fun getDisplayTimezone(apiTimezone: String, context: Context): String {
val noTimezone = context.getString(R.string.selected_no_time_zone)
val parts = apiTimezone.split(":")
if (parts.size != 2) return noTimezone
val hours = parts[0].toIntOrNull() ?: 0
val minutes = parts[1].toIntOrNull() ?: 0
val sign = if (hours >= 0) "+" else "-"
val formattedHours = String.format(Locale.US, "%02d", abs(hours))
val formattedMinutes = String.format(Locale.US, "%02d", minutes)
val searchString = "(GMT$sign$formattedHours:$formattedMinutes)"
return timeZone.find { it.contains(searchString) } ?: noTimezone
}
fun getApiTimezone(displayTimezone: String): String {
val regex = """\(GMT([+-])(\d{2}):(\d{2})\)""".toRegex()
val matchResult = regex.find(displayTimezone)
return if (matchResult != null) {
val (sign, hours, minutes) = matchResult.destructured
val formattedSign = if (sign == "+") "" else "-"
"$formattedSign$hours:$minutes"
} else {
"00:00"
}
}
private fun getSeason(next: Boolean): Pair<String, Int> {
var newSeason = if (next) currentSeason + 1 else currentSeason - 1
var newYear = currentYear
@@ -296,7 +174,7 @@ object Anilist {
"variables" to variables
)
val headers = mutableMapOf(
"Content-Type" to "application/json; charset=utf-8",
"Content-Type" to "application/json",
"Accept" to "application/json"
)
@@ -324,6 +202,7 @@ object Anilist {
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) {

View File

@@ -2,100 +2,13 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext
import com.google.gson.Gson
import kotlinx.serialization.json.JsonObject
class AnilistMutations {
suspend fun updateSettings(
timezone: String? = null,
titleLanguage: String? = null,
staffNameLanguage: String? = null,
activityMergeTime: Int? = null,
airingNotifications: Boolean? = null,
displayAdultContent: Boolean? = null,
restrictMessagesToFollowing: Boolean? = null,
scoreFormat: String? = null,
rowOrder: String? = null,
) {
val query = """
mutation (
${"$"}timezone: String,
${"$"}titleLanguage: UserTitleLanguage,
${"$"}staffNameLanguage: UserStaffNameLanguage,
${"$"}activityMergeTime: Int,
${"$"}airingNotifications: Boolean,
${"$"}displayAdultContent: Boolean,
${"$"}restrictMessagesToFollowing: Boolean,
${"$"}scoreFormat: ScoreFormat,
${"$"}rowOrder: String
) {
UpdateUser(
timezone: ${"$"}timezone,
titleLanguage: ${"$"}titleLanguage,
staffNameLanguage: ${"$"}staffNameLanguage,
activityMergeTime: ${"$"}activityMergeTime,
airingNotifications: ${"$"}airingNotifications,
displayAdultContent: ${"$"}displayAdultContent,
restrictMessagesToFollowing: ${"$"}restrictMessagesToFollowing,
scoreFormat: ${"$"}scoreFormat,
rowOrder: ${"$"}rowOrder,
) {
id
options {
timezone
titleLanguage
staffNameLanguage
activityMergeTime
airingNotifications
displayAdultContent
restrictMessagesToFollowing
}
mediaListOptions {
scoreFormat
rowOrder
}
}
}
""".trimIndent()
val variables = """
{
${timezone?.let { """"timezone":"$it"""" } ?: ""}
${titleLanguage?.let { """"titleLanguage":"$it"""" } ?: ""}
${staffNameLanguage?.let { """"staffNameLanguage":"$it"""" } ?: ""}
${activityMergeTime?.let { """"activityMergeTime":$it""" } ?: ""}
${airingNotifications?.let { """"airingNotifications":$it""" } ?: ""}
${displayAdultContent?.let { """"displayAdultContent":$it""" } ?: ""}
${restrictMessagesToFollowing?.let { """"restrictMessagesToFollowing":$it""" } ?: ""}
${scoreFormat?.let { """"scoreFormat":"$it"""" } ?: ""}
${rowOrder?.let { """"rowOrder":"$it"""" } ?: ""}
}
""".trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}")
executeQuery<JsonObject>(query, variables)
}
suspend fun toggleFav(anime: Boolean = true, id: Int) {
val query = """
mutation (${"$"}animeId: Int, ${"$"}mangaId: Int) {
ToggleFavourite(animeId: ${"$"}animeId, mangaId: ${"$"}mangaId) {
anime {
edges {
id
}
}
manga {
edges {
id
}
}
}
}
""".trimIndent()
val query =
"""mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }"""
val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}"""
executeQuery<JsonObject>(query, variables)
}
@@ -108,17 +21,7 @@ class AnilistMutations {
FavType.STAFF -> "staffId"
FavType.STUDIO -> "studioId"
}
val query = """
mutation {
ToggleFavourite($filter: $id) {
anime {
pageInfo {
total
}
}
}
}
""".trimIndent()
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
val result = executeQuery<JsonObject>(query)
return result?.get("errors") == null && result != null
}
@@ -127,54 +30,6 @@ class AnilistMutations {
ANIME, MANGA, CHARACTER, STAFF, STUDIO
}
suspend fun deleteCustomList(name: String, type: String): Boolean {
val query = """
mutation (${"$"}name: String, ${"$"}type: MediaType) {
DeleteCustomList(customList: ${"$"}name, type: ${"$"}type) {
deleted
}
}
""".trimIndent()
val variables = """
{
"name": "$name",
"type": "$type"
}
""".trimIndent()
val result = executeQuery<JsonObject>(query, variables)
return result?.get("errors") == null
}
suspend fun updateCustomLists(
animeCustomLists: List<String>?,
mangaCustomLists: List<String>?
): Boolean {
val query = """
mutation (${"$"}animeListOptions: MediaListOptionsInput, ${"$"}mangaListOptions: MediaListOptionsInput) {
UpdateUser(animeListOptions: ${"$"}animeListOptions, mangaListOptions: ${"$"}mangaListOptions) {
mediaListOptions {
animeList {
customLists
}
mangaList {
customLists
}
}
}
}
""".trimIndent()
val variables = """
{
${animeCustomLists?.let { """"animeListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""}
${if (animeCustomLists != null && mangaCustomLists != null) "," else ""}
${mangaCustomLists?.let { """"mangaListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""}
}
""".trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}")
val result = executeQuery<JsonObject>(query, variables)
return result?.get("errors") == null
}
suspend fun editList(
mediaID: Int,
progress: Int? = null,
@@ -187,45 +42,14 @@ class AnilistMutations {
completedAt: FuzzyDate? = null,
customList: List<String>? = null
) {
val query = """
mutation (
${"$"}mediaID: Int,
${"$"}progress: Int,
${"$"}private: Boolean,
${"$"}repeat: Int,
${"$"}notes: String,
${"$"}customLists: [String],
${"$"}scoreRaw: Int,
${"$"}status: MediaListStatus,
${"$"}start: FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""},
${"$"}completed: FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""}
) {
SaveMediaListEntry(
mediaId: ${"$"}mediaID,
progress: ${"$"}progress,
repeat: ${"$"}repeat,
notes: ${"$"}notes,
private: ${"$"}private,
scoreRaw: ${"$"}scoreRaw,
status: ${"$"}status,
startedAt: ${"$"}start,
completedAt: ${"$"}completed,
customLists: ${"$"}customLists
) {
score(format: POINT_10_DECIMAL)
startedAt {
year
month
day
}
completedAt {
year
month
day
}
mutation ( ${"$"}mediaID: Int, ${"$"}progress: Int,${"$"}private:Boolean,${"$"}repeat: Int, ${"$"}notes: String, ${"$"}customLists: [String], ${"$"}scoreRaw:Int, ${"$"}status:MediaListStatus, ${"$"}start:FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""}, ${"$"}completed:FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""} ) {
SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) {
score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day}
}
}
""".trimIndent()
""".replace("\n", "").replace(""" """, "")
val variables = """{"mediaID":$mediaID
${if (private != null) ""","private":$private""" else ""}
@@ -241,194 +65,8 @@ class AnilistMutations {
}
suspend fun deleteList(listId: Int) {
val query = """
mutation(${"$"}id: Int) {
DeleteMediaListEntry(id: ${"$"}id) {
deleted
}
}
""".trimIndent()
val query = "mutation(${"$"}id:Int){DeleteMediaListEntry(id:${"$"}id){deleted}}"
val variables = """{"id":"$listId"}"""
executeQuery<JsonObject>(query, variables)
}
suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? {
val query = """
mutation {
RateReview(reviewId: $reviewId, rating: $rating) {
id
mediaId
mediaType
summary
body(asHtml: true)
rating
ratingAmount
userRating
score
private
siteUrl
createdAt
updatedAt
user {
id
name
bannerImage
avatar {
medium
large
}
}
}
}
""".trimIndent()
return executeQuery<Query.RateReviewResponse>(query)
}
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""
mutation {
ToggleFollow(userId: $id) {
id
isFollowing
isFollower
}
}
""".trimIndent()
)
}
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
return executeQuery<ToggleLike>(
"""
mutation Like {
ToggleLikeV2(id: $id, type: $type) {
__typename
}
}
""".trimIndent()
)
}
suspend fun postActivity(text: String, edit: Int? = null): String {
val encodedText = text.stringSanitizer()
val query = """
mutation {
SaveTextActivity(${if (edit != null) "id: $edit," else ""} text: $encodedText) {
siteUrl
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun postMessage(
userId: Int,
text: String,
edit: Int? = null,
isPrivate: Boolean = false
): String {
val encodedText = text.replace("", "").stringSanitizer()
val query = """
mutation {
SaveMessageActivity(
${if (edit != null) "id: $edit," else ""}
recipientId: $userId,
message: $encodedText,
private: $isPrivate
) {
id
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun postReply(activityId: Int, text: String, edit: Int? = null): String {
val encodedText = text.stringSanitizer()
val query = """
mutation {
SaveActivityReply(
${if (edit != null) "id: $edit," else ""}
activityId: $activityId,
text: $encodedText
) {
id
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun postReview(summary: String, body: String, mediaId: Int, score: Int): String {
val encodedSummary = summary.stringSanitizer()
val encodedBody = body.stringSanitizer()
val query = """
mutation {
SaveReview(
mediaId: $mediaId,
summary: $encodedSummary,
body: $encodedBody,
score: $score
) {
siteUrl
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun deleteActivityReply(activityId: Int): Boolean {
val query = """
mutation {
DeleteActivityReply(id: $activityId) {
deleted
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors == null
}
suspend fun deleteActivity(activityId: Int): Boolean {
val query = """
mutation {
DeleteActivity(id: $activityId) {
deleted
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors == null
}
private fun String.stringSanitizer(): String {
val sb = StringBuilder()
var i = 0
while (i < this.length) {
val codePoint = this.codePointAt(i)
if (codePoint > 0xFFFF) {
sb.append("&#").append(codePoint).append(";")
i += 2
} else {
sb.append(this[i])
i++
}
}
return Gson().toJson(sb.toString())
}
}

View File

@@ -5,34 +5,46 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.BuildConfig
import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.media.Media
import ani.dantotsu.others.AppUpdater
import ani.dantotsu.profile.User
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
import kotlinx.coroutines.withContext
suspend fun getUserId(context: Context, block: () -> Unit) {
if (!Anilist.initialized && PrefManager.getVal<String>(PrefName.AnilistToken) != "") {
CoroutineScope(Dispatchers.IO).launch {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
if (userid == null && token != null) {
/*if (!Discord.getUserData())
snackString(context.getString(R.string.error_loading_discord_user_data))*/
//TODO: Discord.getUserData()
}
}
val anilist = if (Anilist.userid == null && Anilist.token != null) {
if (Anilist.query.getUserData()) {
tryWithSuspend {
if (MAL.token != null && !MAL.query.getUserData())
snackString(context.getString(R.string.error_loading_mal_user_data))
}
true
} else {
snackString(context.getString(R.string.error_loading_anilist_user_data))
false
}
}
block.invoke()
} else true
if (anilist) block.invoke()
}
class AnilistHomeViewModel : ViewModel() {
@@ -77,30 +89,16 @@ class AnilistHomeViewModel : ViewModel() {
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
private val userStatus: MutableLiveData<ArrayList<User>> =
MutableLiveData<ArrayList<User>>(null)
fun getUserStatus(): LiveData<ArrayList<User>> = userStatus
suspend fun initUserStatus() {
val res = Anilist.query.getUserStatus()
res?.let { userStatus.postValue(it) }
}
private val hidden: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getHidden(): LiveData<ArrayList<Media>> = hidden
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["currentAnimePlanned"]?.let { animePlanned.postValue(it) }
res["plannedAnime"]?.let { animePlanned.postValue(it) }
res["currentManga"]?.let { mangaContinue.postValue(it) }
res["favoriteManga"]?.let { mangaFav.postValue(it) }
res["currentMangaPlanned"]?.let { mangaPlanned.postValue(it) }
res["plannedManga"]?.let { mangaPlanned.postValue(it) }
res["recommendations"]?.let { recommendation.postValue(it) }
res["hidden"]?.let { hidden.postValue(it) }
}
suspend fun loadMain(context: FragmentActivity) {
@@ -108,15 +106,9 @@ class AnilistHomeViewModel : ViewModel() {
MAL.getSavedToken()
Discord.getSavedToken()
if (!BuildConfig.FLAVOR.contains("fdroid")) {
if (PrefManager.getVal(PrefName.CheckUpdate))
context.lifecycleScope.launch(Dispatchers.IO) {
AppUpdater.check(context, false)
}
}
val ret = Anilist.query.getGenresAndTags()
withContext(Dispatchers.Main) {
genres.value = ret
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
}
genres.postValue(Anilist.query.getGenresAndTags())
}
val empty = MutableLiveData<Boolean>(null)
@@ -128,7 +120,7 @@ class AnilistHomeViewModel : ViewModel() {
class AnilistAnimeViewModel : ViewModel() {
var searched = false
var notSet = true
lateinit var aniMangaSearchResults: AniMangaSearchResults
lateinit var searchResults: SearchResults
private val type = "ANIME"
private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
@@ -137,7 +129,7 @@ class AnilistAnimeViewModel : ViewModel() {
suspend fun loadTrending(i: Int) {
val (season, year) = Anilist.currentSeasons[i]
trending.postValue(
Anilist.query.searchAniManga(
Anilist.query.search(
type,
perPage = 12,
sort = Anilist.sortBy[2],
@@ -150,9 +142,9 @@ class AnilistAnimeViewModel : ViewModel() {
}
private val animePopular = MutableLiveData<AniMangaSearchResults?>(null)
private val animePopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<AniMangaSearchResults?> = animePopular
fun getPopular(): LiveData<SearchResults?> = animePopular
suspend fun loadPopular(
type: String,
searchVal: String? = null,
@@ -161,7 +153,7 @@ class AnilistAnimeViewModel : ViewModel() {
onList: Boolean = true,
) {
animePopular.postValue(
Anilist.query.searchAniManga(
Anilist.query.search(
type,
search = searchVal,
onList = if (onList) null else false,
@@ -173,8 +165,8 @@ class AnilistAnimeViewModel : ViewModel() {
}
suspend fun loadNextPage(r: AniMangaSearchResults) = animePopular.postValue(
Anilist.query.searchAniManga(
suspend fun loadNextPage(r: SearchResults) = animePopular.postValue(
Anilist.query.search(
r.type,
r.page + 1,
r.perPage,
@@ -224,7 +216,7 @@ class AnilistAnimeViewModel : ViewModel() {
class AnilistMangaViewModel : ViewModel() {
var searched = false
var notSet = true
lateinit var aniMangaSearchResults: AniMangaSearchResults
lateinit var searchResults: SearchResults
private val type = "MANGA"
private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
@@ -232,7 +224,7 @@ class AnilistMangaViewModel : ViewModel() {
fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending() =
trending.postValue(
Anilist.query.searchAniManga(
Anilist.query.search(
type,
perPage = 10,
sort = Anilist.sortBy[2],
@@ -242,8 +234,8 @@ class AnilistMangaViewModel : ViewModel() {
)
private val mangaPopular = MutableLiveData<AniMangaSearchResults?>(null)
fun getPopular(): LiveData<AniMangaSearchResults?> = mangaPopular
private val mangaPopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular
suspend fun loadPopular(
type: String,
searchVal: String? = null,
@@ -252,7 +244,7 @@ class AnilistMangaViewModel : ViewModel() {
onList: Boolean = true,
) {
mangaPopular.postValue(
Anilist.query.searchAniManga(
Anilist.query.search(
type,
search = searchVal,
onList = if (onList) null else false,
@@ -264,8 +256,8 @@ class AnilistMangaViewModel : ViewModel() {
}
suspend fun loadNextPage(r: AniMangaSearchResults) = mangaPopular.postValue(
Anilist.query.searchAniManga(
suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue(
Anilist.query.search(
r.type,
r.page + 1,
r.perPage,
@@ -325,131 +317,14 @@ class AnilistMangaViewModel : ViewModel() {
}
class AnilistSearch : ViewModel() {
enum class SearchType {
ANIME, MANGA, CHARACTER, STAFF, STUDIO, USER;
companion object {
fun SearchType.toAnilistString(): String {
return when (this) {
ANIME -> "ANIME"
MANGA -> "MANGA"
CHARACTER -> "CHARACTER"
STAFF -> "STAFF"
STUDIO -> "STUDIO"
USER -> "USER"
}
}
fun fromString(string: String): SearchType {
return when (string.uppercase()) {
"ANIME" -> ANIME
"MANGA" -> MANGA
"CHARACTER" -> CHARACTER
"STAFF" -> STAFF
"STUDIO" -> STUDIO
"USER" -> USER
else -> throw IllegalArgumentException("Invalid search type")
}
}
}
}
var searched = false
var notSet = true
lateinit var aniMangaSearchResults: AniMangaSearchResults
private val aniMangaResult: MutableLiveData<AniMangaSearchResults?> =
MutableLiveData<AniMangaSearchResults?>(null)
lateinit var searchResults: SearchResults
private val result: MutableLiveData<SearchResults?> = MutableLiveData<SearchResults?>(null)
lateinit var characterSearchResults: CharacterSearchResults
private val characterResult: MutableLiveData<CharacterSearchResults?> =
MutableLiveData<CharacterSearchResults?>(null)
lateinit var studioSearchResults: StudioSearchResults
private val studioResult: MutableLiveData<StudioSearchResults?> =
MutableLiveData<StudioSearchResults?>(null)
lateinit var staffSearchResults: StaffSearchResults
private val staffResult: MutableLiveData<StaffSearchResults?> =
MutableLiveData<StaffSearchResults?>(null)
lateinit var userSearchResults: UserSearchResults
private val userResult: MutableLiveData<UserSearchResults?> =
MutableLiveData<UserSearchResults?>(null)
fun <T> getSearch(type: SearchType): MutableLiveData<T?> {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaResult as MutableLiveData<T?>
SearchType.CHARACTER -> characterResult as MutableLiveData<T?>
SearchType.STUDIO -> studioResult as MutableLiveData<T?>
SearchType.STAFF -> staffResult as MutableLiveData<T?>
SearchType.USER -> userResult as MutableLiveData<T?>
}
}
suspend fun loadSearch(type: SearchType) {
when (type) {
SearchType.ANIME, SearchType.MANGA -> loadAniMangaSearch(aniMangaSearchResults)
SearchType.CHARACTER -> loadCharacterSearch(characterSearchResults)
SearchType.STUDIO -> loadStudiosSearch(studioSearchResults)
SearchType.STAFF -> loadStaffSearch(staffSearchResults)
SearchType.USER -> loadUserSearch(userSearchResults)
}
}
suspend fun loadNextPage(type: SearchType) {
when (type) {
SearchType.ANIME, SearchType.MANGA -> loadNextAniMangaPage(aniMangaSearchResults)
SearchType.CHARACTER -> loadNextCharacterPage(characterSearchResults)
SearchType.STUDIO -> loadNextStudiosPage(studioSearchResults)
SearchType.STAFF -> loadNextStaffPage(staffSearchResults)
SearchType.USER -> loadNextUserPage(userSearchResults)
}
}
fun hasNextPage(type: SearchType): Boolean {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.hasNextPage
SearchType.CHARACTER -> characterSearchResults.hasNextPage
SearchType.STUDIO -> studioSearchResults.hasNextPage
SearchType.STAFF -> staffSearchResults.hasNextPage
SearchType.USER -> userSearchResults.hasNextPage
}
}
fun resultsIsNotEmpty(type: SearchType): Boolean {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.isNotEmpty()
SearchType.CHARACTER -> characterSearchResults.results.isNotEmpty()
SearchType.STUDIO -> studioSearchResults.results.isNotEmpty()
SearchType.STAFF -> staffSearchResults.results.isNotEmpty()
SearchType.USER -> userSearchResults.results.isNotEmpty()
}
}
fun size(type: SearchType): Int {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.size
SearchType.CHARACTER -> characterSearchResults.results.size
SearchType.STUDIO -> studioSearchResults.results.size
SearchType.STAFF -> staffSearchResults.results.size
SearchType.USER -> userSearchResults.results.size
}
}
fun clearResults(type: SearchType) {
when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.clear()
SearchType.CHARACTER -> characterSearchResults.results.clear()
SearchType.STUDIO -> studioSearchResults.results.clear()
SearchType.STAFF -> staffSearchResults.results.clear()
SearchType.USER -> userSearchResults.results.clear()
}
}
private suspend fun loadAniMangaSearch(r: AniMangaSearchResults) = aniMangaResult.postValue(
Anilist.query.searchAniManga(
fun getSearch(): LiveData<SearchResults?> = result
suspend fun loadSearch(r: SearchResults) = result.postValue(
Anilist.query.search(
r.type,
r.page,
r.perPage,
@@ -471,36 +346,8 @@ class AnilistSearch : ViewModel() {
)
)
private suspend fun loadCharacterSearch(r: CharacterSearchResults) = characterResult.postValue(
Anilist.query.searchCharacters(
r.page,
r.search,
)
)
private suspend fun loadStudiosSearch(r: StudioSearchResults) = studioResult.postValue(
Anilist.query.searchStudios(
r.page,
r.search,
)
)
private suspend fun loadStaffSearch(r: StaffSearchResults) = staffResult.postValue(
Anilist.query.searchStaff(
r.page,
r.search,
)
)
private suspend fun loadUserSearch(r: UserSearchResults) = userResult.postValue(
Anilist.query.searchUsers(
r.page,
r.search,
)
)
private suspend fun loadNextAniMangaPage(r: AniMangaSearchResults) = aniMangaResult.postValue(
Anilist.query.searchAniManga(
suspend fun loadNextPage(r: SearchResults) = result.postValue(
Anilist.query.search(
r.type,
r.page + 1,
r.perPage,
@@ -521,35 +368,6 @@ class AnilistSearch : ViewModel() {
r.season
)
)
private suspend fun loadNextCharacterPage(r: CharacterSearchResults) =
characterResult.postValue(
Anilist.query.searchCharacters(
r.page + 1,
r.search,
)
)
private suspend fun loadNextStudiosPage(r: StudioSearchResults) = studioResult.postValue(
Anilist.query.searchStudios(
r.page + 1,
r.search,
)
)
private suspend fun loadNextStaffPage(r: StaffSearchResults) = staffResult.postValue(
Anilist.query.searchStaff(
r.page + 1,
r.search,
)
)
private suspend fun loadNextUserPage(r: UserSearchResults) = userResult.postValue(
Anilist.query.searchUsers(
r.page + 1,
r.search,
)
)
}
class GenresViewModel : ViewModel() {

View File

@@ -2,25 +2,15 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.media.Author
import ani.dantotsu.media.Character
import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.profile.User
import java.io.Serializable
interface SearchResults<T> {
var search: String?
var page: Int
var results: MutableList<T>
var hasNextPage: Boolean
}
data class AniMangaSearchResults(
data class SearchResults(
val type: String,
var isAdult: Boolean,
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,
@@ -33,11 +23,10 @@ data class AniMangaSearchResults(
var seasonYear: Int? = null,
var startYear: Int? = null,
var season: String? = null,
override var search: String? = null,
override var page: Int = 1,
override var results: MutableList<Media>,
override var hasNextPage: Boolean,
) : SearchResults<Media>, Serializable {
var page: Int = 1,
var results: MutableList<Media>,
var hasNextPage: Boolean,
) : Serializable {
fun toChipList(): List<SearchChip> {
val list = mutableListOf<SearchChip>()
sort?.let {
@@ -119,33 +108,4 @@ data class AniMangaSearchResults(
val type: String,
val text: String
)
}
data class CharacterSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Character>,
override var hasNextPage: Boolean,
) : SearchResults<Character>, Serializable
data class StudioSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Studio>,
override var hasNextPage: Boolean,
) : SearchResults<Studio>, Serializable
data class StaffSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Author>,
override var hasNextPage: Boolean,
) : SearchResults<Author>, Serializable
data class UserSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<User>,
override var hasNextPage: Boolean,
) : SearchResults<User>, Serializable
}

View File

@@ -1,431 +0,0 @@
package ani.dantotsu.connections.anilist
val standardPageInformation = """
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
""".prepare()
fun String.prepare() = this.trimIndent().replace("\n", " ").replace(""" """, "")
fun characterInformation(includeMediaInfo: Boolean) = """
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
age
gender
description
dateOfBirth {
year
month
day
}
${
if (includeMediaInfo) """
media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) {
$standardPageInformation
edges {
id
voiceActors {
id,
name {
userPreferred
}
languageV2,
image {
medium,
large
}
}
characterRole
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}""".prepare() else ""
}
""".prepare()
fun studioInformation(page: Int, perPage: Int) = """
id
name
isFavourite
favourites
media(page: $page, sort:START_DATE_DESC, perPage: $perPage) {
$standardPageInformation
edges {
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
""".prepare()
fun staffInformation(page: Int, perPage: Int) = """
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
dateOfBirth {
year
month
day
}
dateOfDeath {
year
month
day
}
age
yearsActive
homeTown
staffMedia(page: $page,sort:START_DATE_DESC, perPage: $perPage) {
$standardPageInformation
edges {
staffRole
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
""".prepare()
fun userInformation() = """
id
name
about(asHtml: true)
avatar {
large
medium
}
bannerImage
isFollowing
isFollower
isBlocked
siteUrl
""".prepare()
fun aniMangaSearch(perPage: Int?) = """
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}) {
$standardPageInformation
media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) {
${standardMediaInformation()}
}
}
}
""".prepare()
fun standardMediaInformation() = """
id
idMal
siteUrl
isAdult
status(version: 2)
chapters
episodes
nextAiringEpisode {
episode
airingAt
}
type
genres
meanScore
popularity
favourites
isFavourite
format
bannerImage
countryOfOrigin
coverImage {
large
extraLarge
}
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
""".prepare()
fun fullMediaInformation(id: Int) = """
{
Media(id: $id) {
streamingEpisodes {
title
thumbnail
url
site
}
mediaListEntry {
id
status
score(format: POINT_100)
progress
private
notes
repeat
customLists
updatedAt
startedAt {
year
month
day
}
completedAt {
year
month
day
}
}
reviews(perPage: 3, sort: SCORE_DESC) {
nodes {
id
mediaId
mediaType
summary
body(asHtml: true)
rating
ratingAmount
userRating
score
private
siteUrl
createdAt
updatedAt
user {
id
name
bannerImage
avatar {
medium
large
}
}
}
}
${standardMediaInformation()}
source
duration
season
seasonYear
startDate {
year
month
day
}
endDate {
year
month
day
}
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 {
${standardMediaInformation()}
}
}
}
staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) {
edges {
role
node {
id
image {
large
medium
}
name {
userPreferred
}
}
}
}
recommendations(sort: RATING_DESC) {
nodes {
mediaRecommendation {
${standardMediaInformation()}
}
}
}
externalLinks {
url
site
}
}
Page(page: 1) {
$standardPageInformation
mediaList(isFollowing: true, sort: [STATUS], mediaId: $id) {
id
status
score(format: POINT_100)
progress
progressVolumes
user {
id
name
avatar {
large
medium
}
}
}
}
}
""".prepare()

View File

@@ -138,8 +138,6 @@ class Query {
@SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("Page1") val page1: ActivityPage?,
@SerialName("Page2") val page2: ActivityPage?
)
}
@@ -163,9 +161,13 @@ class Query {
@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?,
)
}
@@ -177,10 +179,15 @@ class Query {
@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?,
)
}
@@ -290,70 +297,6 @@ class Query {
val following: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable
@Serializable
data class ReviewsResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: ReviewPage?
) : java.io.Serializable
}
@Serializable
data class ReviewPage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
@SerialName("reviews")
val reviews: List<Review>?
) : java.io.Serializable
@Serializable
data class RateReviewResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("RateReview")
val rateReview: Review
) : java.io.Serializable
}
@Serializable
data class Review(
@SerialName("id")
val id: Int,
@SerialName("mediaId")
val mediaId: Int,
@SerialName("mediaType")
val mediaType: String,
@SerialName("summary")
val summary: String,
@SerialName("body")
val body: String,
@SerialName("rating")
var rating: Int,
@SerialName("ratingAmount")
var ratingAmount: Int,
@SerialName("userRating")
var userRating: String,
@SerialName("score")
val score: Int,
@SerialName("private")
val private: Boolean,
@SerialName("siteUrl")
val siteUrl: String,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("updatedAt")
val updatedAt: Int?,
@SerialName("user")
val user: ani.dantotsu.connections.anilist.api.User?,
) : java.io.Serializable
@Serializable
data class UserProfile(
@SerialName("id")

View File

@@ -36,7 +36,7 @@ data class Activity(
@SerialName("type")
val type: String,
@SerialName("replyCount")
val replyCount: Int = 0,
val replyCount: Int,
@SerialName("status")
val status: String?,
@SerialName("progress")
@@ -48,9 +48,9 @@ data class Activity(
@SerialName("siteUrl")
val siteUrl: String?,
@SerialName("isLocked")
val isLocked: Boolean?,
val isLocked: Boolean,
@SerialName("isSubscribed")
val isSubscribed: Boolean?,
val isSubscribed: Boolean,
@SerialName("likeCount")
var likeCount: Int?,
@SerialName("isLiked")
@@ -75,24 +75,6 @@ data class Activity(
val likes: List<User>?,
) : java.io.Serializable
@Serializable
data class ReplyResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: ReplyPage
) : java.io.Serializable
}
@Serializable
data class ReplyPage(
@SerialName("activityReplies")
val activityReplies: List<ActivityReply>
) : java.io.Serializable
@Serializable
data class ActivityReply(
@SerialName("id")
@@ -102,9 +84,9 @@ data class ActivityReply(
@SerialName("text")
val text: String,
@SerialName("likeCount")
var likeCount: Int,
val likeCount: Int,
@SerialName("isLiked")
var isLiked: Boolean,
val isLiked: Boolean,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("user")

View File

@@ -143,7 +143,7 @@ data class Media(
@SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?,
// Data and links to legal streaming episodes on external sites
@SerialName("streamingEpisodes") var streamingEpisodes: List<MediaStreamingEpisode>?,
// @SerialName("streamingEpisodes") var streamingEpisodes: List<MediaStreamingEpisode>?,
// The ranking of the media in a particular time span and format compared to other media
// @SerialName("rankings") var rankings: List<MediaRank>?,
@@ -152,7 +152,7 @@ data class Media(
@SerialName("mediaListEntry") var mediaListEntry: MediaList?,
// User reviews of the media
@SerialName("reviews") var reviews: ReviewConnection?,
// @SerialName("reviews") var reviews: ReviewConnection?,
// User recommendations for similar media
@SerialName("recommendations") var recommendations: RecommendationConnection?,
@@ -174,7 +174,7 @@ data class Media(
// Notes for site moderators
@SerialName("modNotes") var modNotes: String?,
) : java.io.Serializable
)
@Serializable
data class MediaTitle(
@@ -189,7 +189,7 @@ data class MediaTitle(
// The currently authenticated users preferred title language. Default romaji for non-authenticated
@SerialName("userPreferred") var userPreferred: String,
) : java.io.Serializable
)
@Serializable
enum class MediaType {
@@ -205,17 +205,15 @@ enum class MediaStatus {
FINISHED, RELEASING, NOT_YET_RELEASED, CANCELLED, HIATUS;
override fun toString(): String {
currContext()?.let {
return when (super.toString()) {
"FINISHED" -> it.getString(R.string.status_finished)
"RELEASING" -> it.getString(R.string.status_releasing)
"NOT_YET_RELEASED" -> it.getString(R.string.status_not_yet_released)
"CANCELLED" -> it.getString(R.string.status_cancelled)
"HIATUS" -> it.getString(R.string.status_hiatus)
else -> ""
}
return when (super.toString()) {
"FINISHED" -> currContext()!!.getString(R.string.status_finished)
"RELEASING" -> currContext()!!.getString(R.string.status_releasing)
"NOT_YET_RELEASED" -> currContext()!!.getString(R.string.status_not_yet_released)
"CANCELLED" -> currContext()!!.getString(R.string.status_cancelled)
"HIATUS" -> currContext()!!.getString(R.string.status_hiatus)
else -> ""
}
return super.toString().replace("_", " ")
}
}
@@ -240,21 +238,6 @@ data class AiringSchedule(
@SerialName("media") var media: Media?,
)
@Serializable
data class MediaStreamingEpisode(
// The title of the episode
@SerialName("title") var title: String?,
// The thumbnail image of the episode
@SerialName("thumbnail") var thumbnail: String?,
// The url of the episode
@SerialName("url") var url: String?,
// The site location of the streaming episode
@SerialName("site") var site: String?,
)
@Serializable
data class MediaCoverImage(
// The cover image url of the media at its largest size. If this size isn't available, large will be provided instead.
@@ -448,7 +431,7 @@ data class MediaEdge(
@SerialName("staffRole") var staffRole: 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>?,
@@ -462,20 +445,17 @@ enum class MediaRelation {
ADAPTATION, PREQUEL, SEQUEL, PARENT, SIDE_STORY, CHARACTER, SUMMARY, ALTERNATIVE, SPIN_OFF, OTHER, SOURCE, COMPILATION, CONTAINS;
override fun toString(): String {
currContext()?.let {
return when (super.toString()) {
"ADAPTATION" -> it.getString(R.string.type_adaptation)
"PARENT" -> it.getString(R.string.type_parent)
"CHARACTER" -> it.getString(R.string.type_character)
"SUMMARY" -> it.getString(R.string.type_summary)
"ALTERNATIVE" -> it.getString(R.string.type_alternative)
"OTHER" -> it.getString(R.string.type_other)
"SOURCE" -> it.getString(R.string.type_source)
"CONTAINS" -> it.getString(R.string.type_contains)
else -> super.toString().replace("_", " ")
}
return when (super.toString()) {
"ADAPTATION" -> currContext()!!.getString(R.string.type_adaptation)
"PARENT" -> currContext()!!.getString(R.string.type_parent)
"CHARACTER" -> currContext()!!.getString(R.string.type_character)
"SUMMARY" -> currContext()!!.getString(R.string.type_summary)
"ALTERNATIVE" -> currContext()!!.getString(R.string.type_alternative)
"OTHER" -> currContext()!!.getString(R.string.type_other)
"SOURCE" -> currContext()!!.getString(R.string.type_source)
"CONTAINS" -> currContext()!!.getString(R.string.type_contains)
else -> super.toString().replace("_", " ")
}
return super.toString().replace("_", " ")
}
}
@@ -552,9 +532,4 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?,
) : java.io.Serializable
@Serializable
data class ReviewConnection(
@SerialName("nodes") var nodes: List<Query.Review>?,
)
) : java.io.Serializable

View File

@@ -2,7 +2,6 @@ package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Locale
enum class NotificationType(val value: String) {
ACTIVITY_MESSAGE("ACTIVITY_MESSAGE"),
@@ -25,20 +24,6 @@ enum class NotificationType(val value: String) {
//custom
COMMENT_REPLY("COMMENT_REPLY"),
COMMENT_WARNING("COMMENT_WARNING"),
DANTOTSU_UPDATE("DANTOTSU_UPDATE"),
SUBSCRIPTION("SUBSCRIPTION");
fun toFormattedString(): String {
return this.value.replace("_", " ").lowercase(Locale.ROOT)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }
}
companion object {
fun String.fromFormattedString(): String {
return this.replace(" ", "_").uppercase(Locale.ROOT)
}
}
}
@Serializable
@@ -111,8 +96,6 @@ data class Notification(
val thread: Thread? = null,
@SerialName("comment")
val comment: ThreadComment? = null,
val image: String? = null,
val banner: String? = null,
) : java.io.Serializable
@Serializable

View File

@@ -69,12 +69,12 @@ data class User(
// The user's previously used names.
// @SerialName("previousNames") var previousNames: List<UserPreviousName>?,
) : java.io.Serializable
)
@Serializable
data class UserOptions(
// The language the user wants to see media titles in
@SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
// @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
// Whether the user has enabled viewing of 18+ content
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
@@ -88,17 +88,17 @@ data class UserOptions(
// // Notification options
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
//
// The user's timezone offset (Auth user only)
@SerialName("timezone") var timezone: String?,
// // The user's timezone offset (Auth user only)
// @SerialName("timezone") var timezone: String?,
//
// Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
@SerialName("activityMergeTime") var activityMergeTime: Int?,
// // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
// @SerialName("activityMergeTime") var activityMergeTime: Int?,
//
// The language the user wants to see staff and character names in
@SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
// // The language the user wants to see staff and character names in
// // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
//
// Whether the user only allow messages from users they follow
@SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
// // Whether the user only allow messages from users they follow
// @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
// The list activity types the user has disabled from being created from list updates
// @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?,
@@ -119,48 +119,6 @@ data class UserStatisticTypes(
@SerialName("manga") var manga: UserStatistics?
)
@Serializable
enum class UserTitleLanguage {
@SerialName("ENGLISH")
ENGLISH,
@SerialName("ROMAJI")
ROMAJI,
@SerialName("NATIVE")
NATIVE
}
@Serializable
enum class UserStaffNameLanguage {
@SerialName("ROMAJI_WESTERN")
ROMAJI_WESTERN,
@SerialName("ROMAJI")
ROMAJI,
@SerialName("NATIVE")
NATIVE
}
@Serializable
enum class ScoreFormat {
@SerialName("POINT_100")
POINT_100,
@SerialName("POINT_10_DECIMAL")
POINT_10_DECIMAL,
@SerialName("POINT_10")
POINT_10,
@SerialName("POINT_5")
POINT_5,
@SerialName("POINT_3")
POINT_3,
}
@Serializable
data class UserStatistics(
//
@@ -206,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?,
@@ -223,8 +181,8 @@ data class MediaListTypeOptions(
// The order each list should be displayed in
@SerialName("sectionOrder") var sectionOrder: List<String>?,
// // If the completed sections of the list should be separated by format
// @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
// If the completed sections of the list should be separated by format
@SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
// The names of the user's custom lists
@SerialName("customLists") var customLists: List<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

@@ -1,13 +1,10 @@
package ani.dantotsu.connections.comments
import android.content.Context
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.isOnline
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.lagradost.nicehttp.NiceResponse
import com.lagradost.nicehttp.Requests
import eu.kanade.tachiyomi.network.NetworkHelper
@@ -27,11 +24,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object CommentsAPI {
private const val API_ADDRESS: String = "https://api.dantotsu.app"
private const val LOCAL_HOST: String = "https://127.0.0.1"
private var isOnline: Boolean = true
private var commentsEnabled = PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1
private val ADDRESS: String get() = if (commentsEnabled) API_ADDRESS else LOCAL_HOST
private const val ADDRESS: String = "https://1224665.xyz:443"
var authToken: String? = null
var userId: String? = null
var isBanned: Boolean = false
@@ -56,8 +49,7 @@ object CommentsAPI {
val json = try {
request.get(url)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to fetch comments")
snackString("Failed to fetch comments")
return null
}
if (!json.text.startsWith("{")) return null
@@ -79,8 +71,7 @@ object CommentsAPI {
val json = try {
request.get(url)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to fetch comments")
snackString("Failed to fetch comments")
return null
}
if (!json.text.startsWith("{")) return null
@@ -102,8 +93,7 @@ object CommentsAPI {
val json = try {
request.get(url)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to fetch comment")
snackString("Failed to fetch comment")
return null
}
if (!json.text.startsWith("{")) return null
@@ -125,8 +115,7 @@ object CommentsAPI {
val json = try {
request.post(url)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to vote")
snackString("Failed to vote")
return false
}
val res = json.code == 200
@@ -152,8 +141,7 @@ object CommentsAPI {
val json = try {
request.post(url, requestBody = body.build())
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to comment")
snackString("Failed to comment")
return null
}
val res = json.code == 200
@@ -164,8 +152,7 @@ object CommentsAPI {
val parsed = try {
Json.decodeFromString<ReturnedComment>(json.text)
} catch (e: Exception) {
Logger.log(e)
errorMessage("Failed to parse comment")
snackString("Failed to parse comment")
return null
}
return Comment(
@@ -192,8 +179,7 @@ object CommentsAPI {
val json = try {
request.delete(url)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to delete comment")
snackString("Failed to delete comment")
return false
}
val res = json.code == 200
@@ -212,8 +198,7 @@ object CommentsAPI {
val json = try {
request.put(url, requestBody = body)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to edit comment")
snackString("Failed to edit comment")
return false
}
val res = json.code == 200
@@ -229,8 +214,7 @@ object CommentsAPI {
val json = try {
request.post(url)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to ban user")
snackString("Failed to ban user")
return false
}
val res = json.code == 200
@@ -257,8 +241,7 @@ object CommentsAPI {
val json = try {
request.post(url, requestBody = body)
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to report comment")
snackString("Failed to report comment")
return false
}
val res = json.code == 200
@@ -313,8 +296,7 @@ object CommentsAPI {
return null
}
suspend fun fetchAuthToken(context: Context, client: OkHttpClient? = null) {
isOnline = isOnline(context)
suspend fun fetchAuthToken(client: OkHttpClient? = null) {
if (authToken != null) return
val MAX_RETRIES = 5
val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days
@@ -343,8 +325,7 @@ object CommentsAPI {
val parsed = try {
Json.decodeFromString<AuthResponse>(json.text)
} catch (e: Exception) {
Logger.log(e)
errorMessage("Failed to login to comments API: ${e.printStackTrace()}")
snackString("Failed to login to comments API: ${e.printStackTrace()}")
return
}
PrefManager.setVal(PrefName.CommentAuthResponse, parsed)
@@ -364,18 +345,12 @@ object CommentsAPI {
return
}
} catch (e: IOException) {
Logger.log(e)
errorMessage("Failed to login to comments API")
snackString("Failed to login to comments API")
return
}
kotlinx.coroutines.delay(60000)
}
errorMessage("Failed to login after multiple attempts")
}
private fun errorMessage(reason: String) {
if (commentsEnabled) Logger.log(reason)
if (isOnline && commentsEnabled) snackString(reason)
snackString("Failed to login after multiple attempts")
}
fun logout() {
@@ -411,7 +386,7 @@ object CommentsAPI {
return map
}
fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
return Requests(
client,
headerBuilder()

View File

@@ -70,7 +70,7 @@ object Discord {
const val application_Id = "1163925779692912771"
const val small_Image: String =
"mp:external/9NqpMxXs4ZNQtMG42L7hqINW92GqqDxgxS9Oh0Sp880/%3Fsize%3D48%26quality%3Dlossless%26name%3DDantotsu/https/cdn.discordapp.com/emojis/1167344924874784828.gif"
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
const val small_Image_AniList: String =
"https://anilist.co/img/icons/android-chrome-512x512.png"
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
}

View File

@@ -1,19 +1,24 @@
package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.Discord.token
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
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.coroutines.CoroutineContext
import ani.dantotsu.client as app
@Suppress("MemberVisibilityCanBePrivate")
open class RPC(val token: String, val coroutineContext: CoroutineContext) {
private val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
}
@@ -22,7 +27,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
companion object {
data class RPCData(
val applicationId: String,
val applicationId: String? = null,
val type: Type? = null,
val activityName: String? = null,
val details: String? = null,
@@ -35,21 +40,22 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val buttons: MutableList<Link> = mutableListOf()
)
@Serializable
data class KizzyApi(val id: String)
val api = "https://kizzy-api.vercel.app/image?url="
private suspend fun String.discordUrl(): String? {
if (startsWith("mp:")) return this
val json = app.get("$api$this").parsedSafe<KizzyApi>()
return json?.id
}
suspend fun createPresence(data: RPCData): String {
val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
val client = OkHttpClient.Builder()
.connectTimeout(10, SECONDS)
.readTimeout(10, SECONDS)
.writeTimeout(10, SECONDS)
.build()
val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json)
suspend fun String.discordUrl() = assetApi.getDiscordUri(this)
return json.encodeToString(Presence.Response(
3,
Presence(

View File

@@ -1,61 +0,0 @@
// this code was kanged from the greatest mind of this era, aka shivam brahmkshatriya
// please subscribe to my only fans here: https://github.com/brahmkshatriya
package ani.dantotsu.connections.discord
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class RPCExternalAsset(
applicationId: String,
private val token: String,
private val client: OkHttpClient,
private val json: Json
) {
@Serializable
data class ExternalAsset(
val url: String? = null,
@SerialName("external_asset_path")
val externalAssetPath: String? = null
)
private val api = "https://discord.com/api/v9/applications/$applicationId/external-assets"
suspend fun getDiscordUri(imageUrl: String): String? {
if (imageUrl.startsWith("mp:")) return imageUrl
val request = Request.Builder().url(api).header("Authorization", token)
.post("{\"urls\":[\"$imageUrl\"]}".toRequestBody("application/json".toMediaType()))
.build()
return runCatching {
val res = client.newCall(request).await()
json.decodeFromString<List<ExternalAsset>>(res.body.string())
.firstOrNull()?.externalAssetPath?.let { "mp:$it" }
}.getOrNull()
}
private suspend inline fun Call.await(): Response {
return suspendCoroutine {
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
it.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
it.resume(response)
}
})
}
}
}

View File

@@ -40,7 +40,6 @@ data class Activity(
@Serializable
data class Timestamps(
val start: Long? = null,
@SerialName("end")
val stop: Long? = null
)
}

View File

@@ -28,7 +28,6 @@ class Contributors {
"rebelonion" -> "Owner & Maintainer"
"sneazy-ibo" -> "Contributor & Comment Moderator"
"WaiWhat" -> "Icon Designer"
"itsmechinmoy" -> "Discord and Telegram Admin/Helper, Comment Moderator & Translator"
else -> "Contributor"
}
developers = developers.plus(
@@ -85,21 +84,15 @@ class Contributors {
"https://anilist.co/user/6244220"
),
Developer(
"Ziadsenior",
"Zaidsenior",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
"Comment Moderator and Arabic Translator",
"Comment Moderator",
"https://anilist.co/user/6049773"
),
Developer(
"Dawnusedyeet",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6237399-RHFvRHriXjwS.png",
"Contributor",
"https://anilist.co/user/Dawnusedyeet/"
),
Developer(
"hastsu",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6183359-9os7zUhYdF64.jpg",
"Comment Moderator and Arabic Translator",
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
"Comment Moderator",
"https://anilist.co/user/6183359"
),
)
@@ -118,4 +111,4 @@ class Contributors {
@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

@@ -1,384 +0,0 @@
package ani.dantotsu.download
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.widget.Toast
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.anime.OfflineAnimeModel
import ani.dantotsu.download.manga.OfflineMangaModel
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Episode
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.util.Logger
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
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 eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.Locale
@Deprecated("external storage is deprecated, use SAF instead")
class DownloadCompat {
companion object {
@Deprecated("external storage is deprecated, use SAF instead")
fun loadMediaCompat(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
return try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
null
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
try {
val mediaModel = loadMediaCompat(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
val totalEpisode =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
val chapters = " Chapters"
val totalEpisodesList =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
?: "~").toString()
return OfflineAnimeModel(
title,
score,
totalEpisode,
totalEpisodesList,
watchedEpisodes,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel(
downloadedType.titleName,
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
try {
val mediaModel = loadMediaCompat(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
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 chapters = " Chapters"
return OfflineMangaModel(
title,
score,
totalchapter,
readchapter,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
downloadedType.titleName,
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadEpisodesCompat(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${animeLocation}/$animeLink"
)
//get all of the folder names and add them to the list
val episodes = mutableListOf<Episode>()
if (directory.exists()) {
directory.listFiles()?.forEach {
//put the title and episdode number in the extra data
val extraData = mutableMapOf<String, String>()
extraData["title"] = animeLink
extraData["episode"] = it.name
if (it.isDirectory) {
val episode = Episode(
it.name,
"$animeLink - ${it.name}",
it.name,
null,
null,
extra = extraData,
sEpisode = SEpisodeImpl()
)
episodes.add(episode)
}
}
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
return episodes
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadChaptersCompat(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$mangaLink"
)
//get all of the folder names and add them to the list
val chapters = mutableListOf<MangaChapter>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = MangaChapter(
it.name,
"$mangaLink/${it.name}",
it.name,
null,
"Unknown",
SChapter.create()
)
chapters.add(chapter)
}
}
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
return chapters
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$chapterLink"
)
val images = mutableListOf<MangaImage>()
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isFile) {
val image = MangaImage(it.absolutePath, false, null)
images.add(image)
}
}
images.sortBy { image ->
val matchResult = imageNumberRegex.find(image.url.url)
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
}
for (image in images) {
Logger.log("imageNumber: ${image.url.url}")
}
return images
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadSubtitleCompat(title: String, episode: String): List<Subtitle>? {
currContext()?.let {
File(
it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$episode"
).listFiles()?.forEach {
if (it.name.contains("subtitle")) {
return listOf(
Subtitle(
"Downloaded Subtitle",
Uri.fromFile(it).toString(),
determineSubtitletype(it.absolutePath)
)
)
}
}
}
return null
}
private fun determineSubtitletype(url: String): SubtitleType {
return when {
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
else -> SubtitleType.SRT
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun removeMediaCompat(context: Context, title: String, type: MediaType) {
val subDirectory = if (type == MediaType.MANGA) {
"Manga"
} else if (type == MediaType.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
if (directory.exists()) {
directory.deleteRecursively()
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun removeDownloadCompat(context: Context, downloadedType: DownloadedType, toast: Boolean) {
val directory = if (downloadedType.type == MediaType.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}"
)
} else if (downloadedType.type == MediaType.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}"
)
}
// Check if the directory exists and delete it recursively
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (toast) {
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT)
.show()
}
}
}
}
private val animeLocation = "Dantotsu/Anime"
}
}

View File

@@ -3,8 +3,6 @@ package ani.dantotsu.download
import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.settings.saving.PrefManager
@@ -13,6 +11,7 @@ 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
@@ -59,8 +58,7 @@ class DownloadsManager(private val context: Context) {
toast: Boolean = true,
onFinished: () -> Unit
) {
removeDownloadCompat(context, downloadedType, toast)
downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
downloadsList.remove(downloadedType)
CoroutineScope(Dispatchers.IO).launch {
removeDirectory(downloadedType, toast)
withContext(Dispatchers.Main) {
@@ -71,7 +69,6 @@ class DownloadsManager(private val context: Context) {
}
fun removeMedia(title: String, type: MediaType) {
removeMediaCompat(context, title, type)
val baseDirectory = getBaseDirectory(context, type)
val directory = baseDirectory?.findFolder(title)
if (directory?.exists() == true) {
@@ -87,15 +84,15 @@ class DownloadsManager(private val context: Context) {
}
when (type) {
MediaType.MANGA -> {
downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA }
downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA }
}
MediaType.ANIME -> {
downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME }
downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME }
}
MediaType.NOVEL -> {
downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL }
downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL }
}
}
saveDownloads()
@@ -118,7 +115,7 @@ class DownloadsManager(private val context: Context) {
if (directory?.exists() == true && directory.isDirectory) {
val files = directory.listFiles()
for (file in files) {
if (!downloadsSubLists.any { it.titleName == file.name }) {
if (!downloadsSubLists.any { it.title == file.name }) {
file.deleteRecursively(context, false)
}
}
@@ -127,8 +124,8 @@ class DownloadsManager(private val context: Context) {
val iterator = downloadsList.iterator()
while (iterator.hasNext()) {
val download = iterator.next()
val downloadDir = directory?.findFolder(download.titleName)
if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) {
val downloadDir = directory?.findFolder(download.title)
if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) {
iterator.remove()
}
}
@@ -140,18 +137,13 @@ class DownloadsManager(private val context: Context) {
newUri: Uri,
finished: (Boolean, String) -> Unit
) {
if (oldUri == newUri) {
Logger.log("Source and destination are the same")
finished(false, "Source and destination are the same")
return
}
if (oldUri == Uri.EMPTY) {
Logger.log("Old Uri is empty")
finished(true, "Old Uri is empty")
return
}
CoroutineScope(Dispatchers.IO).launch {
try {
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 =
@@ -203,17 +195,13 @@ class DownloadsManager(private val context: Context) {
finished(true, "Successfully moved downloads")
super.onCompleted(result)
}
})
} catch (e: Exception) {
snackString("Error: ${e.message}")
Logger.log("Failed to move downloads: ${e.message}")
Logger.log(e)
Logger.log("oldUri: $oldUri, newUri: $newUri")
finished(false, "Failed to move downloads: ${e.message}")
return@launch
}
} catch (e: Exception) {
snackString("Error: ${e.message}")
finished(false, "Failed to move downloads: ${e.message}")
return
}
}
@@ -223,18 +211,17 @@ class DownloadsManager(private val context: Context) {
fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
return if (type == null) {
downloadsList.any { it.titleName == title && it.chapterName == chapter }
downloadsList.any { it.title == title && it.chapter == chapter }
} else {
downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type }
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
}
}
private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
val baseDirectory = getBaseDirectory(context, downloadedType.type)
val directory =
baseDirectory?.findFolder(downloadedType.titleName)
?.findFolder(downloadedType.chapterName)
downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter)
downloadsList.remove(downloadedType)
// Check if the directory exists and delete it recursively
if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false)
@@ -278,7 +265,6 @@ class DownloadsManager(private val context: Context) {
* @param type the type of media
* @return the base directory
*/
@Synchronized
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
@@ -307,7 +293,6 @@ class DownloadsManager(private val context: Context) {
* @param chapter the chapter of the media
* @return the subdirectory
*/
@Synchronized
fun getSubDirectory(
context: Context,
type: MediaType,
@@ -345,34 +330,23 @@ class DownloadsManager(private val context: Context) {
}
}
@Synchronized
private fun getBaseDirectory(context: Context): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
val base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
return base.findOrCreateFolder(BASE_LOCATION, false)
return DocumentFile.fromTreeUri(context, baseDirectory)
}
private val lock = Any()
private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean
): DocumentFile? {
val validName = name.findValidName()
synchronized(lock) {
return if (overwrite) {
findFolder(validName)?.delete()
createDirectory(validName)
} else {
val folder = findFolder(validName)
folder ?: createDirectory(validName)
}
return if (overwrite) {
findFolder(name.findValidName())?.delete()
createDirectory(name.findValidName())
} else {
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
}
}
private fun DocumentFile.findFolder(name: String): DocumentFile? =
listFiles().find { it.name == name && it.isDirectory }
private const val RATIO_THRESHOLD = 95
fun Media.compareName(name: String): Boolean {
val mainName = mainName().findValidName().lowercase()
@@ -390,24 +364,15 @@ class DownloadsManager(private val context: Context) {
}
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
fun String?.findValidName(): String {
return this?.replace("/", "_")?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
private fun String?.findValidName(): String {
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
}
data class DownloadedType(
private val pTitle: String?,
private val pChapter: String?,
val type: MediaType,
@Deprecated("use pTitle instead")
private val title: String? = null,
@Deprecated("use pChapter instead")
private val chapter: String? = null,
val scanlator: String = "Unknown"
val pTitle: String, val pChapter: String, val type: MediaType
) : Serializable {
val titleName: String
get() = title ?: pTitle.findValidName()
val chapterName: String
get() = chapter ?: pChapter.findValidName()
val uniqueName: String
get() = "$chapterName-${scanlator}"
val title: String
get() = pTitle.findValidName()
val chapter: String
get() = pChapter.findValidName()
}

View File

@@ -26,10 +26,11 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.download.findValidName
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
@@ -181,6 +182,7 @@ class AnimeDownloaderService : Service() {
}
private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads"
@@ -200,8 +202,8 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
withContext(Dispatchers.IO) {
try {
try {
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@AnimeDownloaderService,
@@ -213,66 +215,51 @@ class AnimeDownloaderService : Service() {
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
val baseOutputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title
) ?: throw Exception("Failed to create output directory")
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
true,
false,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
val extension = ffExtension!!.getFileExtension()
outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")
?.delete()
val outputFile =
outputDir.createFile(
extension.second,
"${task.getTaskName()}.${extension.first}"
)
?: throw Exception("Failed to create output file")
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(
val path = ffExtension!!.setDownloadPath(
this@AnimeDownloaderService,
outputFile.uri
)
if (!task.video.file.headers.containsKey("User-Agent")
&& !task.video.file.headers.containsKey("user-agent")
) {
val newHeaders = task.video.file.headers.toMutableMap()
newHeaders["User-Agent"] = defaultHeaders["User-Agent"]!!
task.video.file.headers = newHeaders
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(
task.video.file.url,
task.video.file.headers
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(
task.video.file.url,
path,
task.video.file.headers,
task.subtitle,
task.audio,
) {
ffExtension.executeFFMpeg(request) {
// CALLED WHEN SESSION GENERATES STATISTICS
val timeInMilliseconds = it
if (timeInMilliseconds > 0 && totalLength > 0) {
@@ -284,7 +271,18 @@ class AnimeDownloaderService : Service() {
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask
saveMediaInfo(task, baseOutputDir)
saveMediaInfo(task)
task.subtitle?.let {
SubtitleDownloader.downloadSubtitle(
this@AnimeDownloaderService,
it.file.url,
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
)
}
// periodically check if the download is complete
while (ffExtension.getState(ffTask) != "COMPLETED") {
@@ -298,11 +296,7 @@ class AnimeDownloaderService : Service() {
)
} Download failed"
)
if (notifi) {
withContext(Dispatchers.Main) {
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(
@@ -335,9 +329,7 @@ class AnimeDownloaderService : Service() {
percent.coerceAtMost(99)
)
if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
kotlinx.coroutines.delay(2000)
}
@@ -352,19 +344,14 @@ class AnimeDownloaderService : Service() {
)
} Download failed"
)
if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME
),
false
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
@@ -388,11 +375,7 @@ class AnimeDownloaderService : Service() {
)
} Download completed"
)
if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
@@ -410,20 +393,23 @@ class AnimeDownloaderService : Service() {
broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed")
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e)
}
broadcastDownloadFailed(task.episode)
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e)
}
broadcastDownloadFailed(task.episode)
}
}
private fun saveMediaInfo(task: AnimeDownloadTask, directory: DocumentFile) {
private fun saveMediaInfo(task: AnimeDownloadTask) {
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")
@@ -570,8 +556,7 @@ class AnimeDownloaderService : Service() {
val title: String,
val episode: String,
val video: Video,
val subtitle: List<Pair<String, String>> = emptyList(),
val audio: List<Pair<String, String>> = emptyList(),
val subtitle: Subtitle? = null,
val sourceMedia: Media? = null,
val episodeImage: String? = null,
val retries: Int = 2,

View File

@@ -58,8 +58,7 @@ class OfflineAnimeAdapter(
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val episodes = view.findViewById<TextView>(R.id.itemTotal)
val text = " ${context.getString(R.string.episodes)}"
episodes.text = text
episodes.text = context.getString(R.string.episodes)
bannerView.setImageURI(item.banner ?: item.image)
totalEpisodes.text = item.totalEpisodeList
} else if (style == 1) {

View File

@@ -6,6 +6,7 @@ import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -30,13 +31,9 @@ import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.findValidName
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -48,7 +45,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
@@ -92,7 +88,9 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val color = requireContext().getThemeColor(android.R.attr.windowBackground)
val typedValue = TypedValue()
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
@@ -177,7 +175,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
downloadManager.animeDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
media?.let {
lifecycleScope.launch {
val mediaModel = getMedia(it)
@@ -203,24 +201,25 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val type: MediaType = MediaType.ANIME
// Alert dialog to confirm deletion
requireContext().customAlertDialog().apply {
setTitle("Delete ${item.title}?")
setMessage("Are you sure you want to delete ${item.title}?")
setPosButton(R.string.yes) {
downloadManager.removeMedia(item.title, type)
val mediaIds =
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
?: emptySet()
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
getDownloads()
val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
val mediaIds =
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
?: emptySet()
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
setNegButton(R.string.no) {
// Do nothing
}
show()
getDownloads()
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
}
@@ -288,15 +287,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
}
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val animeTitles =
downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads =
downloadManager.animeDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
if (offlineAnimeModel.title == "unknown") offlineAnimeModel.title = title
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
@@ -317,24 +313,21 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
return try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl()
SChapterImpl() // Provide an instance of SChapterImpl
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl()
SAnimeImpl() // Provide an instance of SAnimeImpl
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl()
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val media = directory?.findFile("media.json")
if (media == null) {
Logger.log("No media.json found at ${directory?.uri?.path}")
return loadMediaCompat(downloadedType)
}
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
@@ -359,7 +352,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = directory?.findFile("cover.jpg")
@@ -370,7 +363,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
@@ -399,27 +391,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri
)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
return try {
loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
OfflineAnimeModel(
downloadedType.titleName,
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}

View File

@@ -3,7 +3,7 @@ package ani.dantotsu.download.anime
import android.net.Uri
data class OfflineAnimeModel(
var title: String,
val title: String,
val score: String,
val totalEpisode: String,
val totalEpisodeList: String,

View File

@@ -32,7 +32,6 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import ani.dantotsu.util.NumberConverter.Companion.ofLength
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
@@ -135,15 +134,15 @@ class MangaDownloaderService : Service() {
mutex.withLock {
downloadJobs[task.chapter] = job
}
job.join()
job.join() // Wait for the job to complete before continuing to the next task
mutex.withLock {
downloadJobs.remove(task.chapter)
}
updateNotification()
updateNotification() // Update the notification after each task is completed
}
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf()
stopSelf() // Stop the service when the queue is empty
}
}
}
@@ -182,7 +181,7 @@ class MangaDownloaderService : Service() {
suspend fun download(task: DownloadTask) {
try {
withContext(Dispatchers.IO) {
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@MangaDownloaderService,
@@ -195,27 +194,18 @@ class MangaDownloaderService : Service() {
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
val baseOutputDir = getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title
) ?: throw Exception("Base output directory not found")
val outputDir = getSubDirectory(
getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
) ?: throw Exception("Output directory not found")
outputDir.deleteRecursively(this@MangaDownloaderService, true)
)?.deleteRecursively(this@MangaDownloaderService)
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
if (deferredMap.size >= task.simultaneousDownloads) {
@@ -236,66 +226,60 @@ class MangaDownloaderService : Service() {
}
if (bitmap != null) {
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
}
farthest++
builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress(
task.uniqueName,
task.chapter,
farthest * 100 / task.imageData.size
)
if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
bitmap
}
}
// Wait for any remaining deferred to complete
deferredMap.values.awaitAll()
withContext(Dispatchers.Main) {
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
notificationManager.notify(NOTIFICATION_ID, builder.build())
saveMediaInfo(task, baseOutputDir)
saveMediaInfo(task)
downloadsManager.addDownload(
DownloadedType(
task.title,
task.chapter,
MediaType.MANGA,
scanlator = task.scanlator,
MediaType.MANGA
)
)
broadcastDownloadFinished(task.uniqueName)
broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.uniqueName)
broadcastDownloadFailed(task.chapter)
}
}
private fun saveToDisk(
fileName: String,
directory: DocumentFile,
bitmap: Bitmap
) {
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try {
// Define the directory within the private external storage space
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
file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
@@ -308,8 +292,11 @@ class MangaDownloaderService : Service() {
}
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
private fun saveMediaInfo(task: DownloadTask) {
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")
@@ -424,15 +411,11 @@ class MangaDownloaderService : Service() {
data class DownloadTask(
val title: String,
val chapter: String,
val scanlator: String,
val imageData: List<ImageData>,
val sourceMedia: Media? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
) {
val uniqueName: String
get() = "$chapter-$scanlator"
}
)
companion object {
private const val NOTIFICATION_ID = 1103

View File

@@ -57,8 +57,7 @@ class OfflineMangaAdapter(
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val chapters = view.findViewById<TextView>(R.id.itemTotal)
val text = " ${context.getString(R.string.chapters)}"
chapters.text = text
chapters.text = context.getString(R.string.chapters)
bannerView.setImageURI(item.banner ?: item.image)
totalChapter.text = item.totalChapter
} else if (style == 1) {

View File

@@ -5,6 +5,7 @@ import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -27,14 +28,10 @@ import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat
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.download.findValidName
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
@@ -46,7 +43,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
@@ -86,7 +82,9 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val color = requireContext().getThemeColor(android.R.attr.windowBackground)
val typedValue = TypedValue()
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
@@ -171,12 +169,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media =
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
?: downloadManager.novelDownloadedTypes.firstOrNull {
it.titleName.compareName(
item.title
)
}
downloadManager.mangaDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title.compareName(item.title) }
media?.let {
lifecycleScope.launch {
ContextCompat.startActivity(
@@ -196,21 +190,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val type: MediaType =
if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
MediaType.MANGA
} else {
MediaType.NOVEL
}
// Alert dialog to confirm deletion
requireContext().customAlertDialog().apply {
setTitle("Delete ${item.title}?")
setMessage("Are you sure you want to delete ${item.title}?")
setPosButton(R.string.yes) {
downloadManager.removeMedia(item.title, type)
getDownloads()
}
setNegButton(R.string.no)
}.show()
val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
}
@@ -280,23 +278,20 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloads = listOf()
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val mangaTitles =
downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads =
downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
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.titleName }.distinct()
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads =
downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
@@ -320,18 +315,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
return try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
false, downloadedType.title
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl()
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val media = directory?.findFile("media.json")
if (media == null) {
Logger.log("No media.json found at ${directory?.uri?.path}")
return DownloadCompat.loadMediaCompat(downloadedType)
}
?: return null
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
@@ -347,10 +339,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
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.titleName
false, downloadedType.title
)
val mediaModel = getMedia(downloadedType)!!
val cover = directory?.findFile("cover.jpg")
@@ -361,7 +354,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
@@ -384,26 +376,21 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri
)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
return try {
loadOfflineMangaModelCompat(downloadedType)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
downloadedType.titleName,
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}

View File

@@ -239,13 +239,6 @@ class NovelDownloaderService : Service() {
return@withContext
}
val baseDirectory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
// Start the download
withContext(Dispatchers.IO) {
try {
@@ -341,7 +334,7 @@ class NovelDownloaderService : Service() {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
saveMediaInfo(task, baseDirectory)
saveMediaInfo(task)
downloadsManager.addDownload(
DownloadedType(
task.title,
@@ -361,8 +354,15 @@ class NovelDownloaderService : Service() {
}
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
private fun saveMediaInfo(task: DownloadTask) {
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")

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.download.video
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
@@ -11,32 +12,18 @@ import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
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.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
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.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.Executors
@SuppressLint("UnsafeOptInUsageError")
object Helper {
@@ -46,8 +33,7 @@ object Helper {
title: String,
episode: String,
video: Video,
subtitle: List<Pair<String, String>> = emptyList(),
audio: List<Pair<String, String>> = emptyList(),
subtitle: Subtitle? = null,
sourceMedia: Media? = null,
episodeImage: String? = null
) {
@@ -66,24 +52,23 @@ object Helper {
episode,
video,
subtitle,
audio,
sourceMedia,
episodeImage
)
val downloadsManager = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManager
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, MediaType.ANIME)
if (downloadCheck) {
context.customAlertDialog().apply {
setTitle("Download Exists")
setMessage("A download for this episode already exists. Do you want to overwrite it?")
setPosButton(R.string.yes) {
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") { _, _ ->
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManager.removeDownload(
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
@@ -98,9 +83,8 @@ object Helper {
}
}
}
setNegButton(R.string.no)
show()
}
.setNegativeButton("No") { _, _ -> }
.show()
} else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
@@ -120,98 +104,4 @@ object Helper {
}
return true
}
@Synchronized
@UnstableApi
@Deprecated("exoplayer download manager is no longer used")
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) {
Logger.log("Download Completed")
} else if (download.state == Download.STATE_FAILED) {
Logger.log("Download Failed")
} else if (download.state == Download.STATE_STOPPED) {
Logger.log("Download Stopped")
} else if (download.state == Download.STATE_QUEUED) {
Logger.log("Download Queued")
} else if (download.state == Download.STATE_DOWNLOADING) {
Logger.log("Download Downloading")
}
}
}
)
downloadManager
}
}
@Deprecated("exoplayer download manager is no longer used")
@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!!
}
}
@Synchronized
@Deprecated("exoplayer download manager is no longer used")
private fun getDownloadDirectory(context: Context): File {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null)
if (downloadDirectory == null) {
downloadDirectory = context.filesDir
}
}
return downloadDirectory!!
}
@Deprecated("exoplayer download manager is no longer used")
private var download: DownloadManager? = null
@Deprecated("exoplayer download manager is no longer used")
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Deprecated("exoplayer download manager is no longer used")
private var simpleCache: SimpleCache? = null
@Deprecated("exoplayer download manager is no longer used")
private var downloadDirectory: File? = null
}

View File

@@ -22,9 +22,9 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentAnimeBinding
import ani.dantotsu.media.MediaAdaptor
@@ -36,9 +36,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -100,7 +98,7 @@ class AnimeFragment : Fragment() {
var loading = true
if (model.notSet) {
model.notSet = false
model.aniMangaSearchResults = AniMangaSearchResults(
model.searchResults = SearchResults(
"ANIME",
isAdult = false,
onList = false,
@@ -109,7 +107,7 @@ class AnimeFragment : Fragment() {
sort = Anilist.sortBy[1]
)
}
val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity())
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
val progressAdaptor = ProgressAdapter(searched = model.searched)
val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor)
binding.animePageRecyclerView.adapter = adapter
@@ -142,7 +140,7 @@ class AnimeFragment : Fragment() {
animePageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked
loading = true
model.aniMangaSearchResults.results.clear()
model.searchResults.results.clear()
popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) {
model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked)
@@ -152,17 +150,17 @@ class AnimeFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) {
if (it != null) {
if (oldIncludeList == (it.onList != false)) {
val prev = model.aniMangaSearchResults.results.size
model.aniMangaSearchResults.results.addAll(it.results)
val prev = model.searchResults.results.size
model.searchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else {
model.aniMangaSearchResults.results.addAll(it.results)
model.searchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true
}
model.aniMangaSearchResults.onList = it.onList
model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.aniMangaSearchResults.page = it.page
model.searchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page
if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE
else {
@@ -177,10 +175,10 @@ class AnimeFragment : Fragment() {
RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) {
if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
scope.launch(Dispatchers.IO) {
loading = true
model.loadNextPage(model.aniMangaSearchResults)
model.loadNextPage(model.searchResults)
}
}
}
@@ -206,22 +204,22 @@ class AnimeFragment : Fragment() {
if (i) {
model.getUpdated().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()), it)
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMovies().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()), it)
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()))
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()), it)
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()), it)
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
}
}
if (animePageAdapter.trendingViewPager != null) {
@@ -270,44 +268,25 @@ class AnimeFragment : Fragment() {
true
}
var running = false
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(false) }
live.observe(viewLifecycleOwner) {
if (it && !running) {
running = true
if (it) {
scope.launch {
withContext(Dispatchers.IO) {
Anilist.userid =
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
getUserId(requireContext()) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
getUserId(requireContext()) {
load()
}
}
getUserId(requireContext()) {
load()
}
}
model.loaded = true
val loadTrending = async(Dispatchers.IO) { model.loadTrending(1) }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loaded = true
model.loadTrending(1)
model.loadAll()
model.loadPopular(
"ANIME",
sort = Anilist.sortBy[1],
onList = PrefManager.getVal(PrefName.PopularAnimeList)
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularAnimeList
)
)
}
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false
running = false
}
}
}

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.home
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
@@ -20,16 +21,13 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.MediaPageTransformer
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.getAppString
import ani.dantotsu.getThemeColor
import ani.dantotsu.loadImage
import ani.dantotsu.media.CalendarActivity
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.MediaListViewActivity
import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px
@@ -69,7 +67,10 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val color = binding.root.context.getThemeColor(android.R.attr.windowBackground)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
@@ -81,21 +82,13 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
updateAvatar()
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBar.hint = "ANIME"
trendingBinding.searchBarText.setOnClickListener {
val context = binding.root.context
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
}
trendingBinding.userAvatar.setSafeOnClickListener {
@@ -117,8 +110,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendingBinding.searchBar.performClick()
}
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf(
@@ -203,16 +196,13 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
LayoutAnimationController(setSlideIn(), 0.25f)
}
fun updateRecent(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateRecent(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
animeUpdatedRecyclerView,
animeUpdatedProgressBar,
animeRecently,
animeRecentlyMore,
getAppString(R.string.updated),
media
animeRecently
)
animePopular.visibility = View.VISIBLE
animePopular.startAnimation(setSlideUp())
@@ -223,57 +213,40 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
fun updateMovies(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateMovies(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
animeMoviesRecyclerView,
animeMoviesProgressBar,
animeMovies,
animeMoviesMore,
getAppString(R.string.trending_movies),
media
animeMovies
)
}
}
fun updateTopRated(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
animeTopRatedRecyclerView,
animeTopRatedProgressBar,
animeTopRated,
animeTopRatedMore,
getAppString(R.string.top_rated),
media
animeTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
animeMostFavRecyclerView,
animeMostFavProgressBar,
animeMostFav,
animeMostFavMore,
getAppString(R.string.most_favourite),
media
animeMostFav
)
}
}
fun init(
adaptor: MediaAdaptor,
recyclerView: RecyclerView,
progress: View,
title: View,
more: View,
string: String,
media: MutableList<Media>
) {
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
@@ -282,20 +255,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
LinearLayoutManager.HORIZONTAL,
false
)
more.setOnClickListener {
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string),
null
)
}
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
more.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
more.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
}
@@ -309,8 +271,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
}
}

View File

@@ -30,11 +30,9 @@ import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentHomeBinding
import ani.dantotsu.home.status.UserStatusAdapter
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.MediaListViewActivity
import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.ProfileActivity
@@ -43,14 +41,12 @@ import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.math.max
import kotlin.math.min
@@ -59,6 +55,7 @@ import kotlin.math.min
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -77,9 +74,7 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scope = lifecycleScope
Logger.log("HomeFragment")
fun load() {
Logger.log("Loading HomeFragment")
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
binding.homeUserName.text = Anilist.username
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
@@ -92,7 +87,6 @@ class HomeFragment : Fragment() {
)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener {
@@ -133,12 +127,6 @@ class HomeFragment : Fragment() {
"dialog"
)
}
binding.searchImageContainer.setSafeOnClickListener {
SearchBottomSheet.newInstance().show(
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
"search"
)
}
binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
@@ -217,16 +205,13 @@ class HomeFragment : Fragment() {
recyclerView: RecyclerView,
progress: View,
empty: View,
title: View,
more: View,
string: String
title: View
) {
container.visibility = View.VISIBLE
progress.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
empty.visibility = View.GONE
title.visibility = View.INVISIBLE
more.visibility = View.INVISIBLE
mode.observe(viewLifecycleOwner) {
recyclerView.visibility = View.GONE
@@ -239,14 +224,6 @@ class HomeFragment : Fragment() {
LinearLayoutManager.HORIZONTAL,
false
)
more.setOnClickListener { i ->
MediaListViewActivity.passedMedia = it
ContextCompat.startActivity(
i.context, Intent(i.context, MediaListViewActivity::class.java)
.putExtra("title", string),
null
)
}
recyclerView.visibility = View.VISIBLE
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
@@ -254,9 +231,7 @@ class HomeFragment : Fragment() {
} else {
empty.visibility = View.VISIBLE
}
more.visibility = View.VISIBLE
title.visibility = View.VISIBLE
more.startAnimation(setSlideUp())
title.startAnimation(setSlideUp())
progress.visibility = View.GONE
}
@@ -271,9 +246,7 @@ class HomeFragment : Fragment() {
binding.homeWatchingRecyclerView,
binding.homeWatchingProgressBar,
binding.homeWatchingEmpty,
binding.homeContinueWatch,
binding.homeContinueWatchMore,
getString(R.string.continue_watching)
binding.homeContinueWatch
)
binding.homeWatchingBrowseButton.setOnClickListener {
bottomBar.selectTabAt(0)
@@ -285,9 +258,7 @@ class HomeFragment : Fragment() {
binding.homeFavAnimeRecyclerView,
binding.homeFavAnimeProgressBar,
binding.homeFavAnimeEmpty,
binding.homeFavAnime,
binding.homeFavAnimeMore,
getString(R.string.fav_anime)
binding.homeFavAnime
)
initRecyclerView(
@@ -296,9 +267,7 @@ class HomeFragment : Fragment() {
binding.homePlannedAnimeRecyclerView,
binding.homePlannedAnimeProgressBar,
binding.homePlannedAnimeEmpty,
binding.homePlannedAnime,
binding.homePlannedAnimeMore,
getString(R.string.planned_anime)
binding.homePlannedAnime
)
binding.homePlannedAnimeBrowseButton.setOnClickListener {
bottomBar.selectTabAt(0)
@@ -310,9 +279,7 @@ class HomeFragment : Fragment() {
binding.homeReadingRecyclerView,
binding.homeReadingProgressBar,
binding.homeReadingEmpty,
binding.homeContinueRead,
binding.homeContinueReadMore,
getString(R.string.continue_reading)
binding.homeContinueRead
)
binding.homeReadingBrowseButton.setOnClickListener {
bottomBar.selectTabAt(2)
@@ -324,9 +291,7 @@ class HomeFragment : Fragment() {
binding.homeFavMangaRecyclerView,
binding.homeFavMangaProgressBar,
binding.homeFavMangaEmpty,
binding.homeFavManga,
binding.homeFavMangaMore,
getString(R.string.fav_manga)
binding.homeFavManga
)
initRecyclerView(
@@ -335,9 +300,7 @@ class HomeFragment : Fragment() {
binding.homePlannedMangaRecyclerView,
binding.homePlannedMangaProgressBar,
binding.homePlannedMangaEmpty,
binding.homePlannedManga,
binding.homePlannedMangaMore,
getString(R.string.planned_manga)
binding.homePlannedManga
)
binding.homePlannedMangaBrowseButton.setOnClickListener {
bottomBar.selectTabAt(2)
@@ -349,87 +312,12 @@ class HomeFragment : Fragment() {
binding.homeRecommendedRecyclerView,
binding.homeRecommendedProgressBar,
binding.homeRecommendedEmpty,
binding.homeRecommended,
binding.homeRecommendedMore,
getString(R.string.recommended)
binding.homeRecommended
)
binding.homeUserStatusContainer.visibility = View.VISIBLE
binding.homeUserStatusProgressBar.visibility = View.VISIBLE
binding.homeUserStatusRecyclerView.visibility = View.GONE
model.getUserStatus().observe(viewLifecycleOwner) {
binding.homeUserStatusRecyclerView.visibility = View.GONE
if (it != null) {
if (it.isNotEmpty()) {
PrefManager.getLiveVal(PrefName.RefreshStatus, false).apply {
asLiveBool()
observe(viewLifecycleOwner) { _ ->
binding.homeUserStatusRecyclerView.adapter = UserStatusAdapter(it)
}
}
binding.homeUserStatusRecyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
binding.homeUserStatusRecyclerView.visibility = View.VISIBLE
binding.homeUserStatusRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
} else {
binding.homeUserStatusContainer.visibility = View.GONE
}
binding.homeUserStatusProgressBar.visibility = View.GONE
}
}
binding.homeHiddenItemsContainer.visibility = View.GONE
model.getHidden().observe(viewLifecycleOwner) {
if (it != null) {
if (it.isNotEmpty()) {
binding.homeHiddenItemsRecyclerView.adapter =
MediaAdaptor(0, it, requireActivity())
binding.homeHiddenItemsRecyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
binding.homeContinueWatch.setOnLongClickListener {
binding.homeHiddenItemsContainer.visibility = View.VISIBLE
binding.homeHiddenItemsRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
true
}
binding.homeHiddenItemsMore.setSafeOnClickListener { _ ->
MediaListViewActivity.passedMedia = it
ContextCompat.startActivity(
requireActivity(),
Intent(requireActivity(), MediaListViewActivity::class.java)
.putExtra("title", getString(R.string.hidden)),
null
)
}
binding.homeHiddenItemsTitle.setOnLongClickListener {
binding.homeHiddenItemsContainer.visibility = View.GONE
true
}
} else {
binding.homeContinueWatch.setOnLongClickListener {
snackString(getString(R.string.no_hidden_items))
true
}
}
} else {
binding.homeContinueWatch.setOnLongClickListener {
snackString(getString(R.string.no_hidden_items))
true
}
}
}
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
model.empty.observe(viewLifecycleOwner)
{
model.empty.observe(viewLifecycleOwner) {
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
(binding.homeDantotsuIcon.drawable as Animatable).start()
binding.homeDantotsuContainer.startAnimation(setSlideUp())
@@ -446,8 +334,7 @@ class HomeFragment : Fragment() {
"MangaContinue",
"MangaFav",
"MangaPlanned",
"Recommendation",
"UserStatus",
"Recommendation"
)
val containers = arrayOf(
@@ -457,62 +344,37 @@ class HomeFragment : Fragment() {
binding.homeContinueReadingContainer,
binding.homeFavMangaContainer,
binding.homePlannedMangaContainer,
binding.homeRecommendedContainer,
binding.homeUserStatusContainer,
binding.homeRecommendedContainer
)
var running = false
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
live.observe(viewLifecycleOwner) { shouldRefresh ->
if (!running && shouldRefresh) {
running = true
val live = Refresh.activity.getOrPut(1) { MutableLiveData(false) }
live.observe(viewLifecycleOwner) {
if (it) {
scope.launch {
withContext(Dispatchers.IO) {
// Get user data first
Anilist.userid =
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
withContext(Dispatchers.Main) {
getUserId(requireContext()) {
load()
}
}
} else {
getUserId(requireContext()) {
load()
}
//Get userData First
getUserId(requireContext()) {
load()
}
model.loaded = true
model.setListImages()
}
var empty = true
val homeLayoutShow: List<Boolean> = PrefManager.getVal(PrefName.HomeLayout)
withContext(Dispatchers.Main) {
homeLayoutShow.indices.forEach { i ->
var empty = true
val homeLayoutShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayoutShow)
runBlocking {
model.initHomePage()
}
(array.indices).forEach { i ->
if (homeLayoutShow.elementAt(i)) {
empty = false
} else {
} else withContext(Dispatchers.Main) {
containers[i].visibility = View.GONE
}
}
}
val initHomePage = async(Dispatchers.IO) { model.initHomePage() }
val initUserStatus = async(Dispatchers.IO) { model.initUserStatus() }
initHomePage.await()
initUserStatus.await()
withContext(Dispatchers.Main) {
model.empty.postValue(empty)
binding.homeHiddenItemsContainer.visibility = View.GONE
}
live.postValue(false)
_binding?.homeRefresh?.isRefreshing = false
running = false
}
}
}
@@ -522,7 +384,6 @@ class HomeFragment : Fragment() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume()

View File

@@ -1,23 +1,24 @@
package ani.dantotsu.home
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.FragmentLoginBinding
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.textfield.TextInputEditText
class LoginFragment : Fragment() {
@@ -93,31 +94,38 @@ class LoginFragment : Fragment() {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
userAgentTextBox.hint = "Password"
subtitle.visibility = View.VISIBLE
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
}
val dialogView =
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null)
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
subtitleTextView?.visibility = View.VISIBLE
subtitleTextView?.text = "Enter your password to decrypt the file"
requireActivity().customAlertDialog().apply {
setTitle("Enter Password")
setCustomView(dialogView.root)
setPosButton(R.string.ok) {
val editText = dialogView.userAgentTextBox
if (editText.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
callback(password)
} else {
toast("Password cannot be empty")
}
}
setNegButton(R.string.cancel) {
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup)
.setTitle("Enter Password")
.setView(dialogView)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ ->
password.fill('0')
dialog.dismiss()
callback(null)
}
}.show()
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
}
}
private fun restartApp() {

View File

@@ -20,9 +20,9 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMangaViewModel
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.media.MediaAdaptor
@@ -33,9 +33,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -94,7 +92,7 @@ class MangaFragment : Fragment() {
var loading = true
if (model.notSet) {
model.notSet = false
model.aniMangaSearchResults = AniMangaSearchResults(
model.searchResults = SearchResults(
"MANGA",
isAdult = false,
onList = false,
@@ -103,7 +101,7 @@ class MangaFragment : Fragment() {
sort = Anilist.sortBy[1]
)
}
val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity())
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
val progressAdaptor = ProgressAdapter(searched = model.searched)
binding.mangaPageRecyclerView.adapter =
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
@@ -135,10 +133,10 @@ class MangaFragment : Fragment() {
RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) {
if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
scope.launch(Dispatchers.IO) {
loading = true
model.loadNextPage(model.aniMangaSearchResults)
model.loadNextPage(model.searchResults)
}
}
}
@@ -164,15 +162,12 @@ class MangaFragment : Fragment() {
if (i == true) {
model.getPopularNovel().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()), it)
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
}
}
model.getPopularManga().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManga(
MediaAdaptor(0, it, requireActivity()),
it
)
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
}
}
model.getPopularManhwa().observe(viewLifecycleOwner) {
@@ -182,18 +177,18 @@ class MangaFragment : Fragment() {
0,
it,
requireActivity()
), it
)
)
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()), it)
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()), it)
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
}
}
if (mangaPageAdapter.trendingViewPager != null) {
@@ -223,7 +218,7 @@ class MangaFragment : Fragment() {
mangaPageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked
loading = true
model.aniMangaSearchResults.results.clear()
model.searchResults.results.clear()
popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) {
model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked)
@@ -233,17 +228,17 @@ class MangaFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) {
if (it != null) {
if (oldIncludeList == (it.onList != false)) {
val prev = model.aniMangaSearchResults.results.size
model.aniMangaSearchResults.results.addAll(it.results)
val prev = model.searchResults.results.size
model.searchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else {
model.aniMangaSearchResults.results.addAll(it.results)
model.searchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true
}
model.aniMangaSearchResults.onList = it.onList
model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.aniMangaSearchResults.page = it.page
model.searchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page
if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE
else {
@@ -258,46 +253,25 @@ class MangaFragment : Fragment() {
mangaPageAdapter.updateAvatar()
}
var running = false
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(false) }
live.observe(viewLifecycleOwner) {
if (!running && it) {
running = true
if (it) {
scope.launch {
withContext(Dispatchers.IO) {
Anilist.userid =
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
getUserId(requireContext()) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
getUserId(requireContext()) {
load()
}
}
getUserId(requireContext()) {
load()
}
}
model.loaded = true
val loadTrending = async(Dispatchers.IO) { model.loadTrending() }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loaded = true
model.loadTrending()
model.loadAll()
model.loadPopular(
"MANGA",
sort = Anilist.sortBy[1],
onList = PrefManager.getVal(PrefName.PopularAnimeList)
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularMangaList
)
)
}
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false
running = false
}
}
}

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.home
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
@@ -20,15 +21,12 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.MediaPageTransformer
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.getAppString
import ani.dantotsu.getThemeColor
import ani.dantotsu.loadImage
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.MediaListViewActivity
import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px
@@ -68,7 +66,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val color = binding.root.context.getThemeColor(android.R.attr.windowBackground)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
@@ -80,23 +81,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
updateAvatar()
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBar.hint = "MANGA"
trendingBinding.searchBarText.setOnClickListener {
val context = binding.root.context
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", "MANGA"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
null
)
}
trendingBinding.userAvatar.setSafeOnClickListener {
@@ -190,87 +182,65 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
}
fun updateTrendingManga(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateTrendingManga(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingMangaRecyclerView,
mangaTrendingMangaProgressBar,
mangaTrendingManga,
mangaTrendingMangaMore,
getAppString(R.string.trending_manga),
media
mangaTrendingManga
)
}
}
fun updateTrendingManhwa(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingManhwaRecyclerView,
mangaTrendingManhwaProgressBar,
mangaTrendingManhwa,
mangaTrendingManhwaMore,
getAppString(R.string.trending_manhwa),
media
mangaTrendingManhwa
)
}
}
fun updateNovel(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateNovel(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaNovelRecyclerView,
mangaNovelProgressBar,
mangaNovel,
mangaNovelMore,
getAppString(R.string.trending_novel),
media
mangaNovel
)
}
}
fun updateTopRated(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTopRatedRecyclerView,
mangaTopRatedProgressBar,
mangaTopRated,
mangaTopRatedMore,
getAppString(R.string.top_rated),
media
mangaTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor, media: MutableList<Media>) {
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaMostFavRecyclerView,
mangaMostFavProgressBar,
mangaMostFav,
mangaMostFavMore,
getAppString(R.string.most_favourite),
media
mangaMostFav
)
mangaPopular.visibility = View.VISIBLE
mangaPopular.startAnimation(setSlideUp())
}
}
fun init(
adaptor: MediaAdaptor,
recyclerView: RecyclerView,
progress: View,
title: View,
more: View,
string: String,
media: MutableList<Media>
) {
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
@@ -279,19 +249,9 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
LinearLayoutManager.HORIZONTAL,
false
)
more.setOnClickListener {
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string),
null
)
}
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
more.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
more.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
}
@@ -305,8 +265,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
}
}

View File

@@ -1,74 +0,0 @@
package ani.dantotsu.home
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString
import ani.dantotsu.databinding.BottomSheetSearchBinding
import ani.dantotsu.media.SearchActivity
class SearchBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetSearchBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSearchBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.animeSearch.setOnClickListener {
startActivity(requireContext(), SearchType.ANIME)
dismiss()
}
binding.mangaSearch.setOnClickListener {
startActivity(requireContext(), SearchType.MANGA)
dismiss()
}
binding.characterSearch.setOnClickListener {
startActivity(requireContext(), SearchType.CHARACTER)
dismiss()
}
binding.staffSearch.setOnClickListener {
startActivity(requireContext(), SearchType.STAFF)
dismiss()
}
binding.studioSearch.setOnClickListener {
startActivity(requireContext(), SearchType.STUDIO)
dismiss()
}
binding.userSearch.setOnClickListener {
startActivity(requireContext(), SearchType.USER)
dismiss()
}
}
private fun startActivity(context: Context, type: SearchType) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", type.toAnilistString()),
null
)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
fun newInstance() = SearchBottomSheet()
}
}

View File

@@ -1,83 +0,0 @@
package ani.dantotsu.home.status
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import ani.dantotsu.getThemeColor
class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var parts: Int = 3
private var gapAngle: Float = 12f
private val path = Path()
private var isUser = false
private var booleanList = listOf<Boolean>()
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 6f
strokeCap = Paint.Cap.ROUND
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2f
val centerY = height / 2f
val radius = centerX.coerceAtMost(centerY) - paint.strokeWidth / 2
val totalGapAngle = gapAngle * (parts)
val totalAngle = 360f - totalGapAngle
val primaryColor = context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondColor = context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)
fun setColor(int: Int) {
paint.color = if (int < booleanList.size && booleanList[int]) {
Color.GRAY
} else {
if (isUser) secondColor else primaryColor
}
canvas.drawPath(path, paint)
}
if (parts == 1) {
path.addArc(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius,
0f,
360f
)
setColor(0)
} else {
val effectiveAngle = totalAngle / parts
for (i in 0 until parts) {
val startAngle = i * (effectiveAngle + gapAngle) - 90f
path.reset()
path.addArc(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius,
startAngle,
effectiveAngle
)
setColor(i)
}
}
}
fun setParts(parts: Int, list: List<Boolean> = mutableListOf(), isUser: Boolean) {
this.parts = parts
this.booleanList = list
this.isUser = isUser
invalidate()
}
}

View File

@@ -1,124 +0,0 @@
package ani.dantotsu.home.status
import android.os.Bundle
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.ActivityStatusBinding
import ani.dantotsu.home.status.listener.StoriesCallback
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
class StatusActivity : AppCompatActivity(), StoriesCallback {
private lateinit var activity: ArrayList<User>
private lateinit var binding: ActivityStatusBinding
private var position: Int = -1
private lateinit var slideInLeft: Animation
private lateinit var slideOutRight: Animation
private lateinit var slideOutLeft: Animation
private lateinit var slideInRight: Animation
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityStatusBinding.inflate(layoutInflater)
setContentView(binding.root)
activity = user
position = intent.getIntExtra("position", -1)
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
slideInLeft = AnimationUtils.loadAnimation(this, R.anim.slide_in_left)
slideOutRight = AnimationUtils.loadAnimation(this, R.anim.slide_out_right)
slideOutLeft = AnimationUtils.loadAnimation(this, R.anim.slide_out_left)
slideInRight = AnimationUtils.loadAnimation(this, R.anim.slide_in_right)
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
if (activity.getOrNull(position) != null) {
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.setStoriesList(
activityList = activity[position].activity,
startIndex = startIndex + 1
)
} else {
Logger.log("index out of bounds for position $position of size ${activity.size}")
finish()
}
}
private fun findFirstNonMatch(watchedActivity: Set<Int>, activity: List<Activity>): Int {
for (activityItem in activity) {
if (activityItem.id !in watchedActivity) {
return activity.indexOf(activityItem)
}
}
return -1
}
override fun onPause() {
super.onPause()
binding.stories.pause()
}
override fun onResume() {
super.onResume()
if (hasWindowFocus())
binding.stories.resume()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
binding.stories.resume()
} else {
binding.stories.pause()
}
}
override fun onStoriesEnd() {
position += 1
if (position < activity.size) {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutLeft)
binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
binding.stories.startAnimation(slideInRight)
} else {
finish()
}
}
override fun onStoriesStart() {
position -= 1
if (position >= 0 && activity[position].activity.isNotEmpty()) {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutRight)
binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
binding.stories.startAnimation(slideInLeft)
} else {
finish()
}
}
companion object {
var user: ArrayList<User> = arrayListOf()
}
}

View File

@@ -1,539 +0,0 @@
package ani.dantotsu.home.status
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.ProgressBar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.buildMarkwon
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.FragmentStatusBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.home.status.listener.StoriesCallback
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.profile.UsersDialogFragment
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.profile.activity.RepliesBottomDialog
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.AniMarkdown
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Calendar
import java.util.Locale
import kotlin.math.abs
class Stories @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
private lateinit var binding: FragmentStatusBinding
private lateinit var activityList: List<Activity>
private lateinit var storiesListener: StoriesCallback
private var userClicked: Boolean = false
private var storyIndex: Int = 1
private var primaryColor: Int = 0
private var onPrimaryColor: Int = 0
private var storyDuration: Int = 6
private val timer: StoryTimer = StoryTimer(secondsToMillis(storyDuration))
init {
initLayout()
}
@SuppressLint("ClickableViewAccessibility")
fun initLayout() {
val inflater: LayoutInflater = LayoutInflater.from(context)
binding = FragmentStatusBinding.inflate(inflater, this, false)
addView(binding.root)
primaryColor = context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
onPrimaryColor = context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)
if (context is StoriesCallback) storiesListener = context as StoriesCallback
binding.touchPanel.setOnTouchListener(this)
}
fun setStoriesList(
activityList: List<Activity>, startIndex: Int = 1
) {
this.activityList = activityList
this.storyIndex = startIndex
addLoadingViews(activityList)
}
private fun addLoadingViews(storiesList: List<Activity>) {
var idCounter = 1
for (story in storiesList) {
binding.progressBarContainer.removeView(findViewWithTag<ProgressBar>("story${idCounter}"))
val progressBar = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal)
progressBar.visibility = View.VISIBLE
progressBar.id = idCounter
progressBar.tag = "story${idCounter++}"
progressBar.progressBackgroundTintList = ColorStateList.valueOf(primaryColor)
progressBar.progressTintList = ColorStateList.valueOf(onPrimaryColor)
val params = LayoutParams(0, LayoutParams.WRAP_CONTENT)
params.marginEnd = 5
params.marginStart = 5
binding.progressBarContainer.addView(progressBar, params)
}
val constraintSet = ConstraintSet()
constraintSet.clone(binding.progressBarContainer)
var counter = storiesList.size
for (story in storiesList) {
val progressBar = findViewWithTag<ProgressBar>("story${counter}")
if (progressBar != null) {
if (storiesList.size > 1) {
when (counter) {
storiesList.size -> {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
LayoutParams.PARENT_ID,
ConstraintSet.END
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
getId("story${counter - 1}"),
ConstraintSet.END
)
}
1 -> {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
LayoutParams.PARENT_ID,
ConstraintSet.START
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
getId("story${counter + 1}"),
ConstraintSet.START
)
}
else -> {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
getId("story${counter - 1}"),
ConstraintSet.END
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
getId("story${counter + 1}"),
ConstraintSet.START
)
}
}
} else {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
LayoutParams.PARENT_ID,
ConstraintSet.END
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
LayoutParams.PARENT_ID,
ConstraintSet.START
)
}
}
counter--
}
constraintSet.applyTo(binding.progressBarContainer)
startShowContent()
}
private fun startShowContent() {
showStory()
}
private fun showStory() {
if (storyIndex > 1) {
completeProgressBar(storyIndex - 1)
}
val progressBar = findViewWithTag<ProgressBar>("story${storyIndex}")
binding.androidStoriesLoadingView.visibility = View.VISIBLE
timer.setOnTimerCompletedListener {
Logger.log("onAnimationEnd: $storyIndex")
if (storyIndex - 1 <= activityList.size) {
Logger.log("userNotClicked: $storyIndex")
if (storyIndex < activityList.size) {
storyIndex += 1
showStory()
} else {
// on stories end
binding.androidStoriesLoadingView.visibility = View.GONE
onStoriesCompleted()
}
} else {
// on stories end
binding.androidStoriesLoadingView.visibility = View.GONE
onStoriesCompleted()
}
}
timer.setOnPercentTickListener {
progressBar.progress = it
}
loadStory(activityList[storyIndex - 1])
}
private fun getId(tag: String): Int {
return findViewWithTag<ProgressBar>(tag).id
}
private fun secondsToMillis(seconds: Int): Long {
return (seconds.toLong()).times(1000)
}
private fun resetProgressBar(storyIndex: Int) {
for (i in storyIndex until activityList.size + 1) {
val progressBar = findViewWithTag<ProgressBar>("story${i}")
progressBar?.let {
it.progress = 0
}
}
}
private fun completeProgressBar(storyIndex: Int) {
for (i in 1 until storyIndex + 1) {
val progressBar = findViewWithTag<ProgressBar>("story${i}")
progressBar?.let {
it.progress = 100
}
}
}
private fun rightPanelTouch() {
Logger.log("rightPanelTouch: $storyIndex")
if (storyIndex == activityList.size) {
completeProgressBar(storyIndex)
onStoriesCompleted()
return
}
userClicked = true
timer.cancel()
if (storyIndex <= activityList.size) storyIndex += 1
showStory()
}
private fun leftPanelTouch() {
Logger.log("leftPanelTouch: $storyIndex")
if (storyIndex == 1) {
onStoriesPrevious()
return
}
userClicked = true
timer.cancel()
resetProgressBar(storyIndex)
if (storyIndex > 1) storyIndex -= 1
showStory()
}
private fun onStoriesCompleted() {
Logger.log("onStoriesCompleted")
if (::storiesListener.isInitialized) {
storyIndex = 1
storiesListener.onStoriesEnd()
resetProgressBar(storyIndex)
}
}
private fun onStoriesPrevious() {
if (::storiesListener.isInitialized) {
storyIndex = 1
storiesListener.onStoriesStart()
resetProgressBar(storyIndex)
}
}
fun pause() {
timer.pause()
}
fun resume() {
timer.resume()
}
@SuppressLint("ClickableViewAccessibility")
private fun loadStory(story: Activity) {
val key = "activities"
val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id))
val newList = set.sorted().takeLast(200).toSet()
PrefManager.setCustomVal(key, newList)
binding.statusUserAvatar.loadImage(story.user?.avatar?.large)
binding.statusUserName.text = story.user?.name
binding.statusUserTime.text = ActivityItemBuilder.getDateTime(story.createdAt)
binding.statusUserContainer.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ProfileActivity::class.java).putExtra("userId", story.userId),
null
)
}
binding.textActivity.setOnTouchListener { v, event ->
onTouchView(v, event, true)
v.onTouchEvent(event)
}
binding.textActivityContainer.setOnTouchListener { v, event ->
onTouchView(v, event, true)
v.onTouchEvent(event)
}
fun visible(isList: Boolean) {
binding.textActivity.isVisible = !isList
binding.textActivityContainer.isVisible = !isList
binding.infoText.isVisible = isList
binding.coverImage.isVisible = isList
binding.infoText.visibility = if (isList) View.VISIBLE else View.INVISIBLE
binding.infoText.text = ""
binding.contentImageViewKen.isVisible = isList
binding.contentImageView.isVisible = isList
}
when (story.typename) {
"ListActivity" -> {
visible(true)
val text = "${
story.status?.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.ROOT)
} else {
it.toString()
}
}
} ${story.progress ?: story.media?.title?.userPreferred} " +
if (
story.status?.contains("completed") == false &&
!story.status.contains("plans") &&
!story.status.contains("repeating") &&
!story.status.contains("paused") &&
!story.status.contains("dropped")
) {
"of ${story.media?.title?.userPreferred}"
} else {
""
}
binding.infoText.text = text
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
if (bannerAnimations) binding.contentImageViewKen else binding.contentImageView,
story.media?.bannerImage ?: story.media?.coverImage?.extraLarge
)
binding.coverImage.loadImage(story.media?.coverImage?.extraLarge)
binding.coverImage.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, MediaDetailsActivity::class.java).putExtra(
"mediaId",
story.media?.id
),
ActivityOptionsCompat.makeSceneTransitionAnimation(
(it.context as FragmentActivity),
binding.coverImage,
ViewCompat.getTransitionName(binding.coverImage)!!
).toBundle()
)
}
}
"TextActivity" -> {
visible(false)
if (!(context as android.app.Activity).isDestroyed) {
val markwon = buildMarkwon(context, false)
markwon.setMarkdown(
binding.textActivity, AniMarkdown.getBasicAniHTML(story.text ?: "")
)
}
}
"MessageActivity" -> {
visible(false)
if (!(context as android.app.Activity).isDestroyed) {
val markwon = buildMarkwon(context, false)
markwon.setMarkdown(
binding.textActivity, AniMarkdown.getBasicAniHTML(story.message ?: "")
)
}
}
}
val userList = arrayListOf<User>()
story.likes?.forEach { i ->
userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage))
}
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
binding.replyCount.text = story.replyCount.toString()
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(story.id)
.show((it.context as FragmentActivity).supportFragmentManager, "replies")
}
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.activityLikeCount.text = story.likeCount.toString()
binding.activityLikeContainer.setOnClickListener {
like()
}
binding.activityLikeContainer.setOnLongClickListener {
UsersDialogFragment().apply {
userList(userList)
show((it.context as FragmentActivity).supportFragmentManager, "dialog")
}
true
}
binding.androidStoriesLoadingView.visibility = View.GONE
timer.start()
}
fun like() {
val story = activityList[storyIndex - 1]
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
val res = Anilist.mutation.toggleLike(story.id, "ACTIVITY")
withContext(Dispatchers.Main) {
if (res != null) {
if (story.isLiked == true) {
story.likeCount = story.likeCount?.minus(1)
} else {
story.likeCount = story.likeCount?.plus(1)
}
binding.activityLikeCount.text = (story.likeCount ?: 0).toString()
story.isLiked = !story.isLiked!!
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
} else {
snackString("Failed to like activity")
}
}
}
}
private var startClickTime = 0L
private var startX = 0f
private var startY = 0f
private var isLongPress = false
private val swipeThreshold = 100
override fun onTouch(view: View, event: MotionEvent): Boolean {
onTouchView(view, event)
return true
}
private fun onTouchView(view: View, event: MotionEvent, isText: Boolean = false) {
val maxClickDuration = 200
val screenWidth = view.width
val leftHalf = screenWidth / 2
val leftQuarter = screenWidth * 0.15
val rightQuarter = screenWidth * 0.85
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
startClickTime = Calendar.getInstance().timeInMillis
pause()
isLongPress = false
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - startX
val deltaY = event.y - startY
if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) {
isLongPress = true
}
}
MotionEvent.ACTION_UP -> {
val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
if (isText) {
if (clickDuration < maxClickDuration && !isLongPress) {
if (event.x < leftQuarter) {
leftPanelTouch()
} else if (event.x > rightQuarter) {
rightPanelTouch()
} else {
resume()
}
} else {
resume()
}
} else {
if (clickDuration < maxClickDuration && !isLongPress) {
if (event.x < leftHalf) {
leftPanelTouch()
} else {
rightPanelTouch()
}
} else {
resume()
}
}
val deltaX = event.x - startX
val deltaY = event.y - startY
if (abs(deltaX) > swipeThreshold && !(abs(deltaY) > 10)) {
if (deltaX > 0) onStoriesPrevious()
else onStoriesCompleted()
}
}
}
}
}

View File

@@ -1,64 +0,0 @@
package ani.dantotsu.home.status
import android.os.CountDownTimer
class StoryTimer(
private val updateInterval: Long
) {
private lateinit var timer: CountDownTimer
private var prevVal = 0
private var pauseLength = 0L
var onTimerCompleted: () -> Unit = {}
var percentTick: (Int) -> Unit = {}
var timeLeft: Long = 0
private set
fun start(durationInMillis: Long = updateInterval) {
cancel()
timer = object : CountDownTimer(durationInMillis, 1) {
override fun onTick(millisUntilFinished: Long) {
timeLeft = millisUntilFinished
val percent =
((pauseLength + durationInMillis - millisUntilFinished) * 100 / (pauseLength + durationInMillis)).toInt()
if (percent != prevVal) {
percentTick.invoke(percent)
prevVal = percent
}
}
override fun onFinish() {
onTimerCompleted.invoke()
pauseLength = 0
}
}
timer.start()
}
fun cancel() {
if (::timer.isInitialized) {
timer.cancel()
}
}
fun pause() {
if (::timer.isInitialized) {
timer.cancel()
pauseLength = updateInterval - timeLeft
}
}
fun resume() {
if (::timer.isInitialized && timeLeft > 0) {
start(timeLeft)
timer.start()
}
}
fun setOnTimerCompletedListener(onTimerCompleted: () -> Unit) {
this.onTimerCompleted = onTimerCompleted
}
fun setOnPercentTickListener(percentTick: (Int) -> Unit) {
this.percentTick = percentTick
}
}

View File

@@ -1,92 +0,0 @@
package ani.dantotsu.home.status
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemUserStatusBinding
import ani.dantotsu.getAppString
import ani.dantotsu.loadImage
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.util.ActivityMarkdownCreator
class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
inner class UsersViewHolder(val binding: ItemUserStatusBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (user[bindingAdapterPosition].activity.isEmpty()) {
snackString("No activity")
return@setOnClickListener
}
StatusActivity.user = user
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
StatusActivity::class.java
).putExtra("position", bindingAdapterPosition),
null
)
}
itemView.setOnLongClickListener {
if (user[bindingAdapterPosition].id == Anilist.userid) {
ContextCompat.startActivity(
itemView.context,
Intent(itemView.context, ActivityMarkdownCreator::class.java)
.putExtra("type", "activity"),
null
)
} else {
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
ProfileActivity::class.java
).putExtra("userId", user[bindingAdapterPosition].id),
null
)
}
true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder {
return UsersViewHolder(
ItemUserStatusBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: UsersViewHolder, position: Int) {
val b = holder.binding
setAnimation(b.root.context, b.root)
val user = user[position]
b.profileUserAvatar.loadImage(user.pfp)
b.profileUserName.text =
if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name
val watchedActivity = PrefManager.getCustomVal<Set<Int>>("activities", setOf())
val booleanList = user.activity.map { watchedActivity.contains(it.id) }
b.profileUserStatusIndicator.setParts(
user.activity.size,
booleanList,
user.id == Anilist.userid
)
}
override fun getItemCount(): Int = user.size
}

View File

@@ -1,6 +0,0 @@
package ani.dantotsu.home.status.listener
interface StoriesCallback {
fun onStoriesEnd()
fun onStoriesStart()
}

View File

@@ -7,12 +7,6 @@ data class Author(
var name: String?,
var image: String?,
var role: String?,
var age: Int? = null,
var yearsActive: List<Int>? = null,
var dateOfBirth: String? = null,
var dateOfDeath: String? = null,
var homeTown: String? = null,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
var character: ArrayList<Character>? = null,
var isFav: Boolean = false
var character: ArrayList<Character>? = null
) : Serializable

View File

@@ -1,13 +1,11 @@
package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -18,127 +16,57 @@ import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.EmptyAdapter
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.databinding.ActivityAuthorBinding
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.SpoilerPlugin
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 io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs
class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityCharacterBinding
class AuthorActivity : AppCompatActivity() {
private lateinit var binding: ActivityAuthorBinding
private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels()
private lateinit var author: Author
private var author: Author? = null
private var loaded = false
private var screenWidth: Float = 0f
private val percent = 30
private var mMaxScrollSize = 0
private var isCollapsed = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
binding = ActivityCharacterBinding.inflate(layoutInflater)
binding = ActivityAuthorBinding.inflate(layoutInflater)
setContentView(binding.root)
initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density }
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
val banner =
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
val screenWidth = resources.displayMetrics.run { widthPixels / density }
banner.updateLayoutParams { height += statusBarHeight }
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.characterCollapsing.minimumHeight = statusBarHeight
binding.characterCover.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.characterRecyclerView.updatePadding(bottom = 64f.px + navBarHeight)
binding.characterTitle.isSelected = true
binding.characterAppBar.addOnOffsetChangedListener(this)
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight)
binding.studioTitle.isSelected = true
binding.characterClose.setOnClickListener {
author = intent.getSerialized("author")
binding.studioTitle.text = author?.name
binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
author = intent.getSerialized("author") ?: return
binding.characterTitle.text = author.name
binding.characterCoverImage.loadImage(author.image)
binding.characterCoverImage.setOnLongClickListener {
ImageViewDialog.newInstance(
this,
author.name,
author.image
)
}
val link = "https://anilist.co/staff/${author.id}"
binding.characterShare.setOnClickListener {
val i = Intent(Intent.ACTION_SEND)
i.type = "text/plain"
i.putExtra(Intent.EXTRA_TEXT, link)
startActivity(Intent.createChooser(i, author.name))
}
binding.characterShare.setOnLongClickListener {
openLinkInBrowser(link)
true
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
author.isFav =
Anilist.query.isUserFav(AnilistMutations.FavType.STAFF, author.id)
}
withContext(Dispatchers.Main) {
binding.characterFav.setImageResource(
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
}
}
binding.characterFav.setOnClickListener {
scope.launch {
lifecycleScope.launch {
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, author.id)) {
author.isFav = !author.isFav
binding.characterFav.setImageResource(
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
} else {
snackString("Failed to toggle favorite")
}
}
}
}
model.getAuthor().observe(this) {
if (it != null) {
author = it
loaded = true
binding.characterProgress.visibility = View.GONE
binding.characterRecyclerView.visibility = View.VISIBLE
if (author.yearMedia.isNullOrEmpty()) {
binding.characterRecyclerView.visibility = View.GONE
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
val map = author!!.yearMedia ?: return@observe
val keys = map.keys.toTypedArray()
var pos = 0
@@ -152,10 +80,6 @@ class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener
}
}
}
val desc = createDesc(author)
val markWon = Markwon.builder(this).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.authorCharacterDesc, desc)
for (i in keys.indices) {
val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
@@ -166,18 +90,18 @@ class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty))
}
binding.characterRecyclerView.adapter = concatAdapter
binding.characterRecyclerView.layoutManager = gridLayoutManager
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
binding.authorCharactersRecycler.visibility = View.VISIBLE
binding.AuthorCharactersText.visibility = View.VISIBLE
binding.authorCharactersRecycler.adapter =
CharacterAdapter(author.character ?: arrayListOf())
binding.authorCharactersRecycler.layoutManager =
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.authorCharactersRecycler.visibility = View.GONE
binding.AuthorCharactersText.visibility = View.GONE
if (author!!.character.isNullOrEmpty()) {
binding.charactersRecycler.visibility = View.GONE
binding.charactersText.visibility = View.GONE
}
}
}
@@ -185,28 +109,14 @@ class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener
live.observe(this) {
if (it) {
scope.launch {
withContext(Dispatchers.IO) { model.loadAuthor(author) }
if (author != null)
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
live.postValue(false)
}
}
}
}
private fun createDesc(author: Author): String {
val age = if (author.age != null) "${getString(R.string.age)} ${author.age}" else ""
val yearsActive =
if (author.yearsActive != null) "${getString(R.string.years_active)} ${author.yearsActive}" else ""
val dob =
if (author.dateOfBirth != null) "${getString(R.string.birthday)} ${author.dateOfBirth}" else ""
val homeTown =
if (author.homeTown != null) "${getString(R.string.hometown)} ${author.homeTown}" else ""
val dod =
if (author.dateOfDeath != null) "${getString(R.string.date_of_death)} ${author.dateOfDeath}" else ""
return "$age $yearsActive $dob $homeTown $dod"
}
override fun onDestroy() {
if (Refresh.activity.containsKey(this.hashCode())) {
Refresh.activity.remove(this.hashCode())
@@ -215,31 +125,7 @@ class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener
}
override fun onResume() {
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume()
}
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize
val cap = clamp((percent - percentage) / percent.toFloat(), 0f, 1f)
binding.characterCover.scaleX = 1f * cap
binding.characterCover.scaleY = 1f * cap
binding.characterCover.cardElevation = 32f * cap
binding.characterCover.visibility =
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg)
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
}
}
}

View File

@@ -15,7 +15,7 @@ import ani.dantotsu.setAnimation
import java.io.Serializable
class AuthorAdapter(
private val authorList: MutableList<Author>,
private val authorList: ArrayList<Author>,
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding =
@@ -26,7 +26,7 @@ class AuthorAdapter(
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val author = authorList.getOrNull(position) ?: return
val author = authorList[position]
binding.itemCompactRelation.text = author.role
binding.itemCompactImage.loadImage(author.image)
binding.itemCompactTitle.text = author.name

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.media
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.Window
@@ -13,7 +14,6 @@ import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.settings.saving.PrefManager
@@ -30,7 +30,6 @@ class CalendarActivity : AppCompatActivity() {
private lateinit var binding: ActivityListBinding
private val scope = lifecycleScope
private var selectedTabIdx = 1
private var showOnlyLibrary = false
private val model: OtherDetailsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
@@ -39,9 +38,16 @@ class CalendarActivity : AppCompatActivity() {
ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater)
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
val primaryColor = typedValue.data
val typedValue3 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
val primaryTextColor = typedValue3.data
val typedValue4 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
val secondaryTextColor = typedValue4.data
window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor
@@ -68,7 +74,6 @@ class CalendarActivity : AppCompatActivity() {
binding.listTitle.setText(R.string.release_calendar)
binding.listSort.visibility = View.GONE
binding.random.visibility = View.GONE
binding.search.visibility = View.GONE
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
@@ -78,17 +83,6 @@ class CalendarActivity : AppCompatActivity() {
override fun onTabReselected(tab: TabLayout.Tab?) {}
})
binding.listed.setOnClickListener {
showOnlyLibrary = !showOnlyLibrary
binding.listed.setImageResource(
if (showOnlyLibrary) R.drawable.ic_round_collections_bookmark_24
else R.drawable.ic_round_library_books_24
)
scope.launch {
model.loadCalendar(showOnlyLibrary)
}
}
model.getCalendar().observe(this) {
if (it != null) {
binding.listProgressBar.visibility = View.GONE
@@ -107,10 +101,11 @@ class CalendarActivity : AppCompatActivity() {
live.observe(this) {
if (it) {
scope.launch {
withContext(Dispatchers.IO) { model.loadCalendar(showOnlyLibrary) }
withContext(Dispatchers.IO) { model.loadCalendar() }
live.postValue(false)
}
}
}
}
}

View File

@@ -9,14 +9,13 @@ import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import java.io.Serializable
class CharacterAdapter(
private val characterList: MutableList<Character>
private val characterList: ArrayList<Character>
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
val binding =
@@ -27,8 +26,9 @@ class CharacterAdapter(
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val character = characterList.getOrNull(position) ?: return
val whitespace = "${if (character.role.lowercase() == "null") "" else character.role} "
val character = characterList[position]
val whitespace = "${character.role} "
character.voiceActor
binding.itemCompactRelation.text = whitespace
binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name
@@ -55,11 +55,6 @@ class CharacterAdapter(
).toBundle()
)
}
itemView.setOnLongClickListener {
copyToClipboard(
characterList[bindingAdapterPosition].name ?: ""
); true
}
}
}
}

View File

@@ -9,7 +9,6 @@ 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.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -46,11 +45,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
private lateinit var character: Character
private var loaded = false
private var isCollapsed = false
private val percent = 30
private var mMaxScrollSize = 0
private var screenWidth: Float = 0f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -77,11 +71,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
binding.characterClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.authorCharactersRecycler.isVisible = false
binding.AuthorCharactersText.isVisible = false
binding.authorCharacterDesc.isVisible = false
character = intent.getSerialized("character") ?: return
binding.characterTitle.text = character.name
banner.loadImage(character.banner)
@@ -169,6 +158,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
super.onResume()
}
private var isCollapsed = false
private val percent = 30
private var mMaxScrollSize = 0
private var screenWidth: Float = 0f
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize

View File

@@ -7,9 +7,11 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ItemCharacterDetailsBinding
import ani.dantotsu.others.SpoilerPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
@@ -22,9 +24,7 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding
val desc =
(if (character.id == 4004)
"![za wardo](https://media1.tenor.com/m/_z1tmCJnL2wAAAAd/za-warudo.gif) \n" else "") +
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
(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")
@@ -41,7 +41,8 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
} else "") + "\n" + character.description
binding.characterDesc.isTextSelectable
val markWon = buildMarkwon(activity)
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(

View File

@@ -1,77 +0,0 @@
package ani.dantotsu.media
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemSearchHeaderBinding
abstract class HeaderInterface : RecyclerView.Adapter<HeaderInterface.SearchHeaderViewHolder>() {
private val itemViewType = 6969
var search: Runnable? = null
var requestFocus: Runnable? = null
protected var textWatcher: TextWatcher? = null
protected lateinit var searchHistoryAdapter: SearchHistoryAdapter
protected lateinit var binding: ItemSearchHeaderBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding)
}
fun setHistoryVisibility(visible: Boolean) {
if (visible) {
binding.searchResultLayout.startAnimation(fadeOutAnimation())
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.clearHistory.visibility = View.GONE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
}
}
protected fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
}
}
protected fun updateClearHistoryVisibility() {
binding.clearHistory.visibility =
if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE
}
fun addHistory() {
if (::searchHistoryAdapter.isInitialized && binding.searchBarText.text.toString()
.isNotBlank()
) searchHistoryAdapter.add(binding.searchBarText.text.toString())
}
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemCount(): Int = 1
override fun getItemViewType(position: Int): Int {
return itemViewType
}
}

View File

@@ -1,22 +1,13 @@
package ani.dantotsu.media
import android.graphics.Bitmap
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList
import ani.dantotsu.connections.anilist.api.MediaStreamingEpisode
import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable
import ani.dantotsu.connections.anilist.api.Media as ApiMedia
@@ -71,7 +62,6 @@ data class Media(
var timeUntilAiring: Long? = null,
var characters: ArrayList<Character>? = null,
var review: ArrayList<Query.Review>? = null,
var staff: ArrayList<Author>? = null,
var prequel: Media? = null,
var sequel: Media? = null,
@@ -84,7 +74,7 @@ data class Media(
var nameMAL: String? = null,
var shareLink: String? = null,
var selected: Selected? = null,
var streamingEpisodes: List<MediaStreamingEpisode>? = null,
var idKitsu: String? = null,
var cameFromContinue: Boolean = false
@@ -137,37 +127,6 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
}
fun Media?.deleteFromList(
scope: CoroutineScope,
onSuccess: suspend () -> Unit,
onError: suspend (e: Exception) -> Unit,
onNotFound: suspend () -> Unit
) {
val id = this?.userListId
scope.launch {
withContext(Dispatchers.IO) {
this@deleteFromList?.let { media ->
val _id = id ?: Anilist.query.userMediaDetails(media).userListId
_id?.let { listId ->
try {
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
PrefManager.setCustomVal(
"removeList", removeList.minus(listId)
)
onSuccess()
} catch (e: Exception) {
onError(e)
}
} ?: onNotFound()
}
}
}
}
fun emptyMedia() = Media(
id = 0,
name = "No media found",

View File

@@ -6,6 +6,7 @@ import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.util.TypedValue
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
@@ -37,7 +38,6 @@ import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.media.anime.AnimeWatchFragment
@@ -172,7 +172,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener {
val coverTitle = getString(R.string.cover, media.userPreferredName)
val coverTitle = "${media.userPreferredName}[Cover]"
ImageViewDialog.newInstance(
this,
coverTitle,
@@ -192,7 +192,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
override fun onLongClick(event: MotionEvent) {
val bannerTitle = getString(R.string.banner, media.userPreferredName)
val bannerTitle = "${media.userPreferredName}[Banner]"
ImageViewDialog.newInstance(
this@MediaDetailsActivity,
bannerTitle,
@@ -250,14 +250,22 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("ResourceType")
fun total() {
val text = SpannableStringBuilder().apply {
val white =
this@MediaDetailsActivity.getThemeColor(com.google.android.material.R.attr.colorOnBackground)
val mediaTypedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
mediaTypedValue,
true
)
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 colorSecondary =
getThemeColor(com.google.android.material.R.attr.colorSecondary)
bold { color(colorSecondary) { append("${media.userProgress}") } }
val typedValue = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
bold { color(typedValue.data) { append("${media.userProgress}") } }
append(
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
R.string.chapters_out_of
@@ -295,7 +303,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaTotal.visibility = View.VISIBLE
binding.mediaAddToList.text = userStatus
} else {
binding.mediaAddToList.setText(R.string.add_list)
binding.mediaAddToList.setText(R.string.add)
}
total()
binding.mediaAddToList.setOnClickListener {
@@ -374,9 +382,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab)
navBar.addTab(watchTab)
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
navBar.addTab(commentTab)
}
navBar.addTab(commentTab)
if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1
@@ -428,8 +434,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
override fun onResume() {
if (::navBar.isInitialized)
navBar.selectTabAt(selected)
navBar.selectTabAt(selected)
super.onResume()
}

View File

@@ -13,7 +13,6 @@ import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.Anify
import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu
import ani.dantotsu.parsers.AnimeSources
@@ -101,16 +100,6 @@ class MediaDetailsViewModel : ViewModel() {
}
}
private val anifyEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)
fun getAnifyEpisodes(): LiveData<Map<String, Episode>> = anifyEpisodes
suspend fun loadAnifyEpisodes(s: Int) {
tryWithSuspend {
if (anifyEpisodes.value == null) anifyEpisodes.postValue(Anify.fetchAndParseMetadata(s))
}
}
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)

View File

@@ -34,18 +34,16 @@ 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.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.xwray.groupie.GroupieAdapter
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
@@ -81,8 +79,7 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
val offline: Boolean =
PrefManager.getVal(PrefName.OfflineMode) || !isOnline(requireContext())
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoContainer.isVisible = loaded
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
@@ -105,8 +102,8 @@ class MediaInfoFragment : Fragment() {
}
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE
val infoNameRomaji = tripleTab + media.nameRomaji
binding.mediaInfoNameRomaji.text = infoNameRomaji
val infoNameRomanji = tripleTab + media.nameRomaji
binding.mediaInfoNameRomaji.text = infoNameRomanji
binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji)
true
@@ -252,41 +249,7 @@ class MediaInfoFragment : Fragment() {
}
parent.addView(bind.root)
}
if (!media.users.isNullOrEmpty() && !offline) {
val users: ArrayList<User> = media.users ?: arrayListOf()
if (Anilist.token != null && media.userStatus != null) {
users.add(
0,
User(
id = Anilist.userid!!,
name = getString(R.string.you),
pfp = Anilist.avatar,
banner = "",
status = media.userStatus,
score = media.userScore.toFloat(),
progress = media.userProgress,
totalEpisodes = media.anime?.totalEpisodes
?: media.manga?.totalChapters,
nextAiringEpisode = media.anime?.nextAiringEpisode
)
)
}
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
itemTitle.visibility = View.GONE
itemRecycler.adapter =
MediaSocialAdapter(users, type, requireActivity())
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
if (media.trailer != null && !offline) {
@Suppress("DEPRECATION")
class MyChrome : WebChromeClient() {
@@ -520,26 +483,24 @@ class MediaInfoFragment : Fragment() {
}
parent.addView(root)
}
}
if (!media.review.isNullOrEmpty()) {
ItemTitleRecyclerBinding.inflate(
ItemTitleSearchBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
val adapter = GroupieAdapter()
media.review!!.forEach { adapter.add(ReviewAdapter(it)) }
itemTitle.setText(R.string.reviews)
itemRecycler.adapter = adapter
itemRecycler.layoutManager = LinearLayoutManager(requireContext())
itemMore.visibility = View.VISIBLE
itemMore.setSafeOnClickListener {
startActivity(
Intent(requireContext(), ReviewActivity::class.java)
.putExtra("mediaId", media.id)
)
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)
}
}
@@ -611,6 +572,23 @@ class MediaInfoFragment : Fragment() {
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)
}
}
}
}

View File

@@ -20,7 +20,6 @@ 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.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.tryWith
import com.google.android.material.materialswitch.MaterialSwitch
@@ -188,12 +187,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
media?.isListPrivate = checked
}
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
var remove: Boolean? = null
binding.mediaListShow.isChecked = media?.id in removeList
binding.mediaListShow.setOnCheckedChangeListener { _, checked ->
remove = checked
}
media?.userRepeat?.apply {
binding.mediaListRewatch.setText(this.toString())
}
@@ -259,11 +253,6 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
)
}
}
if (remove == true) {
PrefManager.setCustomVal("removeList", removeList.plus(media!!.id))
} else if (remove == false) {
PrefManager.setCustomVal("removeList", removeList.minus(media!!.id))
}
Refresh.all()
snackString(getString(R.string.list_updated))
dismissAllowingStateLoss()
@@ -271,23 +260,28 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
}
binding.mediaListDelete.setOnClickListener {
var id = media!!.userListId
scope.launch {
media?.deleteFromList(scope, onSuccess = {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
}, onError = { e ->
withContext(Dispatchers.Main) {
snackString(
getString(
R.string.delete_fail_reason, e.message
)
)
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)
}
}
}, onNotFound = {
snackString(getString(R.string.no_list_id))
})
}
}
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
snackString(getString(R.string.no_list_id))
}
}
}

View File

@@ -18,7 +18,6 @@ import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -63,24 +62,36 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListDelete.setOnClickListener {
var id = media.userListId
viewLifecycleOwner.lifecycleScope.launch {
scope.launch {
media.deleteFromList(scope, onSuccess = {
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()
}, onError = { e ->
withContext(Dispatchers.Main) {
snackString(
getString(
R.string.delete_fail_reason, e.message
)
)
}
}, onNotFound = {
} else {
snackString(getString(R.string.no_list_id))
})
}
}
}
}
@@ -159,12 +170,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
media.isListPrivate = checked
}
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
var remove: Boolean? = null
binding.mediaListShow.isChecked = media.id in removeList
binding.mediaListShow.setOnCheckedChangeListener { _, checked ->
remove = checked
}
binding.mediaListSave.setOnClickListener {
scope.launch {
withContext(Dispatchers.IO) {
@@ -192,11 +198,6 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
)
}
}
if (remove == true) {
PrefManager.setCustomVal("removeList", removeList.plus(media.id))
} else if (remove == false) {
PrefManager.setCustomVal("removeList", removeList.minus(media.id))
}
Refresh.all()
snackString(getString(R.string.list_updated))
dismissAllowingStateLoss()

View File

@@ -1,93 +0,0 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.Window
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.R
import ani.dantotsu.databinding.ActivityMediaListViewBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.initActivity
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
class MediaListViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityMediaListViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMediaListViewBinding.inflate(layoutInflater)
ThemeManager(this).applyTheme()
initActivity(this)
if (!PrefManager.getVal<Boolean>(PrefName.ImmersiveMode)) {
this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.root.fitsSystemWindows = true
} else {
binding.root.fitsSystemWindows = false
requestWindowFeature(Window.FEATURE_NO_TITLE)
hideSystemBarsExtendView()
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
}
setContentView(binding.root)
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor
binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor)
val screenWidth = resources.displayMetrics.run { widthPixels / density }
val mediaList =
passedMedia ?: intent.getSerialized("media") as? ArrayList<Media> ?: ArrayList()
if (passedMedia != null) passedMedia = null
val view = PrefManager.getCustomVal("mediaView", 0)
var mediaView: View = when (view) {
1 -> binding.mediaList
0 -> binding.mediaGrid
else -> binding.mediaGrid
}
mediaView.alpha = 1f
fun changeView(mode: Int, current: View) {
mediaView.alpha = 0.33f
mediaView = current
current.alpha = 1f
PrefManager.setCustomVal("mediaView", mode)
binding.mediaRecyclerView.adapter = MediaAdaptor(mode, mediaList, this)
binding.mediaRecyclerView.layoutManager = GridLayoutManager(
this,
if (mode == 1) 1 else (screenWidth / 120f).toInt()
)
}
binding.mediaList.setOnClickListener {
changeView(1, binding.mediaList)
}
binding.mediaGrid.setOnClickListener {
changeView(0, binding.mediaGrid)
}
val text = "${intent.getStringExtra("title")} (${mediaList.count()})"
binding.listTitle.text = text
binding.mediaRecyclerView.adapter = MediaAdaptor(view, mediaList, this)
binding.mediaRecyclerView.layoutManager = GridLayoutManager(
this,
if (view == 1) 1 else (screenWidth / 120f).toInt()
)
}
companion object {
var passedMedia: ArrayList<Media>? = null
}
}

View File

@@ -70,7 +70,7 @@ object MediaNameAdapter {
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
text.toIntOrNull()
null
}
}
@@ -93,7 +93,7 @@ object MediaNameAdapter {
}
}
} else {
text.toFloatOrNull()
null
}
}
@@ -139,7 +139,7 @@ object MediaNameAdapter {
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
text.toFloatOrNull()
null
}
}
}

View File

@@ -1,32 +1,27 @@
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.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemFollowerGridBinding
import ani.dantotsu.getAppString
import ani.dantotsu.loadImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
class MediaSocialAdapter(
val user: ArrayList<User>,
val type: String,
val activity: FragmentActivity
) : RecyclerView.Adapter<MediaSocialAdapter.FollowerGridViewHolder>() {
class MediaSocialAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<MediaSocialAdapter.DeveloperViewHolder>() {
inner class FollowerGridViewHolder(val binding: ItemFollowerGridBinding) :
inner class DeveloperViewHolder(val binding: ItemFollowerGridBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowerGridViewHolder {
return FollowerGridViewHolder(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder {
return DeveloperViewHolder(
ItemFollowerGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
@@ -35,8 +30,8 @@ class MediaSocialAdapter(
)
}
override fun onBindViewHolder(holder: FollowerGridViewHolder, position: Int) {
@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
@@ -44,17 +39,14 @@ class MediaSocialAdapter(
profileUserName.text = user.name
profileInfo.apply {
text = when (user.status) {
"CURRENT" -> if (type == "ANIME") getAppString(R.string.watching) else getAppString(
R.string.reading
)
"CURRENT" -> "WATCHING"
else -> user.status ?: ""
}
visibility = View.VISIBLE
}
profileCompactUserProgress.text = user.progress.toString()
profileCompactScore.text = score.toString()
" | ${user.totalEpisodes ?: "~"}".also { profileCompactTotal.text = it }
profileCompactTotal.text = " | ${user.totalEpisodes ?: "~"}"
profileUserAvatar.loadImage(user.pfp)
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
@@ -66,19 +58,10 @@ class MediaSocialAdapter(
profileCompactProgressContainer.visibility = View.VISIBLE
profileUserAvatar.setOnClickListener {
ContextCompat.startActivity(
root.context,
Intent(root.context, ProfileActivity::class.java)
.putExtra("userId", user.id),
null
)
}
profileUserAvatarContainer.setOnLongClickListener {
ImageViewDialog.newInstance(
activity,
activity.getString(R.string.avatar, user.name),
user.pfp
)
val intent = Intent(root.context, ProfileActivity::class.java).apply {
putExtra("userId", user.id)
}
ContextCompat.startActivity(root.context, intent, null)
}
}
}

View File

@@ -26,50 +26,25 @@ class OtherDetailsViewModel : ViewModel() {
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
}
private var cachedAllCalendarData: Map<String, MutableList<Media>>? = null
private var cachedLibraryCalendarData: Map<String, MutableList<Media>>? = null
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar(showOnlyLibrary: Boolean = false) {
if (cachedAllCalendarData == null || cachedLibraryCalendarData == null) {
val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL)
val allMap = mutableMapOf<String, MutableList<Media>>()
val libraryMap = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String, MutableList<Int>>()
val userId = Anilist.userid ?: 0
val userLibrary = Anilist.query.getMediaLists(true, userId)
val libraryMediaIds = userLibrary.flatMap { it.value }.map { it.id }
res.forEach {
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
val dateInfo = df.format(Date(v[1] * 1000))
val list = allMap.getOrPut(dateInfo) { mutableListOf() }
val libraryList = if (libraryMediaIds.contains(it.id)) {
libraryMap.getOrPut(dateInfo) { mutableListOf() }
} else {
null
}
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
it.relation = "Episode ${v[0]}"
if (!idList.contains(it.id)) {
idList.add(it.id)
list.add(it)
libraryList?.add(it)
}
suspend fun loadCalendar() {
val curr = System.currentTimeMillis() / 1000
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>>()
res?.forEach {
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
val dateInfo = df.format(Date(v[1] * 1000))
val list = map.getOrPut(dateInfo) { mutableListOf() }
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
it.relation = "Episode ${v[0]}"
if (!idList.contains(it.id)) {
idList.add(it.id)
list.add(it)
}
cachedAllCalendarData = allMap
cachedLibraryCalendarData = libraryMap
}
val cacheToUse: Map<String, MutableList<Media>> = if (showOnlyLibrary) {
cachedLibraryCalendarData ?: emptyMap()
} else {
cachedAllCalendarData ?: emptyMap()
}
calendar.postValue(cacheToUse)
calendar.postValue(map)
}
}

View File

@@ -1,126 +0,0 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ActivityFollowBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ReviewActivity : AppCompatActivity() {
private lateinit var binding: ActivityFollowBinding
val adapter = GroupieAdapter()
private val reviews = mutableListOf<Query.Review>()
var mediaId = 0
private var currentPage: Int = 1
private var hasNextPage: Boolean = true
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityFollowBinding.inflate(layoutInflater)
binding.listToolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
binding.listFrameLayout.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
setContentView(binding.root)
mediaId = intent.getIntExtra("mediaId", -1)
if (mediaId == -1) {
finish()
return
}
binding.followerGrid.visibility = View.GONE
binding.followerList.visibility = View.GONE
binding.followFilterButton.setImageResource(R.drawable.ic_add)
binding.followFilterButton.setOnClickListener {
ContextCompat.startActivity(
this,
Intent(this, ActivityMarkdownCreator::class.java)
.putExtra("type", "review"),
null
)
}
binding.followFilterButton.visibility = View.GONE
binding.listTitle.text = getString(R.string.reviews)
binding.listRecyclerView.adapter = adapter
binding.listRecyclerView.layoutManager = LinearLayoutManager(
this,
LinearLayoutManager.VERTICAL,
false
)
binding.listProgressBar.visibility = View.VISIBLE
binding.listBack.setOnClickListener { onBackPressedDispatcher.onBackPressed() }
lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.query.getReviews(mediaId)?.data?.page
withContext(Dispatchers.Main) {
binding.listProgressBar.visibility = View.GONE
binding.listRecyclerView.setOnTouchListener { _, event ->
if (event?.action == MotionEvent.ACTION_UP) {
if (hasNextPage && !binding.listRecyclerView.canScrollVertically(1) && !binding.followRefresh.isVisible
&& binding.listRecyclerView.adapter!!.itemCount != 0 &&
(binding.listRecyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() == (binding.listRecyclerView.adapter!!.itemCount - 1)
) {
binding.followRefresh.visibility = ViewGroup.VISIBLE
loadPage(++currentPage) {
binding.followRefresh.visibility = ViewGroup.GONE
}
}
}
false
}
currentPage = response?.pageInfo?.currentPage ?: 1
hasNextPage = response?.pageInfo?.hasNextPage ?: false
response?.reviews?.let {
reviews.addAll(it)
fillList()
}
}
}
}
private fun loadPage(page: Int, callback: () -> Unit) {
lifecycleScope.launch(Dispatchers.IO) {
val response = Anilist.query.getReviews(mediaId, page)
currentPage = response?.data?.page?.pageInfo?.currentPage ?: 1
hasNextPage = response?.data?.page?.pageInfo?.hasNextPage ?: false
withContext(Dispatchers.Main) {
response?.data?.page?.reviews?.let {
reviews.addAll(it)
fillList()
}
callback()
}
}
}
private fun fillList() {
adapter.clear()
reviews.forEach {
adapter.add(ReviewAdapter(it))
}
}
}

View File

@@ -1,160 +0,0 @@
package ani.dantotsu.media
import android.content.Intent
import android.view.View
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ItemReviewsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.openImage
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.toast
import com.xwray.groupie.viewbinding.BindableItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ReviewAdapter(
private var review: Query.Review,
) : BindableItem<ItemReviewsBinding>() {
private lateinit var binding: ItemReviewsBinding
override fun bind(viewBinding: ItemReviewsBinding, position: Int) {
binding = viewBinding
val context = binding.root.context
binding.reviewUserName.text = review.user?.name
binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium)
binding.reviewText.text = review.summary
binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
val text = "[${review.score / 10.0f}]"
binding.reviewTag.text = text
binding.root.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ReviewViewActivity::class.java)
.putExtra("review", review),
null
)
}
binding.reviewUserName.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ProfileActivity::class.java)
.putExtra("userId", review.user?.id),
null
)
}
binding.reviewUserAvatar.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ProfileActivity::class.java)
.putExtra("userId", review.user?.id),
null
)
}
binding.reviewUserAvatar.openImage(
context.getString(
R.string.avatar,
review.user?.name
),
review.user?.avatar?.medium ?: ""
)
userVote(review.userRating)
enableVote()
binding.reviewTotalVotes.text = review.rating.toString()
}
override fun getLayout(): Int {
return R.layout.item_reviews
}
override fun initializeViewBinding(view: View): ItemReviewsBinding {
return ItemReviewsBinding.bind(view)
}
private fun userVote(type: String) {
when (type) {
"NO_VOTE" -> {
binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewUpVote.alpha = 0.6f
binding.reviewDownVote.alpha = 0.6f
}
"UP_VOTE" -> {
binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewUpVote.alpha = 1f
binding.reviewDownVote.alpha = 0.6f
}
"DOWN_VOTE" -> {
binding.reviewUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
binding.reviewDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
binding.reviewDownVote.alpha = 1f
binding.reviewUpVote.alpha = 0.6f
}
}
}
private fun rateReview(rating: String) {
disableVote()
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
val result = Anilist.mutation.rateReview(review.id, rating)
if (result != null) {
withContext(Dispatchers.Main) {
val res = result.data.rateReview
review.rating = res.rating
review.ratingAmount = res.ratingAmount
review.userRating = res.userRating
userVote(review.userRating)
binding.reviewTotalVotes.text = review.rating.toString()
userVote(review.userRating)
enableVote()
}
} else {
withContext(Dispatchers.Main) {
toast(
binding.root.context.getString(R.string.error_message, "response is null")
)
enableVote()
}
}
}
}
private fun disableVote() {
binding.reviewUpVote.setOnClickListener(null)
binding.reviewDownVote.setOnClickListener(null)
binding.reviewUpVote.isEnabled = false
binding.reviewDownVote.isEnabled = false
}
private fun enableVote() {
binding.reviewUpVote.setOnClickListener {
if (review.userRating == "UP_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("UP_VOTE")
}
disableVote()
}
binding.reviewDownVote.setOnClickListener {
if (review.userRating == "DOWN_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("DOWN_VOTE")
}
disableVote()
}
binding.reviewUpVote.isEnabled = true
binding.reviewDownVote.isEnabled = true
}
}

View File

@@ -1,197 +0,0 @@
package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ActivityReviewViewBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.openImage
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.toast
import ani.dantotsu.util.AniMarkdown
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ReviewViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityReviewViewBinding
private lateinit var review: Query.Review
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityReviewViewBinding.inflate(layoutInflater)
binding.userContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
binding.reviewContent.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin += navBarHeight
}
setContentView(binding.root)
review = intent.getSerializableExtra("review") as Query.Review
binding.userName.text = review.user?.name
binding.userAvatar.loadImage(review.user?.avatar?.medium)
binding.userTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
binding.userContainer.setOnClickListener {
startActivity(
Intent(this, ProfileActivity::class.java)
.putExtra("userId", review.user?.id)
)
}
binding.userAvatar.openImage(
binding.root.context.getString(R.string.avatar, review.user?.name),
review.user?.avatar?.medium ?: ""
)
binding.userAvatar.setOnClickListener {
startActivity(
Intent(this, ProfileActivity::class.java)
.putExtra("userId", review.user?.id)
)
}
binding.profileUserBio.settings.loadWithOverviewMode = true
binding.profileUserBio.settings.useWideViewPort = true
binding.profileUserBio.setInitialScale(1)
val styledHtml = AniMarkdown.getFullAniHTML(
review.body,
ContextCompat.getColor(this, R.color.bg_opp)
)
binding.profileUserBio.loadDataWithBaseURL(
null,
styledHtml,
"text/html",
"utf-8",
null
)
binding.profileUserBio.setBackgroundColor(
ContextCompat.getColor(
this,
android.R.color.transparent
)
)
binding.profileUserBio.setLayerType(View.LAYER_TYPE_HARDWARE, null)
binding.profileUserBio.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
binding.profileUserBio.setBackgroundColor(
ContextCompat.getColor(
this@ReviewViewActivity,
android.R.color.transparent
)
)
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return true
}
}
userVote(review.userRating)
enableVote()
binding.voteCount.text = review.rating.toString()
binding.voteText.text = getString(
R.string.vote_out_of_total,
review.rating.toString(),
review.ratingAmount.toString()
)
}
private fun userVote(type: String) {
val selectedColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val unselectedColor = getThemeColor(androidx.appcompat.R.attr.colorControlNormal)
when (type) {
"NO_VOTE" -> {
binding.upvote.setColorFilter(unselectedColor)
binding.downvote.setColorFilter(unselectedColor)
}
"UP_VOTE" -> {
binding.upvote.setColorFilter(selectedColor)
binding.downvote.setColorFilter(unselectedColor)
}
"DOWN_VOTE" -> {
binding.upvote.setColorFilter(unselectedColor)
binding.downvote.setColorFilter(selectedColor)
}
}
}
private fun rateReview(rating: String) {
disableVote()
lifecycleScope.launch {
val result = Anilist.mutation.rateReview(review.id, rating)
if (result != null) {
withContext(Dispatchers.Main) {
val res = result.data.rateReview
review.rating = res.rating
review.ratingAmount = res.ratingAmount
review.userRating = res.userRating
userVote(review.userRating)
binding.voteCount.text = review.rating.toString()
binding.voteText.text = getString(
R.string.vote_out_of_total,
review.rating.toString(),
review.ratingAmount.toString()
)
userVote(review.userRating)
enableVote()
}
} else {
withContext(Dispatchers.Main) {
toast(
getString(R.string.error_message, "response is null")
)
enableVote()
}
}
}
}
private fun disableVote() {
binding.upvote.setOnClickListener(null)
binding.downvote.setOnClickListener(null)
binding.upvote.isEnabled = false
binding.downvote.isEnabled = false
}
private fun enableVote() {
binding.upvote.setOnClickListener {
if (review.userRating == "UP_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("UP_VOTE")
}
disableVote()
}
binding.downvote.setOnClickListener {
if (review.userRating == "DOWN_VOTE") {
rateReview("NO_VOTE")
} else {
rateReview("DOWN_VOTE")
}
disableVote()
}
binding.upvote.isEnabled = true
binding.downvote.isEnabled = true
}
}

View File

@@ -13,18 +13,12 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.CharacterSearchResults
import ani.dantotsu.connections.anilist.StaffSearchResults
import ani.dantotsu.connections.anilist.StudioSearchResults
import ani.dantotsu.connections.anilist.UserSearchResults
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.UsersAdapter
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
@@ -41,25 +35,14 @@ class SearchActivity : AppCompatActivity() {
val model: AnilistSearch by viewModels()
var style: Int = 0
lateinit var searchType: SearchType
private var screenWidth: Float = 0f
private lateinit var mediaAdaptor: MediaAdaptor
private lateinit var characterAdaptor: CharacterAdapter
private lateinit var studioAdaptor: StudioAdapter
private lateinit var staffAdaptor: AuthorAdapter
private lateinit var usersAdapter: UsersAdapter
private lateinit var progressAdapter: ProgressAdapter
private lateinit var concatAdapter: ConcatAdapter
private lateinit var headerAdaptor: HeaderInterface
lateinit var aniMangaResult: AniMangaSearchResults
lateinit var characterResult: CharacterSearchResults
lateinit var studioResult: StudioSearchResults
lateinit var staffResult: StaffSearchResults
lateinit var userResult: UserSearchResults
private lateinit var headerAdaptor: SearchAdapter
lateinit var result: SearchResults
lateinit var updateChips: (() -> Unit)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -76,117 +59,39 @@ class SearchActivity : AppCompatActivity() {
bottom = navBarHeight + 80f.px
)
style = PrefManager.getVal(PrefName.SearchStyle)
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null
val notSet = model.notSet
searchType = SearchType.fromString(intent.getStringExtra("type") ?: "ANIME")
when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
style = PrefManager.getVal(PrefName.SearchStyle)
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null
if (model.notSet) {
model.notSet = false
model.aniMangaSearchResults = AniMangaSearchResults(
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 = 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
)
}
aniMangaResult = model.aniMangaSearchResults
mediaAdaptor =
MediaAdaptor(
style,
model.aniMangaSearchResults.results,
this,
matchParent = true
)
}
SearchType.CHARACTER -> {
if (model.notSet) {
model.notSet = false
model.characterSearchResults = CharacterSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
characterResult = model.characterSearchResults
characterAdaptor = CharacterAdapter(model.characterSearchResults.results)
}
}
SearchType.STUDIO -> {
if (model.notSet) {
model.notSet = false
model.studioSearchResults = StudioSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
studioResult = model.studioSearchResults
studioAdaptor = StudioAdapter(model.studioSearchResults.results)
}
}
SearchType.STAFF -> {
if (model.notSet) {
model.notSet = false
model.staffSearchResults = StaffSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
staffResult = model.staffSearchResults
staffAdaptor = AuthorAdapter(model.staffSearchResults.results)
}
}
SearchType.USER -> {
if (model.notSet) {
model.notSet = false
model.userSearchResults = UserSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
userResult = model.userSearchResults
usersAdapter = UsersAdapter(model.userSearchResults.results, grid = true)
}
}
if (model.notSet) {
model.notSet = false
model.searchResults = SearchResults(
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 = 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
)
}
result = model.searchResults
progressAdapter = ProgressAdapter(searched = model.searched)
headerAdaptor = if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
SearchAdapter(this, searchType)
} else {
SupportingSearchAdapter(this, searchType)
}
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
headerAdaptor = SearchAdapter(this, model.searchResults.type)
val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize)
@@ -203,27 +108,7 @@ class SearchActivity : AppCompatActivity() {
}
}
concatAdapter = when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter)
}
SearchType.CHARACTER -> {
ConcatAdapter(headerAdaptor, characterAdaptor, progressAdapter)
}
SearchType.STUDIO -> {
ConcatAdapter(headerAdaptor, studioAdaptor, progressAdapter)
}
SearchType.STAFF -> {
ConcatAdapter(headerAdaptor, staffAdaptor, progressAdapter)
}
SearchType.USER -> {
ConcatAdapter(headerAdaptor, usersAdapter, progressAdapter)
}
}
concatAdapter = ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter)
binding.searchRecyclerView.layoutManager = gridLayoutManager
binding.searchRecyclerView.adapter = concatAdapter
@@ -232,9 +117,9 @@ class SearchActivity : AppCompatActivity() {
RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) {
if (model.hasNextPage(searchType) && model.resultsIsNotEmpty(searchType) && !loading) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
scope.launch(Dispatchers.IO) {
model.loadNextPage(searchType)
model.loadNextPage(model.searchResults)
}
}
}
@@ -242,110 +127,34 @@ class SearchActivity : AppCompatActivity() {
}
})
when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
model.getSearch<AniMangaSearchResults>(searchType).observe(this) {
if (it != null) {
model.aniMangaSearchResults.apply {
onList = it.onList
isAdult = it.isAdult
perPage = it.perPage
search = it.search
sort = it.sort
genres = it.genres
excludedGenres = it.excludedGenres
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
}
val prev = model.aniMangaSearchResults.results.size
model.aniMangaSearchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
model.getSearch().observe(this) {
if (it != null) {
model.searchResults.apply {
onList = it.onList
isAdult = it.isAdult
perPage = it.perPage
search = it.search
sort = it.sort
genres = it.genres
excludedGenres = it.excludedGenres
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
}
}
SearchType.CHARACTER -> {
model.getSearch<CharacterSearchResults>(searchType).observe(this) {
if (it != null) {
model.characterSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.searchResults.results.size
model.searchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
val prev = model.characterSearchResults.results.size
model.characterSearchResults.results.addAll(it.results)
characterAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.STUDIO -> {
model.getSearch<StudioSearchResults>(searchType).observe(this) {
if (it != null) {
model.studioSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.studioSearchResults.results.size
model.studioSearchResults.results.addAll(it.results)
studioAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.STAFF -> {
model.getSearch<StaffSearchResults>(searchType).observe(this) {
if (it != null) {
model.staffSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.staffSearchResults.results.size
model.staffSearchResults.results.addAll(it.results)
staffAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.USER -> {
model.getSearch<UserSearchResults>(searchType).observe(this) {
if (it != null) {
model.userSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.userSearchResults.results.size
model.userSearchResults.results.addAll(it.results)
usersAdapter.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
@@ -370,35 +179,8 @@ class SearchActivity : AppCompatActivity() {
fun emptyMediaAdapter() {
searchTimer.cancel()
searchTimer.purge()
when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
mediaAdaptor.notifyItemRangeRemoved(0, model.aniMangaSearchResults.results.size)
model.aniMangaSearchResults.results.clear()
}
SearchType.CHARACTER -> {
characterAdaptor.notifyItemRangeRemoved(
0,
model.characterSearchResults.results.size
)
model.characterSearchResults.results.clear()
}
SearchType.STUDIO -> {
studioAdaptor.notifyItemRangeRemoved(0, model.studioSearchResults.results.size)
model.studioSearchResults.results.clear()
}
SearchType.STAFF -> {
staffAdaptor.notifyItemRangeRemoved(0, model.staffSearchResults.results.size)
model.staffSearchResults.results.clear()
}
SearchType.USER -> {
usersAdapter.notifyItemRangeRemoved(0, model.userSearchResults.results.size)
model.userSearchResults.results.clear()
}
}
mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size)
model.searchResults.results.clear()
progressAdapter.bar?.visibility = View.GONE
}
@@ -406,30 +188,10 @@ class SearchActivity : AppCompatActivity() {
private var loading = false
fun search() {
headerAdaptor.setHistoryVisibility(false)
val size = model.size(searchType)
model.clearResults(searchType)
val size = model.searchResults.results.size
model.searchResults.results.clear()
binding.searchRecyclerView.post {
when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
mediaAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.CHARACTER -> {
characterAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.STUDIO -> {
studioAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.STAFF -> {
staffAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.USER -> {
usersAdapter.notifyItemRangeRemoved(0, size)
}
}
mediaAdaptor.notifyItemRangeRemoved(0, size)
}
progressAdapter.bar?.visibility = View.VISIBLE
@@ -440,7 +202,7 @@ class SearchActivity : AppCompatActivity() {
override fun run() {
scope.launch(Dispatchers.IO) {
loading = true
model.loadSearch(searchType)
model.loadSearch(result)
loading = false
}
}
@@ -451,10 +213,8 @@ class SearchActivity : AppCompatActivity() {
@SuppressLint("NotifyDataSetChanged")
fun recycler() {
if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
}
mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
}
var state: Parcelable? = null

View File

@@ -9,6 +9,8 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
@@ -20,8 +22,8 @@ import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
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
@@ -34,11 +36,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
HeaderInterface() {
class SearchAdapter(private val activity: SearchActivity, private val type: String) :
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
private val itemViewType = 6969
var search: Runnable? = null
var requestFocus: Runnable? = null
private var textWatcher: TextWatcher? = null
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.aniMangaResult.sort) {
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
@@ -51,13 +60,18 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
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)
return SearchHeaderViewHolder(binding)
}
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
binding = holder.binding
searchHistoryAdapter = SearchHistoryAdapter(type) {
binding.searchBarText.setText(it)
binding.searchBarText.setSelection(it.length)
}
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
binding.searchHistoryList.adapter = searchHistoryAdapter
@@ -65,10 +79,6 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
if (activity.searchType != SearchType.MANGA && activity.searchType != SearchType.ANIME) {
throw IllegalArgumentException("Invalid search type (wrong adapter)")
}
when (activity.style) {
0 -> {
binding.searchResultGrid.alpha = 1f
@@ -81,7 +91,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
}
}
binding.searchBar.hint = activity.aniMangaResult.type
binding.searchBar.hint = activity.result.type
if (PrefManager.getVal(PrefName.Incognito)) {
val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? =
@@ -89,11 +99,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
binding.searchBar.startIconDrawable = startIconDrawable
}
var adult = activity.aniMangaResult.isAdult
var listOnly = activity.aniMangaResult.onList
var adult = activity.result.isAdult
var listOnly = activity.result.onList
binding.searchBarText.removeTextChangedListener(textWatcher)
binding.searchBarText.setText(activity.aniMangaResult.search)
binding.searchBarText.setText(activity.result.search)
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
@@ -114,49 +124,49 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.aniMangaResult.sort = Anilist.sortBy[0]
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.aniMangaResult.sort = Anilist.sortBy[1]
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.aniMangaResult.sort = Anilist.sortBy[2]
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.aniMangaResult.sort = Anilist.sortBy[3]
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.aniMangaResult.sort = Anilist.sortBy[4]
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.aniMangaResult.sort = Anilist.sortBy[5]
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.aniMangaResult.sort = Anilist.sortBy[6]
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
@@ -167,20 +177,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
popupMenu.show()
true
}
if (activity.aniMangaResult.type != "ANIME") {
binding.searchByImage.visibility = View.GONE
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
binding.clearHistory.setOnClickListener {
it.startAnimation(fadeOutAnimation())
it.visibility = View.GONE
searchHistoryAdapter.clearHistory()
}
updateClearHistoryVisibility()
fun searchTitle() {
activity.aniMangaResult.apply {
activity.result.apply {
search =
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
onList = listOnly
@@ -282,12 +283,60 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
requestFocus = Runnable { binding.searchBarText.requestFocus() }
}
fun setHistoryVisibility(visible: Boolean) {
if (visible) {
binding.searchResultLayout.startAnimation(fadeOutAnimation())
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
}
}
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
fillAfter = true
}
}
private fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
fillAfter = true
}
}
fun addHistory() {
searchHistoryAdapter.add(binding.searchBarText.text.toString())
}
override fun getItemCount(): Int = 1
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemViewType(position: Int): Int {
return itemViewType
}
class SearchChipAdapter(
val activity: SearchActivity,
private val searchAdapter: SearchAdapter
) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.aniMangaResult.toChipList()
private var chips = activity.result.toChipList()
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root)
@@ -304,7 +353,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
holder.binding.root.apply {
text = chip.text.replace("_", " ")
setOnClickListener {
activity.aniMangaResult.removeChip(chip)
activity.result.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
@@ -314,7 +363,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
@SuppressLint("NotifyDataSetChanged")
fun update() {
chips = activity.aniMangaResult.toChipList()
chips = activity.result.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
@@ -322,3 +371,4 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Sear
override fun getItemCount(): Int = chips.size
}
}

View File

@@ -57,7 +57,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
private fun setSortByFilterImage() {
val filterDrawable = when (activity.aniMangaResult.sort) {
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
@@ -71,10 +71,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
private fun resetSearchFilter() {
activity.aniMangaResult.sort = null
activity.result.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter)
activity.aniMangaResult.countryOfOrigin = null
activity.result.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
@@ -98,10 +98,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
activity = requireActivity() as SearchActivity
selectedGenres = activity.aniMangaResult.genres ?: mutableListOf()
exGenres = activity.aniMangaResult.excludedGenres ?: mutableListOf()
selectedTags = activity.aniMangaResult.tags ?: mutableListOf()
exTags = activity.aniMangaResult.excludedTags ?: mutableListOf()
selectedGenres = activity.result.genres ?: mutableListOf()
exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf()
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
@@ -126,7 +126,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch {
activity.aniMangaResult.apply {
activity.result.apply {
status =
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source =
@@ -135,7 +135,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.aniMangaResult.sort
sort = activity.result.sort
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@@ -155,43 +155,43 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.sort_by_score -> {
activity.aniMangaResult.sort = Anilist.sortBy[0]
activity.result.sort = Anilist.sortBy[0]
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation()
}
R.id.sort_by_popular -> {
activity.aniMangaResult.sort = Anilist.sortBy[1]
activity.result.sort = Anilist.sortBy[1]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation()
}
R.id.sort_by_trending -> {
activity.aniMangaResult.sort = Anilist.sortBy[2]
activity.result.sort = Anilist.sortBy[2]
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation()
}
R.id.sort_by_recent -> {
activity.aniMangaResult.sort = Anilist.sortBy[3]
activity.result.sort = Anilist.sortBy[3]
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation()
}
R.id.sort_by_a_z -> {
activity.aniMangaResult.sort = Anilist.sortBy[4]
activity.result.sort = Anilist.sortBy[4]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation()
}
R.id.sort_by_z_a -> {
activity.aniMangaResult.sort = Anilist.sortBy[5]
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.aniMangaResult.sort = Anilist.sortBy[6]
activity.result.sort = Anilist.sortBy[6]
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation()
}
@@ -212,25 +212,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
R.id.country_china -> {
activity.aniMangaResult.countryOfOrigin = "CN"
activity.result.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_south_korea -> {
activity.aniMangaResult.countryOfOrigin = "KR"
activity.result.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_japan -> {
activity.aniMangaResult.countryOfOrigin = "JP"
activity.result.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_taiwan -> {
activity.aniMangaResult.countryOfOrigin = "TW"
activity.result.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
@@ -241,18 +241,18 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
binding.searchFilterApply.setOnClickListener {
activity.aniMangaResult.apply {
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 }
if (activity.aniMangaResult.type == "ANIME") {
if (activity.result.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else {
startYear = binding.searchYear.text.toString().toIntOrNull()
}
sort = activity.aniMangaResult.sort
countryOfOrigin = activity.aniMangaResult.countryOfOrigin
sort = activity.result.sort
countryOfOrigin = activity.result.countryOfOrigin
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@@ -266,8 +266,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
dismiss()
}
val format =
if (activity.aniMangaResult.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.aniMangaResult.status?.replace("_", " "))
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
binding.searchStatus.setAdapter(
ArrayAdapter(
binding.root.context,
@@ -276,7 +276,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
)
)
binding.searchSource.setText(activity.aniMangaResult.source?.replace("_", " "))
binding.searchSource.setText(activity.result.source?.replace("_", " "))
binding.searchSource.setAdapter(
ArrayAdapter(
binding.root.context,
@@ -285,19 +285,19 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
)
)
binding.searchFormat.setText(activity.aniMangaResult.format)
binding.searchFormat.setText(activity.result.format)
binding.searchFormat.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(if (activity.aniMangaResult.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
)
)
if (activity.aniMangaResult.type == "ANIME") {
binding.searchYear.setText(activity.aniMangaResult.seasonYear?.toString())
if (activity.result.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString())
} else {
binding.searchYear.setText(activity.aniMangaResult.startYear?.toString())
binding.searchYear.setText(activity.result.startYear?.toString())
}
binding.searchYear.setAdapter(
ArrayAdapter(
@@ -308,9 +308,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
)
)
if (activity.aniMangaResult.type == "MANGA") binding.searchSeasonCont.visibility = GONE
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
else {
binding.searchSeason.setText(activity.aniMangaResult.season)
binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setAdapter(
ArrayAdapter(
binding.root.context,
@@ -346,9 +346,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchGenresGrid.isChecked = false
binding.searchFilterTags.adapter =
FilterChipAdapter(
Anilist.tags?.get(activity.aniMangaResult.isAdult) ?: listOf()
) { chip ->
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
val tag = chip.text.toString()
chip.isChecked = selectedTags.contains(tag)
chip.isCloseIconVisible = exTags.contains(tag)

View File

@@ -7,75 +7,48 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.databinding.ItemSearchHistoryBinding
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveClass
import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceClassLiveData
import java.io.Serializable
import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData
import java.util.Locale
data class SearchHistory(val search: String, val time: Long) : Serializable {
companion object {
private const val serialVersionUID = 1L
}
}
class SearchHistoryAdapter(type: SearchType, private val searchClicked: (String) -> Unit) :
class SearchHistoryAdapter(private val type: String, private val searchClicked: (String) -> Unit) :
ListAdapter<String, SearchHistoryAdapter.SearchHistoryViewHolder>(
DIFF_CALLBACK_INSTALLED
) {
private var searchHistoryLiveData: SharedPreferenceClassLiveData<List<SearchHistory>>? = null
private var searchHistory: MutableList<SearchHistory>? = null
private var historyType: PrefName = when (type) {
SearchType.ANIME -> PrefName.SortedAnimeSH
SearchType.MANGA -> PrefName.SortedMangaSH
SearchType.CHARACTER -> PrefName.SortedCharacterSH
SearchType.STAFF -> PrefName.SortedStaffSH
SearchType.STUDIO -> PrefName.SortedStudioSH
SearchType.USER -> PrefName.SortedUserSH
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null
private var searchHistory: MutableSet<String>? = null
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) {
"anime" -> PrefName.AnimeSearchHistory
"manga" -> PrefName.MangaSearchHistory
else -> throw IllegalArgumentException("Invalid type")
}
private fun MutableList<SearchHistory>?.sorted(): List<String>? =
this?.sortedByDescending { it.time }?.map { it.search }
init {
searchHistoryLiveData =
PrefManager.getLiveVal(historyType, mutableListOf<SearchHistory>()).asLiveClass()
searchHistoryLiveData?.observeForever { data ->
searchHistory = data.toMutableList()
submitList(searchHistory?.sorted())
PrefManager.getLiveVal(historyType, mutableSetOf<String>()).asLiveStringSet()
searchHistoryLiveData?.observeForever {
searchHistory = it.toMutableSet()
submitList(searchHistory?.toList())
}
}
fun remove(item: String) {
searchHistory?.let { list ->
list.removeAll { it.search == item }
}
searchHistory?.remove(item)
PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.sorted())
submitList(searchHistory?.toList())
}
fun add(item: String) {
val maxSize = 25
if (searchHistory?.any { it.search == item } == true || item.isBlank()) return
if (searchHistory?.contains(item) == true || item.isBlank()) return
if (PrefManager.getVal(PrefName.Incognito)) return
searchHistory?.add(SearchHistory(item, System.currentTimeMillis()))
if ((searchHistory?.size ?: 0) > maxSize) {
searchHistory?.removeAt(
searchHistory?.sorted()?.lastIndex ?: 0
)
}
submitList(searchHistory?.sorted())
searchHistory?.add(item)
submitList(searchHistory?.toList())
PrefManager.setVal(historyType, searchHistory)
}
fun clearHistory() {
searchHistory?.clear()
PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.sorted())
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int

View File

@@ -5,8 +5,5 @@ import java.io.Serializable
data class Studio(
val id: String,
val name: String,
val isFavourite: Boolean?,
val favourites: Int?,
val imageUrl: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : Serializable

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