Compare commits

..

412 Commits

Author SHA1 Message Date
Koitharu
beba4f029a Fix per-app locale selection 2025-05-08 20:46:58 +03:00
Koitharu
7cf7a62881 Update details info card color
(cherry picked from commit 8d325aea0a)
2025-05-08 19:36:16 +03:00
kadirkid
c1e84715fb Switch per language support to manual
The current automatic support setup has a bug where the app language will change for users with Android 15 when there is a configuration change like rotating a screen. It seems that that using generateLocaleConfig on AGP 8.8+ triggers a bug in Android 15 (android:defaultLocale) which causes this issue

(cherry picked from commit 104d8da655)
2025-05-08 19:36:08 +03:00
Koitharu
a3cc5726ee Update parsers 2025-05-08 19:35:45 +03:00
Koitharu
3023c02f12 Update parsers 2025-05-03 08:37:46 +03:00
Koitharu
efff034dc6 Remove duplicated warnlist tags 2025-05-03 08:33:36 +03:00
Draken
2bb5673446 Update tags_warnlist
(cherry picked from commit 8d78b19128)
2025-05-03 08:31:39 +03:00
Koitharu
0983885fa2 Update private notifications visibility 2025-05-03 08:30:36 +03:00
Koitharu
4449996a91 Fix search suggestions
(cherry picked from commit 1a8045b89f)
2025-05-02 14:44:39 +03:00
Koitharu
9cf496b7c4 AVIF images downsampling
(cherry picked from commit 5d890cb3d0)
2025-05-02 14:44:25 +03:00
Koitharu
4fb1db47ab Fix image loading
(cherry picked from commit 257f583f78)
2025-05-02 14:44:12 +03:00
Koitharu
14b89fbee2 Use pagination for bookmarks backup 2025-04-19 08:31:47 +03:00
Koitharu
8291c55fc9 Fix some database-related crashes 2025-04-19 08:16:08 +03:00
Koitharu
46ddcb7518 Update page loading ui 2025-04-13 11:33:04 +03:00
Koitharu
cf2d1aa6fb Fix main navbar height 2025-04-13 11:21:29 +03:00
Koitharu
ab3dd8aacb Merge pull request #1374 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2025-04-13 10:56:48 +03:00
srntskl-111
ae868fa9d1 Translated using Weblate (Indonesian)
Currently translated at 98.8% (805 of 814 strings)

Co-authored-by: srntskl-111 <maskhraish@outlook.co.id>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-04-13 09:55:44 +02:00
Frosted
4ecbf5978e Translated using Weblate (Turkish)
Currently translated at 100.0% (814 of 814 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-04-13 09:55:43 +02:00
Koitharu
31586cf48f Fix avif image decoding 2025-04-13 10:47:37 +03:00
Koitharu
3725a6e58f Update page loading ui 2025-04-13 10:01:24 +03:00
Koitharu
313c2ab2bf Respect rounded corners for page numbers (#1360) 2025-04-08 09:09:36 +03:00
Koitharu
fe5d37f45e Fix hiding page loading indicator (close #1357) 2025-04-08 09:09:36 +03:00
Koitharu
92f6221ba0 Merge pull request #1367 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2025-04-08 09:03:34 +03:00
大王叫我来巡山
0590a0c56f Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (811 of 814 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-04-07 18:01:57 +02:00
Koitharu
13ffc3a515 Translated using Weblate (Russian)
Currently translated at 100.0% (814 of 814 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-04-07 18:01:55 +02:00
Nicola Bortoletto
74b36226f2 Translated using Weblate (Italian)
Currently translated at 100.0% (814 of 814 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-04-07 18:01:53 +02:00
Boqirz
d501d0304a Translated using Weblate (Indonesian)
Currently translated at 98.7% (804 of 814 strings)

Co-authored-by: Boqirz <alveromodar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-04-07 18:01:51 +02:00
Draken
1059933c87 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (814 of 814 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (814 of 814 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-04-07 18:01:49 +02:00
Koitharu
5fa58b931e Fix Cloudflare protection resolving 2025-04-06 18:10:34 +03:00
Koitharu
ddecc72de7 Update page state management in reader 2025-04-06 16:26:13 +03:00
Koitharu
d35a0c5e1e Allow to open reader when details is not loaded yet 2025-04-03 19:41:44 +03:00
Koitharu
340994ce77 Fix reader slider behavior 2025-04-03 13:59:26 +03:00
Koitharu
42b2f21c4d Fix bottom navigation insets #1341 2025-04-03 13:19:10 +03:00
Koitharu
e4b9da54dd Update parsers 2025-04-03 12:22:51 +03:00
Koitharu
ccc41314ae UI fixes 2025-04-03 12:21:06 +03:00
Koitharu
93eb6a19a5 Update page loading ui 2025-04-03 12:21:06 +03:00
Koitharu
e4f2e19d2c Merge pull request #1358 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2025-04-03 12:19:44 +03:00
Thinker
73a687c9a7 Translated using Weblate (Bengali)
Currently translated at 21.8% (178 of 814 strings)

Co-authored-by: Thinker <sayakkundu711@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
2025-04-03 03:49:15 +02:00
Draken
32ca3c11fa Translated using Weblate (Vietnamese)
Currently translated at 100.0% (814 of 814 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-04-03 03:49:14 +02:00
Deivinni Silva
0d648dd188 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (814 of 814 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-04-03 03:49:14 +02:00
Frosted
86b7989c89 Translated using Weblate (Turkish)
Currently translated at 100.0% (814 of 814 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-04-03 03:49:14 +02:00
Alvoracz
01be6ab596 Translated using Weblate (Czech)
Currently translated at 99.3% (809 of 814 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-04-03 03:49:14 +02:00
Infy's Tagalog Translations
a3d01e8d34 Translated using Weblate (Filipino)
Currently translated at 99.7% (812 of 814 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-04-03 03:49:14 +02:00
gekka
808bd47b64 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (811 of 814 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-04-03 03:49:13 +02:00
Nicola Bortoletto
f4b506b26b Translated using Weblate (Italian)
Currently translated at 99.8% (813 of 814 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-04-03 03:49:13 +02:00
Koitharu
1f0d2e2039 Fix crash when open non-http url in browser 2025-03-31 10:19:31 +03:00
Koitharu
e3e315e2a6 Skip local directories with .notamanga file 2025-03-30 10:43:05 +03:00
Koitharu
bfc733784f Option to disable dangerous genres highlighting #1348 2025-03-30 10:34:27 +03:00
Koitharu
3ff25de252 Fix crash 2025-03-30 10:21:42 +03:00
Koitharu
3c726c1c56 Update dependencies 2025-03-30 10:21:42 +03:00
Koitharu
9cb7ff691f Merge pull request #1349 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2025-03-30 10:21:06 +03:00
lenn
645ae3124f Translated using Weblate (Polish)
Currently translated at 96.0% (780 of 812 strings)

Co-authored-by: lenn <l3ennec@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2025-03-29 14:02:13 +01:00
Ore Ki
a3d1922913 Translated using Weblate (Indonesian)
Currently translated at 100.0% (812 of 812 strings)

Co-authored-by: Ore Ki <ramadrizkyyy@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-03-29 14:02:11 +01:00
gekka
62d2ea8f15 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.6% (809 of 812 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.5% (808 of 812 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-03-29 14:02:09 +01:00
Draken
823752076b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (812 of 812 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-03-29 14:02:06 +01:00
Lorenzo Stella
3cbd392c72 Translated using Weblate (English)
Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 2.4% (20 of 812 strings)

Translated using Weblate (English)

Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (English)

Currently translated at 100.0% (812 of 812 strings)

Co-authored-by: Lorenzo Stella <lorenzo.stella.1408@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en_GB/
Translation: Kotatsu/Strings
2025-03-29 14:02:04 +01:00
Anonymous
57f62f5860 Translated using Weblate (Malayalam)
Currently translated at 2.8% (23 of 812 strings)

Translated using Weblate (Sinhala)

Currently translated at 2.7% (22 of 812 strings)

Translated using Weblate (Croatian)

Currently translated at 97.5% (792 of 812 strings)

Translated using Weblate (Hungarian)

Currently translated at 76.3% (620 of 812 strings)

Translated using Weblate (Malay)

Currently translated at 38.4% (312 of 812 strings)

Translated using Weblate (Estonian)

Currently translated at 67.6% (549 of 812 strings)

Translated using Weblate (Thai)

Currently translated at 55.7% (453 of 812 strings)

Translated using Weblate (Czech)

Currently translated at 98.0% (796 of 812 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.1% (805 of 812 strings)

Translated using Weblate (Kazakh)

Currently translated at 66.9% (544 of 812 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 46.9% (381 of 812 strings)

Translated using Weblate (Filipino)

Currently translated at 98.2% (798 of 812 strings)

Translated using Weblate (Hindi)

Currently translated at 82.2% (668 of 812 strings)

Translated using Weblate (Polish)

Currently translated at 93.1% (756 of 812 strings)

Translated using Weblate (Korean)

Currently translated at 41.9% (341 of 812 strings)

Translated using Weblate (Greek)

Currently translated at 66.2% (538 of 812 strings)

Translated using Weblate (Serbian)

Currently translated at 97.9% (795 of 812 strings)

Translated using Weblate (Arabic)

Currently translated at 98.7% (802 of 812 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 80.9% (657 of 812 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.0% (804 of 812 strings)

Translated using Weblate (Ukrainian)

Currently translated at 81.6% (663 of 812 strings)

Translated using Weblate (Russian)

Currently translated at 98.5% (800 of 812 strings)

Translated using Weblate (Japanese)

Currently translated at 57.1% (464 of 812 strings)

Translated using Weblate (Turkish)

Currently translated at 99.6% (809 of 812 strings)

Translated using Weblate (French)

Currently translated at 99.6% (809 of 812 strings)

Translated using Weblate (Portuguese)

Currently translated at 96.6% (785 of 812 strings)

Translated using Weblate (Italian)

Currently translated at 99.5% (808 of 812 strings)

Translated using Weblate (German)

Currently translated at 78.3% (636 of 812 strings)

Translated using Weblate (Spanish)

Currently translated at 94.9% (771 of 812 strings)

Translated using Weblate (Belarusian)

Currently translated at 92.1% (748 of 812 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/et/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ml/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/si/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2025-03-29 14:02:02 +01:00
Koitharu
648fab6be5 Fix Open online manga variant 2025-03-27 20:39:36 +02:00
Koitharu
817ae68e67 Option to toggle ssiv debug 2025-03-26 19:24:32 +02:00
Koitharu
7c4b91ddc4 UI Fixes 2025-03-26 19:23:06 +02:00
Koitharu
d54e015195 Fix Favorite/Favourite strings 2025-03-25 10:08:16 +02:00
Koitharu
e369d1ba9d Merge remote-tracking branch 'weblate/devel' into devel 2025-03-25 10:01:27 +02:00
Koitharu
1a4358998b Merge pull request #1334 from Arararararagi/patch-1
typos and coherence check in strings
2025-03-25 09:47:37 +02:00
MrCrystallized
c53a833d9d Translated using Weblate (Bengali)
Currently translated at 22.1% (180 of 812 strings)

Translated using Weblate (Bengali)

Currently translated at 88.8% (8 of 9 strings)

Co-authored-by: MrCrystallized <mrcrystallizedbruh@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-03-24 21:54:54 +01:00
Lorenzo Stella
afff700ad3 Translated using Weblate (Romanian)
Currently translated at 11.0% (90 of 812 strings)

Translated using Weblate (Khmer (Central))

Currently translated at 17.7% (144 of 812 strings)

Translated using Weblate (Latvian)

Currently translated at 21.6% (176 of 812 strings)

Translated using Weblate (Lithuanian)

Currently translated at 5.5% (45 of 812 strings)

Translated using Weblate (Catalan)

Currently translated at 10.9% (89 of 812 strings)

Translated using Weblate (Hungarian)

Currently translated at 76.6% (622 of 812 strings)

Translated using Weblate (Punjabi)

Currently translated at 4.1% (34 of 812 strings)

Translated using Weblate (Malay)

Currently translated at 38.6% (314 of 812 strings)

Translated using Weblate (Thai)

Currently translated at 56.0% (455 of 812 strings)

Translated using Weblate (Kazakh)

Currently translated at 67.2% (546 of 812 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 47.1% (383 of 812 strings)

Translated using Weblate (Nepali)

Currently translated at 30.5% (248 of 812 strings)

Translated using Weblate (Hindi)

Currently translated at 82.6% (671 of 812 strings)

Translated using Weblate (Polish)

Currently translated at 93.5% (760 of 812 strings)

Translated using Weblate (Korean)

Currently translated at 42.2% (343 of 812 strings)

Translated using Weblate (Greek)

Currently translated at 66.5% (540 of 812 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 81.2% (660 of 812 strings)

Translated using Weblate (Ukrainian)

Currently translated at 82.0% (666 of 812 strings)

Translated using Weblate (Japanese)

Currently translated at 57.3% (466 of 812 strings)

Translated using Weblate (Persian)

Currently translated at 35.5% (289 of 812 strings)

Translated using Weblate (Finnish)

Currently translated at 32.0% (260 of 812 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (German)

Currently translated at 78.6% (639 of 812 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 35.4% (288 of 812 strings)

Translated using Weblate (Spanish)

Currently translated at 95.4% (775 of 812 strings)

Translated using Weblate (English)

Currently translated at 100.0% (812 of 812 strings)

Co-authored-by: Lorenzo Stella <lorenzo.stella.1408@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/km/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/lt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/lv/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pa/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ro/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2025-03-24 21:54:54 +01:00
Anon
5bc00bc7f5 Translated using Weblate (Serbian)
Currently translated at 98.2% (798 of 812 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-03-24 21:54:54 +01:00
Dragibus Noir
e2ace90cdb Translated using Weblate (French)
Currently translated at 100.0% (812 of 812 strings)

Co-authored-by: Dragibus Noir <dragibusnoir@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-03-24 21:54:54 +01:00
Lumiini
1afbd2b6a8 Translated using Weblate (Finnish)
Currently translated at 66.6% (6 of 9 strings)

Co-authored-by: Lumiini <bennokaynak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fi/
Translation: Kotatsu/plurals
2025-03-24 21:54:53 +01:00
Frosted
d36c5af0c4 Translated using Weblate (Turkish)
Currently translated at 100.0% (812 of 812 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-03-24 21:54:53 +01:00
Nicola Bortoletto
705bb2b084 Translated using Weblate (Italian)
Currently translated at 100.0% (812 of 812 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-03-24 21:54:53 +01:00
gekka
a208d13930 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.5% (808 of 812 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-03-24 21:54:53 +01:00
Kusou
44d8861b7f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (808 of 808 strings)

Co-authored-by: Kusou <orion26br@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-03-24 21:54:53 +01:00
ruban s
9821f06ca1 Translated using Weblate (Tamil)
Currently translated at 100.0% (808 of 808 strings)

Co-authored-by: ruban s <rubans013@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ta/
Translation: Kotatsu/Strings
2025-03-24 21:54:52 +01:00
Amato Fugosi
92f9f56f59 Translated using Weblate (Bengali)
Currently translated at 19.9% (161 of 808 strings)

Translated using Weblate (Bengali)

Currently translated at 88.8% (8 of 9 strings)

Co-authored-by: Amato Fugosi <fugosiamato@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/bn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/bn/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-03-24 21:54:52 +01:00
Koitharu
424c4d8827 Multiple authors support 2025-03-24 14:18:04 +02:00
Koitharu
24cf2a2725 Use page preview in reader while loading 2025-03-22 16:18:35 +02:00
Koitharu
1a5c3c1f6f Update dependencies 2025-03-21 09:03:31 +02:00
Koitharu
0b8fbf892a Improve details activity 2025-03-20 18:00:58 +02:00
Koitharu
a2f9356b8a Fix crashes 2025-03-20 08:20:43 +02:00
Koitharu
7003463bac Option to use disabled sources for suggestions 2025-03-20 07:52:08 +02:00
Koitharu
7a663fa9c1 Merge branch 'devel' 2025-03-18 19:36:42 +02:00
Koitharu
a3345d11e7 Update parsers 2025-03-18 19:08:41 +02:00
Koitharu
f1ab65ec32 Fixes 2025-03-18 19:06:46 +02:00
Koitharu
6282d25d3d Merge pull request #1333 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2025-03-18 19:04:06 +02:00
Saterz_
47c3f9ff3b Translated using Weblate (French)
Currently translated at 99.8% (807 of 808 strings)

Co-authored-by: Saterz_ <saterzstudio@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-03-18 07:39:01 +01:00
gekka
5cd2f1b9e6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.5% (804 of 808 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-03-18 07:39:01 +01:00
Frosted
5d9b18ec11 Translated using Weblate (Turkish)
Currently translated at 100.0% (808 of 808 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-03-18 07:39:00 +01:00
Nicola Bortoletto
5aec1f644d Translated using Weblate (Italian)
Currently translated at 100.0% (808 of 808 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-03-18 07:39:00 +01:00
Draken
aee092f0b3 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (808 of 808 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (807 of 807 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-03-18 07:39:00 +01:00
I bit my tongue
9cc1cdac62 typos and coherence check 2025-03-16 21:13:54 +00:00
Koitharu
1e73739ddb Synchronized SieveCache wrapper 2025-03-16 12:54:00 +02:00
Koitharu
d1d7cc9adf Fix crashes 2025-03-16 12:43:31 +02:00
Koitharu
6a0ad7f79b Improve FileNotFoundException handling (#1332) 2025-03-15 10:38:41 +02:00
Koitharu
f7c70577ae Fix local chapters names (close #1323) 2025-03-15 09:00:25 +02:00
Koitharu
937ed798cf Update parsers 2025-03-15 08:16:30 +02:00
Koitharu
8da4f0e180 Merge pull request #1309 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2025-03-15 08:13:53 +02:00
Drama Lover
170d12f143 Translated using Weblate (Arabic)
Currently translated at 88.8% (8 of 9 strings)

Co-authored-by: Drama Lover <loverdrama053@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/
Translation: Kotatsu/plurals
2025-03-14 19:37:56 +00:00
Akhil Raj
0fe3409577 Translated using Weblate (Malayalam)
Currently translated at 11.1% (1 of 9 strings)

Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ml/
Translation: Kotatsu/plurals
2025-03-14 19:37:55 +00:00
Laura
36e431a1ca Translated using Weblate (French)
Currently translated at 100.0% (805 of 805 strings)

Added translation using Weblate (Arabic (Algerian))

Translated using Weblate (Arabic)

Currently translated at 100.0% (805 of 805 strings)

Co-authored-by: Laura <hankmaroua@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-03-14 19:37:54 +00:00
Ore Ki
f30ebda851 Translated using Weblate (Indonesian)
Currently translated at 95.5% (769 of 805 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.4% (768 of 805 strings)

Co-authored-by: Ore Ki <ramadrizkyyy@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-03-14 19:37:53 +00:00
Koitharu
0f021a2d6e Translated using Weblate (Russian)
Currently translated at 100.0% (803 of 803 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-03-14 19:37:53 +00:00
Alvoracz
f816c8ca6e Translated using Weblate (Czech)
Currently translated at 99.5% (799 of 803 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-03-14 19:37:52 +00:00
Y Ok
fe0c4605f7 Translated using Weblate (Indonesian)
Currently translated at 95.5% (767 of 803 strings)

Co-authored-by: Y Ok <yok111263@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-03-14 19:37:51 +00:00
Milo Ivir
196bbff103 Translated using Weblate (Croatian)
Currently translated at 100.0% (795 of 795 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
2025-03-14 19:37:51 +00:00
Anon
80b26e62e9 Translated using Weblate (Serbian)
Currently translated at 98.6% (784 of 795 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-03-14 19:37:50 +00:00
Infy's Tagalog Translations
f877637fd2 Translated using Weblate (Filipino)
Currently translated at 99.7% (801 of 803 strings)

Translated using Weblate (Filipino)

Currently translated at 99.7% (793 of 795 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-03-14 19:37:49 +00:00
Frosted
5037b4ef84 Translated using Weblate (Turkish)
Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Turkish)

Currently translated at 99.8% (802 of 803 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (799 of 799 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (798 of 798 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (795 of 795 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-03-14 19:37:49 +00:00
gekka
11b7696d31 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.7% (801 of 803 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (797 of 799 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (796 of 798 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (793 of 795 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.6% (792 of 795 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-03-14 19:37:48 +00:00
Draken
4ad361dab8 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (795 of 795 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-03-14 19:37:47 +00:00
Nicola Bortoletto
1b88857e4d Translated using Weblate (Italian)
Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (799 of 799 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (795 of 795 strings)

Translated using Weblate (Italian)

Currently translated at 99.8% (794 of 795 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-03-14 19:37:46 +00:00
Koitharu
7823bff063 Fixes and improvements batch 2025-03-14 20:59:37 +02:00
Koitharu
947de6c7c9 Fix tags highlighting 2025-03-11 08:44:55 +02:00
Koitharu
f689bf0cf7 Improve manga link sharing 2025-03-11 08:33:11 +02:00
Koitharu
b3028258ca Apply proxy settings to WebView 2025-03-08 18:51:03 +02:00
Koitharu
2c8476cabd Improve alternatives search functionality 2025-03-08 14:33:23 +02:00
Koitharu
5373e58807 UI tuning 2025-03-08 12:26:39 +02:00
Koitharu
4fdb781622 Merge pull request #1320 from dragonx943/patch-2
Update sync domain
2025-03-08 11:45:46 +02:00
Koitharu
0981ba771a Improve window insets handling 2025-03-08 10:40:20 +02:00
Koitharu
7cec7f5359 Fix window insets handling 2025-03-05 16:56:16 +02:00
Draken
8e55739685 Update constants.xml 2025-03-05 17:10:55 +07:00
Koitharu
d4a2d97071 Fixes 2025-03-04 14:33:50 +02:00
Koitharu
d51790811a Update error details dialog 2025-03-04 14:05:45 +02:00
Koitharu
93e8e87b03 Improve global search 2025-03-03 17:38:05 +02:00
Koitharu
09590cfab0 Update reader actions bar 2025-03-03 14:03:47 +02:00
Koitharu
5d91e20844 Update parsers and fix compatibility 2025-03-02 19:31:01 +02:00
Koitharu
d918b1e274 Fix database migration 2025-03-02 13:35:09 +02:00
Koitharu
7fc2d2f36f Fixes 2025-03-01 17:07:02 +02:00
Koitharu
7a01fdd04c Update parsers and adjust database 2025-02-28 15:35:56 +02:00
Koitharu
8724f5b30c Fixes 2025-02-27 09:36:32 +02:00
Koitharu
fcc05e5e5d Merge pull request #1312 from dragonx943/patch-2 2025-02-26 19:34:23 +02:00
Draken
f6284e7107 Use domain for sync server 2025-02-27 00:19:29 +07:00
Koitharu
5e9dc87470 Fixes and improvements batch 2025-02-26 16:51:33 +02:00
Koitharu
b2f0da9245 Fix "Deflater has been closed" error 2025-02-26 08:12:52 +02:00
Koitharu
b27d6dbe9a Refactor: change window insets handling approach 2025-02-26 08:06:57 +02:00
Koitharu
a7caf9848e Dynamic peek height for BS in details 2025-02-25 13:59:41 +02:00
Koitharu
8d44ad8866 Safe way to getQuantityString 2025-02-25 08:53:45 +02:00
Koitharu
e98f5b9d54 Fix locale changing 2025-02-24 19:58:44 +02:00
Koitharu
30d1d47cdc Fixes 2025-02-24 19:42:18 +02:00
Koitharu
8d7f44d2da Fix DateTimeAgo formatting
(cherry picked from commit 5f0514638a)
2025-02-24 18:09:03 +02:00
Koitharu
930d4dfd83 Fix checking for removed manga updates (close #1064)
(cherry picked from commit 1b8d35d424)
2025-02-24 18:07:49 +02:00
Koitharu
290cb652ee Update parsers 2025-02-24 18:05:51 +02:00
Koitharu
1fa470fd00 Option to disable captcha notification for specific source (#1253 #1300) 2025-02-24 14:57:45 +02:00
Koitharu
c835ebff3f Fix scrobbling bs ui (close #1304) 2025-02-24 13:58:14 +02:00
Koitharu
0e76e69aab Update parsers 2025-02-24 13:21:31 +02:00
Koitharu
1857d9f4e9 Merge pull request #1296 from weblate/weblate-kotatsu-strings
Translations update from Hosted Weblate
2025-02-24 12:55:59 +02:00
Макар Разин
34f13ebd52 Translated using Weblate (Russian)
Currently translated at 100.0% (793 of 793 strings)

Translated using Weblate (Belarusian)

Currently translated at 94.7% (751 of 793 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-02-24 11:53:10 +01:00
Draken
2c4a71cbaa Translated using Weblate (Vietnamese)
Currently translated at 100.0% (793 of 793 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (793 of 793 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-02-24 11:53:10 +01:00
Anon
48515a13da Translated using Weblate (Serbian)
Currently translated at 98.6% (782 of 793 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-02-24 11:53:10 +01:00
Infy's Tagalog Translations
f42cda1584 Translated using Weblate (Filipino)
Currently translated at 99.7% (791 of 793 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-02-24 11:53:10 +01:00
Sixten Lund
3fbbb01e27 Translated using Weblate (Swedish)
Currently translated at 94.4% (749 of 793 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Swedish)

Currently translated at 92.5% (734 of 793 strings)

Translated using Weblate (Swedish)

Currently translated at 68.8% (546 of 793 strings)

Translated using Weblate (Swedish)

Currently translated at 66.8% (530 of 793 strings)

Co-authored-by: Sixten Lund <arbitraryindices@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/sv/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-02-24 11:53:10 +01:00
Frosted
182b8abd7a Translated using Weblate (Turkish)
Currently translated at 100.0% (793 of 793 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-02-24 11:53:10 +01:00
Nicola Bortoletto
dd0445ee79 Translated using Weblate (Italian)
Currently translated at 100.0% (793 of 793 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-02-24 11:53:10 +01:00
Koitharu
d10b64c17f Merge pull request #1297 from dragonx943/patch-2
Add new sync server (Singapore)
2025-02-24 12:53:06 +02:00
Koitharu
ea13c7dbd8 Use WakeLock for background operations 2025-02-23 19:10:05 +02:00
Koitharu
fc5ad9ff90 Include reader tap settings into backups 2025-02-23 18:48:37 +02:00
Koitharu
4cee432a82 Dim navbar in details 2025-02-23 18:06:35 +02:00
Draken
d7c8b12d66 Update constants.xml 2025-02-22 15:13:40 +07:00
Koitharu
87a05ed28a Fix author search 2025-02-20 18:01:16 +02:00
Koitharu
87c242e2bb Dim navbar in details screen 2025-02-20 17:51:47 +02:00
Koitharu
1a7b9c6969 Fix nightly launcher icon 2025-02-20 16:39:12 +02:00
Koitharu
2786e1a2d5 Revert readme updates 2025-02-18 20:18:48 +02:00
Koitharu
05c37da667 Fix build 2025-02-18 20:14:52 +02:00
Koitharu
ed9ed8e964 Fix warnings 2025-02-18 20:08:03 +02:00
Johan Eliott Liebwert
cb36772fd9 Translated using Weblate (Czech)
Currently translated at 100.0% (789 of 789 strings)

Co-authored-by: Johan Eliott Liebwert <joelli@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
Sixten Lund
cc8e31995b Translated using Weblate (Swedish)
Currently translated at 66.0% (521 of 789 strings)

Translated using Weblate (Swedish)

Currently translated at 44.4% (351 of 789 strings)

Co-authored-by: Sixten Lund <arbitraryindices@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sv/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
Priit Jõerüüt
82cd8024e9 Translated using Weblate (Estonian)
Currently translated at 69.7% (550 of 789 strings)

Co-authored-by: Priit Jõerüüt <hwlate@joeruut.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/et/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
Anon
ffc7222b3f Translated using Weblate (Serbian)
Currently translated at 98.7% (779 of 789 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
Master Dwarf
08c48e9997 Translated using Weblate (Portuguese)
Currently translated at 99.8% (788 of 789 strings)

Co-authored-by: Master Dwarf <aoc.anao78@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
Draken
32b6db7343 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (792 of 792 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (790 of 790 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (789 of 789 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
Infy's Tagalog Translations
4dfe0f0d88 Translated using Weblate (Filipino)
Currently translated at 99.8% (788 of 789 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
gekka
0e2224eaf7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (789 of 789 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-02-18 20:05:00 +02:00
Koitharu
151777cf61 UI improvements and author search support 2025-02-18 19:29:14 +02:00
Koitharu
47a22064a5 Merge pull request #1295 from MAPKOBKA135/devel
fix 100% problem
2025-02-18 15:29:09 +02:00
Koitharu
1b8d35d424 Fix checking for removed manga updates (close #1064) 2025-02-18 15:27:10 +02:00
Koitharu
604efef832 Improve global search 2025-02-18 15:20:25 +02:00
Koitharu
6f67bd7542 Bring back reader info bar outline 2025-02-18 14:37:34 +02:00
Mac135135
52592ba765 correct display of a fully read manga
shows 100% in the details menu if the manga has actually been read in full.
2025-02-18 01:24:48 +03:00
Mac135135
9bb97c72a1 "Check mark" fix on cover
if the manga has been read in full, the "check mark" will be displayed correctly on the cover
2025-02-18 00:46:03 +03:00
Koitharu
b44cf370aa Fix favorite categories visibility (close #1281) 2025-02-17 18:06:20 +02:00
Koitharu
74900970e1 Advanced global search 2025-02-17 17:56:45 +02:00
Koitharu
4ee52e149e Configurable manga lists badges 2025-02-16 19:20:01 +02:00
Koitharu
4148f4a4b9 Update dependencies 2025-02-16 16:26:41 +02:00
Koitharu
a59b6a418d Merge pull request #1291 from TheBestF22/FutureProofing-Local-indexes
Future-Proofing Local Storage by Updating Local Archive indexes/Padding for Pages and Chapters - devel branch
2025-02-16 16:21:18 +02:00
TheBest_F-22!.
d6ae67ba07 Merge branch 'devel' into FutureProofing-Local-indexes 2025-02-16 07:57:35 +00:00
TheBest_F-22!.
be455bc897 Future-Proofing Chapters and Pages Local Storage indexes.
Closes #1289 .
2025-02-16 07:22:44 +00:00
Koitharu
129035bda3 Fix picking directory on some devices 2025-02-15 11:24:31 +02:00
Koitharu
d558c2fcc0 Saved and favorites indicators in manga lists (Draft implementation)(#1286) 2025-02-15 10:53:29 +02:00
Koitharu
cb5df0d73f UI fixes 2025-02-11 14:39:35 +02:00
Koitharu
19e8e3a618 Update kotlin to 2.1 2025-02-11 14:23:18 +02:00
Koitharu
5f0514638a Fix DateTimeAgo formatting 2025-02-09 17:40:28 +02:00
Koitharu
28ae785142 Fix bottom navigation height 2025-02-09 17:03:46 +02:00
Koitharu
8c59f97b02 Merge branch 'master' into devel 2025-02-09 10:55:55 +02:00
Koitharu
8a26587250 Database migration for downgrade from 24 to 23 (#1269, #1270) 2025-02-09 10:15:24 +02:00
Koitharu
bb68869fe1 Fix crashes 2025-02-09 10:11:24 +02:00
Koitharu
e60ca7115a Update parsers 2025-02-09 10:01:28 +02:00
Koitharu
ee4a780acf Update parsers 2025-02-08 17:04:22 +02:00
gekka
8dd6ce2739 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.8% (788 of 789 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Deleted User
41ad7e90d3 Translated using Weblate (Italian)
Currently translated at 100.0% (789 of 789 strings)

Co-authored-by: Deleted User <noreply+104791@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
大王叫我来巡山
31be55d67a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (784 of 784 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Dragibus Noir
a50b83d876 Translated using Weblate (French)
Currently translated at 99.8% (783 of 784 strings)

Co-authored-by: Dragibus Noir <dragibusnoir@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
gekka
68ce789db1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (782 of 782 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
urfv ksnph
4702862af3 Translated using Weblate (Indonesian)
Currently translated at 98.2% (768 of 782 strings)

Co-authored-by: urfv ksnph <urfvksnph@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Frosted
c403705b48 Translated using Weblate (Turkish)
Currently translated at 100.0% (789 of 789 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (784 of 784 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (782 of 782 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Nicola Bortoletto
22a5f2d5ee Translated using Weblate (Italian)
Currently translated at 100.0% (782 of 782 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Infy's Tagalog Translations
0f1d8d2835 Translated using Weblate (Filipino)
Currently translated at 99.7% (787 of 789 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Bruno Fragoso
1a6b1672b3 Translated using Weblate (Portuguese)
Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Juan Rubin
8e6006177b Translated using Weblate (Portuguese)
Currently translated at 99.7% (779 of 781 strings)

Co-authored-by: Juan Rubin <juancrubin08@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Draken
4e2f010260 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (789 of 789 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Nizar Yazidi
8a191f8f04 Translated using Weblate (Italian)
Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: Nizar Yazidi <fedekikamakeup@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Milan Bhandari
88aca02234 Translated using Weblate (Nepali)
Currently translated at 31.6% (247 of 781 strings)

Co-authored-by: Milan Bhandari <milanbhandari604@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Hosted Weblate
9189307d00 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
zmni
00fc579824 Translated using Weblate (Indonesian)
Currently translated at 97.9% (765 of 781 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.9% (765 of 781 strings)

Co-authored-by: zmni <zmni@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Anon
7f8d78e0c9 Translated using Weblate (Serbian)
Currently translated at 98.6% (778 of 789 strings)

Translated using Weblate (Serbian)

Currently translated at 99.2% (775 of 781 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Alvoracz
13bd3e918e Translated using Weblate (Czech)
Currently translated at 98.2% (767 of 781 strings)

Translated using Weblate (Czech)

Currently translated at 97.1% (759 of 781 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-02-08 16:44:06 +02:00
Koitharu
e1c46f0604 Fix closing welcome bottom sheet 2025-02-05 17:43:02 +02:00
Koitharu
79f3fe196c Update emoji flags mapping 2025-02-05 16:57:18 +02:00
Koitharu
87a47207ab Save page button in reader 2025-02-05 16:06:53 +02:00
Koitharu
772fd5cc6a Screen rotation button in reader 2025-02-05 15:54:54 +02:00
Koitharu
26a2ffcbb1 Reader bottom bar actions configurable 2025-02-05 14:56:41 +02:00
Koitharu
cfecfce51e Update parsers 2025-02-03 18:56:05 +02:00
Koitharu
23c814bf53 Allow to keep user data on app removal 2025-02-03 18:56:04 +02:00
Koitharu
8ca11b214c Reorganize storage usage settings 2025-02-03 18:56:04 +02:00
Koitharu
008f2d705a Restore backups in background 2025-02-03 18:56:00 +02:00
ViAnh
c37f795dac Fix scroller handle being cut off 2025-02-03 18:21:03 +02:00
Koitharu
5d74bdd3b4 Fix reader info bar background settings 2025-02-02 11:56:08 +02:00
Koitharu
382b44accc Transparent reader info bar option 2025-02-01 09:33:11 +02:00
Koitharu
78cd0eff09 Fix reader info bar icon color 2025-02-01 09:33:11 +02:00
Koitharu
12c954600f Merge pull request #1256 from dragonx943/telegram-feature 2025-01-30 09:58:23 +02:00
Talkc0n
a7d4f3b784 Add Telegram backup feature 2025-01-30 14:09:16 +07:00
Koitharu
91f9feba59 Fix details ui 2025-01-30 08:44:01 +02:00
Koitharu
e03a200c32 Fix build 2025-01-25 12:51:13 +02:00
Koitharu
8713faa487 Update parsers 2025-01-25 12:14:01 +02:00
Koitharu
b3933848e9 Local manga parsing fixes 2025-01-22 10:15:12 +02:00
Koitharu
e39e5bf9c4 Remove Telegram backup functionality 2025-01-22 09:18:13 +02:00
Koitharu
3a42dce45f Support nested cbz covers 2025-01-19 16:26:00 +02:00
Koitharu
169539f42f Support closing Okio FileSystem 2025-01-19 15:18:07 +02:00
Koitharu
6360731f34 Update reader info bar 2025-01-19 15:02:34 +02:00
Koitharu
914dd9670a Option to disable LeakCanary in debug builds 2025-01-19 13:48:55 +02:00
Koitharu
498b9aed26 Track services using LeakCanary 2025-01-19 13:18:19 +02:00
Koitharu
b8b601821a Fix nullability for ParcelableManga 2025-01-19 11:55:13 +02:00
Koitharu
5e8aa4cec7 Update parsers and adjust imports 2025-01-19 11:53:38 +02:00
Koitharu
54bb02937d Merge branch 'master' into devel 2025-01-19 10:33:34 +02:00
Koitharu
74fe786c00 Detect miui 2025-01-19 09:45:51 +02:00
Koitharu
e8f9d22128 Update dependencies 2025-01-18 15:24:45 +02:00
Draken
fa60ae2947 Add new resources (#1242) 2025-01-17 15:55:35 +02:00
Koitharu
663602282a Made SyncAuthActivity exported 2025-01-13 20:01:22 +02:00
Koitharu
98dbc20cb0 Update parsers and adjust fields nullability 2025-01-13 20:01:22 +02:00
Draken
cc28293bed Update docs (README file) (#1238)
* Update README + Add new images

* [Docs] Tweak + Fixes

* [Docs] Fix image size

* Add some informations + Docs

* [vi] Update screenshots

* Add new counter

* Fix color

* [ru] Add new screenshots

* Update readme

* Update Readme

* Update Readme

* Update Readme

* Refactor images

* Refactor images

* Fix refactor images

* Update descriptions

* Fix themes

* Fix themes

* Final ?

---------

Co-authored-by: Draken <dragonx943@users.noreply.github.com>
2025-01-11 15:36:36 +02:00
Koitharu
dddb00d5ef Improve local manga chapter names 2025-01-11 14:54:02 +02:00
Koitharu
c9d878a0b7 Upgrade agp 2025-01-11 14:37:49 +02:00
Koitharu
dcb92ed1af Fix manga importing 2025-01-11 14:37:30 +02:00
Maple Javora
749bc4a837 Translated using Weblate (Czech)
Currently translated at 97.0% (758 of 781 strings)

Co-authored-by: Maple Javora <jindrous101@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Alvoracz
94807b7788 Translated using Weblate (Czech)
Currently translated at 97.0% (758 of 781 strings)

Translated using Weblate (Czech)

Currently translated at 94.2% (736 of 781 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Maple Javora
0fe7c66850 Translated using Weblate (Czech)
Currently translated at 94.3% (737 of 781 strings)

Translated using Weblate (Czech)

Currently translated at 94.2% (736 of 781 strings)

Co-authored-by: Maple Javora <jindrous101@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Alvoracz
20cd8413dc Translated using Weblate (Czech)
Currently translated at 94.2% (736 of 781 strings)

Co-authored-by: Alvoracz <sedlor@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
தமிழ்நேரம்
30df4ede6c Translated using Weblate (Tamil)
Currently translated at 100.0% (781 of 781 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (9 of 9 strings)

Added translation using Weblate (Tamil)

Added translation using Weblate (Tamil)

Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ta/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ta/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2025-01-11 11:21:58 +01:00
Milan Bhandari
4aa6baf569 Translated using Weblate (Nepali)
Currently translated at 30.7% (240 of 781 strings)

Co-authored-by: Milan Bhandari <githubmilan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
abdelbasset jabrane
d8a4303c50 Translated using Weblate (Arabic)
Currently translated at 100.0% (781 of 781 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: abdelbasset jabrane <ribago9317@cubene.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Макар Разин
b355e2ee88 Translated using Weblate (Russian)
Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Infy's Tagalog Translations
55e3b5fb9b Translated using Weblate (Filipino)
Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
gekka
a59853e37a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (781 of 781 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Milo Ivir
ccc665d218 Translated using Weblate (Croatian)
Currently translated at 99.7% (776 of 778 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Anon
02650f5c2a Translated using Weblate (Serbian)
Currently translated at 98.5% (767 of 778 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Hugo Cardoso
24172a1137 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.4% (774 of 778 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.4% (774 of 778 strings)

Co-authored-by: Hugo Cardoso <hugocardosolopes@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Draken
034d69d490 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (781 of 781 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (781 of 781 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (778 of 778 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Justine Kyle Cobar
12fc0542d3 Translated using Weblate (Filipino)
Currently translated at 100.0% (778 of 778 strings)

Co-authored-by: Justine Kyle Cobar <cobarjustinekyle583@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Nicola Bortoletto
dcf80ed396 Translated using Weblate (Italian)
Currently translated at 100.0% (781 of 781 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (778 of 778 strings)

Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Dragibus Noir
28badb7f6c Translated using Weblate (French)
Currently translated at 100.0% (781 of 781 strings)

Translated using Weblate (French)

Currently translated at 100.0% (778 of 778 strings)

Translated using Weblate (French)

Currently translated at 99.8% (777 of 778 strings)

Co-authored-by: Dragibus Noir <dragibusnoir@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-01-11 11:21:58 +01:00
Itsmechinmoy
19cc158ef8 Translated using Weblate (Assamese)
Currently translated at 100.0% (9 of 9 strings)

Added translation using Weblate (Assamese)

Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/as/
Translation: Kotatsu/plurals
2025-01-11 11:21:57 +01:00
大王叫我来巡山
a2eeae3319 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (778 of 778 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2025-01-11 11:21:57 +01:00
Frosted
c9336a753d Translated using Weblate (Turkish)
Currently translated at 100.0% (781 of 781 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (778 of 778 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2025-01-11 11:21:57 +01:00
Anonymous
ea23468ecd Translated using Weblate (French)
Currently translated at 99.2% (772 of 778 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2025-01-11 11:21:57 +01:00
Koitharu
143643fcd8 Fix crashes 2025-01-11 12:21:40 +02:00
Koitharu
25eb05d305 Fix local chapters deletion 2025-01-09 08:41:27 +02:00
Koitharu
bf217b3cbf Skip description for ParcelableManga 2025-01-09 08:32:53 +02:00
Koitharu
9e2b60e15e Fix pages cache usage 2025-01-09 08:26:43 +02:00
Koitharu
4dba90361c Fix crashes 2025-01-09 08:19:43 +02:00
Koitharu
8dea483f64 Fix drawables state 2025-01-05 10:17:40 +02:00
Koitharu
dc2e603356 Improve drawable and views state management 2025-01-04 16:22:13 +02:00
Koitharu
14973298a0 Emoji flags in details 2025-01-04 13:53:13 +02:00
Koitharu
7efc47724e Improve mime-type handling 2025-01-04 12:01:48 +02:00
Koitharu
c51218240e Fix settings menu 2025-01-02 15:45:17 +02:00
Koitharu
2762caaa8f Option to enable all sources 2025-01-02 15:38:43 +02:00
Koitharu
70d66e5a90 Merge branch 'master' into devel 2025-01-01 15:39:51 +02:00
Koitharu
3173e30caf Fix details cover corners 2025-01-01 13:49:14 +02:00
Koitharu
0dccc66f54 Fix settings search 2025-01-01 13:36:02 +02:00
Koitharu
6b3dd23c01 UI fixes 2025-01-01 12:36:52 +02:00
Koitharu
1c6a125174 Skip non-existing local chapters 2025-01-01 12:16:28 +02:00
Koitharu
15f37644c0 Update list badges 2024-12-30 09:48:50 +02:00
Koitharu
c2079ebca5 Update dependencies 2024-12-30 07:51:45 +02:00
Koitharu
1146269992 Merge branch 'weblate-kotatsu-strings' of github.com:weblate/Kotatsu into weblate-weblate-kotatsu-strings 2024-12-23 10:43:10 +02:00
Koitharu
099362d198 Update reader ui 2024-12-23 10:38:27 +02:00
Koitharu
22d203fc60 Fix Telegram backups uploading 2024-12-22 09:01:18 +02:00
Anonymous
19602144ef Translated using Weblate (Norwegian Nynorsk)
Currently translated at 48.8% (380 of 778 strings)

Translated using Weblate (Finnish)

Currently translated at 33.0% (257 of 778 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 36.6% (285 of 778 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2024-12-22 05:01:27 +00:00
Lennard
44bbcd7fe3 Translated using Weblate (Lithuanian)
Currently translated at 5.6% (44 of 778 strings)

Translated using Weblate (Punjabi)

Currently translated at 4.2% (33 of 778 strings)

Translated using Weblate (Czech)

Currently translated at 86.7% (675 of 778 strings)

Translated using Weblate (Arabic)

Currently translated at 85.8% (668 of 778 strings)

Translated using Weblate (English)

Currently translated at 100.0% (778 of 778 strings)

Co-authored-by: Lennard <lennardsdrojek42@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/lt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pa/
Translation: Kotatsu/Strings
2024-12-22 05:01:25 +00:00
Frosted
efe5e07c2c Translated using Weblate (Turkish)
Currently translated at 100.0% (778 of 778 strings)

Translated using Weblate (Turkish)

Currently translated at 99.6% (775 of 778 strings)

Translated using Weblate (Turkish)

Currently translated at 98.7% (766 of 776 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-12-22 05:01:24 +00:00
Geovani Amaral
4e633ff735 Translated using Weblate (Portuguese)
Currently translated at 100.0% (776 of 776 strings)

Co-authored-by: Geovani Amaral <geovani.af4@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-12-22 05:01:21 +00:00
Dragibus Noir
fef8333763 Translated using Weblate (French)
Currently translated at 99.7% (774 of 776 strings)

Co-authored-by: Dragibus Noir <dragibusnoir@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2024-12-22 05:01:20 +00:00
大王叫我来巡山
a741f8451a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (778 of 778 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (776 of 776 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-12-22 05:01:18 +00:00
Draken
55baf5a3f3 Translated using Weblate (Vietnamese)
Currently translated at 99.8% (777 of 778 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (776 of 776 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (772 of 772 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-12-22 05:01:16 +00:00
Anon
6390774d86 Translated using Weblate (Serbian)
Currently translated at 99.0% (769 of 776 strings)

Translated using Weblate (Serbian)

Currently translated at 99.0% (769 of 776 strings)

Translated using Weblate (Serbian)

Currently translated at 99.2% (766 of 772 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-12-22 05:01:13 +00:00
gekka
51a5128e70 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (772 of 772 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-12-22 05:01:12 +00:00
Erekotr
53d81507e4 Translated using Weblate (Turkish)
Currently translated at 98.9% (764 of 772 strings)

Co-authored-by: Erekotr <ereko1ereko55@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-12-22 05:01:11 +00:00
gallegonovato
dcf7236ba2 Translated using Weblate (Spanish)
Currently translated at 100.0% (776 of 776 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (772 of 772 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-12-22 05:01:09 +00:00
Koitharu
a54744abc6 Option to clear manga database 2024-12-20 12:30:48 +02:00
Koitharu
22e2411c77 Cache chapters list (close #812) 2024-12-20 11:33:16 +02:00
Koitharu
3f66c142b8 Merge branch 'master' into devel 2024-12-19 18:30:16 +02:00
Koitharu
734846a018 Fix checking for new chapters in some cases (#1212, #1195, #1190) 2024-12-18 18:24:41 +02:00
Koitharu
e75035b33a Update parsers 2024-12-18 15:38:56 +02:00
Koitharu
f675c606a2 Refactor navigation 2024-12-18 12:00:25 +02:00
Koitharu
a5199e2f06 New favorite dialog 2024-12-16 19:17:25 +02:00
Koitharu
1b80e48ed4 Telegram backups refactoring stage 2 2024-12-15 09:44:57 +02:00
Koitharu
07e81f21c7 Telegram backups refactoring stage 1 2024-12-14 16:26:37 +02:00
Koitharu
0dbd01f6fc Merge branch 'MAPKOBKA135-devel' into devel 2024-12-14 15:48:18 +02:00
Koitharu
4b453b58dd Fix reader slider visibility 2024-12-14 15:48:09 +02:00
Koitharu
1575bb5242 Merge branch 'devel' of github.com:MAPKOBKA135/Kotatsu into MAPKOBKA135-devel 2024-12-14 15:47:41 +02:00
Infy's Tagalog Translations
55137cf899 Translated using Weblate (Filipino)
Currently translated at 99.4% (762 of 766 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
Lennard
f190ff810e Translated using Weblate (German)
Currently translated at 83.3% (639 of 767 strings)

Translated using Weblate (German)

Currently translated at 83.0% (636 of 766 strings)

Co-authored-by: Lennard <lennardsdrojek42@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
Anon
47c13b46f7 Translated using Weblate (Serbian)
Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.6% (759 of 762 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
Frosted
2ad9f38906 Translated using Weblate (Turkish)
Currently translated at 100.0% (762 of 762 strings)

Co-authored-by: Frosted <frosted@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
gekka
2783c62ace Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (766 of 766 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (762 of 762 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
Draken
c1a65f8055 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (766 of 766 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (762 of 762 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
大王叫我来巡山
6e5d8e99ca Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (762 of 762 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
Dragibus Noir
020c3b8bba Translated using Weblate (French)
Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.8% (761 of 762 strings)

Translated using Weblate (French)

Currently translated at 99.7% (760 of 762 strings)

Co-authored-by: Dragibus Noir <dragibusnoir@pm.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2024-12-14 15:41:11 +02:00
Koitharu
76162a06e3 Reader ui updates 2024-12-14 15:39:13 +02:00
Koitharu
19f398d309 Merge branch 'master' into devel 2024-12-14 14:25:48 +02:00
Koitharu
25ae23963e Update reader interface 2024-12-14 09:26:01 +02:00
Koitharu
146ba95af6 Details activity improvements 2024-12-11 13:22:07 +02:00
Koitharu
ee10b013a1 Branch selection in chapters list 2024-12-08 19:26:18 +02:00
Koitharu
8c79df3d35 Details ui updates 2024-12-08 18:20:46 +02:00
Koitharu
2c2db1ca96 Rollback kotlin 2024-12-08 09:58:18 +02:00
Koitharu
f556c0b127 Merge branch 'master' into devel 2024-12-07 15:55:32 +02:00
Koitharu
66645d93f8 Update parsers 2024-12-05 15:52:16 +02:00
Koitharu
f2582bce1d Update dependencies 2024-12-03 14:36:47 +02:00
Mac135135
3ef7c6adb0 Added an periodical backup to the telegram bot 2024-11-10 15:11:40 +03:00
Mac135135
62e7e5d8c3 Merge remote-tracking branch 'origin/devel' into devel 2024-11-10 14:19:06 +03:00
Mac135135
30e43d3bfe Merge remote-tracking branch 'origin/devel' into devel
# Conflicts:
#	app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupSettingsFragment.kt
#	app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/PeriodicalBackupWorker.kt
2024-11-07 23:47:47 +03:00
Mac135135
0162eaed97 Merge remote-tracking branch 'origin/devel' into devel
# Conflicts:
#	app/src/main/res/values-es/strings.xml
#	app/src/main/res/values-zh-rCN/strings.xml
2024-09-30 01:10:25 +03:00
Koitharu
15ca4111c0 Reapply "Update sources catalog ui"
This reverts commit 8d5bde6e60.
2024-09-30 01:09:00 +03:00
Koitharu
dc45e0f5df Revert "Update sources catalog ui"
This reverts commit 597ad01e8f.
2024-09-30 01:09:00 +03:00
Koitharu
09b6a967a1 Refactor descrambling bitmap 2024-09-30 01:09:00 +03:00
AwkwardPeak7
1cff0eeac4 implement basic methods for descrambling images 2024-09-30 01:09:00 +03:00
Kristian de Frutos
44349c4ede Translated using Weblate (Czech)
Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Czech)

Currently translated at 83.0% (528 of 636 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Kristian de Frutos <kristiandef@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-09-30 01:09:00 +03:00
Koitharu
8e8953b07f Skip error for local manga list (close #1113, close #1115) 2024-09-30 01:09:00 +03:00
Felipe Nascimento
150e3d554f Translated using Weblate (Portuguese)
Currently translated at 98.6% (718 of 728 strings)

Co-authored-by: Felipe Nascimento <f.kgb@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-09-30 01:08:58 +03:00
Draken
be3b5a1897 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (724 of 724 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-09-30 01:08:58 +03:00
Matt
9be0e8595f Translated using Weblate (Japanese)
Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Matt <contact.mattdev@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ja/
Translation: Kotatsu/plurals
2024-09-30 01:08:56 +03:00
Oğuz Ersen
f38370592e Translated using Weblate (Turkish)
Currently translated at 100.0% (728 of 728 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (724 of 724 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-09-30 01:08:56 +03:00
Koitharu
6a54d42867 Update SSIV 2024-09-30 01:08:53 +03:00
Mac135135
49d29ae675 Added an periodical backup to the telegram bot 2024-09-30 01:08:53 +03:00
Mac135135
27d7a6a8cb Added an periodical backup to the telegram bot 2024-09-30 01:08:53 +03:00
Koitharu
e8d04644f8 Remove loggers and reorganize settings 2024-09-30 01:08:52 +03:00
Mac135135
26b512d42e Added an periodical backup to the telegram bot 2024-09-30 01:08:48 +03:00
Koitharu
4fb3173185 Update readme 2024-09-30 01:06:32 +03:00
Koitharu
826587b2c9 Translated using Weblate (Russian)
Currently translated at 99.7% (722 of 724 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-09-30 01:06:32 +03:00
Hosted Weblate
4efdb1d8d1 Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2024-09-30 01:06:30 +03:00
Infy's Tagalog Translations
1b9f886d1b Translated using Weblate (Filipino)
Currently translated at 98.4% (713 of 724 strings)

Translated using Weblate (Filipino)

Currently translated at 98.8% (712 of 720 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-09-30 01:06:30 +03:00
Felipe Nascimento
3241ae5db5 Translated using Weblate (Portuguese)
Currently translated at 98.7% (711 of 720 strings)

Co-authored-by: Felipe Nascimento <f.kgb@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-09-30 01:06:30 +03:00
Макар Разин
30f1b2c73a Translated using Weblate (Russian)
Currently translated at 100.0% (720 of 720 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (720 of 720 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-09-30 01:06:30 +03:00
Koitharu
8d35101e98 Update parsers 2024-09-30 01:06:30 +03:00
Koitharu
41cfd99d32 Fix applying filter 2024-09-30 01:06:30 +03:00
Koitharu
c8d04e4eb7 Migrate external sources to new filter 2024-09-30 01:06:29 +03:00
Koitharu
956831f9d7 Fix sync auth activity ui 2024-09-30 01:06:29 +03:00
Draken
d65874080b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (720 of 720 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-09-30 01:06:29 +03:00
Infy's Tagalog Translations
bf35a8ffd7 Translated using Weblate (Filipino)
Currently translated at 98.8% (712 of 720 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-09-30 01:06:29 +03:00
Oğuz Ersen
eeb8dd8c5b Translated using Weblate (Turkish)
Currently translated at 100.0% (720 of 720 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-09-30 01:06:25 +03:00
Faiz Faadhillah
299093f863 Improve Spen integration support 2024-09-30 01:06:25 +03:00
Koitharu
86dea2953a Update parsers 2024-09-30 01:06:25 +03:00
gallegonovato
81794e6eb2 Translated using Weblate (Spanish)
Currently translated at 100.0% (720 of 720 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
2024-09-30 01:06:25 +03:00
Draken
d43887e288 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (717 of 717 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (698 of 698 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (692 of 692 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-09-30 01:05:59 +03:00
Oğuz Ersen
e2cf22e054 Translated using Weblate (Turkish)
Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (717 of 717 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (698 of 698 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (692 of 692 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-09-30 01:05:59 +03:00
Koitharu
5a75fe77fd Various fixes 2024-09-30 01:05:41 +03:00
Koitharu
8c0617c525 Context menus 2024-09-30 01:05:41 +03:00
Anonymous
38b8966c16 Translated using Weblate (Hungarian)
Currently translated at 86.6% (622 of 718 strings)

Translated using Weblate (Nepali)

Currently translated at 32.3% (232 of 718 strings)

Translated using Weblate (Hindi)

Currently translated at 93.0% (668 of 718 strings)

Translated using Weblate (Portuguese)

Currently translated at 92.7% (666 of 718 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-09-30 01:05:41 +03:00
desu sude
59f4ff8a3e Translated using Weblate (Latvian)
Currently translated at 24.4% (175 of 717 strings)

Co-authored-by: desu sude <cobsonslittlecocksleeve@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/lv/
Translation: Kotatsu/Strings
2024-09-30 01:05:41 +03:00
Anon
357263b496 Translated using Weblate (Serbian)
Currently translated at 100.0% (698 of 698 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-09-30 01:05:41 +03:00
Milo Ivir
4af6fc165b Translated using Weblate (Croatian)
Currently translated at 98.7% (689 of 698 strings)

Co-authored-by: Milo Ivir <mail@milotype.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hr/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Infy's Tagalog Translations
a4de58b9b3 Translated using Weblate (Filipino)
Currently translated at 100.0% (698 of 698 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (698 of 698 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
abc0922001
5696ad7fa2 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 92.1% (643 of 698 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Amirreza Safavi
63bfca6d3e Translated using Weblate (Persian)
Currently translated at 41.6% (288 of 692 strings)

Co-authored-by: Amirreza Safavi <amirxcatsanddragons@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
gekka
0fecf996e1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.7% (716 of 718 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (696 of 698 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (696 of 698 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (691 of 692 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Shayan
3df2682332 Translated using Weblate (Persian)
Currently translated at 41.6% (288 of 692 strings)

Co-authored-by: Shayan <shayans31516@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Amirreza Safavi
dd9df6e9dc Translated using Weblate (Persian)
Currently translated at 41.6% (288 of 692 strings)

Co-authored-by: Amirreza Safavi <amirxcatsanddragons@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
gallegonovato
0889c2cc28 Translated using Weblate (Spanish)
Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (717 of 717 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (698 of 698 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (698 of 698 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (692 of 692 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Draken
010b1264ae Translated using Weblate (Vietnamese)
Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (717 of 717 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (698 of 698 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (692 of 692 strings)

Co-authored-by: Draken <premieregirl26@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Oğuz Ersen
66ff32e14d Translated using Weblate (Turkish)
Currently translated at 100.0% (718 of 718 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (717 of 717 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (698 of 698 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (692 of 692 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Fikri Akbar
addb642cc9 Translated using Weblate (Indonesian)
Currently translated at 99.8% (688 of 689 strings)

Co-authored-by: Fikri Akbar <akbarfikri1221@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-09-30 01:05:40 +03:00
Koitharu
720c389dbd Search in history, favorites and local 2024-09-30 01:05:40 +03:00
Koitharu
2191d9c83b Fix sources catalog content types 2024-09-30 01:05:40 +03:00
Koitharu
0ee1cda0e4 Local manga source filter 2024-09-30 01:05:40 +03:00
Koitharu
90226b7b78 Update supported domains 2024-09-30 01:05:39 +03:00
Koitharu
6d84294533 Improve quick filters 2024-09-30 01:05:39 +03:00
Koitharu
36bd3cc438 Local manga index in database 2024-09-30 01:05:39 +03:00
Koitharu
e0c983f4eb Search manga with filters 2024-09-30 01:05:39 +03:00
Koitharu
ea5ce23335 Improve filter 2024-09-30 01:05:39 +03:00
Koitharu
26a33e5d9d Add new filter fields 2024-09-30 01:05:39 +03:00
Koitharu
9ab7159cb9 Update parsers and filters 2024-09-30 01:05:39 +03:00
Koitharu
ad21321a1d Update parsers 2024-09-30 01:05:39 +03:00
Koitharu
fe2bb05895 Update dependencies 2024-09-30 01:05:39 +03:00
Koitharu
e48beae324 Batch manga fix functionality 2024-09-30 01:05:39 +03:00
Amirreza Safavi
10109ab2c0 Translated using Weblate (Persian)
Currently translated at 77.7% (7 of 9 strings)

Translated using Weblate (Persian)

Currently translated at 37.4% (258 of 689 strings)

Co-authored-by: Amirreza Safavi <amirxcatsanddragons@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fa/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-09-30 01:05:38 +03:00
Anon
df17bb5af8 Translated using Weblate (Serbian)
Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-09-30 01:05:38 +03:00
Henrique
b4592015fb Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.6% (666 of 689 strings)

Co-authored-by: Henrique <heluis110@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-09-30 01:05:38 +03:00
Макар Разин
3fe9ec6918 Translated using Weblate (Russian)
Currently translated at 100.0% (689 of 689 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (689 of 689 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-09-30 01:05:38 +03:00
Dilip Patel
23ac9df844 grammar fix 2024-09-30 01:05:38 +03:00
Koitharu
c480992f63 Option to automatically download new chapters (close #425, close #602, close #955) 2024-09-30 01:05:38 +03:00
Koitharu
85d397def0 Update dependencies 2024-09-30 01:05:38 +03:00
Mac135135
7c74c87524 Merge remote-tracking branch 'origin/devel' into devel 2024-09-07 15:26:13 +03:00
Mac135135
f86ee7d5c2 Merge remote-tracking branch 'origin/devel' into devel 2024-08-04 19:46:52 +03:00
Mac135135
6e5519419d Merge remote-tracking branch 'origin/devel' into devel 2024-07-31 19:16:41 +03:00
Mac135135
2c53b63847 Merge remote-tracking branch 'origin/devel' into devel
# Conflicts:
#	app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt
2024-07-11 01:35:18 +03:00
Mac135135
45b5e48676 Add functionality to expand manga title on click 2024-07-05 21:23:15 +03:00
655 changed files with 15226 additions and 7947 deletions

BIN
.github/assets/vtuber.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -1,114 +0,0 @@
name: Build automatic release
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-24.04
outputs:
should_build: ${{ steps.check-updates.outputs.has_changes }}
steps:
- name: Check for updates 🌏
id: check-updates
run: |
last_run=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.created_at')
kotatsu_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/Kotatsu/commits?since=$last_run" | jq '. | length')
parsers_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/kotatsu-parsers/commits?since=$last_run" | jq '. | length')
if [ "$kotatsu_updated" -gt "0" ] || [ "$parsers_updated" -gt "0" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
build:
needs: check
if: needs.check.outputs.should_build == 'true'
runs-on: ubuntu-24.04
outputs:
new_tag: ${{ steps.tagger.outputs.new_tag }}
steps:
- uses: actions/checkout@v3
with:
ref: autobuild
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Setup Android SDK 💻
uses: android-actions/setup-android@v3
- name: Grant permissions 💻
run: chmod a+x gradlew
- name: Generate build number 📆
id: tagger
run: |
echo "new_tag=$(./gradlew -q versionInfo -DbuildNumberIncrement=true)" >> $GITHUB_OUTPUT
echo "formatted_date=$(date +'%Y/%m/%d')" >> $GITHUB_OUTPUT
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1
with:
fileName: 'keystore/kotatsu.jks'
encodedString: ${{ secrets.ANDROID_SIGNING_KEY }}
- name: Building new APK 💻
run: >-
./gradlew assembleRelease
-DparsersVersionOverride=$(curl -s https://api.github.com/repos/kotatsuapp/kotatsu-parsers/commits/master -H "Accept: application/vnd.github.sha" | cut -c -10)
- name: Prepare to Upload 🌏
run: |
mv ${{steps.sign_app.outputs.signedFile}} app/build/outputs/apk/release/release.apk
echo "SIGNED_APK=app/build/outputs/apk/release/release.apk" >> $GITHUB_ENV
- name: Get latest changes 📑
id: changelog
run: |
CHANGELOG=$(cat CHANGELOG.txt)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create new GH Release + Uploading 🌏
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.tagger.outputs.new_tag }}
name: "Build ${{ steps.tagger.outputs.new_tag }}"
body: |
Automated build generated on ${{ steps.tagger.outputs.formatted_date }}
${{ steps.changelog.outputs.content }}
files: ${{ env.SIGNED_APK }}
prerelease: false
update:
needs: build
if: needs.check.outputs.should_build == 'true'
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
ref: autobuild
fetch-depth: 0
- name: Commit 🌏
run: |
git config --local user.email "autorelease@users.noreply.github.com"
git config --local user.name "autorelease"
if [[ -n $(git status -s) ]]; then
git add README.md
git commit -m "Automatic release v${{ needs.build.outputs.new_tag }}"
git push origin autobuild
else
echo "No changes to push!"
fi

2
.gitignore vendored
View File

@@ -26,4 +26,4 @@
.cxx
/.idea/deviceManager.xml
/.kotlin/
/.idea/AndroidProjectSystem.xml
/.idea/AndroidProjectSystem.xml

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

3
.idea/gradle.xml generated
View File

@@ -6,14 +6,13 @@
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="gradleJvm" value="jbr-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

106
README.md
View File

@@ -1,57 +1,107 @@
# Kotatsu
<div align="center">
Kotatsu is a free and open-source manga reader for Android with built-in online content sources.
<a href="https://kotatsu.app">
<img src="./.github/assets/vtuber.png" alt="Kotatsu Logo" title="Kotatsu" width="600"/>
</a>
[![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C)](https://github.com/KotatsuApp/kotatsu-parsers) ![F-Droid Version](https://img.shields.io/f-droid/v/org.koitharu.kotatsu) ![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF)](https://t.me/kotatsuapp) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) [![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu)](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
# [Kotatsu](https://kotatsu.app)
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is a free and open-source manga reader for Android with built-in online content sources.**
![Downloads count](https://img.shields.io/github/downloads/KotatsuApp/Kotatsu/total?color=1976d2) ![Latest Stable version](https://img.shields.io/github/v/release/KotatsuApp/Kotatsu?color=2596be&label=latest) ![Android 5.0](https://img.shields.io/badge/android-5.0+-brightgreen) [![Sources count](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FKotatsuApp%2Fkotatsu-parsers%2Frefs%2Fheads%2Fmaster%2F.github%2Fsummary.yaml&query=total&label=manga%20sources&color=%23E9321C)](https://github.com/KotatsuApp/kotatsu-parsers) [![weblate](https://hosted.weblate.org/widgets/kotatsu/-/strings/svg-badge.svg)](https://hosted.weblate.org/engage/kotatsu/) [![Discord](https://img.shields.io/discord/898363402467045416?color=5865f2&label=discord)](https://discord.gg/NNJ5RgVBC5) [![Telegram](https://img.shields.io/badge/chat-telegram-60ACFF?)](https://t.me/kotatsuapp) [![License](https://img.shields.io/github/license/KotatsuApp/Kotatsu)](https://github.com/KotatsuApp/Kotatsu/blob/devel/LICENSE)
### Download
- **Recommended:** Download and install APK from **[GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest)**. Application has a built-in self-updating feature.
- Get it on **[F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu)**. The F-Droid build may be a bit outdated and some fixes might be missing.
- Also [nightly builds](https://github.com/KotatsuApp/Kotatsu-nightly/releases) are available (very unstable, use at your own risk).
<div align="left">
* **Recommended:** Download and install APK from [GitHub Releases](https://github.com/KotatsuApp/Kotatsu/releases/latest). Application has a built-in self-updating feature.
* Get it on [F-Droid](https://f-droid.org/packages/org.koitharu.kotatsu). The F-Droid build may be a bit outdated and some fixes might be missing.
* Also [nightly builds](https://github.com/KotatsuApp/Kotatsu-nightly/releases) are available (very unstable, use at your own risk).
</div>
### Main Features
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
<div align="left">
* Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers) (with 1100+ manga sources)
* Search manga by name, genres, and more filters
* Reading history and bookmarks
* Favorites organized by user-defined categories
* Downloading manga and reading it offline. Third-party CBZ archives also supported
* Tablet-optimized Material You UI
* Standard and Webtoon-optimized customizable reader
* Notifications about new chapters with updates feed
* Reading history, bookmarks, and incognito mode support
* Download manga and read it offline. Third-party CBZ archives are also supported
* Clean and convenient Material You UI, optimized for phones, tablets, and desktop
* Standard and Webtoon-optimized customizable reader, gesture support on reading interface
* Notifications about new chapters with updates feed, manga recommendations (with filters)
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
* Password/fingerprint-protected access to the app
* Password / fingerprint-protected access to the app
* Automatically sync app data with other devices on the same account
* Support for older devices running Android 5+
### Screenshots
</div>
| ![Screenshot_20200226-210337](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/1.png) | ![](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/2.png) | ![Screenshot_20200226-210232](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/3.png) |
|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|
| ![Screenshot_20200226-210405](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/4.png) | ![Screenshot_20200226-210151](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/5.png) | ![Screenshot_20200226-210223](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/phoneScreenshots/6.png) |
### In-App Screenshots
| ![](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/1.png) | ![](https://github.com/KotatsuApp/Kotatsu/raw/devel/metadata/en-US/images/tenInchScreenshots/2.png) |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
<div align="center">
<img src="./metadata/en-US/images/phoneScreenshots/1.png" alt="Mobile view" width="250"/>
<img src="./metadata/en-US/images/phoneScreenshots/2.png" alt="Mobile view" width="250"/>
<img src="./metadata/en-US/images/phoneScreenshots/3.png" alt="Mobile view" width="250"/>
<img src="./metadata/en-US/images/phoneScreenshots/4.png" alt="Mobile view" width="250"/>
<img src="./metadata/en-US/images/phoneScreenshots/5.png" alt="Mobile view" width="250"/>
<img src="./metadata/en-US/images/phoneScreenshots/6.png" alt="Mobile view" width="250"/>
</div>
<br>
<div align="center">
<img src="./metadata/en-US/images/tenInchScreenshots/1.png" alt="Tablet view" width="400"/>
<img src="./metadata/en-US/images/tenInchScreenshots/2.png" alt="Tablet view" width="400"/>
</div>
### Localization
[<img src="https://hosted.weblate.org/widgets/kotatsu/-/287x66-white.png" alt="Translation status">](https://hosted.weblate.org/engage/kotatsu/)
<a href="https://hosted.weblate.org/engage/kotatsu/">
<img src="https://hosted.weblate.org/widget/kotatsu/horizontal-auto.png" alt="Translation status" />
</a>
Kotatsu is localized in a number of different languages, if you would like to help improve these or add new languages,
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)
**[Kotatsu](https://github.com/KotatsuApp/Kotatsu) is localized in a number of different languages.**<br>
**📌 If you would like to help improve these or add new languages,
please head over to the [Weblate project page](https://hosted.weblate.org/engage/kotatsu/)**
### Contributing
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the guidelines.
<br>
<a href="https://github.com/KotatsuApp/Kotatsu">
<picture>
<source srcset="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu&bg_color=0d1117&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" media="(prefers-color-scheme: dark)">
<img src="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" alt="Kotatsu GitHub Repository">
</picture>
</a>
<a href="https://github.com/KotatsuApp/Kotatsu-parsers">
<picture>
<source srcset="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu-parsers&bg_color=0d1117&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" media="(prefers-color-scheme: dark)">
<img src="https://github-readme-stats.vercel.app/api/pin/?username=KotatsuApp&repo=Kotatsu-parsers&text_color=1976d2&title_color=1976d2&icon_color=0877d2&border_radius=10&description_lines_count=2&show_owner=true" alt="Kotatsu-parsers GitHub Repository">
</picture>
</a><br></br>
</br>
**📌 Pull requests are welcome, if you want: See [CONTRIBUTING.md](https://github.com/KotatsuApp/Kotatsu/blob/devel/CONTRIBUTING.md) for the guidelines**
### License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html)
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications
to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build &
install instructions.
<div align="left">
You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
</div>
### DMCA disclaimer
The developers of this application do not have any affiliation with the content available in the app.
It collects content from sources that are freely available through any web browser
<div align="left">
The developers of this application do not have any affiliation with the content available in the app. It collects content from sources that are freely available through any web browser.
</div>

View File

@@ -7,51 +7,92 @@ plugins {
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
id 'androidx.room'
}
def Properties versionProps = getVersionProps()
android {
compileSdk = 35
buildToolsVersion = '35.0.0'
namespace = 'org.koitharu.kotatsu'
defaultConfig {
def code = versionProps['code'].toInteger()
def base = versionProps['base'].trim()
def build = versionProps['build'].toInteger()
def variant = versionProps['variant'].trim()
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 35
versionCode = code * 1000 + build
versionName = base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
versionCode = 1012
versionName = '8.1.6'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
arg('room.generateKotlin', 'true')
arg('room.schemaLocation', "$projectDir/schemas")
}
androidResources {
generateLocaleConfig true
}
}
signingConfigs {
release {
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
def allFilesFromDir = new File(tmpFilePath).listFiles()
if (allFilesFromDir != null) {
def keystoreFile = allFilesFromDir.first()
keystoreFile.renameTo("keystore/kotatsu.jks")
}
storeFile = file("keystore/kotatsu.jks")
storePassword System.getenv("SIGNING_STORE_PASSWORD")
keyAlias System.getenv("SIGNING_KEY_ALIAS")
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
generateLocaleConfig false
}
resourceConfigurations += [
"en",
"ab",
"ar",
"arq",
"as",
"be",
"bn",
"ca",
"cs",
"de",
"el",
"en-rGB",
"enm",
"es",
"et",
"eu",
"fa",
"fi",
"fil",
"fr",
"frp",
"gu",
"hi",
"hr",
"hu",
"in",
"it",
"iw",
"ja",
"kk",
"km",
"ko",
"lt",
"lv",
"lzh",
"ml",
"ms",
"my",
"nb-rNO",
"ne",
"nn",
"or",
"pa",
"pa-rPK",
"pl",
"pt",
"pt-rBR",
"ro",
"ru",
"si",
"sr",
"sv",
"ta",
"th",
"tr",
"uk",
"vi",
"zh-rCN",
"zh-rTW",
// Specific BCP 47 locales
"b+zh+Hans+MO",
"b+zh+Hant+MO"
]
}
buildTypes {
debug {
@@ -61,7 +102,6 @@ android {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
nightly {
initWith release
@@ -98,8 +138,14 @@ android {
'-opt-in=kotlinx.coroutines.FlowPreview',
'-opt-in=kotlin.contracts.ExperimentalContracts',
'-opt-in=coil3.annotation.ExperimentalCoilApi',
'-opt-in=coil3.annotation.InternalCoilApi',
'-Xjspecify-annotations=strict',
'-Xtype-enhancement-improvements-strict-mode',
]
}
room {
schemaDirectory "$projectDir/schemas"
}
lint {
abortOnError true
disable 'MissingTranslation', 'PrivateResource', 'SetJavaScriptEnabled', 'SimpleDateFormat'
@@ -218,22 +264,3 @@ dependencies {
androidTestImplementation libs.hilt.android.testing
kaptAndroidTest libs.hilt.android.compiler
}
tasks.register('versionInfo') {
def base = versionProps['base'].trim()
def build = versionProps['build'].toInteger()
def variant = versionProps['variant'].trim()
println base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
}
def getVersionProps() {
def versionPropsFile = file('version.properties')
def Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
if (System.getProperty('buildNumberIncrement') == 'true') {
def code = versionProps['build'].toInteger() + 1
versionProps['build'] = code.toString()
versionProps.store(versionPropsFile.newWriter(), null)
}
return versionProps
}

View File

@@ -1,21 +1,37 @@
package org.koitharu.kotatsu
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.os.StrictMode
import androidx.core.content.edit
import androidx.fragment.app.strictmode.FragmentStrictMode
import leakcanary.LeakCanary
import org.koitharu.kotatsu.core.BaseApp
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderViewModel
class KotatsuApp : BaseApp() {
var isLeakCanaryEnabled: Boolean
get() = getDebugPreferences(this).getBoolean(KEY_LEAK_CANARY, true)
set(value) {
getDebugPreferences(this).edit { putBoolean(KEY_LEAK_CANARY, value) }
configureLeakCanary()
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
enableStrictMode()
configureLeakCanary()
}
private fun configureLeakCanary() {
LeakCanary.config = LeakCanary.config.copy(
dumpHeap = isLeakCanaryEnabled,
)
}
private fun enableStrictMode() {
@@ -50,12 +66,11 @@ class KotatsuApp : BaseApp() {
setClassInstanceLimit(PagesCache::class.java, 1)
setClassInstanceLimit(MangaLoaderContext::class.java, 1)
setClassInstanceLimit(PageLoader::class.java, 1)
setClassInstanceLimit(ReaderViewModel::class.java, 1)
penaltyLog()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) {
penaltyListener(notifier.executor, notifier)
}
}.build()
}.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder().apply {
detectWrongFragmentContainer()
@@ -70,4 +85,13 @@ class KotatsuApp : BaseApp() {
}
}.build()
}
private companion object {
const val PREFS_DEBUG = "_debug"
const val KEY_LEAK_CANARY = "leak_canary"
fun getDebugPreferences(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_DEBUG, MODE_PRIVATE)
}
}

View File

@@ -55,7 +55,7 @@ class StrictModeNotifier(
.setContentIntent(
PendingIntentCompat.getActivity(
context,
0,
violation.hashCode(),
ShareHelper(context).getShareTextIntent(violation.stackTraceToString()),
0,
false,

View File

@@ -0,0 +1,21 @@
package org.koitharu.kotatsu.core.ui
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleService
import leakcanary.AppWatcher
abstract class BaseService : LifecycleService() {
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(ContextCompat.getContextForLanguage(newBase))
}
override fun onDestroy() {
super.onDestroy()
AppWatcher.objectWatcher.watch(
watchedObject = this,
description = "${javaClass.simpleName} service received Service#onDestroy() callback",
)
}
}

View File

@@ -5,7 +5,9 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import leakcanary.LeakCanary
import org.koitharu.kotatsu.KotatsuApp
import org.koitharu.kotatsu.R
import org.koitharu.workinspector.WorkInspector
@@ -13,10 +15,19 @@ class SettingsMenuProvider(
private val context: Context,
) : MenuProvider {
private val application: KotatsuApp
get() = context.applicationContext as KotatsuApp
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_settings, menu)
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_leakcanary).isChecked = application.isLeakCanaryEnabled
menu.findItem(R.id.action_ssiv_debug).isChecked = SubsamplingScaleImageView.isDebug
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_leaks -> {
context.startActivity(LeakCanary.newLeakDisplayActivityIntent())
@@ -28,6 +39,20 @@ class SettingsMenuProvider(
true
}
R.id.action_leakcanary -> {
val checked = !menuItem.isChecked
menuItem.isChecked = checked
application.isLeakCanaryEnabled = checked
true
}
R.id.action_ssiv_debug -> {
val checked = !menuItem.isChecked
menuItem.isChecked = checked
SubsamplingScaleImageView.isDebug = checked
true
}
else -> false
}
}

View File

@@ -1,15 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@id/action_leaks"
android:id="@+id/action_ssiv_debug"
android:checkable="true"
android:title="SSIV debug"
app:showAsAction="never"
tools:ignore="HardcodedText" />
<item
android:id="@+id/action_leakcanary"
android:checkable="true"
android:title="LeakCanary"
app:showAsAction="never"
tools:ignore="HardcodedText" />
<item
android:id="@+id/action_leaks"
android:title="@string/leak_canary_display_activity_label"
app:showAsAction="never" />
<item
android:id="@id/action_works"
android:id="@+id/action_works"
android:title="@string/wi_lib_name"
app:showAsAction="never" />

View File

@@ -49,8 +49,10 @@
android:enableOnBackInvokedCallback="@bool/is_predictive_back_enabled"
android:fullBackupContent="@xml/backup_content"
android:fullBackupOnly="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
@@ -280,6 +282,10 @@
<service
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
android:label="@string/local_manga_processing" />
<service
android:name="org.koitharu.kotatsu.settings.backup.RestoreService"
android:foregroundServiceType="dataSync"
android:label="@string/restore_backup" />
<service
android:name="org.koitharu.kotatsu.local.ui.ImportService"
android:foregroundServiceType="dataSync"

View File

@@ -3,88 +3,76 @@ package org.koitharu.kotatsu.alternatives.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.domain.SearchV2Helper
import java.util.Locale
import javax.inject.Inject
private const val MAX_PARALLELISM = 4
private const val MATCH_THRESHOLD_DEFAULT = 0.2f
class AlternativesUseCase @Inject constructor(
private val sourcesRepository: MangaSourcesRepository,
private val searchHelperFactory: SearchV2Helper.Factory,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
suspend operator fun invoke(manga: Manga): Flow<Manga> = invoke(manga, MATCH_THRESHOLD_DEFAULT)
suspend operator fun invoke(manga: Manga, matchThreshold: Float): Flow<Manga> {
val sources = getSources(manga.source)
suspend operator fun invoke(manga: Manga, throughDisabledSources: Boolean): Flow<Manga> {
val sources = getSources(manga.source, throughDisabledSources)
if (sources.isEmpty()) {
return emptyFlow()
}
val semaphore = Semaphore(MAX_PARALLELISM)
return channelFlow {
for (source in sources) {
val repository = mangaRepositoryFactory.create(source)
if (!repository.filterCapabilities.isSearchSupported) {
continue
}
launch {
val searchHelper = searchHelperFactory.create(source)
val list = runCatchingCancellable {
semaphore.withPermit {
repository.getList(offset = 0, SortOrder.RELEVANCE, MangaListFilter(query = manga.title))
searchHelper(manga.title, SearchKind.TITLE)?.manga
}
}.getOrDefault(emptyList())
for (item in list) {
if (item.matches(manga, matchThreshold)) {
send(item)
}.getOrNull()
list?.forEach { m ->
if (m.id != manga.id) {
launch {
val details = runCatchingCancellable {
mangaRepositoryFactory.create(m.source).getDetails(m)
}.getOrDefault(m)
send(details)
}
}
}
}
}
}.map {
runCatchingCancellable {
mangaRepositoryFactory.create(it.source).getDetails(it)
}.getOrDefault(it)
}
}
private suspend fun getSources(ref: MangaSource): List<MangaSource> {
val result = ArrayList<MangaSource>(MangaParserSource.entries.size - 2)
result.addAll(sourcesRepository.getEnabledSources())
result.sortByDescending { it.priority(ref) }
result.addAll(sourcesRepository.getDisabledSources().sortedByDescending { it.priority(ref) })
return result
}
private fun Manga.matches(ref: Manga, threshold: Float): Boolean {
return matchesTitles(title, ref.title, threshold) ||
matchesTitles(title, ref.altTitle, threshold) ||
matchesTitles(altTitle, ref.title, threshold) ||
matchesTitles(altTitle, ref.altTitle, threshold)
}
private fun matchesTitles(a: String?, b: String?, threshold: Float): Boolean {
return !a.isNullOrEmpty() && !b.isNullOrEmpty() && a.almostEquals(b, threshold)
}
private suspend fun getSources(ref: MangaSource, disabled: Boolean): List<MangaSource> = if (disabled) {
sourcesRepository.getDisabledSources()
} else {
sourcesRepository.getEnabledSources()
}.sortedByDescending { it.priority(ref) }
private fun MangaSource.priority(ref: MangaSource): Int {
var res = 0
if (this is MangaParserSource && ref is MangaParserSource) {
if (locale == ref.locale) res += 2
if (contentType == ref.contentType) res++
if (locale == ref.locale) {
res += 4
} else if (locale.toLocale() == Locale.getDefault()) {
res += 2
}
if (contentType == ref.contentType) {
res++
}
}
return res
}

View File

@@ -15,6 +15,7 @@ import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.concat
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.concurrent.TimeUnit
@@ -29,12 +30,14 @@ class AutoFixUseCase @Inject constructor(
) {
suspend operator fun invoke(mangaId: Long): Pair<Manga, Manga?> {
val seed = checkNotNull(mangaDataRepository.findMangaById(mangaId)) { "Manga $mangaId not found" }
.getDetailsSafe()
val seed = checkNotNull(
mangaDataRepository.findMangaById(mangaId, withChapters = true),
) { "Manga $mangaId not found" }.getDetailsSafe()
if (seed.isHealthy()) {
return seed to null // no fix required
}
val replacement = alternativesUseCase(seed, matchThreshold = 0.02f)
val replacement = alternativesUseCase(seed, throughDisabledSources = false)
.concat(alternativesUseCase(seed, throughDisabledSources = true))
.filter { it.isHealthy() }
.runningFold<Manga, Manga?>(null) { best, candidate ->
if (best == null || best < candidate) {

View File

@@ -4,6 +4,7 @@ import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.ImageRequest
@@ -26,6 +27,7 @@ import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
import org.koitharu.kotatsu.core.util.ext.mangaExtra
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.newImageRequest
@@ -51,10 +53,22 @@ fun alternativeAD(
binding.chipSource.setOnClickListener(clickListener)
bind { payloads ->
binding.textViewTitle.text = item.manga.title
binding.textViewTitle.text = item.mangaModel.title
with(binding.iconsView) {
clearIcons()
if (item.mangaModel.isSaved) addIcon(R.drawable.ic_storage)
if (item.mangaModel.isFavorite) addIcon(R.drawable.ic_heart_outline)
isVisible = iconsCount > 0
}
binding.textViewSubtitle.text = buildSpannedString {
if (item.chaptersCount > 0) {
append(context.resources.getQuantityString(R.plurals.chapters, item.chaptersCount, item.chaptersCount))
append(
context.resources.getQuantityStringSafe(
R.plurals.chapters,
item.chaptersCount,
item.chaptersCount,
),
)
} else {
append(context.getString(R.string.no_chapters))
}
@@ -70,7 +84,10 @@ fun alternativeAD(
}
}
}
binding.progressView.setProgress(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
binding.progressView.setProgress(
item.mangaModel.progress,
ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads,
)
binding.chipSource.also { chip ->
chip.text = item.manga.source.getTitle(chip.context)
ImageRequest.Builder(context)

View File

@@ -1,41 +1,40 @@
package org.koitharu.kotatsu.alternatives.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import coil3.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.systemBarsInsets
import org.koitharu.kotatsu.databinding.ActivityAlternativesBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.adapter.buttonFooterAD
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject
@AndroidEntryPoint
class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
ListStateHolderListener,
OnListItemClickListener<MangaAlternativeModel> {
@Inject
@@ -55,6 +54,7 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
.addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, this, null))
.addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
.addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
.addDelegate(ListItemType.FOOTER_BUTTON, buttonFooterAD(this))
with(viewBinding.recyclerView) {
setHasFixedSize(true)
addItemDecoration(TypedListSpacingDecoration(context, addHorizontalPadding = false))
@@ -62,39 +62,46 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
}
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.content.observe(this, listAdapter)
viewModel.list.observe(this, listAdapter)
viewModel.onMigrated.observeEvent(this) {
Toast.makeText(this, R.string.migration_completed, Toast.LENGTH_SHORT).show()
startActivity(DetailsActivity.newIntent(this, it))
router.openDetails(it)
finishAfterTransition()
}
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)
override fun onApplyWindowInsets(
v: View,
insets: WindowInsetsCompat
): WindowInsetsCompat {
val barsInsets = insets.systemBarsInsets
viewBinding.recyclerView.updatePadding(
bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
left = barsInsets.left,
right = barsInsets.right,
bottom = barsInsets.bottom,
)
viewBinding.appbar.updatePadding(
left = barsInsets.left,
right = barsInsets.right,
top = barsInsets.top,
)
return insets.consumeAllSystemBarsInsets()
}
override fun onItemClick(item: MangaAlternativeModel, view: View) {
when (view.id) {
R.id.chip_source -> startActivity(
MangaListActivity.newIntent(
this,
item.manga.source,
MangaListFilter(query = viewModel.manga.title),
),
)
R.id.chip_source -> router.openSearch(item.manga.source, viewModel.manga.title)
R.id.button_migrate -> confirmMigration(item.manga)
else -> startActivity(DetailsActivity.newIntent(this, item.manga))
else -> router.openDetails(item.manga)
}
}
override fun onRetryClick(error: Throwable) = viewModel.retry()
override fun onEmptyActionClick() = Unit
override fun onFooterButtonClick() = viewModel.continueSearch()
private fun confirmMigration(target: Manga) {
buildAlertDialog(this, isCentered = true) {
setIcon(R.drawable.ic_replace)
@@ -114,10 +121,4 @@ class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
}
}.show()
}
companion object {
fun newIntent(context: Context, manga: Manga) = Intent(context, AlternativesActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
}
}

View File

@@ -1,33 +1,40 @@
package org.koitharu.kotatsu.alternatives.ui
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.runningFold
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.domain.AlternativesUseCase
import org.koitharu.kotatsu.alternatives.domain.MigrateUseCase
import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.append
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.domain.MangaListMapper
import org.koitharu.kotatsu.list.ui.model.ButtonFooter
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrDefault
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import javax.inject.Inject
@HiltViewModel
@@ -36,46 +43,67 @@ class AlternativesViewModel @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val alternativesUseCase: AlternativesUseCase,
private val migrateUseCase: MigrateUseCase,
private val historyRepository: HistoryRepository,
private val settings: AppSettings,
private val mangaListMapper: MangaListMapper,
) : BaseViewModel() {
val manga = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
val manga = savedStateHandle.require<ParcelableManga>(AppRouter.KEY_MANGA).manga
private var includeDisabledSources = MutableStateFlow(false)
private val results = MutableStateFlow<List<MangaAlternativeModel>>(emptyList())
private var migrationJob: Job? = null
private var searchJob: Job? = null
private val mangaDetails = suspendLazy {
mangaRepositoryFactory.create(manga.source).getDetails(manga)
}
val onMigrated = MutableEventFlow<Manga>()
val content = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
private var migrationJob: Job? = null
val list: StateFlow<List<ListModel>> = combine(
results,
isLoading,
includeDisabledSources,
) { list, loading, includeDisabled ->
when {
list.isEmpty() -> listOf(
when {
loading -> LoadingState
else -> EmptyState(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
)
},
)
loading -> list + LoadingFooter()
includeDisabled -> list
else -> list + ButtonFooter(R.string.search_disabled_sources)
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
init {
launchJob(Dispatchers.Default) {
val ref = runCatchingCancellable {
mangaRepositoryFactory.create(manga.source).getDetails(manga)
}.getOrDefault(manga)
val refCount = ref.chaptersCount()
alternativesUseCase(ref)
.map {
MangaAlternativeModel(
manga = it,
progress = getProgress(it.id),
referenceChapters = refCount,
)
}.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item ->
acc.filterIsInstance<MangaAlternativeModel>() + item + LoadingFooter()
}.onEmpty {
emit(
listOf(
EmptyState(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
),
),
)
}.collect {
content.value = it
}
content.value = content.value.filterNot { it is LoadingFooter }
doSearch(throughDisabledSources = false)
}
fun retry() {
searchJob?.cancel()
results.value = emptyList()
includeDisabledSources.value = false
doSearch(throughDisabledSources = false)
}
fun continueSearch() {
if (includeDisabledSources.value) {
return
}
val prevJob = searchJob
searchJob = launchLoadingJob(Dispatchers.Default) {
includeDisabledSources.value = true
prevJob?.join()
doSearch(throughDisabledSources = true)
}
}
@@ -89,7 +117,20 @@ class AlternativesViewModel @Inject constructor(
}
}
private suspend fun getProgress(mangaId: Long): ReadingProgress? {
return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
private fun doSearch(throughDisabledSources: Boolean) {
val prevJob = searchJob
searchJob = launchLoadingJob(Dispatchers.Default) {
prevJob?.cancelAndJoin()
val ref = mangaDetails.getOrDefault(manga)
val refCount = ref.chaptersCount()
alternativesUseCase.invoke(ref, throughDisabledSources)
.collect {
val model = MangaAlternativeModel(
mangaModel = mangaListMapper.toListModel(it, ListMode.GRID) as MangaGridModel,
referenceChapters = refCount,
)
results.append(model)
}
}
}
}

View File

@@ -10,7 +10,6 @@ import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import coil3.ImageLoader
import coil3.request.ImageRequest
@@ -20,17 +19,19 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.domain.AutoFixUseCase
import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
import com.google.android.material.R as materialR
import androidx.appcompat.R as appcompatR
@AndroidEntryPoint
class AutoFixService : CoroutineIntentService() {
@@ -52,12 +53,14 @@ class AutoFixService : CoroutineIntentService() {
val ids = requireNotNull(intent.getLongArrayExtra(DATA_IDS))
startForeground(this)
for (mangaId in ids) {
val result = runCatchingCancellable {
autoFixUseCase.invoke(mangaId)
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(result)
notificationManager.notify(TAG, startId, notification)
powerManager.withPartialWakeLock(TAG) {
val result = runCatchingCancellable {
autoFixUseCase.invoke(mangaId)
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(result)
notificationManager.notify(TAG, startId, notification)
}
}
}
}
@@ -92,7 +95,7 @@ class AutoFixService : CoroutineIntentService() {
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
.addAction(
materialR.drawable.material_ic_clear_black_24dp,
appcompatR.drawable.abc_ic_clear_material,
applicationContext.getString(android.R.string.cancel),
jobContext.getCancelIntent(),
)
@@ -122,7 +125,7 @@ class AutoFixService : CoroutineIntentService() {
).toBitmapOrNull(),
)
notification.setSubText(replacement.title)
val intent = DetailsActivity.newIntent(applicationContext, replacement)
val intent = AppRouter.detailsIntent(applicationContext, replacement)
notification.setContentIntent(
PendingIntentCompat.getActivity(
applicationContext,

View File

@@ -1,16 +1,18 @@
package org.koitharu.kotatsu.alternatives.ui
import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaAlternativeModel(
val manga: Manga,
val progress: ReadingProgress?,
val mangaModel: MangaGridModel,
private val referenceChapters: Int,
) : ListModel {
val manga: Manga
get() = mangaModel.manga
val chaptersCount = manga.chaptersCount()
val chaptersDiff: Int
@@ -19,4 +21,10 @@ data class MangaAlternativeModel(
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaAlternativeModel && other.manga.id == manga.id
}
override fun getChangePayload(previousState: ListModel): Any? = if (previousState is MangaAlternativeModel) {
mangaModel.getChangePayload(previousState.mangaModel)
} else {
null
}
}

View File

@@ -17,9 +17,9 @@ abstract class BookmarksDao {
@Transaction
@Query(
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent",
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent LIMIT :limit OFFSET :offset",
)
abstract suspend fun findAll(): Map<MangaWithTags, List<BookmarkEntity>>
abstract suspend fun findAll(offset: Int, limit: Int): Map<MangaWithTags, List<BookmarkEntity>>
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page ORDER BY percent")
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>

View File

@@ -1,54 +1,5 @@
package org.koitharu.kotatsu.bookmarks.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import com.google.android.material.appbar.AppBarLayout
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.core.ui.FragmentContainerActivity
@AndroidEntryPoint
class AllBookmarksActivity :
BaseActivity<ActivityContainerBinding>(),
AppBarOwner,
SnackbarOwner {
override val appBar: AppBarLayout
get() = viewBinding.appbar
override val snackbarHost: CoordinatorLayout
get() = viewBinding.root
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
replace(R.id.container, AllBookmarksFragment::class.java, null)
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)
}
companion object {
fun newIntent(context: Context) = Intent(context, AllBookmarksActivity::class.java)
}
}
class AllBookmarksActivity : FragmentContainerActivity(AllBookmarksFragment::class.java)

View File

@@ -9,9 +9,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import coil3.ImageLoader
@@ -20,17 +18,20 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.nav.ReaderIntent
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.consumeAllSystemBarsInsets
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.systemBarsInsets
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.GridSpanResolver
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
@@ -39,7 +40,6 @@ import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import javax.inject.Inject
@AndroidEntryPoint
@@ -107,6 +107,18 @@ class AllBookmarksFragment :
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
}
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
val barsInsets = insets.systemBarsInsets
val basePadding = resources.getDimensionPixelOffset(R.dimen.list_spacing_normal)
viewBinding?.recyclerView?.setPadding(
barsInsets.left + basePadding,
barsInsets.top + basePadding,
barsInsets.right + basePadding,
barsInsets.bottom + basePadding,
)
return insets.consumeAllSystemBarsInsets()
}
override fun onDestroyView() {
super.onDestroyView()
bookmarksAdapter = null
@@ -115,26 +127,26 @@ class AllBookmarksFragment :
override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.pageId) != true) {
val intent = ReaderActivity.IntentBuilder(view.context)
val intent = ReaderIntent.Builder(view.context)
.bookmark(item)
.incognito(true)
.build()
startActivity(intent)
router.openReader(intent)
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
}
override fun onListHeaderClick(item: ListHeader, view: View) {
val manga = item.payload as? Manga ?: return
startActivity(DetailsActivity.newIntent(view.context, manga))
router.openDetails(manga)
}
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
return selectionController?.onItemLongClick(view, item.pageId) ?: false
return selectionController?.onItemLongClick(view, item.pageId) == true
}
override fun onItemContextClick(item: Bookmark, view: View): Boolean {
return selectionController?.onItemContextClick(view, item.pageId) ?: false
return selectionController?.onItemContextClick(view, item.pageId) == true
}
override fun onRetryClick(error: Throwable) = Unit
@@ -177,16 +189,6 @@ class AllBookmarksFragment :
}
}
override fun onWindowInsetsChanged(insets: Insets) {
val rv = requireViewBinding().recyclerView
rv.updatePadding(
bottom = insets.bottom + rv.paddingTop,
)
rv.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup(), Runnable {
init {
@@ -208,16 +210,4 @@ class AllBookmarksFragment :
invalidateSpanIndexCache()
}
}
companion object {
@Deprecated(
"",
ReplaceWith(
"BookmarksFragment()",
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
),
)
fun newInstance() = AllBookmarksFragment()
}
}

View File

@@ -1,38 +0,0 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil3.ImageLoader
import coil3.request.allowRgb565
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.bookmarkExtra
import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
// TODO check usages
fun bookmarkListAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
) = adapterDelegateViewBinding<Bookmark, Bookmark, ItemBookmarkBinding>(
{ inflater, parent -> ItemBookmarkBinding.inflate(inflater, parent, false) },
) {
AdapterDelegateClickListenerAdapter(this, clickListener).attach(itemView)
bind {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
defaultPlaceholders(context)
allowRgb565(true)
bookmarkExtra(item)
decodeRegion(item.scroll)
enqueueWith(coil)
}
}
}

View File

@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
@@ -25,6 +26,7 @@ class BookmarksAdapter(
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))

View File

@@ -0,0 +1,107 @@
package org.koitharu.kotatsu.browser
import android.os.Bundle
import android.view.View
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.consumeAll
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import javax.inject.Inject
@AndroidEntryPoint
abstract class BaseBrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
@Inject
lateinit var proxyProvider: ProxyProvider
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
onBackPressedDispatcher.addCallback(onBackPressedCallback)
val mangaSource = MangaSource(intent?.getStringExtra(AppRouter.KEY_SOURCE))
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
val userAgent = intent?.getStringExtra(AppRouter.KEY_USER_AGENT)?.nullIfEmpty()
?: repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
viewBinding.webView.configureForParser(userAgent)
onCreate2(savedInstanceState, mangaSource, repository)
}
protected abstract fun onCreate2(
savedInstanceState: Bundle?,
source: MangaSource,
repository: ParserMangaRepository?
)
override fun onApplyWindowInsets(
v: View,
insets: WindowInsetsCompat
): WindowInsetsCompat {
val type = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime()
val barsInsets = insets.getInsets(type)
viewBinding.webView.updatePadding(
left = barsInsets.left,
right = barsInsets.right,
bottom = barsInsets.bottom,
)
viewBinding.appbar.updatePadding(
left = barsInsets.left,
right = barsInsets.right,
top = barsInsets.top,
)
return insets.consumeAll(type)
}
override fun onPause() {
viewBinding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
viewBinding.webView.onResume()
}
override fun onDestroy() {
super.onDestroy()
if (hasViewBinding()) {
viewBinding.webView.stopLoading()
viewBinding.webView.destroy()
}
}
override fun onLoadingStateChanged(isLoading: Boolean) {
viewBinding.progressBar.isVisible = isLoading
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
this.title = title
supportActionBar?.subtitle = subtitle
}
override fun onHistoryChanged() {
onBackPressedCallback.onHistoryChanged()
}
}

View File

@@ -1,68 +1,45 @@
package org.koitharu.kotatsu.browser
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.webkit.CookieManager
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
class BrowserActivity : BaseBrowserActivity() {
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
val mangaSource = MangaSource(intent?.getStringExtra(EXTRA_SOURCE))
val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository
val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT)
viewBinding.webView.configureForParser(userAgent)
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)
onBackPressedDispatcher.addCallback(onBackPressedCallback)
if (savedInstanceState != null) {
return
}
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finishAfterTransition()
} else {
onTitleChanged(
intent?.getStringExtra(EXTRA_TITLE) ?: getString(R.string.loading_),
url,
)
viewBinding.webView.loadUrl(url)
lifecycleScope.launch {
try {
proxyProvider.applyWebViewConfig()
} catch (e: Exception) {
e.printStackTraceDebug()
Snackbar.make(viewBinding.webView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
}
if (savedInstanceState == null) {
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finishAfterTransition()
} else {
onTitleChanged(
intent?.getStringExtra(AppRouter.KEY_TITLE) ?: getString(R.string.loading_),
url,
)
viewBinding.webView.loadUrl(url)
}
}
}
}
@@ -80,73 +57,12 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
R.id.action_browser -> {
val url = viewBinding.webView.url?.toUriOrNull()
if (url != null) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = url
try {
startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) {
}
if (!router.openExternalBrowser(viewBinding.webView.url.orEmpty(), item.title)) {
Snackbar.make(viewBinding.webView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
}
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onPause() {
viewBinding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
viewBinding.webView.onResume()
}
override fun onDestroy() {
super.onDestroy()
if (hasViewBinding()) {
viewBinding.webView.stopLoading()
viewBinding.webView.destroy()
}
}
override fun onLoadingStateChanged(isLoading: Boolean) {
viewBinding.progressBar.isVisible = isLoading
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
this.title = title
supportActionBar?.subtitle = subtitle
}
override fun onHistoryChanged() {
onBackPressedCallback.onHistoryChanged()
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.appbar.updatePadding(
top = insets.top,
)
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
}
companion object {
private const val EXTRA_TITLE = "title"
private const val EXTRA_SOURCE = "source"
fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent {
return Intent(context, BrowserActivity::class.java)
.setData(Uri.parse(url))
.putExtra(EXTRA_TITLE, title)
.putExtra(EXTRA_SOURCE, source?.name)
}
}
}

View File

@@ -2,9 +2,11 @@ package org.koitharu.kotatsu.browser
import android.graphics.Bitmap
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.webkit.WebViewClientCompat
open class BrowserClient(private val callback: BrowserCallback) : WebViewClient() {
open class BrowserClient(
private val callback: BrowserCallback
) : WebViewClientCompat() {
override fun onPageFinished(webView: WebView, url: String) {
super.onPageFinished(webView, url)
@@ -16,7 +18,7 @@ open class BrowserClient(private val callback: BrowserCallback) : WebViewClient(
callback.onLoadingStateChanged(isLoading = true)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
override fun onPageCommitVisible(view: WebView, url: String) {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title.orEmpty(), url)
}

View File

@@ -17,6 +17,8 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -28,6 +30,9 @@ class CaptchaNotifier(
if (!context.checkNotificationPermission(CHANNEL_ID)) {
return
}
if (exception.source != null && SourceSettings(context, exception.source).isCaptchaNotificationsDisabled) {
return
}
val manager = NotificationManagerCompat.from(context)
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
.setName(context.getString(R.string.captcha_required))
@@ -38,7 +43,7 @@ class CaptchaNotifier(
.build()
manager.createNotificationChannel(channel)
val intent = CloudFlareActivity.newIntent(context, exception)
val intent = AppRouter.cloudFlareResolveIntent(context, exception)
.setData(exception.url.toUri())
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(channel.name)

View File

@@ -1,43 +1,36 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.webkit.CookieManager
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets
import androidx.core.net.toUri
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.yield
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.browser.BaseBrowserActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.core.parser.ParserMangaRepository
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.CloudFlareHelper
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCallback {
class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback {
private var pendingResult = RESULT_CANCELED
@@ -45,43 +38,27 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
lateinit var cookieJar: MutableCookieJar
private lateinit var cfClient: CloudFlareClient
private var onBackPressedCallback: WebViewBackPressedCallback? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
override fun onCreate2(savedInstanceState: Bundle?, source: MangaSource, repository: ParserMangaRepository?) {
setDisplayHomeAsUp(isEnabled = true, showUpAsClose = true)
val url = intent?.dataString
if (url.isNullOrEmpty()) {
finishAfterTransition()
return
}
cfClient = CloudFlareClient(cookieJar, this, url)
viewBinding.webView.configureForParser(intent?.getStringExtra(ARG_UA))
viewBinding.webView.webViewClient = cfClient
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
onBackPressedDispatcher.addCallback(it)
lifecycleScope.launch {
try {
proxyProvider.applyWebViewConfig()
} catch (e: Exception) {
Snackbar.make(viewBinding.webView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
}
if (savedInstanceState == null) {
onTitleChanged(getString(R.string.loading_), url)
viewBinding.webView.loadUrl(url)
}
}
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
if (savedInstanceState == null) {
onTitleChanged(getString(R.string.loading_), url)
viewBinding.webView.loadUrl(url)
}
}
override fun onDestroy() {
runCatching {
viewBinding.webView
}.onSuccess {
it.stopLoading()
it.destroy()
}
super.onDestroy()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@@ -89,17 +66,6 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
return super.onCreateOptionsMenu(menu)
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.appbar.updatePadding(
top = insets.top,
)
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom,
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> {
viewBinding.webView.stopLoading()
@@ -115,21 +81,13 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
else -> super.onOptionsItemSelected(item)
}
override fun onResume() {
super.onResume()
viewBinding.webView.onResume()
}
override fun onPause() {
viewBinding.webView.onPause()
super.onPause()
}
override fun finish() {
setResult(pendingResult)
super.finish()
}
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
override fun onPageLoaded() {
viewBinding.progressBar.isInvisible = true
}
@@ -140,25 +98,16 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
override fun onCheckPassed() {
pendingResult = RESULT_OK
val source = intent?.getStringExtra(ARG_SOURCE)
val source = intent?.getStringExtra(AppRouter.KEY_SOURCE)
if (source != null) {
CaptchaNotifier(this).dismiss(MangaSource(source))
}
finishAfterTransition()
}
override fun onLoadingStateChanged(isLoading: Boolean) {
viewBinding.progressBar.isVisible = isLoading
}
override fun onHistoryChanged() {
onBackPressedCallback?.onHistoryChanged()
}
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
setTitle(title)
supportActionBar?.subtitle =
subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.host.ifNullOrEmpty { subtitle }
}
private fun restartCheck() {
@@ -182,38 +131,16 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
class Contract : ActivityResultContract<CloudFlareProtectedException, Boolean>() {
override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
return newIntent(context, input)
return AppRouter.cloudFlareResolveIntent(context, input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
return resultCode == Activity.RESULT_OK
return resultCode == RESULT_OK
}
}
companion object {
const val TAG = "CloudFlareActivity"
private const val ARG_UA = "ua"
private const val ARG_SOURCE = "_source"
fun newIntent(context: Context, exception: CloudFlareProtectedException) = newIntent(
context = context,
url = exception.url,
source = exception.source,
headers = exception.headers,
)
private fun newIntent(
context: Context,
url: String,
source: MangaSource?,
headers: Headers?,
) = Intent(context, CloudFlareActivity::class.java).apply {
data = url.toUri()
putExtra(ARG_SOURCE, source?.name)
headers?.get(CommonHeaders.USER_AGENT)?.let {
putExtra(ARG_UA, it)
}
}
}
}

View File

@@ -4,8 +4,6 @@ import org.koitharu.kotatsu.browser.BrowserCallback
interface CloudFlareCallback : BrowserCallback {
override fun onLoadingStateChanged(isLoading: Boolean) = Unit
override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) = Unit
fun onPageLoaded()

View File

@@ -22,7 +22,7 @@ class CloudFlareClient(
checkClearance()
}
override fun onPageCommitVisible(view: WebView, url: String?) {
override fun onPageCommitVisible(view: WebView, url: String) {
super.onPageCommitVisible(view, url)
callback.onPageLoaded()
}

View File

@@ -6,6 +6,7 @@ import android.os.Build
import android.provider.SearchRecentSuggestions
import android.text.Html
import androidx.collection.arraySetOf
import androidx.core.content.ContextCompat
import androidx.room.InvalidationTracker
import androidx.work.WorkManager
import coil3.ImageLoader
@@ -76,6 +77,12 @@ interface AppModule {
companion object {
@Provides
@LocalizedAppContext
fun provideLocalizedContext(
@ApplicationContext context: Context,
): Context = ContextCompat.getContextForLanguage(context)
@Provides
@Singleton
fun provideNetworkState(
@@ -92,7 +99,7 @@ interface AppModule {
@Provides
@Singleton
fun provideCoil(
@ApplicationContext context: Context,
@LocalizedAppContext context: Context,
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor,

View File

@@ -13,7 +13,6 @@ import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.acra.ACRA
import org.acra.ReportField
import org.acra.config.dialog
@@ -26,12 +25,14 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.os.AppValidator
import org.koitharu.kotatsu.core.os.RomCompat
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.data.index.LocalMangaIndex
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import java.security.Security
import javax.inject.Inject
@@ -82,17 +83,14 @@ open class BaseApp : Application(), Configuration.Provider {
return
}
AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setApplicationLocales(settings.appLocales)
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
setupActivityLifecycleCallbacks()
processLifecycleScope.launch {
val isOriginalApp = withContext(Dispatchers.Default) {
appValidator.isOriginalApp
}
ACRA.errorReporter.putCustomData("isOriginalApp", isOriginalApp.toString())
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.getOrNull().toString())
ACRA.errorReporter.putCustomData("isMiui", RomCompat.isMiui.getOrNull().toString())
}
processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers()
@@ -104,6 +102,9 @@ open class BaseApp : Application(), Configuration.Provider {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
if (ACRA.isACRASenderServiceProcess()) {
return
}
initAcra {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON

View File

@@ -8,6 +8,7 @@ import android.net.Uri
import android.os.BadParcelableException
import androidx.core.app.PendingIntentCompat
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.report
@@ -15,20 +16,19 @@ import org.koitharu.kotatsu.core.util.ext.report
class ErrorReporterReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val e = intent?.getSerializableExtraCompat<Throwable>(EXTRA_ERROR) ?: return
val e = intent?.getSerializableExtraCompat<Throwable>(AppRouter.KEY_ERROR) ?: return
e.report()
}
companion object {
private const val EXTRA_ERROR = "err"
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
fun getPendingIntent(context: Context, e: Throwable): PendingIntent? = try {
val intent = Intent(context, ErrorReporterReceiver::class.java)
intent.setAction(ACTION_REPORT)
intent.setData(Uri.parse("err://${e.hashCode()}"))
intent.putExtra(EXTRA_ERROR, e)
intent.putExtra(AppRouter.KEY_ERROR, e)
PendingIntentCompat.getBroadcast(context, 0, intent, 0, false)
} catch (e: BadParcelableException) {
e.printStackTraceDebug()

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core
import javax.inject.Qualifier
@Qualifier
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER,
AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.FIELD,
)
annotation class LocalizedAppContext

View File

@@ -16,6 +16,7 @@ class BackupEntry(
CATEGORIES("categories"),
FAVOURITES("favourites"),
SETTINGS("settings"),
SETTINGS_READER_GRID("reader_grid"),
BOOKMARKS("bookmarks"),
SOURCES("sources"),
}

View File

@@ -1,15 +1,18 @@
package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import kotlinx.coroutines.flow.FlowCollector
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.parsers.util.json.asTypedList
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.data.TapGridSettings
import java.util.Date
import javax.inject.Inject
@@ -18,13 +21,14 @@ private const val PAGE_SIZE = 10
class BackupRepository @Inject constructor(
private val db: MangaDatabase,
private val settings: AppSettings,
private val tapGridSettings: TapGridSettings,
) {
suspend fun dumpHistory(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.Name.HISTORY, JSONArray())
while (true) {
val history = db.getHistoryDao().findAll(offset, PAGE_SIZE)
val history = db.getHistoryDao().findAll(offset = offset, limit = PAGE_SIZE)
if (history.isEmpty()) {
break
}
@@ -55,7 +59,7 @@ class BackupRepository @Inject constructor(
var offset = 0
val entry = BackupEntry(BackupEntry.Name.FAVOURITES, JSONArray())
while (true) {
val favourites = db.getFavouritesDao().findAllRaw(offset, PAGE_SIZE)
val favourites = db.getFavouritesDao().findAllRaw(offset = offset, limit = PAGE_SIZE)
if (favourites.isEmpty()) {
break
}
@@ -74,19 +78,26 @@ class BackupRepository @Inject constructor(
}
suspend fun dumpBookmarks(): BackupEntry {
var offset = 0
val entry = BackupEntry(BackupEntry.Name.BOOKMARKS, JSONArray())
val all = db.getBookmarksDao().findAll()
for ((m, b) in all) {
val json = JSONObject()
val manga = JsonSerializer(m.manga).toJson()
json.put("manga", manga)
val tags = JSONArray()
m.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
json.put("tags", tags)
val bookmarks = JSONArray()
b.forEach { bookmarks.put(JsonSerializer(it).toJson()) }
json.put("bookmarks", bookmarks)
entry.data.put(json)
while (true) {
val bookmarks = db.getBookmarksDao().findAll(offset = offset, limit = PAGE_SIZE)
if (bookmarks.isEmpty()) {
break
}
offset += bookmarks.size
for ((m, b) in bookmarks) {
val json = JSONObject()
val manga = JsonSerializer(m.manga).toJson()
json.put("manga", manga)
val tags = JSONArray()
m.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
json.put("tags", tags)
val bookmarks = JSONArray()
b.forEach { bookmarks.put(JsonSerializer(it).toJson()) }
json.put("bookmarks", bookmarks)
entry.data.put(json)
}
}
return entry
}
@@ -103,6 +114,14 @@ class BackupRepository @Inject constructor(
return entry
}
fun dumpReaderGridSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.SETTINGS_READER_GRID, JSONArray())
val settingsDump = tapGridSettings.getAllValues()
val json = JsonSerializer(settingsDump).toJson()
entry.data.put(json)
return entry
}
suspend fun dumpSources(): BackupEntry {
val entry = BackupEntry(BackupEntry.Name.SOURCES, JSONArray())
val all = db.getSourcesDao().findAll()
@@ -128,9 +147,11 @@ class BackupRepository @Inject constructor(
return if (timestamp == 0L) null else Date(timestamp)
}
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
suspend fun restoreHistory(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
val list = entry.data.asTypedList<JSONObject>()
outProgress?.emit(Progress(progress = 0, total = list.size))
for ((index, item) in list.withIndex()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = mangaJson.getJSONArray("tags").mapJSON {
@@ -144,6 +165,7 @@ class BackupRepository @Inject constructor(
db.getHistoryDao().upsert(history)
}
}
outProgress?.emit(Progress(progress = index, total = list.size))
}
return result
}
@@ -159,9 +181,11 @@ class BackupRepository @Inject constructor(
return result
}
suspend fun restoreFavourites(entry: BackupEntry): CompositeResult {
suspend fun restoreFavourites(entry: BackupEntry, outProgress: FlowCollector<Progress>?): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
val list = entry.data.asTypedList<JSONObject>()
outProgress?.emit(Progress(progress = 0, total = list.size))
for ((index, item) in list.withIndex()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = mangaJson.getJSONArray("tags").mapJSON {
@@ -175,6 +199,7 @@ class BackupRepository @Inject constructor(
db.getFavouritesDao().upsert(favourite)
}
}
outProgress?.emit(Progress(progress = index, total = list.size))
}
return result
}
@@ -221,4 +246,14 @@ class BackupRepository @Inject constructor(
}
return result
}
fun restoreReaderGridSettings(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.asTypedList<JSONObject>()) {
result += runCatchingCancellable {
tapGridSettings.upsertAll(JsonDeserializer(item).toMap())
}
}
return result
}
}

View File

@@ -27,6 +27,10 @@ class CompositeResult {
}
}
operator fun plusAssign(error: Throwable) {
errors.add(error)
}
operator fun plusAssign(other: CompositeResult) {
this.successCount += other.successCount
this.errors += other.errors

View File

@@ -28,15 +28,16 @@ class JsonDeserializer(private val json: JSONObject) {
fun toMangaEntity() = MangaEntity(
id = json.getLong("id"),
title = json.getString("title"),
altTitle = json.getStringOrNull("alt_title"),
altTitles = json.getStringOrNull("alt_title"),
url = json.getString("url"),
publicUrl = json.getStringOrNull("public_url").orEmpty(),
rating = json.getDouble("rating").toFloat(),
isNsfw = json.getBooleanOrDefault("nsfw", false),
contentRating = json.getStringOrNull("content_rating"),
coverUrl = json.getString("cover_url"),
largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"),
authors = json.getStringOrNull("author"),
source = json.getString("source"),
)

View File

@@ -58,15 +58,16 @@ class JsonSerializer private constructor(private val json: JSONObject) {
JSONObject().apply {
put("id", e.id)
put("title", e.title)
put("alt_title", e.altTitle)
put("alt_title", e.altTitles)
put("url", e.url)
put("public_url", e.publicUrl)
put("rating", e.rating)
put("nsfw", e.isNsfw)
put("content_rating", e.contentRating)
put("cover_url", e.coverUrl)
put("large_cover_url", e.largeCoverUrl)
put("state", e.state)
put("author", e.author)
put("author", e.authors)
put("source", e.source)
},
)

View File

@@ -0,0 +1,93 @@
package org.koitharu.kotatsu.core.backup
import android.content.Context
import androidx.annotation.CheckResult
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.Response
import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
import org.koitharu.kotatsu.parsers.util.parseJson
import java.io.File
import javax.inject.Inject
class TelegramBackupUploader @Inject constructor(
private val settings: AppSettings,
@BaseHttpClient private val client: OkHttpClient,
@ApplicationContext private val context: Context,
) {
private val botToken = context.getString(R.string.tg_backup_bot_token)
suspend fun uploadBackup(file: File) {
val requestBody = file.asRequestBody("application/zip".toMediaTypeOrNull())
val multipartBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("chat_id", requireChatId())
.addFormDataPart("document", file.name, requestBody)
.build()
val request = Request.Builder()
.url(urlOf("sendDocument").build())
.post(multipartBody)
.build()
client.newCall(request).await().consume()
}
suspend fun sendTestMessage() {
val request = Request.Builder()
.url(urlOf("getMe").build())
.build()
client.newCall(request).await().consume()
sendMessage(context.getString(R.string.backup_tg_echo))
}
@CheckResult
fun openBotInApp(router: AppRouter): Boolean {
val botUsername = context.getString(R.string.tg_backup_bot_name)
return router.openExternalBrowser("tg://resolve?domain=$botUsername") ||
router.openExternalBrowser("https://t.me/$botUsername")
}
private suspend fun sendMessage(message: String) {
val url = urlOf("sendMessage")
.addQueryParameter("chat_id", requireChatId())
.addQueryParameter("text", message)
.build()
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).await().consume()
}
private fun requireChatId() = checkNotNull(settings.backupTelegramChatId) {
"Telegram chat ID not set in settings"
}
private fun Response.consume() {
if (isSuccessful) {
closeQuietly()
return
}
val jo = parseJson()
if (!jo.getBooleanOrDefault("ok", true)) {
throw RuntimeException(jo.getStringOrNull("description"))
}
}
private fun urlOf(method: String) = HttpUrl.Builder()
.scheme("https")
.host("api.telegram.org")
.addPathSegment("bot$botToken")
.addPathSegment(method)
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.cache
import androidx.collection.LruCache
import org.koitharu.kotatsu.core.util.SynchronizedSieveCache
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.concurrent.TimeUnit
import org.koitharu.kotatsu.core.cache.MemoryContentCache.Key as CacheKey
@@ -8,11 +9,9 @@ class ExpiringLruCache<T>(
val maxSize: Int,
private val lifetime: Long,
private val timeUnit: TimeUnit,
) : Iterable<CacheKey> {
) {
private val cache = LruCache<CacheKey, ExpiringValue<T>>(maxSize)
override fun iterator(): Iterator<CacheKey> = cache.snapshot().keys.iterator()
private val cache = SynchronizedSieveCache<CacheKey, ExpiringValue<T>>(maxSize)
operator fun get(key: CacheKey): T? {
val value = cache[key] ?: return null
@@ -23,7 +22,8 @@ class ExpiringLruCache<T>(
}
operator fun set(key: CacheKey, value: T) {
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
val value = ExpiringValue(value, lifetime, timeUnit)
cache.put(key, value)
}
fun clear() {
@@ -37,4 +37,8 @@ class ExpiringLruCache<T>(
fun remove(key: CacheKey) {
cache.remove(key)
}
fun removeAll(source: MangaSource) {
cache.removeIf { key, _ -> key.source == source }
}
}

View File

@@ -81,11 +81,7 @@ class MemoryContentCache @Inject constructor(application: Application) : Compone
}
private fun clearCache(cache: ExpiringLruCache<*>, source: MangaSource) {
cache.forEach { key ->
if (key.source == source) {
cache.remove(key)
}
}
cache.removeAll(source)
}
data class Key(

View File

@@ -12,11 +12,13 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.bookmarks.data.BookmarksDao
import org.koitharu.kotatsu.core.db.dao.ChaptersDao
import org.koitharu.kotatsu.core.db.dao.MangaDao
import org.koitharu.kotatsu.core.db.dao.MangaSourcesDao
import org.koitharu.kotatsu.core.db.dao.PreferencesDao
import org.koitharu.kotatsu.core.db.dao.TagsDao
import org.koitharu.kotatsu.core.db.dao.TrackLogsDao
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
@@ -36,6 +38,9 @@ import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration23To24
import org.koitharu.kotatsu.core.db.migrations.Migration24To23
import org.koitharu.kotatsu.core.db.migrations.Migration24To25
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -63,14 +68,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 23
const val DATABASE_VERSION = 25
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, ChapterEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class,
TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, ScrobblingEntity::class,
MangaSourceEntity::class, StatsEntity::class, LocalMangaIndexEntity::class,
],
version = DATABASE_VERSION,
)
@@ -103,6 +108,8 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun getStatsDao(): StatsDao
abstract fun getLocalMangaIndexDao(): LocalMangaIndexDao
abstract fun getChaptersDao(): ChaptersDao
}
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
@@ -128,6 +135,9 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration20To21(),
Migration21To22(),
Migration22To23(),
Migration23To24(),
Migration24To23(),
Migration24To25(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -7,3 +7,4 @@ const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
const val TABLE_HISTORY = "history"
const val TABLE_MANGA_TAGS = "manga_tags"
const val TABLE_SOURCES = "sources"
const val TABLE_CHAPTERS = "chapters"

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import org.koitharu.kotatsu.core.db.entity.ChapterEntity
@Dao
abstract class ChaptersDao {
@Query("SELECT * FROM chapters WHERE manga_id = :mangaId ORDER BY `index` ASC")
abstract suspend fun findAll(mangaId: Long): List<ChapterEntity>
@Query("DELETE FROM chapters WHERE manga_id = :mangaId")
abstract suspend fun deleteAll(mangaId: Long)
@Query("DELETE FROM chapters WHERE manga_id NOT IN (SELECT manga_id FROM history WHERE deleted_at = 0) AND manga_id NOT IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
abstract suspend fun gc()
@Transaction
open suspend fun replaceAll(mangaId: Long, entities: Collection<ChapterEntity>) {
deleteAll(mangaId)
insert(entities)
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun insert(entities: Collection<ChapterEntity>)
}

View File

@@ -20,6 +20,9 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE manga_id = :id")
abstract suspend fun find(id: Long): MangaWithTags?
@Query("SELECT EXISTS(SELECT * FROM manga WHERE manga_id = :id)")
abstract suspend operator fun contains(id: Long): Boolean
@Transaction
@Query("SELECT * FROM manga WHERE public_url = :publicUrl")
abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags?
@@ -55,6 +58,19 @@ abstract class MangaDao {
@Delete
abstract suspend fun delete(subjects: Collection<MangaEntity>)
@Query(
"""
DELETE FROM manga WHERE NOT EXISTS(SELECT * FROM history WHERE history.manga_id == manga.manga_id)
AND NOT EXISTS(SELECT * FROM favourites WHERE favourites.manga_id == manga.manga_id)
AND NOT EXISTS(SELECT * FROM bookmarks WHERE bookmarks.manga_id == manga.manga_id)
AND NOT EXISTS(SELECT * FROM suggestions WHERE suggestions.manga_id == manga.manga_id)
AND NOT EXISTS(SELECT * FROM scrobblings WHERE scrobblings.manga_id == manga.manga_id)
AND NOT EXISTS(SELECT * FROM local_index WHERE local_index.manga_id == manga.manga_id)
AND manga.manga_id NOT IN (:idsToKeep)
""",
)
abstract suspend fun cleanup(idsToKeep: Set<Long>)
@Transaction
open suspend fun upsert(manga: MangaEntity, tags: Iterable<TagEntity>? = null) {
upsert(manga)

View File

@@ -10,7 +10,6 @@ import androidx.room.Upsert
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.explore.data.SourcesSortOrder
@@ -61,21 +60,11 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources WHERE pinned = 1")
abstract suspend fun findAllPinned(): List<MangaSourceEntity>
fun observeEnabled(order: SourcesSortOrder): Flow<List<MangaSourceEntity>> {
val orderBy = getOrderBy(order)
fun observeAll(enabledOnly: Boolean, order: SourcesSortOrder): Flow<List<MangaSourceEntity>> =
observeImpl(getQuery(enabledOnly, order))
@Language("RoomSql")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
return observeImpl(query)
}
suspend fun findAllEnabled(order: SourcesSortOrder): List<MangaSourceEntity> {
val orderBy = getOrderBy(order)
@Language("RoomSql")
val query = SimpleSQLiteQuery("SELECT * FROM sources WHERE enabled = 1 ORDER BY pinned DESC, $orderBy")
return findAllImpl(query)
}
suspend fun findAll(enabledOnly: Boolean, order: SourcesSortOrder): List<MangaSourceEntity> =
findAllImpl(getQuery(enabledOnly, order))
@Transaction
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
@@ -101,6 +90,17 @@ abstract class MangaSourcesDao {
@RawQuery
protected abstract suspend fun findAllImpl(query: SupportSQLiteQuery): List<MangaSourceEntity>
private fun getQuery(enabledOnly: Boolean, order: SourcesSortOrder) = SimpleSQLiteQuery(
buildString {
append("SELECT * FROM sources ")
if (enabledOnly) {
append("WHERE enabled = 1 ")
}
append("ORDER BY pinned DESC, ")
append(getOrderBy(order))
},
)
private fun getOrderBy(order: SourcesSortOrder) = when (order) {
SourcesSortOrder.ALPHABETIC -> "source ASC"
SourcesSortOrder.POPULARITY -> "(SELECT COUNT(*) FROM manga WHERE source = sources.source) DESC"

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.TABLE_CHAPTERS
@Entity(
tableName = TABLE_CHAPTERS,
primaryKeys = ["manga_id", "chapter_id"],
foreignKeys = [
ForeignKey(
entity = MangaEntity::class,
parentColumns = ["manga_id"],
childColumns = ["manga_id"],
onDelete = ForeignKey.CASCADE,
),
],
)
data class ChapterEntity(
@ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "name") val title: String,
@ColumnInfo(name = "number") val number: Float,
@ColumnInfo(name = "volume") val volume: Int,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "scanlator") val scanlator: String?,
@ColumnInfo(name = "upload_date") val uploadDate: Long,
@ColumnInfo(name = "branch") val branch: String?,
@ColumnInfo(name = "source") val source: String,
@ColumnInfo(name = "index") val index: Int,
)

View File

@@ -1,14 +1,20 @@
package org.koitharu.kotatsu.core.db.entity
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.longHashCode
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.parsers.util.toArraySet
import org.koitharu.kotatsu.parsers.util.toTitleCase
private const val VALUES_DIVIDER = '\n'
// Entity to model
fun TagEntity.toMangaTag() = MangaTag(
@@ -21,26 +27,42 @@ fun Collection<TagEntity>.toMangaTags() = mapToSet(TagEntity::toMangaTag)
fun Collection<TagEntity>.toMangaTagsList() = map(TagEntity::toMangaTag)
fun MangaEntity.toManga(tags: Set<MangaTag>) = Manga(
fun MangaEntity.toManga(tags: Set<MangaTag>, chapters: List<ChapterEntity>?) = Manga(
id = this.id,
title = this.title,
altTitle = this.altTitle,
altTitles = this.altTitles?.split(VALUES_DIVIDER)?.toArraySet().orEmpty(),
state = this.state?.let { MangaState(it) },
rating = this.rating,
isNsfw = this.isNsfw,
contentRating = ContentRating(this.contentRating)
?: if (isNsfw) ContentRating.ADULT else null,
url = this.url,
publicUrl = this.publicUrl,
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
author = this.author,
authors = this.authors?.split(VALUES_DIVIDER)?.toArraySet().orEmpty(),
source = MangaSource(this.source),
tags = tags,
chapters = chapters?.toMangaChapters(),
)
fun MangaWithTags.toManga() = manga.toManga(tags.toMangaTags())
fun MangaWithTags.toManga(chapters: List<ChapterEntity>? = null) = manga.toManga(tags.toMangaTags(), chapters)
fun Collection<MangaWithTags>.toMangaList() = map { it.toManga() }
fun ChapterEntity.toMangaChapter() = MangaChapter(
id = chapterId,
title = title.nullIfEmpty(),
number = number,
volume = volume,
url = url,
scanlator = scanlator,
uploadDate = uploadDate,
branch = branch,
source = MangaSource(source),
)
fun Collection<ChapterEntity>.toMangaChapters() = map { it.toMangaChapter() }
// Model to entity
fun Manga.toEntity() = MangaEntity(
@@ -50,12 +72,13 @@ fun Manga.toEntity() = MangaEntity(
source = source.name,
largeCoverUrl = largeCoverUrl,
coverUrl = coverUrl.orEmpty(),
altTitle = altTitle,
altTitles = altTitles.joinToString(VALUES_DIVIDER.toString()),
rating = rating,
isNsfw = isNsfw,
contentRating = contentRating?.name,
state = state?.name,
title = title,
author = author,
authors = authors.joinToString(VALUES_DIVIDER.toString()),
)
fun MangaTag.toEntity() = TagEntity(
@@ -67,6 +90,22 @@ fun MangaTag.toEntity() = TagEntity(
fun Collection<MangaTag>.toEntities() = map(MangaTag::toEntity)
fun Iterable<IndexedValue<MangaChapter>>.toEntities(mangaId: Long) = map { (index, chapter) ->
ChapterEntity(
chapterId = chapter.id,
mangaId = mangaId,
title = chapter.title.orEmpty(),
number = chapter.number,
volume = chapter.volume,
url = chapter.url,
scanlator = chapter.scanlator,
uploadDate = chapter.uploadDate,
branch = chapter.branch,
source = chapter.source.name,
index = index,
)
}
// Other
fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
@@ -76,3 +115,7 @@ fun SortOrder(name: String, fallback: SortOrder): SortOrder = runCatching {
fun MangaState(name: String): MangaState? = runCatching {
MangaState.valueOf(name)
}.getOrNull()
fun ContentRating(name: String?): ContentRating? = runCatching {
ContentRating.valueOf(name ?: return@runCatching null)
}.getOrNull()

View File

@@ -10,14 +10,15 @@ data class MangaEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "alt_title") val altTitle: String?,
@ColumnInfo(name = "alt_title") val altTitles: String?,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "public_url") val publicUrl: String,
@ColumnInfo(name = "rating") val rating: Float, // normalized value [0..1] or -1
@ColumnInfo(name = "nsfw") val isNsfw: Boolean, // TODO change to contentRating
@ColumnInfo(name = "nsfw") val isNsfw: Boolean,
@ColumnInfo(name = "content_rating") val contentRating: String?,
@ColumnInfo(name = "cover_url") val coverUrl: String,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?,
@ColumnInfo(name = "author") val author: String?,
@ColumnInfo(name = "author") val authors: String?,
@ColumnInfo(name = "source") val source: String,
)

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration23To24 : Migration(23, 24) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE IF NOT EXISTS `chapters` (`chapter_id` INTEGER NOT NULL, `manga_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `number` REAL NOT NULL, `volume` INTEGER NOT NULL, `url` TEXT NOT NULL, `scanlator` TEXT, `upload_date` INTEGER NOT NULL, `branch` TEXT, `source` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `chapter_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
}
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration24To23 : Migration(24, 23) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS `chapters`")
}
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration24To25 : Migration(24, 25) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE manga ADD COLUMN content_rating TEXT DEFAULT NULL")
db.execSQL("UPDATE manga SET content_rating = 'ADULT' WHERE nsfw = 1")
}
}

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.core.exceptions
import android.net.Uri
class NonFileUriException(
val uri: Uri,
) : IllegalArgumentException("Cannot resolve file name of \"$uri\"")

View File

@@ -6,7 +6,6 @@ import androidx.core.util.Consumer
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.isSerializable
import org.koitharu.kotatsu.parsers.exception.ParseException
@@ -32,10 +31,10 @@ class DialogErrorObserver(
if (canResolve(value)) {
dialogBuilder.setPositiveButton(ExceptionResolver.getResolveStringId(value), listener)
} else if (value is ParseException) {
val fm = fragmentManager
if (fm != null && value.isSerializable()) {
val router = router()
if (router != null && value.isSerializable()) {
dialogBuilder.setPositiveButton(R.string.details) { _, _ ->
ErrorDetailsDialog.show(fm, value, value.url)
router.showErrorDialog(value)
}
}
}

View File

@@ -4,6 +4,7 @@ import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.util.Consumer
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
@@ -11,6 +12,7 @@ import androidx.lifecycle.coroutineScope
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
@@ -33,6 +35,8 @@ abstract class ErrorObserver(
return resolver != null && ExceptionResolver.canResolve(error)
}
protected fun router() = fragment?.router ?: (activity as? FragmentActivity)?.router
private fun isAlive(): Boolean {
return when {
fragment != null -> fragment.view != null
@@ -44,7 +48,7 @@ abstract class ErrorObserver(
protected fun resolve(error: Throwable) {
if (isAlive()) {
lifecycleScope.launch {
val isResolved = resolver?.resolve(error) ?: false
val isResolved = resolver?.resolve(error) == true
if (isActive) {
onResolved?.accept(isResolved)
}

View File

@@ -5,20 +5,22 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultCaller
import androidx.annotation.StringRes
import androidx.collection.MutableScatterMap
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.router
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
import org.koitharu.kotatsu.core.util.ext.restartApplication
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException
@@ -26,7 +28,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredException
import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import java.security.cert.CertPathValidatorException
import javax.inject.Provider
@@ -49,8 +50,8 @@ class ExceptionResolver @AssistedInject constructor(
handleActivityResult(CloudFlareActivity.TAG, it)
}
fun showDetails(e: Throwable, url: String?) {
ErrorDetailsDialog.show(host.getChildFragmentManager(), e, url)
fun showErrorDetails(e: Throwable, url: String? = null) {
host.router()?.showErrorDialog(e, url)
}
suspend fun resolve(e: Throwable): Boolean = when (e) {
@@ -63,9 +64,7 @@ class ExceptionResolver @AssistedInject constructor(
}
is ProxyConfigException -> {
host.withContext {
startActivity(SettingsActivity.newProxySettingsIntent(this))
}
host.router()?.openProxySettings()
false
}
@@ -85,9 +84,7 @@ class ExceptionResolver @AssistedInject constructor(
true
} else {
host.withContext {
authHelper.startAuth(this, e.scrobbler).onFailure {
showDetails(it, null)
}
authHelper.startAuth(this, e.scrobbler).onFailure(::showErrorDetails)
}
false
}
@@ -106,12 +103,12 @@ class ExceptionResolver @AssistedInject constructor(
sourceAuthContract.launch(source)
}
private fun openInBrowser(url: String) = host.withContext {
startActivity(BrowserActivity.newIntent(this, url, null, null))
private fun openInBrowser(url: String) {
host.router()?.openBrowser(url, null, null)
}
private fun openAlternatives(manga: Manga) = host.withContext {
startActivity(AlternativesActivity.newIntent(this, manga))
private fun openAlternatives(manga: Manga) {
host.router()?.openAlternatives(manga)
}
private fun handleActivityResult(tag: String, result: Boolean) {
@@ -140,6 +137,12 @@ class ExceptionResolver @AssistedInject constructor(
getContext()?.apply(block)
}
private fun Host.router(): AppRouter? = when (this) {
is FragmentActivity -> router
is Fragment -> router
else -> null
}
interface Host : ActivityResultCaller {
fun getChildFragmentManager(): FragmentManager
@@ -161,7 +164,7 @@ class ExceptionResolver @AssistedInject constructor(
is ScrobblerAuthRequiredException,
is AuthRequiredException -> R.string.sign_in
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
is NotFoundException -> if (e.url.isHttpUrl()) R.string.open_in_browser else 0
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
is SSLException,
is CertPathValidatorException -> R.string.fix

View File

@@ -5,7 +5,6 @@ import androidx.core.util.Consumer
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.isSerializable
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
@@ -33,10 +32,10 @@ class SnackbarErrorObserver(
resolve(value)
}
} else if (value is ParseException) {
val fm = fragmentManager
if (fm != null && value.isSerializable()) {
val router = router()
if (router != null && value.isSerializable()) {
snackbar.setAction(R.string.details) {
ErrorDetailsDialog.show(fm, value, value.url)
router.showErrorDialog(value)
}
}
}

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.getOrNull
import javax.inject.Inject
import javax.inject.Singleton
@@ -42,6 +43,9 @@ class AppUpdateRepository @Inject constructor(
append("/releases?page=1&per_page=10")
}
val isUpdateAvailable: Boolean
get() = availableUpdate.value != null
fun observeAvailableUpdate() = availableUpdate.asStateFlow()
suspend fun getAvailableVersions(): List<AppVersion> {
@@ -85,8 +89,8 @@ class AppUpdateRepository @Inject constructor(
}
@Suppress("KotlinConstantConditions")
fun isUpdateSupported(): Boolean {
return BuildConfig.BUILD_TYPE != BUILD_TYPE_RELEASE || appValidator.isOriginalApp
suspend fun isUpdateSupported(): Boolean {
return BuildConfig.BUILD_TYPE != BUILD_TYPE_RELEASE || appValidator.isOriginalApp.getOrNull() == true
}
suspend fun getCurrentVersionChangelog(): String? {

View File

@@ -1,18 +1,23 @@
package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap
import androidx.core.graphics.createBitmap
import androidx.core.graphics.scale
import coil3.ImageLoader
import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.request.Options
import coil3.request.maxBitmapSize
import coil3.util.component1
import coil3.util.component2
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import kotlinx.coroutines.runInterruptible
import org.aomedia.avif.android.AvifDecoder
import org.aomedia.avif.android.AvifDecoder.Info
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
import org.koitharu.kotatsu.core.util.ext.readByteBuffer
class AvifImageDecoder(
private val source: ImageSource,
@@ -20,27 +25,52 @@ class AvifImageDecoder(
) : Decoder {
override suspend fun decode(): DecodeResult = runInterruptible {
val bytes = source.source().use {
it.inputStream().toByteBuffer()
}
val info = Info()
if (!AvifDecoder.getInfo(bytes, bytes.remaining(), info)) {
throw ImageDecodeException(
null,
"avif",
"Requested to decode byte buffer which cannot be handled by AvifDecoder",
)
}
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
val bitmap = Bitmap.createBitmap(info.width, info.height, config)
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
bitmap.recycle()
throw ImageDecodeException(null, "avif")
}
DecodeResult(
image = bitmap.asImage(),
isSampled = false,
val bytes = source.source().readByteBuffer()
val decoder = AvifDecoder.create(bytes) ?: throw ImageDecodeException(
uri = source.fileOrNull()?.toString(),
format = "avif",
message = "Requested to decode byte buffer which cannot be handled by AvifDecoder",
)
try {
val config = if (decoder.depth == 8 || decoder.alphaPresent) {
Bitmap.Config.ARGB_8888
} else {
Bitmap.Config.RGB_565
}
val bitmap = createBitmap(decoder.width, decoder.height, config)
val result = decoder.nextFrame(bitmap)
if (result != 0) {
bitmap.recycle()
throw ImageDecodeException(
uri = source.fileOrNull()?.toString(),
format = "avif",
message = AvifDecoder.resultToString(result),
)
}
// downscaling
val (dstWidth, dstHeight) = DecodeUtils.computeDstSize(
srcWidth = bitmap.width,
srcHeight = bitmap.height,
targetSize = options.size,
scale = options.scale,
maxSize = options.maxBitmapSize,
)
if (dstWidth < bitmap.width || dstHeight < bitmap.height) {
val scaled = bitmap.scale(dstWidth, dstHeight)
bitmap.recycle()
DecodeResult(
image = scaled.asImage(),
isSampled = true,
)
} else {
DecodeResult(
image = bitmap.asImage(),
isSampled = false,
)
}
} finally {
decoder.release()
}
}
class Factory : Decoder.Factory {

View File

@@ -2,29 +2,36 @@ package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.ImageDecoder
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi
import androidx.core.graphics.createBitmap
import com.davemorrissey.labs.subscaleview.decoder.ImageDecodeException
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okio.IOException
import okio.buffer
import okio.source
import org.aomedia.avif.android.AvifDecoder
import org.aomedia.avif.android.AvifDecoder.Info
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.MimeType
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.readByteBuffer
import org.koitharu.kotatsu.core.util.ext.toByteBuffer
import org.koitharu.kotatsu.core.util.ext.toMimeTypeOrNull
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.io.File
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.file.Files
object BitmapDecoderCompat {
private const val FORMAT_AVIF = "avif"
@Blocking
fun decode(file: File): Bitmap = when (val format = getMimeType(file)?.subtype) {
FORMAT_AVIF -> file.inputStream().use { decodeAvif(it.toByteBuffer()) }
fun decode(file: File): Bitmap = when (val format = probeMimeType(file)?.subtype) {
FORMAT_AVIF -> file.source().buffer().use { decodeAvif(it.readByteBuffer()) }
else -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(file))
} else {
@@ -33,7 +40,7 @@ object BitmapDecoderCompat {
}
@Blocking
fun decode(stream: InputStream, type: MediaType?, isMutable: Boolean = false): Bitmap {
fun decode(stream: InputStream, type: MimeType?, isMutable: Boolean = false): Bitmap {
val format = type?.subtype
if (format == FORMAT_AVIF) {
return decodeAvif(stream.toByteBuffer())
@@ -51,12 +58,33 @@ object BitmapDecoderCompat {
}
}
private fun getMimeType(file: File): MediaType? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Files.probeContentType(file.toPath())?.toMediaTypeOrNull()
} else {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)?.toMediaTypeOrNull()
@Blocking
fun createRegionDecoder(inoutStream: InputStream): BitmapRegionDecoder? = try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(inoutStream)
} else {
@Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(inoutStream, false)
}
} catch (e: IOException) {
e.printStackTraceDebug()
null
}
@Blocking
fun probeMimeType(file: File): MimeType? {
return MimeTypes.probeMimeType(file) ?: detectBitmapType(file)
}
@Blocking
private fun detectBitmapType(file: File): MimeType? = runCatchingCancellable {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(file.path, options)?.recycle()
options.outMimeType?.toMimeTypeOrNull()
}.getOrNull()
private fun checkBitmapNotNull(bitmap: Bitmap?, format: String?): Bitmap =
bitmap ?: throw ImageDecodeException(null, format)
@@ -70,7 +98,7 @@ object BitmapDecoderCompat {
)
}
val config = if (info.depth == 8 || info.alphaPresent) Bitmap.Config.ARGB_8888 else Bitmap.Config.RGB_565
val bitmap = Bitmap.createBitmap(info.width, info.height, config)
val bitmap = createBitmap(info.width, info.height, config)
if (!AvifDecoder.decode(bytes, bytes.remaining(), bitmap)) {
bitmap.recycle()
throw ImageDecodeException(null, FORMAT_AVIF)

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core.image
import android.net.Uri
import android.webkit.MimeTypeMap
import coil3.ImageLoader
import coil3.decode.DataSource
import coil3.decode.ImageSource
@@ -12,6 +11,7 @@ import coil3.toAndroidUri
import kotlinx.coroutines.runInterruptible
import okio.Path.Companion.toPath
import okio.openZip
import org.koitharu.kotatsu.core.util.MimeTypes
import org.koitharu.kotatsu.core.util.ext.isZipUri
import coil3.Uri as CoilUri
@@ -23,9 +23,10 @@ class CbzFetcher(
override suspend fun fetch() = runInterruptible {
val filePath = uri.schemeSpecificPart.toPath()
val entryName = requireNotNull(uri.fragment)
val fs = options.fileSystem.openZip(filePath)
SourceFetchResult(
source = ImageSource(entryName.toPath(), options.fileSystem.openZip(filePath)),
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(entryName.substringAfterLast('.', "")),
source = ImageSource(entryName.toPath(), fs),
mimeType = MimeTypes.getMimeTypeFromExtension(entryName)?.toString(),
dataSource = DataSource.DISK,
)
}

View File

@@ -0,0 +1,48 @@
package org.koitharu.kotatsu.core.image
import android.os.Parcel
import android.os.Parcelable
import android.view.View
import androidx.collection.ArrayMap
import coil3.memory.MemoryCache
import coil3.request.SuccessResult
import coil3.util.CoilUtils
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
@Parcelize
class CoilMemoryCacheKey(
val data: MemoryCache.Key
) : Parcelable {
companion object : Parceler<CoilMemoryCacheKey> {
override fun CoilMemoryCacheKey.write(parcel: Parcel, flags: Int) = with(data) {
parcel.writeString(key)
parcel.writeInt(extras.size)
for (entry in extras.entries) {
parcel.writeString(entry.key)
parcel.writeString(entry.value)
}
}
override fun create(parcel: Parcel): CoilMemoryCacheKey = CoilMemoryCacheKey(
MemoryCache.Key(
key = parcel.readString().orEmpty(),
extras = run {
val size = parcel.readInt()
val map = ArrayMap<String, String>(size)
repeat(size) {
map.put(parcel.readString(), parcel.readString())
}
map
},
),
)
fun from(view: View): CoilMemoryCacheKey? {
return (CoilUtils.result(view) as? SuccessResult)?.memoryCacheKey?.let {
CoilMemoryCacheKey(it)
}
}
}
}

View File

@@ -2,7 +2,6 @@ package org.koitharu.kotatsu.core.image
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.graphics.Rect
import android.os.Build
import coil3.Extras
@@ -11,7 +10,6 @@ import coil3.asImage
import coil3.decode.DecodeResult
import coil3.decode.DecodeUtils
import coil3.decode.Decoder
import coil3.decode.ImageSource
import coil3.fetch.SourceFetchResult
import coil3.getExtra
import coil3.request.Options
@@ -25,24 +23,37 @@ import coil3.size.Scale
import coil3.size.Size
import coil3.size.isOriginal
import coil3.size.pxOrElse
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.util.ext.copyWithNewSource
import kotlin.math.roundToInt
class RegionBitmapDecoder(
private val source: ImageSource,
private val fetchResult: SourceFetchResult,
private val options: Options,
private val imageLoader: ImageLoader,
) : Decoder {
override suspend fun decode(): DecodeResult = runInterruptible {
val regionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
BitmapRegionDecoder.newInstance(source.source().inputStream())
} else {
@Suppress("DEPRECATION")
BitmapRegionDecoder.newInstance(source.source().inputStream(), false)
override suspend fun decode(): DecodeResult? {
val regionDecoder = BitmapDecoderCompat.createRegionDecoder(fetchResult.source.source().inputStream())
if (regionDecoder == null) {
val revivedFetchResult = fetchResult.copyWithNewSource()
return try {
val fallbackDecoder = imageLoader.components.newDecoder(
result = revivedFetchResult,
options = options,
imageLoader = imageLoader,
startIndex = 0,
)?.first
if (fallbackDecoder == null || fallbackDecoder is RegionBitmapDecoder) {
null
} else {
fallbackDecoder.decode()
}
} finally {
revivedFetchResult.source.close()
}
}
checkNotNull(regionDecoder)
val bitmapOptions = BitmapFactory.Options()
try {
return try {
val rect = bitmapOptions.configureScale(regionDecoder.width, regionDecoder.height)
bitmapOptions.configureConfig()
val bitmap = regionDecoder.decodeRegion(rect, bitmapOptions)
@@ -149,7 +160,7 @@ class RegionBitmapDecoder(
result: SourceFetchResult,
options: Options,
imageLoader: ImageLoader
): Decoder = RegionBitmapDecoder(result.source, options)
): Decoder = RegionBitmapDecoder(result, options, imageLoader)
override fun equals(other: Any?) = other is Factory

View File

@@ -1,10 +1,12 @@
package org.koitharu.kotatsu.core.model
import android.content.res.Resources
import android.net.Uri
import android.text.SpannableStringBuilder
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.collection.MutableObjectIntMap
import androidx.core.net.toUri
import androidx.core.os.LocaleListCompat
import androidx.core.text.buildSpannedString
import androidx.core.text.strikeThrough
@@ -124,7 +126,8 @@ val Manga.isBroken: Boolean
get() = source == UnknownMangaSource
val Manga.appUrl: Uri
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
get() = "https://kotatsu.app/manga".toUri()
.buildUpon()
.appendQueryParameter("source", source.name)
.appendQueryParameter("name", title)
.appendQueryParameter("url", url)
@@ -146,6 +149,8 @@ fun Manga.chaptersCount(): Int {
return max
}
fun Manga.isNsfw(): Boolean = contentRating == ContentRating.ADULT || source.isNsfw()
fun MangaListFilter.getSummary() = buildSpannedString {
if (!query.isNullOrEmpty()) {
append(query)
@@ -168,3 +173,24 @@ private fun SpannableStringBuilder.appendTagsSummary(filter: MangaListFilter) {
}
}
}
fun MangaChapter.getLocalizedTitle(resources: Resources, index: Int = -1): String {
title?.let {
if (it.isNotBlank()) {
return it
}
}
val num = numberString()
val vol = volumeString()
return when {
num != null && vol != null -> resources.getString(R.string.chapter_volume_number, vol, num)
num != null -> resources.getString(R.string.chapter_number, num)
index > 0 -> resources.getString(
R.string.chapters_time_pattern,
resources.getString(R.string.unnamed_chapter),
index.toString(),
)
else -> resources.getString(R.string.unnamed_chapter)
}
}

View File

@@ -2,22 +2,29 @@ package org.koitharu.kotatsu.core.model
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.RelativeSizeSpan
import android.text.style.SuperscriptSpan
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.text.inSpans
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.util.ext.getDisplayName
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.toLocale
import org.koitharu.kotatsu.core.util.ext.toLocaleOrNull
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.splitTwoParts
import com.google.android.material.R as materialR
import java.util.Locale
import androidx.appcompat.R as appcompatR
data object LocalMangaSource : MangaSource {
override val name = "LOCAL"
@@ -74,6 +81,8 @@ tailrec fun MangaSource.unwrap(): MangaSource = if (this is MangaSourceInfo) {
this
}
fun MangaSource.getLocale(): Locale? = (unwrap() as? MangaParserSource)?.locale?.toLocaleOrNull()
fun MangaSource.getSummary(context: Context): String? = when (val source = unwrap()) {
is MangaParserSource -> {
val type = context.getString(source.contentType.titleResId)
@@ -94,9 +103,22 @@ fun MangaSource.getTitle(context: Context): String = when (val source = unwrap()
}
fun SpannableStringBuilder.appendNsfwLabel(context: Context) = inSpans(
ForegroundColorSpan(context.getThemeColor(materialR.attr.colorError, Color.RED)),
ForegroundColorSpan(context.getThemeColor(appcompatR.attr.colorError, Color.RED)),
RelativeSizeSpan(0.74f),
SuperscriptSpan(),
) {
append(context.getString(R.string.nsfw))
}
fun SpannableStringBuilder.appendIcon(textView: TextView, @DrawableRes resId: Int): SpannableStringBuilder {
val icon = ContextCompat.getDrawable(textView.context, resId) ?: return this
icon.setTintList(textView.textColors)
val size = textView.lineHeight
icon.setBounds(0, 0, size, size)
val alignment = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ImageSpan.ALIGN_CENTER
} else {
ImageSpan.ALIGN_BOTTOM
}
return inSpans(ImageSpan(icon, alignment)) { append(' ') }
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.domain.ListFilterOption
fun ListFilterOption.toChipModel(isChecked: Boolean) = ChipsView.ChipModel(
title = titleText,
titleResId = titleResId,
icon = iconResId,
iconData = getIconData(),
isChecked = isChecked,
data = this,
)

View File

@@ -17,7 +17,7 @@ data class ParcelableChapter(
override fun create(parcel: Parcel) = ParcelableChapter(
MangaChapter(
id = parcel.readLong(),
name = parcel.readString().orEmpty(),
title = parcel.readString(),
number = parcel.readFloat(),
volume = parcel.readInt(),
url = parcel.readString().orEmpty(),
@@ -30,7 +30,7 @@ data class ParcelableChapter(
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
parcel.writeLong(id)
parcel.writeString(name)
parcel.writeString(title)
parcel.writeFloat(number)
parcel.writeInt(volume)
parcel.writeString(url)

View File

@@ -7,6 +7,8 @@ import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.core.util.ext.readStringSet
import org.koitharu.kotatsu.core.util.ext.writeStringSet
import org.koitharu.kotatsu.parsers.model.Manga
@Parcelize
@@ -20,7 +22,7 @@ data class ParcelableManga(
override fun ParcelableManga.write(parcel: Parcel, flags: Int) = with(manga) {
parcel.writeLong(id)
parcel.writeString(title)
parcel.writeString(altTitle)
parcel.writeStringSet(altTitles)
parcel.writeString(url)
parcel.writeString(publicUrl)
parcel.writeFloat(rating)
@@ -30,7 +32,7 @@ data class ParcelableManga(
parcel.writeString(description.takeIf { withDescription })
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
parcel.writeSerializable(state)
parcel.writeString(author)
parcel.writeStringSet(authors)
parcel.writeString(source.name)
}
@@ -38,7 +40,7 @@ data class ParcelableManga(
Manga(
id = parcel.readLong(),
title = requireNotNull(parcel.readString()),
altTitle = parcel.readString(),
altTitles = parcel.readStringSet(),
url = requireNotNull(parcel.readString()),
publicUrl = requireNotNull(parcel.readString()),
rating = parcel.readFloat(),
@@ -48,7 +50,7 @@ data class ParcelableManga(
description = parcel.readString(),
tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags,
state = parcel.readSerializableCompat(),
author = parcel.readString(),
authors = parcel.readStringSet(),
chapters = null,
source = MangaSource(parcel.readString()),
),

View File

@@ -30,6 +30,7 @@ object MangaListFilterParceler : Parceler<MangaListFilter> {
parcel.writeInt(year)
parcel.writeInt(yearFrom)
parcel.writeInt(yearTo)
parcel.writeString(author)
}
override fun create(parcel: Parcel) = MangaListFilter(
@@ -45,6 +46,7 @@ object MangaListFilterParceler : Parceler<MangaListFilter> {
year = parcel.readInt(),
yearFrom = parcel.readInt(),
yearTo = parcel.readInt(),
author = parcel.readString(),
)
}

View File

@@ -0,0 +1,809 @@
package org.koitharu.kotatsu.core.nav
import android.accounts.Account
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.View
import androidx.annotation.CheckResult
import androidx.annotation.UiContext
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.findFragment
import androidx.lifecycle.LifecycleOwner
import dagger.hilt.android.EntryPointAccessors
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.image.CoilMemoryCacheKey
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.MangaSourceInfo
import org.koitharu.kotatsu.core.model.appUrl
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.model.isBroken
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaListFilter
import org.koitharu.kotatsu.core.model.parcelable.ParcelableMangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.external.ExternalMangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.prefs.TriStateOption
import org.koitharu.kotatsu.core.ui.dialog.BigButtonsAlertDialog
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.util.ext.getThemeDrawable
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingInfoSheet
import org.koitharu.kotatsu.download.ui.dialog.DownloadDialogFragment
import org.koitharu.kotatsu.download.ui.list.DownloadsActivity
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity
import org.koitharu.kotatsu.favourites.ui.categories.edit.FavouritesCategoryEditActivity
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteDialog
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
import org.koitharu.kotatsu.filter.ui.sheet.FilterSheetFragment
import org.koitharu.kotatsu.filter.ui.tags.TagsCatalogSheet
import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.list.ui.config.ListConfigBottomSheet
import org.koitharu.kotatsu.list.ui.config.ListConfigSection
import org.koitharu.kotatsu.local.ui.ImportDialogFragment
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
import org.koitharu.kotatsu.main.ui.MainActivity
import org.koitharu.kotatsu.main.ui.welcome.WelcomeSheet
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.parsers.util.isNullOrEmpty
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity
import org.koitharu.kotatsu.reader.ui.config.ReaderConfigSheet
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerService
import org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.domain.SearchKind
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.multi.SearchActivity
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.about.AppUpdateActivity
import org.koitharu.kotatsu.settings.backup.BackupDialogFragment
import org.koitharu.kotatsu.settings.backup.RestoreDialogFragment
import org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity
import org.koitharu.kotatsu.settings.storage.MangaDirectorySelectDialog
import org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity
import org.koitharu.kotatsu.settings.tracker.categories.TrackerCategoriesConfigSheet
import org.koitharu.kotatsu.stats.ui.StatsActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
import org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity
import org.koitharu.kotatsu.tracker.ui.updates.UpdatesActivity
import java.io.File
import androidx.appcompat.R as appcompatR
class AppRouter private constructor(
private val activity: FragmentActivity?,
private val fragment: Fragment?,
) {
constructor(activity: FragmentActivity) : this(activity, null)
constructor(fragment: Fragment) : this(null, fragment)
private val settings: AppSettings by lazy {
EntryPointAccessors.fromApplication<AppRouterEntryPoint>(checkNotNull(contextOrNull())).settings
}
/** Activities **/
fun openList(source: MangaSource, filter: MangaListFilter?, sortOrder: SortOrder?) {
startActivity(listIntent(contextOrNull() ?: return, source, filter, sortOrder))
}
fun openList(tag: MangaTag) = openList(tag.source, MangaListFilter(tags = setOf(tag)), null)
fun openSearch(query: String, kind: SearchKind = SearchKind.SIMPLE) {
startActivity(
Intent(contextOrNull() ?: return, SearchActivity::class.java)
.putExtra(KEY_QUERY, query)
.putExtra(KEY_KIND, kind),
)
}
fun openSearch(source: MangaSource, query: String) = openList(source, MangaListFilter(query = query), null)
fun openDetails(manga: Manga) {
startActivity(detailsIntent(contextOrNull() ?: return, manga))
}
fun openDetails(mangaId: Long) {
startActivity(detailsIntent(contextOrNull() ?: return, mangaId))
}
fun openDetails(link: Uri) {
startActivity(
Intent(contextOrNull() ?: return, DetailsActivity::class.java)
.setData(link),
)
}
fun openReader(manga: Manga, anchor: View? = null) {
openReader(
ReaderIntent.Builder(contextOrNull() ?: return)
.manga(manga)
.build(),
anchor,
)
}
fun openReader(intent: ReaderIntent, anchor: View? = null) {
startActivity(intent.intent, anchor?.let { view -> scaleUpActivityOptionsOf(view) })
}
fun openAlternatives(manga: Manga) {
startActivity(
Intent(contextOrNull() ?: return, AlternativesActivity::class.java)
.putExtra(KEY_MANGA, ParcelableManga(manga)),
)
}
fun openRelated(manga: Manga) {
startActivity(
Intent(contextOrNull(), RelatedMangaActivity::class.java)
.putExtra(KEY_MANGA, ParcelableManga(manga)),
)
}
fun openImage(url: String, source: MangaSource?, anchor: View? = null, preview: CoilMemoryCacheKey? = null) {
startActivity(
Intent(contextOrNull(), ImageActivity::class.java)
.setData(url.toUri())
.putExtra(KEY_SOURCE, source?.name)
.putExtra(KEY_PREVIEW, preview),
anchor?.let { scaleUpActivityOptionsOf(it) },
)
}
fun openBookmarks() = startActivity(AllBookmarksActivity::class.java)
fun openAppUpdate() = startActivity(AppUpdateActivity::class.java)
fun openSuggestions() {
startActivity(suggestionsIntent(contextOrNull() ?: return))
}
fun openSourcesCatalog() = startActivity(SourcesCatalogActivity::class.java)
fun openDownloads() = startActivity(DownloadsActivity::class.java)
fun openDirectoriesSettings() = startActivity(MangaDirectoriesActivity::class.java)
fun openBrowser(url: String, source: MangaSource?, title: String?) {
startActivity(
Intent(contextOrNull() ?: return, BrowserActivity::class.java)
.setData(url.toUri())
.putExtra(KEY_TITLE, title)
.putExtra(KEY_SOURCE, source?.name),
)
}
fun openColorFilterConfig(manga: Manga, page: MangaPage) {
startActivity(
Intent(contextOrNull(), ColorFilterConfigActivity::class.java)
.putExtra(KEY_MANGA, ParcelableManga(manga))
.putExtra(KEY_PAGES, ParcelableMangaPage(page)),
)
}
fun openHistory() = startActivity(HistoryActivity::class.java)
fun openFavorites() = startActivity(FavouritesActivity::class.java)
fun openFavorites(category: FavouriteCategory) {
startActivity(
Intent(contextOrNull() ?: return, FavouritesActivity::class.java)
.putExtra(KEY_ID, category.id)
.putExtra(KEY_TITLE, category.title),
)
}
fun openFavoriteCategories() = startActivity(FavouriteCategoriesActivity::class.java)
fun openFavoriteCategoryEdit(categoryId: Long) {
startActivity(
Intent(contextOrNull() ?: return, FavouritesCategoryEditActivity::class.java)
.putExtra(KEY_ID, categoryId),
)
}
fun openFavoriteCategoryCreate() = openFavoriteCategoryEdit(FavouritesCategoryEditActivity.NO_ID)
fun openMangaUpdates() {
startActivity(mangaUpdatesIntent(contextOrNull() ?: return))
}
fun openSettings() = startActivity(SettingsActivity::class.java)
fun openReaderSettings() {
startActivity(readerSettingsIntent(contextOrNull() ?: return))
}
fun openProxySettings() {
startActivity(proxySettingsIntent(contextOrNull() ?: return))
}
fun openDownloadsSetting() {
startActivity(downloadsSettingsIntent(contextOrNull() ?: return))
}
fun openSourceSettings(source: MangaSource) {
startActivity(sourceSettingsIntent(contextOrNull() ?: return, source))
}
fun openSuggestionsSettings() {
startActivity(suggestionsSettingsIntent(contextOrNull() ?: return))
}
fun openSourcesSettings() {
startActivity(sourcesSettingsIntent(contextOrNull() ?: return))
}
fun openReaderTapGridSettings() = startActivity(ReaderTapGridConfigActivity::class.java)
fun openScrobblerSettings(scrobbler: ScrobblerService) {
startActivity(
Intent(contextOrNull() ?: return, ScrobblerConfigActivity::class.java)
.putExtra(KEY_ID, scrobbler.id),
)
}
fun openSourceAuth(source: MangaSource) {
startActivity(sourceAuthIntent(contextOrNull() ?: return, source))
}
fun openManageSources() {
startActivity(
manageSourcesIntent(contextOrNull() ?: return),
)
}
fun openStatistic() = startActivity(StatsActivity::class.java)
@CheckResult
fun openExternalBrowser(url: String, chooserTitle: CharSequence? = null): Boolean {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = url.toUriOrNull() ?: return false
return startActivitySafe(
if (!chooserTitle.isNullOrEmpty()) {
Intent.createChooser(intent, chooserTitle)
} else {
intent
},
)
}
@CheckResult
fun openSystemSyncSettings(account: Account): Boolean {
val args = Bundle(1)
args.putParcelable(ACCOUNT_KEY, account)
val intent = Intent(ACTION_ACCOUNT_SYNC_SETTINGS)
intent.putExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS, args)
return startActivitySafe(intent)
}
/** Dialogs **/
fun showDownloadDialog(manga: Manga, snackbarHost: View?) = showDownloadDialog(setOf(manga), snackbarHost)
fun showDownloadDialog(manga: Collection<Manga>, snackbarHost: View?) {
if (manga.isEmpty()) {
return
}
val fm = getFragmentManager() ?: return
if (snackbarHost != null) {
getLifecycleOwner()?.let { lifecycleOwner ->
DownloadDialogFragment.registerCallback(fm, lifecycleOwner, snackbarHost)
}
} else {
DownloadDialogFragment.unregisterCallback(fm)
}
DownloadDialogFragment().withArgs(1) {
putParcelableArray(KEY_MANGA, manga.mapToArray { ParcelableManga(it, withDescription = false) })
}.showDistinct()
}
fun showLocalInfoDialog(manga: Manga) {
LocalInfoDialog().withArgs(1) {
putParcelable(KEY_MANGA, ParcelableManga(manga))
}.showDistinct()
}
fun showDirectorySelectDialog() {
MangaDirectorySelectDialog().showDistinct()
}
fun showFavoriteDialog(manga: Manga) = showFavoriteDialog(setOf(manga))
fun showFavoriteDialog(manga: Collection<Manga>) {
if (manga.isEmpty()) {
return
}
FavoriteDialog().withArgs(1) {
putParcelableArrayList(
KEY_MANGA_LIST,
manga.mapTo(ArrayList(manga.size)) { ParcelableManga(it, withDescription = false) },
)
}.showDistinct()
}
fun showTagDialog(tag: MangaTag) {
buildAlertDialog(contextOrNull() ?: return) {
setIcon(R.drawable.ic_tag)
setTitle(tag.title)
setItems(
arrayOf(
context.getString(R.string.search_on_s, tag.source.getTitle(context)),
context.getString(R.string.search_everywhere),
),
) { _, which ->
when (which) {
0 -> openList(tag)
1 -> openSearch(tag.title, SearchKind.TAG)
}
}
setNegativeButton(R.string.close, null)
setCancelable(true)
}.show()
}
fun showAuthorDialog(author: String, source: MangaSource) {
buildAlertDialog(contextOrNull() ?: return) {
setIcon(R.drawable.ic_user)
setTitle(author)
setItems(
arrayOf(
context.getString(R.string.search_on_s, source.getTitle(context)),
context.getString(R.string.search_everywhere),
),
) { _, which ->
when (which) {
0 -> openList(source, MangaListFilter(author = author), null)
1 -> openSearch(author, SearchKind.AUTHOR)
}
}
setNegativeButton(R.string.close, null)
setCancelable(true)
}.show()
}
fun showShareDialog(manga: Manga) {
if (manga.isBroken) {
return
}
if (manga.isLocal) {
manga.url.toUri().toFileOrNull()?.let {
shareFile(it)
}
return
}
buildAlertDialog(contextOrNull() ?: return) {
setIcon(context.getThemeDrawable(appcompatR.attr.actionModeShareDrawable))
setTitle(R.string.share)
setItems(
arrayOf(
context.getString(R.string.link_to_manga_in_app),
context.getString(R.string.link_to_manga_on_s, manga.source.getTitle(context)),
),
) { _, which ->
val link = when (which) {
0 -> manga.appUrl.toString()
1 -> manga.publicUrl
else -> return@setItems
}
shareLink(link, manga.title)
}
setNegativeButton(android.R.string.cancel, null)
setCancelable(true)
}.show()
}
fun showErrorDialog(error: Throwable, url: String? = null) {
ErrorDetailsDialog().withArgs(2) {
putSerializable(KEY_ERROR, error)
putString(KEY_URL, url)
}.show()
}
fun showBackupRestoreDialog(fileUri: Uri) {
RestoreDialogFragment().withArgs(1) {
putString(KEY_FILE, fileUri.toString())
}.show()
}
fun showBackupCreateDialog() {
BackupDialogFragment().show()
}
fun showImportDialog() {
ImportDialogFragment().showDistinct()
}
fun showFilterSheet(): Boolean = if (isFilterSupported()) {
FilterSheetFragment().showDistinct()
} else {
false
}
fun showTagsCatalogSheet(excludeMode: Boolean) {
if (!isFilterSupported()) {
return
}
TagsCatalogSheet().withArgs(1) {
putBoolean(KEY_EXCLUDE, excludeMode)
}.showDistinct()
}
fun showListConfigSheet(section: ListConfigSection) {
ListConfigBottomSheet().withArgs(1) {
putParcelable(KEY_LIST_SECTION, section)
}.showDistinct()
}
fun showStatisticSheet(manga: Manga) {
MangaStatsSheet().withArgs(1) {
putParcelable(KEY_MANGA, ParcelableManga(manga))
}.showDistinct()
}
fun showReaderConfigSheet(mode: ReaderMode) {
ReaderConfigSheet().withArgs(1) {
putInt(KEY_READER_MODE, mode.id)
}.showDistinct()
}
fun showWelcomeSheet() {
WelcomeSheet().showDistinct()
}
fun showChapterPagesSheet() {
ChaptersPagesSheet().showDistinct()
}
fun showChapterPagesSheet(defaultTab: Int) {
ChaptersPagesSheet().withArgs(1) {
putInt(KEY_TAB, defaultTab)
}.showDistinct()
}
fun showScrobblingSelectorSheet(manga: Manga, scrobblerService: ScrobblerService?) {
ScrobblingSelectorSheet().withArgs(2) {
putParcelable(KEY_MANGA, ParcelableManga(manga))
if (scrobblerService != null) {
putInt(KEY_ID, scrobblerService.id)
}
}.show()
}
fun showScrobblingInfoSheet(index: Int) {
ScrobblingInfoSheet().withArgs(1) {
putInt(KEY_INDEX, index)
}.showDistinct()
}
fun showTrackerCategoriesConfigSheet() {
TrackerCategoriesConfigSheet().showDistinct()
}
fun askForDownloadOverMeteredNetwork(onConfirmed: (allow: Boolean) -> Unit) {
val context = contextOrNull() ?: return
when (settings.allowDownloadOnMeteredNetwork) {
TriStateOption.ENABLED -> onConfirmed(true)
TriStateOption.DISABLED -> onConfirmed(false)
TriStateOption.ASK -> {
if (!context.connectivityManager.isActiveNetworkMetered) {
onConfirmed(true)
return
}
val listener = DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
settings.allowDownloadOnMeteredNetwork = TriStateOption.ENABLED
onConfirmed(true)
}
DialogInterface.BUTTON_NEUTRAL -> {
onConfirmed(true)
}
DialogInterface.BUTTON_NEGATIVE -> {
settings.allowDownloadOnMeteredNetwork = TriStateOption.DISABLED
onConfirmed(false)
}
}
}
BigButtonsAlertDialog.Builder(context)
.setIcon(R.drawable.ic_network_cellular)
.setTitle(R.string.download_cellular_confirm)
.setPositiveButton(R.string.allow_always, listener)
.setNeutralButton(R.string.allow_once, listener)
.setNegativeButton(R.string.dont_allow, listener)
.create()
.show()
}
}
}
/** Public utils **/
fun isFilterSupported(): Boolean = when {
fragment != null -> fragment.activity is FilterCoordinator.Owner
activity != null -> activity is FilterCoordinator.Owner
else -> false
}
fun isChapterPagesSheetShown(): Boolean {
val sheet = getFragmentManager()?.findFragmentByTag(fragmentTag<ChaptersPagesSheet>()) as? ChaptersPagesSheet
return sheet?.dialog?.isShowing == true
}
fun closeWelcomeSheet(): Boolean {
val tag = fragmentTag<WelcomeSheet>()
val sheet = fragment?.findFragmentByTagRecursive(tag)
?: activity?.supportFragmentManager?.findFragmentByTag(tag)
?: return false
return if (sheet is WelcomeSheet) {
sheet.dismissAllowingStateLoss()
true
} else {
false
}
}
/** Private utils **/
private fun startActivity(intent: Intent, options: Bundle? = null) {
fragment?.also {
if (it.host != null) {
it.startActivity(intent, options)
}
} ?: activity?.startActivity(intent, options)
}
private fun startActivitySafe(intent: Intent): Boolean = try {
startActivity(intent)
true
} catch (_: ActivityNotFoundException) {
false
}
private fun startActivity(activityClass: Class<out Activity>) {
startActivity(Intent(contextOrNull() ?: return, activityClass))
}
private fun getFragmentManager(): FragmentManager? {
return fragment?.childFragmentManager ?: activity?.supportFragmentManager
}
private fun shareLink(link: String, title: String) {
val context = contextOrNull() ?: return
ShareCompat.IntentBuilder(context)
.setText(link)
.setType(TYPE_TEXT)
.setChooserTitle(context.getString(R.string.share_s, title.ellipsize(12)))
.startChooser()
}
private fun shareFile(file: File) { // TODO directory sharing support
val context = contextOrNull() ?: return
val intentBuilder = ShareCompat.IntentBuilder(context)
.setType(TYPE_CBZ)
val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file)
intentBuilder.addStream(uri)
intentBuilder.setChooserTitle(context.getString(R.string.share_s, file.name))
intentBuilder.startChooser()
}
@UiContext
private fun contextOrNull(): Context? = activity ?: fragment?.context
private fun getLifecycleOwner(): LifecycleOwner? = activity ?: fragment?.viewLifecycleOwner
private fun DialogFragment.showDistinct(): Boolean {
val fm = this@AppRouter.getFragmentManager() ?: return false
val tag = javaClass.fragmentTag()
val existing = fm.findFragmentByTag(tag) as? DialogFragment?
if (existing != null && existing.isVisible && existing.arguments == this.arguments) {
return false
}
show(fm, tag)
return true
}
private fun DialogFragment.show() {
show(
this@AppRouter.getFragmentManager() ?: return,
javaClass.fragmentTag(),
)
}
private fun Fragment.findFragmentByTagRecursive(fragmentTag: String): Fragment? {
childFragmentManager.findFragmentByTag(fragmentTag)?.let {
return it
}
val parent = parentFragment
return if (parent != null) {
parent.findFragmentByTagRecursive(fragmentTag)
} else {
parentFragmentManager.findFragmentByTag(fragmentTag)
}
}
companion object {
fun from(view: View): AppRouter? = runCatching {
AppRouter(view.findFragment<Fragment>())
}.getOrElse {
(view.context.findActivity() as? FragmentActivity)?.let(::AppRouter)
}
fun detailsIntent(context: Context, manga: Manga) = Intent(context, DetailsActivity::class.java)
.putExtra(KEY_MANGA, ParcelableManga(manga))
fun detailsIntent(context: Context, mangaId: Long) = Intent(context, DetailsActivity::class.java)
.putExtra(KEY_ID, mangaId)
fun listIntent(context: Context, source: MangaSource, filter: MangaListFilter?, sortOrder: SortOrder?): Intent =
Intent(context, MangaListActivity::class.java)
.setAction(ACTION_MANGA_EXPLORE)
.putExtra(KEY_SOURCE, source.name)
.apply {
if (!filter.isNullOrEmpty()) {
putExtra(KEY_FILTER, ParcelableMangaListFilter(filter))
}
if (sortOrder != null) {
putExtra(KEY_SORT_ORDER, sortOrder)
}
}
fun cloudFlareResolveIntent(context: Context, exception: CloudFlareProtectedException): Intent =
Intent(context, CloudFlareActivity::class.java).apply {
data = exception.url.toUri()
putExtra(KEY_SOURCE, exception.source?.name)
exception.headers[CommonHeaders.USER_AGENT]?.let {
putExtra(KEY_USER_AGENT, it)
}
}
fun suggestionsIntent(context: Context) = Intent(context, SuggestionsActivity::class.java)
fun homeIntent(context: Context) = Intent(context, MainActivity::class.java)
fun mangaUpdatesIntent(context: Context) = Intent(context, UpdatesActivity::class.java)
fun readerSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_READER)
fun suggestionsSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SUGGESTIONS)
fun trackerSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_TRACKER)
fun proxySettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_PROXY)
fun historySettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_HISTORY)
fun sourcesSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SOURCES)
fun manageSourcesIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_MANAGE_SOURCES)
fun downloadsSettingsIntent(context: Context) =
Intent(context, SettingsActivity::class.java)
.setAction(ACTION_MANAGE_DOWNLOADS)
fun sourceSettingsIntent(context: Context, source: MangaSource): Intent = when (source) {
is MangaSourceInfo -> sourceSettingsIntent(context, source.mangaSource)
is ExternalMangaSource -> Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", source.packageName, null))
else -> Intent(context, SettingsActivity::class.java)
.setAction(ACTION_SOURCE)
.putExtra(KEY_SOURCE, source.name)
}
fun sourceAuthIntent(context: Context, source: MangaSource): Intent {
return Intent(context, SourceAuthActivity::class.java)
.putExtra(KEY_SOURCE, source.name)
}
fun isShareSupported(manga: Manga): Boolean = when {
manga.isBroken -> false
manga.isLocal -> manga.url.toUri().toFileOrNull() != null
else -> true
}
const val KEY_DATA = "data"
const val KEY_ENTRIES = "entries"
const val KEY_ERROR = "error"
const val KEY_EXCLUDE = "exclude"
const val KEY_FILE = "file"
const val KEY_FILTER = "filter"
const val KEY_ID = "id"
const val KEY_INDEX = "index"
const val KEY_KIND = "kind"
const val KEY_LIST_SECTION = "list_section"
const val KEY_MANGA = "manga"
const val KEY_MANGA_LIST = "manga_list"
const val KEY_PAGES = "pages"
const val KEY_PREVIEW = "preview"
const val KEY_QUERY = "query"
const val KEY_READER_MODE = "reader_mode"
const val KEY_SORT_ORDER = "sort_order"
const val KEY_SOURCE = "source"
const val KEY_TAB = "tab"
const val KEY_TITLE = "title"
const val KEY_URL = "url"
const val KEY_USER_AGENT = "user_agent"
const val ACTION_HISTORY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_HISTORY"
const val ACTION_MANAGE_DOWNLOADS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_DOWNLOADS"
const val ACTION_MANAGE_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES_LIST"
const val ACTION_MANGA_EXPLORE = "${BuildConfig.APPLICATION_ID}.action.EXPLORE_MANGA"
const val ACTION_PROXY = "${BuildConfig.APPLICATION_ID}.action.MANAGE_PROXY"
const val ACTION_READER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_READER_SETTINGS"
const val ACTION_SOURCE = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCE_SETTINGS"
const val ACTION_SOURCES = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SOURCES"
const val ACTION_SUGGESTIONS = "${BuildConfig.APPLICATION_ID}.action.MANAGE_SUGGESTIONS"
const val ACTION_TRACKER = "${BuildConfig.APPLICATION_ID}.action.MANAGE_TRACKER"
private const val ACCOUNT_KEY = "account"
private const val ACTION_ACCOUNT_SYNC_SETTINGS = "android.settings.ACCOUNT_SYNC_SETTINGS"
private const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
private const val TYPE_TEXT = "text/plain"
private const val TYPE_IMAGE = "image/*"
private const val TYPE_CBZ = "application/x-cbz"
private fun Class<out Fragment>.fragmentTag() = name // TODO
private inline fun <reified F : Fragment> fragmentTag() = F::class.java.fragmentTag()
}
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.nav
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.core.prefs.AppSettings
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppRouterEntryPoint {
val settings: AppSettings
}

View File

@@ -1,11 +1,12 @@
package org.koitharu.kotatsu.core.parser
package org.koitharu.kotatsu.core.nav
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.nav.AppRouter.Companion.KEY_ID
import org.koitharu.kotatsu.core.nav.AppRouter.Companion.KEY_MANGA
import org.koitharu.kotatsu.core.util.ext.getParcelableCompat
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.parsers.model.Manga
@@ -25,7 +26,7 @@ class MangaIntent private constructor(
constructor(savedStateHandle: SavedStateHandle) : this(
manga = savedStateHandle.get<ParcelableManga>(KEY_MANGA)?.manga,
id = savedStateHandle[KEY_ID] ?: ID_NONE,
uri = savedStateHandle[BaseActivity.EXTRA_DATA],
uri = savedStateHandle[AppRouter.KEY_DATA],
)
constructor(args: Bundle?) : this(
@@ -41,9 +42,6 @@ class MangaIntent private constructor(
const val ID_NONE = 0L
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
fun of(manga: Manga) = MangaIntent(manga, manga.id, null)
}
}

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.core.nav
import android.app.ActivityOptions
import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
inline val FragmentActivity.router: AppRouter
get() = AppRouter(this)
inline val Fragment.router: AppRouter
get() = AppRouter(this)
tailrec fun Fragment.dismissParentDialog(): Boolean {
return when (val parent = parentFragment) {
null -> return false
is DialogFragment -> {
parent.dismiss()
true
}
else -> parent.dismissParentDialog()
}
}
fun scaleUpActivityOptionsOf(view: View): Bundle? = if (view.context.isAnimationsEnabled) {
ActivityOptions.makeScaleUpAnimation(
view,
0,
0,
view.width,
view.height,
).toBundle()
} else {
null
}

View File

@@ -0,0 +1,61 @@
package org.koitharu.kotatsu.core.nav
import android.content.Context
import android.content.Intent
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
@JvmInline
value class ReaderIntent private constructor(
val intent: Intent,
) {
class Builder(context: Context) {
private val intent = Intent(context, ReaderActivity::class.java)
.setAction(ACTION_MANGA_READ)
fun manga(manga: Manga) = apply {
intent.putExtra(AppRouter.KEY_MANGA, ParcelableManga(manga))
}
fun mangaId(mangaId: Long) = apply {
intent.putExtra(AppRouter.KEY_ID, mangaId)
}
fun incognito(incognito: Boolean) = apply {
intent.putExtra(EXTRA_INCOGNITO, incognito)
}
fun branch(branch: String?) = apply {
intent.putExtra(EXTRA_BRANCH, branch)
}
fun state(state: ReaderState?) = apply {
intent.putExtra(EXTRA_STATE, state)
}
fun bookmark(bookmark: Bookmark) = manga(
bookmark.manga,
).state(
ReaderState(
chapterId = bookmark.chapterId,
page = bookmark.page,
scroll = bookmark.scroll,
),
)
fun build() = ReaderIntent(intent)
}
companion object {
const val ACTION_MANGA_READ = "${BuildConfig.APPLICATION_ID}.action.READ_MANGA"
const val EXTRA_STATE = "state"
const val EXTRA_BRANCH = "branch"
const val EXTRA_INCOGNITO = "incognito"
}
}

View File

@@ -1,51 +0,0 @@
package org.koitharu.kotatsu.core.network
import okio.IOException
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.ProxySelector
import java.net.SocketAddress
import java.net.URI
class AppProxySelector(
private val settings: AppSettings,
) : ProxySelector() {
init {
setDefault(this)
}
private var cachedProxy: Proxy? = null
override fun select(uri: URI?): List<Proxy> {
return listOf(getProxy())
}
override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
ioe?.printStackTraceDebug()
}
private fun getProxy(): Proxy {
val type = settings.proxyType
val address = settings.proxyAddress
val port = settings.proxyPort
if (type == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY
}
if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) {
throw ProxyConfigException()
}
cachedProxy?.let {
val addr = it.address() as? InetSocketAddress
if (addr != null && it.type() == type && addr.port == port && addr.hostString == address) {
return it
}
}
val proxy = Proxy(type, InetSocketAddress(address, port))
cachedProxy = proxy
return proxy
}
}

View File

@@ -65,7 +65,7 @@ class CommonHeadersInterceptor @Inject constructor(
private fun Interceptor.interceptSafe(chain: Chain): Response = runCatchingCancellable {
intercept(chain)
}.getOrElse { e ->
if (e is IOException) {
if (e is IOException || e is Error) {
throw e
} else {
// only IOException can be safely thrown from an Interceptor

View File

@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.imageproxy.RealImageProxyInterceptor
import org.koitharu.kotatsu.core.network.proxy.ProxyProvider
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@@ -62,14 +63,15 @@ interface NetworkModule {
cache: Cache,
cookieJar: CookieJar,
settings: AppSettings,
proxyProvider: ProxyProvider,
): OkHttpClient = OkHttpClient.Builder().apply {
assertNotInMainThread()
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(cookieJar)
proxySelector(AppProxySelector(settings))
proxyAuthenticator(ProxyAuthenticator(settings))
proxySelector(proxyProvider.selector)
proxyAuthenticator(proxyProvider.authenticator)
dns(DoHManager(cache, settings))
if (settings.isSSLBypassEnabled) {
disableCertificateVerification()

View File

@@ -1,45 +0,0 @@
package org.koitharu.kotatsu.core.network
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.core.prefs.AppSettings
import java.net.PasswordAuthentication
import java.net.Proxy
class ProxyAuthenticator(
private val settings: AppSettings,
) : Authenticator, java.net.Authenticator() {
init {
setDefault(this)
}
override fun authenticate(route: Route?, response: Response): Request? {
if (!isProxyEnabled()) {
return null
}
if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) {
return null
}
val login = settings.proxyLogin ?: return null
val password = settings.proxyPassword ?: return null
val credential = Credentials.basic(login, password)
return response.request.newBuilder()
.header(CommonHeaders.PROXY_AUTHORIZATION, credential)
.build()
}
override fun getPasswordAuthentication(): PasswordAuthentication? {
if (!isProxyEnabled()) {
return null
}
val login = settings.proxyLogin ?: return null
val password = settings.proxyPassword ?: return null
return PasswordAuthentication(login, password.toCharArray())
}
private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT
}

View File

@@ -0,0 +1,150 @@
package org.koitharu.kotatsu.core.network.proxy
import androidx.webkit.ProxyConfig
import androidx.webkit.ProxyController
import androidx.webkit.WebViewFeature
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.net.InetSocketAddress
import java.net.PasswordAuthentication
import java.net.Proxy
import java.net.ProxySelector
import java.net.SocketAddress
import java.net.URI
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import java.net.Authenticator as JavaAuthenticator
@Singleton
class ProxyProvider @Inject constructor(
private val settings: AppSettings,
) {
private var cachedProxy: Proxy? = null
val selector = object : ProxySelector() {
override fun select(uri: URI?): List<Proxy> {
return listOf(getProxy())
}
override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: okio.IOException?) {
ioe?.printStackTraceDebug()
}
}
val authenticator = ProxyAuthenticator()
init {
ProxySelector.setDefault(selector)
JavaAuthenticator.setDefault(authenticator)
}
suspend fun applyWebViewConfig() {
val isProxyEnabled = isProxyEnabled()
if (!WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
if (isProxyEnabled) {
throw IllegalArgumentException("Proxy for WebView is not supported") // TODO localize
}
} else {
val controller = ProxyController.getInstance()
if (settings.proxyType == Proxy.Type.DIRECT) {
suspendCoroutine { cont ->
controller.clearProxyOverride(
(cont.context[CoroutineDispatcher] ?: Dispatchers.Main).asExecutor(),
) {
cont.resume(Unit)
}
}
} else {
val url = buildString {
when (settings.proxyType) {
Proxy.Type.DIRECT -> Unit
Proxy.Type.HTTP -> append("http")
Proxy.Type.SOCKS -> append("socks")
}
append("://")
append(settings.proxyAddress)
append(':')
append(settings.proxyPort)
}
if (settings.proxyType == Proxy.Type.SOCKS) {
System.setProperty("java.net.socks.username", settings.proxyLogin);
System.setProperty("java.net.socks.password", settings.proxyPassword);
}
val proxyConfig = ProxyConfig.Builder()
.addProxyRule(url)
.build()
suspendCoroutine { cont ->
controller.setProxyOverride(
proxyConfig,
(cont.context[CoroutineDispatcher] ?: Dispatchers.Main).asExecutor(),
) {
cont.resume(Unit)
}
}
}
}
}
private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT
private fun getProxy(): Proxy {
val type = settings.proxyType
val address = settings.proxyAddress
val port = settings.proxyPort
if (type == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY
}
if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) {
throw ProxyConfigException()
}
cachedProxy?.let {
val addr = it.address() as? InetSocketAddress
if (addr != null && it.type() == type && addr.port == port && addr.hostString == address) {
return it
}
}
val proxy = Proxy(type, InetSocketAddress(address, port))
cachedProxy = proxy
return proxy
}
inner class ProxyAuthenticator : Authenticator, JavaAuthenticator() {
override fun authenticate(route: Route?, response: Response): Request? {
if (!isProxyEnabled()) {
return null
}
if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) {
return null
}
val login = settings.proxyLogin ?: return null
val password = settings.proxyPassword ?: return null
val credential = Credentials.basic(login, password)
return response.request.newBuilder()
.header(CommonHeaders.PROXY_AUTHORIZATION, credential)
.build()
}
public override fun getPasswordAuthentication(): PasswordAuthentication? {
if (!isProxyEnabled()) {
return null
}
val login = settings.proxyLogin ?: return null
val password = settings.proxyPassword ?: return null
return PasswordAuthentication(login, password.toCharArray())
}
}
}

View File

@@ -23,6 +23,8 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.model.getTitle
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.nav.ReaderIntent
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -36,8 +38,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity
import javax.inject.Inject
import javax.inject.Singleton
@@ -133,7 +133,7 @@ class AppShortcutManager @Inject constructor(
}
}
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat {
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat = withContext(Dispatchers.Default) {
val icon = runCatchingCancellable {
coil.execute(
ImageRequest.Builder(context)
@@ -149,17 +149,17 @@ class AppShortcutManager @Inject constructor(
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
)
mangaRepository.storeManga(manga)
return ShortcutInfoCompat.Builder(context, manga.id.toString())
ShortcutInfoCompat.Builder(context, manga.id.toString())
.setShortLabel(manga.title)
.setLongLabel(manga.title)
.setIcon(icon)
.setLongLived(true)
.setIntent(
ReaderActivity.IntentBuilder(context)
ReaderIntent.Builder(context)
.mangaId(manga.id)
.build(),
)
.build()
.build()
.intent,
).build()
}
private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat = withContext(Dispatchers.Default) {
@@ -181,7 +181,7 @@ class AppShortcutManager @Inject constructor(
.setLongLabel(title)
.setIcon(icon)
.setLongLived(true)
.setIntent(MangaListActivity.newIntent(context, source, null))
.setIntent(AppRouter.listIntent(context, source, null, null))
.build()
}
}

View File

@@ -1,9 +1,12 @@
package org.koitharu.kotatsu.core.os
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.pm.PackageInfoCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import javax.inject.Inject
import javax.inject.Singleton
@@ -11,8 +14,8 @@ import javax.inject.Singleton
class AppValidator @Inject constructor(
@ApplicationContext private val context: Context,
) {
@Suppress("NewApi")
val isOriginalApp by lazy {
@SuppressLint("InlinedApi")
val isOriginalApp = suspendLazy(Dispatchers.Default) {
val certificates = mapOf(CERT_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256)
PackageInfoCompat.hasSignatures(context.packageManager, context.packageName, certificates, false)
}

View File

@@ -69,7 +69,7 @@ class NetworkState(
return true
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activeNetwork?.let { isOnline(it) } ?: false
activeNetwork?.let { isOnline(it) } == true
} else {
@Suppress("DEPRECATION")
activeNetworkInfo?.isConnected == true

View File

@@ -0,0 +1,92 @@
package org.koitharu.kotatsu.core.os
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityOptionsCompat
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
// https://stackoverflow.com/questions/77555641/saf-no-activity-found-to-handle-intent-android-intent-action-open-document-tr
class OpenDocumentTreeHelper(
activityResultCaller: ActivityResultCaller,
flags: Int,
callback: ActivityResultCallback<Uri?>
) : ActivityResultLauncher<Uri?>() {
constructor(activityResultCaller: ActivityResultCaller, callback: ActivityResultCallback<Uri?>) : this(
activityResultCaller,
0,
callback,
)
private val pickFileTreeLauncherQ = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activityResultCaller.registerForActivityResult(OpenDocumentTreeContractQ(flags), callback)
} else {
null
}
private val pickFileTreeLauncherLegacy = activityResultCaller.registerForActivityResult(
contract = OpenDocumentTreeContractLegacy(flags),
callback = callback,
)
override fun launch(input: Uri?, options: ActivityOptionsCompat?) {
if (pickFileTreeLauncherQ == null) {
pickFileTreeLauncherLegacy.launch(input, options)
return
}
try {
pickFileTreeLauncherQ.launch(input, options)
} catch (e: Exception) {
e.printStackTraceDebug()
pickFileTreeLauncherLegacy.launch(input, options)
}
}
override fun unregister() {
pickFileTreeLauncherQ?.unregister()
pickFileTreeLauncherLegacy.unregister()
}
override val contract: ActivityResultContract<Uri?, *>
get() = pickFileTreeLauncherQ?.contract ?: pickFileTreeLauncherLegacy.contract
private open class OpenDocumentTreeContractLegacy(
private val flags: Int,
) : ActivityResultContracts.OpenDocumentTree() {
override fun createIntent(context: Context, input: Uri?): Intent {
val intent = super.createIntent(context, input)
intent.addFlags(flags)
return intent
}
}
@RequiresApi(Build.VERSION_CODES.Q)
private class OpenDocumentTreeContractQ(
private val flags: Int,
) : OpenDocumentTreeContractLegacy(flags) {
override fun createIntent(context: Context, input: Uri?): Intent {
val intent = (context.getSystemService(Context.STORAGE_SERVICE) as? StorageManager)
?.primaryStorageVolume
?.createOpenDocumentTreeIntent()
if (intent == null) { // fallback
return super.createIntent(context, input)
}
intent.addFlags(flags)
if (input != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, input)
}
return intent
}
}
}

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.os
import kotlinx.coroutines.Dispatchers
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
import java.io.InputStreamReader
object RomCompat {
val isMiui = suspendLazy<Boolean>(Dispatchers.IO) {
getProp("ro.miui.ui.version.name").isNotEmpty()
}
@Blocking
private fun getProp(propName: String) = Runtime.getRuntime().exec("getprop $propName").inputStream.use {
it.reader().use(InputStreamReader::readText).trim()
}
}

View File

@@ -2,22 +2,22 @@ package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.core.AbstractMangaParser
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaListFilterCapabilities
import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQuery
import org.koitharu.kotatsu.parsers.model.search.MangaSearchQueryCapabilities
import java.util.EnumSet
/**
* This parser is just for parser development, it should not be used in releases
*/
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaParserSource.DUMMY) {
class DummyParser(context: MangaLoaderContext) : AbstractMangaParser(context, MangaParserSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("localhost")
@@ -25,14 +25,14 @@ class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaParse
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java)
override val filterCapabilities: MangaListFilterCapabilities
get() = MangaListFilterCapabilities()
override suspend fun getFilterOptions(): MangaListFilterOptions = stub(null)
override val searchQueryCapabilities: MangaSearchQueryCapabilities
get() = MangaSearchQueryCapabilities()
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
override suspend fun getList(offset: Int, order: SortOrder, filter: MangaListFilter): List<Manga> = stub(null)
override suspend fun getFilterOptions(): MangaListFilterOptions = stub(null)
override suspend fun getList(query: MangaSearchQuery): List<Manga> = stub(null)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)

View File

@@ -14,6 +14,8 @@ import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.model.LocalMangaSource
import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.nav.MangaIntent
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
import org.koitharu.kotatsu.parsers.model.Manga
@@ -27,6 +29,7 @@ import javax.inject.Provider
class MangaDataRepository @Inject constructor(
private val db: MangaDatabase,
private val resolverProvider: Provider<MangaLinkResolver>,
private val appShortcutManagerProvider: Provider<AppShortcutManager>,
) {
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
@@ -45,8 +48,8 @@ class MangaDataRepository @Inject constructor(
entity.copy(
cfBrightness = colorFilter?.brightness ?: 0f,
cfContrast = colorFilter?.contrast ?: 0f,
cfInvert = colorFilter?.isInverted ?: false,
cfGrayscale = colorFilter?.isGrayscale ?: false,
cfInvert = colorFilter?.isInverted == true,
cfGrayscale = colorFilter?.isGrayscale == true,
),
)
}
@@ -70,8 +73,13 @@ class MangaDataRepository @Inject constructor(
.distinctUntilChanged()
}
suspend fun findMangaById(mangaId: Long): Manga? {
return db.getMangaDao().find(mangaId)?.toManga()
suspend fun findMangaById(mangaId: Long, withChapters: Boolean): Manga? {
val chapters = if (withChapters) {
db.getChaptersDao().findAll(mangaId).takeUnless { it.isEmpty() }
} else {
null
}
return db.getMangaDao().find(mangaId)?.toManga(chapters)
}
suspend fun findMangaByPublicUrl(publicUrl: String): Manga? {
@@ -80,7 +88,7 @@ class MangaDataRepository @Inject constructor(
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != 0L -> findMangaById(intent.mangaId)
intent.mangaId != 0L -> findMangaById(intent.mangaId, true)
intent.uri != null -> resolverProvider.get().resolve(intent.uri)
else -> null
}
@@ -97,10 +105,26 @@ class MangaDataRepository @Inject constructor(
val tags = manga.tags.toEntities()
db.getTagsDao().upsert(tags)
db.getMangaDao().upsert(manga.toEntity(), tags)
if (!manga.isLocal) {
manga.chapters?.let { chapters ->
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
}
}
}
}
}
suspend fun updateChapters(manga: Manga) {
val chapters = manga.chapters
if (!chapters.isNullOrEmpty() && manga.id in db.getMangaDao()) {
db.getChaptersDao().replaceAll(manga.id, chapters.withIndex().toEntities(manga.id))
}
}
suspend fun gcChaptersCache() {
db.getChaptersDao().gc()
}
suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.getTagsDao().findTags(source.name).toMangaTags()
}
@@ -114,6 +138,14 @@ class MangaDataRepository @Inject constructor(
}
}
suspend fun cleanupDatabase() {
db.withTransaction {
gcChaptersCache()
val idsFromShortcuts = appShortcutManagerProvider.get().getMangaShortcuts()
db.getMangaDao().cleanup(idsFromShortcuts)
}
}
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {
return if (cfBrightness != 0f || cfContrast != 0f || cfInvert || cfGrayscale) {
ReaderColorFilter(cfBrightness, cfContrast, cfInvert, cfGrayscale)

View File

@@ -6,6 +6,7 @@ import dagger.Reusable
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.UnknownMangaSource
import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.util.ext.isHttpUrl
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
@@ -109,4 +110,11 @@ class MangaLinkResolver @Inject constructor(
chapters = null,
source = source,
)
companion object {
fun isValidLink(str: String): Boolean {
return str.isHttpUrl() || str.startsWith("kotatsu://", ignoreCase = true)
}
}
}

View File

@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.core.util.ext.toMimeType
import org.koitharu.kotatsu.core.util.ext.use
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
@@ -78,13 +79,14 @@ class MangaLoaderContextImpl @Inject constructor(
override fun redrawImageResponse(response: Response, redraw: (image: Bitmap) -> Bitmap): Response {
return response.map { body ->
BitmapDecoderCompat.decode(body.byteStream(), body.contentType(), isMutable = true).use { bitmap ->
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
Buffer().also {
result.compressTo(it.outputStream())
}.asResponseBody("image/jpeg".toMediaType())
BitmapDecoderCompat.decode(body.byteStream(), body.contentType()?.toMimeType(), isMutable = true)
.use { bitmap ->
(redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper).use { result ->
Buffer().also {
result.compressTo(it.outputStream())
}.asResponseBody("image/jpeg".toMediaType())
}
}
}
}
}

View File

@@ -18,7 +18,6 @@ import org.koitharu.kotatsu.parsers.model.MangaListFilterOptions
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.suspendlazy.suspendLazy
@@ -58,13 +57,7 @@ class ParserMangaRepository(
val domains: Array<out String>
get() = parser.configKeyDomain.presetValues
override fun intercept(chain: Interceptor.Chain): Response {
return if (parser is Interceptor) {
parser.intercept(chain)
} else {
chain.proceed(chain.request())
}
}
override fun intercept(chain: Interceptor.Chain): Response = parser.intercept(chain)
override suspend fun getList(offset: Int, order: SortOrder?, filter: MangaListFilter?): List<Manga> {
return mirrorSwitchInterceptor.withMirrorSwitching {
@@ -96,7 +89,7 @@ class ParserMangaRepository(
parser.getDetails(manga)
}
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
fun getAuthProvider(): MangaParserAuthProvider? = parser.authorizationProvider
fun getRequestHeaders() = parser.getRequestHeaders()

View File

@@ -42,7 +42,7 @@ class ExternalMangaRepository(
override var defaultSortOrder: SortOrder
get() = capabilities?.availableSortOrders?.firstOrNull() ?: SortOrder.ALPHABETICAL
set(_) = Unit
set(value) = Unit
override suspend fun getFilterOptions(): MangaListFilterOptions = filterOptions.get()

View File

@@ -229,7 +229,7 @@ class ExternalPluginContentSource(
do {
result += MangaChapter(
id = cursor.getLong(COLUMN_ID),
name = cursor.getString(COLUMN_NAME),
title = cursor.getStringOrNull(COLUMN_NAME),
number = cursor.getFloatOrDefault(COLUMN_NUMBER, 0f),
volume = cursor.getIntOrDefault(COLUMN_VOLUME, 0),
url = cursor.getString(COLUMN_URL),
@@ -252,7 +252,7 @@ class ExternalPluginContentSource(
publicUrl = getString(COLUMN_PUBLIC_URL),
rating = getFloat(COLUMN_RATING),
isNsfw = getBooleanOrDefault(COLUMN_IS_NSFW, false),
coverUrl = getString(COLUMN_COVER_URL),
coverUrl = getStringOrNull(COLUMN_COVER_URL),
tags = getStringOrNull(COLUMN_TAGS)?.split(':')?.mapNotNullToSet {
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
MangaTag(key = parts.first, title = parts.second, source = source)

View File

@@ -20,7 +20,7 @@ class ExternalPluginCursor(private val source: ExternalMangaSource, cursor: Curs
return when {
columnIndex < 0 -> null
isNull(columnIndex) -> null
else -> getString(columnIndex)
else -> getString(columnIndex).takeUnless { it == "null" }
}
}

View File

@@ -15,12 +15,13 @@ import androidx.core.os.LocaleListCompat
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import org.json.JSONArray
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.network.DoHProvider
import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.putAll
import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
@@ -44,6 +45,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val connectivityManager = context.connectivityManager
private val mangaListBadgesDefault = ArraySet(context.resources.getStringArray(R.array.values_list_badges))
var listMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
@@ -103,6 +105,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_LIST_MODE_FAVORITES, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_FAVORITES, value) }
val isTagsWarningsEnabled: Boolean
get() = prefs.getBoolean(KEY_TAGS_WARNINGS, true)
var isNsfwContentDisabled: Boolean
get() = prefs.getBoolean(KEY_DISABLE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_DISABLE_NSFW, value) }
@@ -141,6 +146,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isReaderOptimizationEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_OPTIMIZE, false)
val readerControls: Set<ReaderControl>
get() = prefs.getStringSet(KEY_READER_CONTROLS, null)?.mapNotNullTo(EnumSet.noneOf(ReaderControl::class.java)) {
ReaderControl.entries.find(it)
} ?: ReaderControl.DEFAULT
val isOfflineCheckDisabled: Boolean
get() = prefs.getBoolean(KEY_OFFLINE_DISABLED, false)
@@ -299,6 +309,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getInt(KEY_SOURCES_VERSION, 0)
set(value) = prefs.edit { putInt(KEY_SOURCES_VERSION, value) }
var isAllSourcesEnabled: Boolean
get() = prefs.getBoolean(KEY_SOURCES_ENABLED_ALL, false)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_ENABLED_ALL, value) }
val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
@@ -348,6 +362,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isSuggestionsExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
val isSuggestionsIncludeDisabledSources: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_DISABLED_SOURCES, false)
val isSuggestionsNotificationAvailable: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, false)
@@ -363,8 +380,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isReaderBarEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_BAR, true)
val isReaderSliderEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
val isReaderBarTransparent: Boolean
get() = prefs.getBoolean(KEY_READER_BAR_TRANSPARENT, true)
val isReaderKeepScreenOn: Boolean
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
@@ -489,6 +506,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
val isBackupTelegramUploadEnabled: Boolean
get() = prefs.getBoolean(KEY_BACKUP_TG_ENABLED, false)
val backupTelegramChatId: String?
get() = prefs.getString(KEY_BACKUP_TG_CHAT, null)?.nullIfEmpty()
val isReadingTimeEstimationEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_TIME, true)
@@ -531,6 +554,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
prefs.edit { putString(KEY_PAGES_SAVE_DIR, uri?.toString()) }
}
fun getMangaListBadges(): Int {
val raw = prefs.getStringSet(KEY_MANGA_LIST_BADGES, mangaListBadgesDefault).orEmpty()
var result = 0
for (item in raw) {
result = result or item.toInt()
}
return result
}
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}
@@ -543,19 +575,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
fun getAllValues(): Map<String, *> = prefs.all
fun upsertAll(m: Map<String, *>) {
prefs.edit {
m.forEach { e ->
when (val v = e.value) {
is Boolean -> putBoolean(e.key, v)
is Int -> putInt(e.key, v)
is Long -> putLong(e.key, v)
is Float -> putFloat(e.key, v)
is String -> putString(e.key, v)
is JSONArray -> putStringSet(e.key, v.toStringSet())
}
}
}
fun upsertAll(m: Map<String, *>) = prefs.edit {
clear()
putAll(m)
}
private fun isBackgroundNetworkRestricted(): Boolean {
@@ -566,15 +588,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
private fun JSONArray.toStringSet(): Set<String> {
val len = length()
val result = ArraySet<String>(len)
for (i in 0 until len) {
result.add(getString(i))
}
return result
}
companion object {
const val TRACK_HISTORY = "history"
@@ -621,6 +634,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info"
const val KEY_READER_ANIMATION = "reader_animation2"
const val KEY_READER_CONTROLS = "reader_controls"
const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_READER_CROP = "reader_crop"
@@ -650,6 +664,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SUGGESTIONS_WIFI_ONLY = "suggestions_wifi"
const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags"
const val KEY_SUGGESTIONS_DISABLED_SOURCES = "suggestions_disabled_sources"
const val KEY_SUGGESTIONS_NOTIFICATIONS = "suggestions_notifications"
const val KEY_SHIKIMORI = "shikimori"
const val KEY_ANILIST = "anilist"
@@ -664,7 +679,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SYNC = "sync"
const val KEY_SYNC_SETTINGS = "sync_settings"
const val KEY_READER_BAR = "reader_bar"
const val KEY_READER_SLIDER = "reader_slider"
const val KEY_READER_BAR_TRANSPARENT = "reader_bar_transparent"
const val KEY_READER_BACKGROUND = "reader_background"
const val KEY_READER_SCREEN_ON = "reader_screen_on"
const val KEY_SHORTCUTS = "dynamic_shortcuts"
@@ -715,13 +730,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_FEED_HEADER = "feed_header"
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
const val KEY_SOURCES_VERSION = "sources_version"
const val KEY_SOURCES_ENABLED_ALL = "sources_enabled_all"
const val KEY_QUICK_FILTER = "quick_filter"
const val KEY_BACKUP_TG_ENABLED = "backup_periodic_tg_enabled"
const val KEY_BACKUP_TG_CHAT = "backup_periodic_tg_chat_id"
const val KEY_MANGA_LIST_BADGES = "manga_list_badges"
const val KEY_TAGS_WARNINGS = "tags_warnings"
// keys for non-persistent preferences
const val KEY_APP_VERSION = "app_version"
const val KEY_IGNORE_DOZE = "ignore_dose"
const val KEY_TRACKER_DEBUG = "tracker_debug"
const val KEY_APP_UPDATE = "app_update"
const val KEY_LINK_WEBLATE = "about_app_translation"
const val KEY_LINK_TELEGRAM = "about_telegram"
const val KEY_LINK_GITHUB = "about_github"
@@ -729,6 +748,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_TEST = "proxy_test"
const val KEY_OPEN_BROWSER = "open_browser"
const val KEY_HANDLE_LINKS = "handle_links"
const val KEY_BACKUP_TG_OPEN = "backup_periodic_tg_open"
const val KEY_BACKUP_TG_TEST = "backup_periodic_tg_test"
const val KEY_CLEAR_MANGA_DATA = "manga_data_clear"
const val KEY_STORAGE_USAGE = "storage_usage"
const val KEY_WEBVIEW_CLEAR = "webview_clear"
// old keys are for migration only
private const val KEY_IMAGES_PROXY_OLD = "images_proxy"

View File

@@ -1,11 +1,13 @@
package org.koitharu.kotatsu.core.prefs
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.ContextThemeWrapper
import androidx.annotation.Keep
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import org.koitharu.kotatsu.core.util.ext.getThemeDrawable
import org.koitharu.kotatsu.core.util.ext.isNightMode
import com.google.android.material.R as materialR
@Keep
@@ -13,7 +15,7 @@ enum class ReaderBackground {
DEFAULT, LIGHT, DARK, WHITE, BLACK;
fun resolve(context: Context) = when (this) {
fun resolve(context: Context): Drawable? = when (this) {
DEFAULT -> context.getThemeDrawable(android.R.attr.windowBackground)
LIGHT -> ContextThemeWrapper(context, materialR.style.ThemeOverlay_Material3_Light)
.getThemeDrawable(android.R.attr.windowBackground)
@@ -24,4 +26,14 @@ enum class ReaderBackground {
WHITE -> ContextCompat.getColor(context, android.R.color.white).toDrawable()
BLACK -> ContextCompat.getColor(context, android.R.color.black).toDrawable()
}
fun isLight(context: Context): Boolean = when (this) {
DEFAULT -> !context.resources.isNightMode
LIGHT,
WHITE -> true
DARK,
BLACK -> false
}
}

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.core.prefs
import java.util.EnumSet
enum class ReaderControl {
PREV_CHAPTER, NEXT_CHAPTER, SLIDER, PAGES_SHEET, SCREEN_ROTATION, SAVE_PAGE;
companion object {
val DEFAULT: Set<ReaderControl> = EnumSet.of(
PREV_CHAPTER, NEXT_CHAPTER, SLIDER, PAGES_SHEET,
)
}
}

View File

@@ -25,6 +25,9 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
val isSlowdownEnabled: Boolean
get() = prefs.getBoolean(KEY_SLOWDOWN, false)
val isCaptchaNotificationsDisabled: Boolean
get() = prefs.getBoolean(KEY_NO_CAPTCHA, false)
@Suppress("UNCHECKED_CAST")
override fun <T> get(key: ConfigKey<T>): T {
return when (key) {
@@ -65,5 +68,6 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
const val KEY_SORT_ORDER = "sort_order"
const val KEY_SLOWDOWN = "slowdown"
const val KEY_NO_CAPTCHA = "no_captcha"
}
}

View File

@@ -1,18 +1,22 @@
package org.koitharu.kotatsu.core.ui
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.view.WindowCompat
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.fragment.app.FragmentManager
import androidx.viewbinding.ViewBinding
import dagger.hilt.android.EntryPointAccessors
@@ -21,16 +25,17 @@ import kotlinx.coroutines.flow.flowOf
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.nav.AppRouter
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
import org.koitharu.kotatsu.main.ui.protect.ScreenshotPolicyHelper
import androidx.appcompat.R as appcompatR
abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(),
ExceptionResolver.Host,
ScreenshotPolicyHelper.ContentContainer,
WindowInsetsDelegate.WindowInsetsListener {
OnApplyWindowInsetsListener,
ScreenshotPolicyHelper.ContentContainer {
private var isAmoledTheme = false
@@ -40,16 +45,20 @@ abstract class BaseActivity<B : ViewBinding> :
protected lateinit var exceptionResolver: ExceptionResolver
private set
@JvmField
protected val insetsDelegate = WindowInsetsDelegate()
@JvmField
val actionModeDelegate = ActionModeDelegate()
private var defaultStatusBarColor = Color.TRANSPARENT
private lateinit var entryPoint: BaseActivityEntryPoint
override fun attachBaseContext(newBase: Context) {
entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(newBase.applicationContext)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
AppCompatDelegate.setApplicationLocales(entryPoint.settings.appLocales)
}
super.attachBaseContext(newBase)
}
override fun onCreate(savedInstanceState: Bundle?) {
val entryPoint = EntryPointAccessors.fromApplication<BaseActivityEntryPoint>(this)
val settings = entryPoint.settings
isAmoledTheme = settings.isAmoledTheme
setTheme(settings.colorScheme.styleResId)
@@ -58,10 +67,8 @@ abstract class BaseActivity<B : ViewBinding> :
}
putDataToExtras(intent)
exceptionResolver = entryPoint.exceptionResolverFactory.create(this)
enableEdgeToEdge()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
insetsDelegate.handleImeInsets = true
insetsDelegate.addInsetsListener(this)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -75,16 +82,10 @@ abstract class BaseActivity<B : ViewBinding> :
}
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(layoutResID: Int) {
super.setContentView(layoutResID)
setupToolbar()
}
override fun setContentView(layoutResID: Int) = throw UnsupportedOperationException()
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(view: View?) {
super.setContentView(view)
setupToolbar()
}
override fun setContentView(view: View?) = throw UnsupportedOperationException()
override fun getContext() = this
@@ -93,9 +94,18 @@ abstract class BaseActivity<B : ViewBinding> :
protected fun setContentView(binding: B) {
this.viewBinding = binding
super.setContentView(binding.root)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, this)
val toolbar = (binding.root.findViewById<View>(R.id.toolbar) as? Toolbar)
toolbar?.let(this::setSupportActionBar)
insetsDelegate.onViewCreated(binding.root)
}
protected fun setDisplayHomeAsUp(isEnabled: Boolean, showUpAsClose: Boolean) {
supportActionBar?.run {
setDisplayHomeAsUpEnabled(isEnabled)
if (showUpAsClose) {
setHomeAsUpIndicator(appcompatR.drawable.abc_ic_clear_material)
}
}
}
override fun onSupportNavigateUp(): Boolean {
@@ -123,10 +133,6 @@ abstract class BaseActivity<B : ViewBinding> :
return super.onKeyDown(keyCode, event)
}
private fun setupToolbar() {
(findViewById<View>(R.id.toolbar) as? Toolbar)?.let(this::setSupportActionBar)
}
protected fun isDarkAmoledTheme(): Boolean {
val uiMode = resources.configuration.uiMode
val isNight = uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
@@ -159,7 +165,7 @@ abstract class BaseActivity<B : ViewBinding> :
override fun isNsfwContent(): Flow<Boolean> = flowOf(false)
private fun putDataToExtras(intent: Intent?) {
intent?.putExtra(EXTRA_DATA, intent.data)
intent?.putExtra(AppRouter.KEY_DATA, intent.data)
}
protected fun setContentViewWebViewSafe(viewBindingProducer: () -> B): Boolean {
@@ -178,9 +184,4 @@ abstract class BaseActivity<B : ViewBinding> :
}
protected fun hasViewBinding() = ::viewBinding.isInitialized
companion object {
const val EXTRA_DATA = "data"
}
}

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