Compare commits

..

263 Commits
v5.2.2 ... v6.0

Author SHA1 Message Date
Koitharu
61e9796269 Fix amoled theme 2023-08-24 14:43:49 +03:00
Koitharu
54597eb8f0 Udpate parsers 2023-08-24 14:43:49 +03:00
Dpper
e07ea0552f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Koitharu
b8e90719ce Translated using Weblate (Russian)
Currently translated at 100.0% (476 of 476 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Reza Almanda
2ec716973c Translated using Weblate (Indonesian)
Currently translated at 100.0% (476 of 476 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (475 of 475 strings)

Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
J. Lavoie
0000c97e6a Translated using Weblate (French)
Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Nayuki
70684de683 Translated using Weblate (Thai)
Currently translated at 42.9% (203 of 473 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
gallegonovato
5a2288eb2d Translated using Weblate (Spanish)
Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Макар Разин
6a023fa976 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (473 of 473 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/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-24 13:50:13 +03:00
Zakhar Timoshenko
3cc0fbe7bc Update UI 2023-08-23 20:42:40 +03:00
Koitharu
b3b022807a Fix sources reordering 2023-08-23 17:07:14 +03:00
Koitharu
ba0ea5a9fc UI fixes 2023-08-22 16:58:31 +03:00
Koitharu
ca1380e2b1 Refactor application class 2023-08-22 16:18:34 +03:00
Koitharu
05dbd11fc1 Restore covers only once 2023-08-22 13:38:20 +03:00
Koitharu
aa650d44d3 Revert "Limit TrackWorker parallelistm when device in use"
This reverts commit 0778f34db7.
2023-08-22 13:28:38 +03:00
Koitharu
99b698ad12 Improve theme chooser 2023-08-22 13:13:01 +03:00
Koitharu
2f9c2d9ab6 Option to swap first two navigation items 2023-08-22 12:11:09 +03:00
Koitharu
ab753787b0 Fix page loading retry 2023-08-21 17:33:43 +03:00
Koitharu
478ca351eb Merge pull request #463 from thonsi/devel 2023-08-21 10:49:12 +03:00
Thonsi
559b2cfd64 update gradles to latest 2023-08-20 20:55:49 +05:30
Koitharu
a1554f81ff Fix cover restoration 2023-08-19 18:02:22 +03:00
Koitharu
4ce7f74b9d Update parsers and gradle 2023-08-19 17:44:16 +03:00
Koitharu
0778f34db7 Limit TrackWorker parallelistm when device in use 2023-08-19 15:59:58 +03:00
Koitharu
bc488a6878 Improve search suggestions 2023-08-19 15:11:22 +03:00
Koitharu
856524c9f8 Revert "Remove WorkManager bug workaround"
This reverts commit af654b8c40.
2023-08-19 13:30:30 +03:00
ppg00
c75160d83c Translated using Weblate (Arabic)
Currently translated at 16.9% (80 of 473 strings)

Translated using Weblate (Arabic)

Currently translated at 28.5% (2 of 7 strings)

Co-authored-by: ppg00 <vx2dsk@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-18 16:59:59 +03:00
Koitharu
d9c826524f Translated using Weblate (Russian)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
Christian Elbrianno
40196205eb Translated using Weblate (Indonesian)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Christian Elbrianno <crse@protonmail.ch>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/id/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-18 16:59:59 +03:00
Dpper
681ac492d6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (473 of 473 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
Nayuki
95f4606661 Translated using Weblate (Thai)
Currently translated at 26.6% (126 of 472 strings)

Translated using Weblate (Thai)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/th/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-18 16:59:59 +03:00
gallegonovato
b406834d4a Translated using Weblate (Spanish)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (472 of 472 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
InfinityDouki56
2693ec5335 Translated using Weblate (Filipino)
Currently translated at 90.6% (427 of 471 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-08-18 16:59:59 +03:00
Koitharu
ead8a3d6df Restore broken covers and bookmark previews 2023-08-18 16:52:14 +03:00
Koitharu
df04dcc8a3 Show error on ImageActivity 2023-08-18 15:40:56 +03:00
Koitharu
c103773c19 Fix handling favicon loading errors 2023-08-18 15:19:08 +03:00
Koitharu
0a28f131ee Update strings 2023-08-18 15:12:49 +03:00
Koitharu
c8e4842b6e Unify list spacings 2023-08-18 14:33:30 +03:00
Koitharu
d54d489494 Fixes 2023-08-18 12:59:45 +03:00
Koitharu
c77cb4cb3c Fix sources migration 2023-08-18 10:13:56 +03:00
Koitharu
d951306a90 Fix MangaListActivity header scrim 2023-08-18 10:12:12 +03:00
Koitharu
c871757893 Fix colorBackground attribute 2023-08-18 09:43:26 +03:00
Zakhar Timoshenko
64bf671e8b Merge remote-tracking branch 'origin/devel' into devel 2023-08-18 07:50:00 +03:00
Zakhar Timoshenko
b00bc22ead Update UI 2023-08-18 07:49:29 +03:00
Koitharu
fdea2b47da Fix sources migration 2023-08-17 17:10:41 +03:00
Koitharu
91266183c2 Hide categories notification indicators if tracking disabled 2023-08-17 17:00:48 +03:00
Koitharu
20a7e5a6a8 Handle offline mode in history list 2023-08-15 17:02:39 +03:00
Koitharu
6fa99791b6 Handle manga links 2023-08-15 12:46:46 +03:00
Koitharu
4090c8ad6a Fix crash on bookmarks 2023-08-15 10:14:11 +03:00
Koitharu
c72ed7cc34 Update dependencies and Gradle 2023-08-15 10:04:27 +03:00
Koitharu
925c24471e Upgrade targetSdk to 34 2023-08-14 15:10:08 +03:00
Koitharu
7e31b1384e Option to disable related manga 2023-08-14 14:18:09 +03:00
Koitharu
e1efb5fb1e Fix tests 2023-08-14 13:44:55 +03:00
Koitharu
29b5655efb Update screenshots 2023-08-14 13:15:56 +03:00
Koitharu
68bdd22634 Remove deprecated preferences 2023-08-14 10:19:25 +03:00
Koitharu
2e7867f60c Merge branch 'devel' 2023-08-14 10:09:56 +03:00
Koitharu
e4c2972cae Update parsers 2023-08-14 09:59:00 +03:00
Koitharu
af654b8c40 Remove WorkManager bug workaround 2023-08-14 09:44:56 +03:00
Koitharu
9eace89d4f Update error messages 2023-08-14 09:44:02 +03:00
Zakhar Timoshenko
7dbec8eb8f Update bottom nav item animations 2023-08-13 16:09:43 +03:00
Zakhar Timoshenko
111f816f18 Update themes 2023-08-13 15:59:05 +03:00
Koitharu
d4589716aa Improve tablet layout 2023-08-12 18:20:15 +03:00
Koitharu
1c4bd6da28 Retry tracker after errors 2023-08-12 13:55:13 +03:00
Koitharu
c83538f66d Merge pull request #450 from weblate/weblate-kotatsu-strings 2023-08-12 12:59:38 +03:00
gallegonovato
885cae7635 Translated using Weblate (Arabic)
Currently translated at 16.9% (80 of 471 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (471 of 471 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-08-12 06:39:19 +02:00
InfinityDouki56
c66d36abf9 Translated using Weblate (Filipino)
Currently translated at 91.0% (428 of 470 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-08-12 06:39:19 +02:00
Макар Разин
4bd9c6df81 Translated using Weblate (Chinese (Simplified))
Currently translated at 91.4% (430 of 470 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-08-12 06:39:18 +02:00
Nayuki
b835cb98b7 Translated using Weblate (Thai)
Currently translated at 25.5% (120 of 470 strings)

Translated using Weblate (Thai)

Currently translated at 20.8% (98 of 470 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2023-08-12 06:39:18 +02:00
Balog Ferenc József
7cb1f90155 Translated using Weblate (Hungarian)
Currently translated at 28.5% (2 of 7 strings)

Added translation using Weblate (Hungarian)

Co-authored-by: Balog Ferenc József <ferencb2412@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/hu/
Translation: Kotatsu/plurals
2023-08-12 06:39:17 +02:00
Koitharu
96bac81b84 Fix captcha detection 2023-08-11 15:13:25 +03:00
Koitharu
b59f933031 Fix strings 2023-08-11 14:51:56 +03:00
Koitharu
caebca36de Option to hide nsfw content 2023-08-11 14:50:56 +03:00
Koitharu
03cb458d92 Remove custom activity animation 2023-08-11 14:09:21 +03:00
Koitharu
0788f5f05e Adjust cells content to grid size 2023-08-11 13:58:26 +03:00
Koitharu
0271ed2ba9 Show updated manga on top of feed 2023-08-11 13:18:23 +03:00
Koitharu
788c7b862a Fallback to another favicon on error 2023-08-11 12:30:47 +03:00
Koitharu
f4f84099cc Update parsers 2023-08-11 12:23:08 +03:00
Koitharu
3bea94bf1f Handle 429 TooManyRequests error 2023-08-11 11:21:25 +03:00
Koitharu
746eed698f Refactor tracker 2023-08-11 10:36:58 +03:00
Koitharu
fa0289eb27 Show search results once received 2023-08-10 09:09:04 +03:00
Koitharu
c874d73c04 Fix warnings and enable locales-config auto-generating 2023-08-09 16:17:59 +03:00
Koitharu
edb91c46d4 Option to disable pages animation #406 2023-08-09 14:50:52 +03:00
Koitharu
4b9f4f9af2 GC manga updates 2023-08-09 13:17:13 +03:00
J. Lavoie
a07117087a Translated using Weblate (French)
Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Italian)

Currently translated at 78.9% (371 of 470 strings)

Translated using Weblate (German)

Currently translated at 99.3% (467 of 470 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
kuragehime
ce0ffca197 Translated using Weblate (Japanese)
Currently translated at 100.0% (470 of 470 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
TzurS11
eece4d8f00 Added translation using Weblate (Hebrew)
Co-authored-by: TzurS11 <tzurshafriri11@gmail.com>
2023-08-09 12:27:20 +03:00
Nayuki
419e2e578b Translated using Weblate (Thai)
Currently translated at 11.0% (52 of 470 strings)

Translated using Weblate (Thai)

Currently translated at 71.4% (5 of 7 strings)

Added translation using Weblate (Thai)

Co-authored-by: Nayuki <notkungz.suphakorn@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/th/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Cookies
0a7387c22e Translated using Weblate (Vietnamese)
Currently translated at 85.2% (399 of 468 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
InfinityDouki56
2a23c3b3b3 Translated using Weblate (Filipino)
Currently translated at 91.0% (428 of 470 strings)

Translated using Weblate (Filipino)

Currently translated at 91.4% (428 of 468 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
Masowick
0566aa4e6a Translated using Weblate (German)
Currently translated at 98.9% (465 of 470 strings)

Translated using Weblate (German)

Currently translated at 93.8% (441 of 470 strings)

Translated using Weblate (German)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: Masowick <Demian@gmx.co.uk>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Макар Разин
3f27acf1aa Translated using Weblate (Ukrainian)
Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (468 of 468 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/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
JA Sunny
d48c3fbe1b Translated using Weblate (Bengali)
Currently translated at 18.4% (86 of 467 strings)

Translated using Weblate (Bengali)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: JA Sunny <ayasbinshams2003@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
2023-08-09 12:27:20 +03:00
kuragehime
6720474667 Translated using Weblate (Japanese)
Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (467 of 467 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Eric
ad42ca5085 Translated using Weblate (Chinese (Simplified))
Currently translated at 92.0% (429 of 466 strings)

Co-authored-by: Eric <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
gallegonovato
d5e40d79ec Translated using Weblate (Spanish)
Currently translated at 100.0% (470 of 470 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (7 of 7 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
Макар Разин
7853b9a73e Translated using Weblate (Belarusian)
Currently translated at 100.0% (468 of 468 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (467 of 467 strings)

Translated using Weblate (Korean)

Currently translated at 76.3% (356 of 466 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (466 of 466 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (466 of 466 strings)

Translated using Weblate (Japanese)

Currently translated at 96.3% (449 of 466 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (466 of 466 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-08-09 12:27:20 +03:00
return_null
cb71a24d81 Translated using Weblate (Chinese (Simplified))
Currently translated at 95.5% (427 of 447 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-08-09 12:27:20 +03:00
Koitharu
a5d99db105 Use nio for File.listFiles() #449 2023-08-09 12:22:52 +03:00
Koitharu
6cc13784e4 Remove invalid track logs 2023-08-07 17:59:26 +03:00
Koitharu
cf9aab9afe Update parsers 2023-08-06 09:22:47 +03:00
Koitharu
83a919f9f6 Add contribution guidelines 2023-08-06 09:12:51 +03:00
Koitharu
70bf80ad8f Update parsers 2023-08-06 09:11:30 +03:00
Koitharu
05a724eae0 Add contribution guidelines 2023-08-04 17:08:50 +03:00
Koitharu
496f3637c4 Update gradle and kotlin 2023-08-04 16:32:08 +03:00
Koitharu
c5c907c8dc UI improvements 2023-08-02 16:45:01 +03:00
Koitharu
af4845a770 Revert "Use ConnectivityManagerCompat.getRestrictBackgroundStatus()"
This reverts commit bfad632b8c.
2023-08-02 14:55:22 +03:00
Koitharu
511f9af991 Limit pages loading parallelism 2023-08-02 14:55:15 +03:00
Koitharu
baf6f6d2c9 Re-use ZipFile instances in PageLoader 2023-08-02 14:35:55 +03:00
Koitharu
8b6a0a8c87 Improve downloads list 2023-08-02 12:42:57 +03:00
Koitharu
e7ee261680 Grouping history by progress 2023-08-02 12:08:28 +03:00
Koitharu
2949fdd2c6 Recover history if chapterId id changed 2023-08-02 11:03:38 +03:00
Koitharu
829ea01b18 Migrate some classes to data classes 2023-08-02 10:26:18 +03:00
Koitharu
08b173b94a Update AppShortcutManager 2023-08-02 10:08:46 +03:00
Isira Seneviratne
7b090c4ccd Add default constructor values 2023-08-02 10:06:27 +03:00
Isira Seneviratne
bf1b8e8b75 Convert ListHeader to a data class 2023-08-02 10:06:27 +03:00
Isira Seneviratne
bfad632b8c Use ConnectivityManagerCompat.getRestrictBackgroundStatus() 2023-08-02 10:06:08 +03:00
Isira Seneviratne
73e6f730e1 Convert lazy value to Size 2023-07-31 15:35:39 +03:00
Isira Seneviratne
62d8b848b2 Add uses of ShortcutManagerCompat methods 2023-07-31 15:35:39 +03:00
Koitharu
2793f6ce52 Update sources manage screen 2023-07-31 12:09:24 +03:00
Koitharu
b107801188 Update bookmarks list screen 2023-07-31 11:28:46 +03:00
Koitharu
97de27dfb3 Recover manga after NotFoundException 2023-07-31 10:08:59 +03:00
Koitharu
cd4317dec5 Show captcha notfication for images 2023-07-30 17:44:24 +03:00
Koitharu
50554c6936 Fix tags suggestions 2023-07-28 16:42:01 +03:00
Koitharu
694297f49b Update bottom navigation 2023-07-28 16:15:34 +03:00
Koitharu
3e48ce85fd Show notification if captcha required in background 2023-07-28 16:09:46 +03:00
Koitharu
0f7bceb268 Fix new sources tip 2023-07-28 14:01:21 +03:00
Koitharu
00187c0d17 Fix description scrolling 2023-07-28 12:22:17 +03:00
Koitharu
2378d104c3 Option to open random manga from source 2023-07-28 12:15:03 +03:00
Koitharu
f105f4b496 Mark nsfw sources 2023-07-28 12:00:36 +03:00
Isira Seneviratne
01e27ba91f Add uses of NotificationManagerCompat and related classes 2023-07-28 10:51:13 +03:00
Koitharu
2342594885 Unify list spacing approach 2023-07-26 16:40:10 +03:00
Koitharu
61a7f1c830 Improve ui 2023-07-26 15:27:43 +03:00
Koitharu
01c23bc3b8 Manga preview in list on tablet 2023-07-26 12:11:42 +03:00
Koitharu
7c7106a63c History sorting #428 2023-07-25 15:36:50 +03:00
Koitharu
ac1a919476 Fix instrumented tests 2023-07-25 12:44:07 +03:00
Koitharu
234f74aa0d Fix database migrations 2023-07-25 12:28:50 +03:00
Koitharu
1711ebe616 Merge branch 'devel' into next 2023-07-25 10:11:44 +03:00
Koitharu
07af79a6bd Update parsers 2023-07-25 09:42:55 +03:00
Koitharu
e4942b0d93 Fix new manga sources enabling 2023-07-24 20:21:57 +03:00
Koitharu
5ca22f1419 Fix crash with empty EnumSet 2023-07-24 16:57:24 +03:00
Koitharu
345a878d83 Merge branch 'devel' into next 2023-07-24 16:38:51 +03:00
Koitharu
42bb5a65ab Fix crash in ScrobblingInfoSheet 2023-07-24 16:08:25 +03:00
Koitharu
0c37265a5b Update parsers 2023-07-24 16:03:08 +03:00
plum7x
7a65ae3ea7 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: plum7x <plumgift@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-07-24 15:56:11 +03:00
Koitharu
376cee1859 Store manga sources in database #426 2023-07-24 15:47:52 +03:00
InfinityDouki56
ee027cd64f Translated using Weblate (Filipino)
Currently translated at 91.0% (407 of 447 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-07-22 13:59:48 +03:00
Макар Разин
7b2bb5ea8f Translated using Weblate (Polish)
Currently translated at 100.0% (447 of 447 strings)

Translated using Weblate (Korean)

Currently translated at 79.6% (356 of 447 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2023-07-22 13:59:48 +03:00
plum7x
eff2d6bcb6 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: plum7x <plumgift@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-07-22 13:59:48 +03:00
Koitharu
03b92c4898 Improve wrokers parallelism 2023-07-21 15:46:28 +03:00
Koitharu
6dcb537a9a Update search ui 2023-07-21 15:26:26 +03:00
Koitharu
052cfe26b1 Show chapters count in suggestion notification 2023-07-21 10:32:10 +03:00
Koitharu
45b2f2337a Favourites manage activity 2023-07-21 10:23:02 +03:00
Koitharu
5785a2d5d1 Remove shelf 2023-07-20 16:11:03 +03:00
Koitharu
bc273bfb8f Storage usage preference 2023-07-20 15:50:32 +03:00
Koitharu
513aa1a285 Code cleanup 2023-07-20 14:09:07 +03:00
Koitharu
82a3b93214 Expandable manga description 2023-07-20 13:33:48 +03:00
Koitharu
80149b1ce7 Related manga activity 2023-07-20 10:56:15 +03:00
Koitharu
297029a659 Options to run background workers only using wifi 2023-07-20 09:29:26 +03:00
Koitharu
08acf2d882 Fix crashes 2023-07-19 15:18:30 +03:00
Koitharu
dafca9e1e1 Update suggestions 2023-07-19 15:10:11 +03:00
Koitharu
e174bc68af Merge branch 'devel' into next 2023-07-19 13:48:28 +03:00
Koitharu
1d78c64350 Move coroutines from UserDataSettingsFragment to ViewModel 2023-07-19 13:32:02 +03:00
Koitharu
321a9ecf62 Update parsers 2023-07-19 12:30:58 +03:00
Koitharu
83cf6aa997 Temporarily replace ViewPager2 within ViewPager in favourites 2023-07-18 16:22:02 +03:00
Koitharu
e7bd74429e Cache related manga lists 2023-07-18 15:43:28 +03:00
Koitharu
9ba87640c0 Bookmarks bottom sheet 2023-07-18 15:34:56 +03:00
Koitharu
fff77cf208 Backup and restore bookmarks 2023-07-18 14:27:14 +03:00
Koitharu
967e8df7c9 Fix backup restoring 2023-07-18 12:28:02 +03:00
Koitharu
f86d873361 Merge branch 'feature/backup-settings' of github.com:javlonrahimov/Kotatsu into javlonrahimov-feature/backup-settings 2023-07-18 12:05:23 +03:00
Koitharu
2d5332d8df Merge branch 'devel' into next 2023-07-18 12:04:41 +03:00
Koitharu
439a01c43f Fix bookmark has direct url detection #424 2023-07-18 11:43:31 +03:00
Koitharu
3a9d0def7d Update parsers 2023-07-18 10:13:46 +03:00
javlon
33a45ac5b3 remove clutter code 2023-07-17 14:49:28 +02:00
Koitharu
e4c80b4443 Remove rubbish file 2023-07-17 14:14:23 +03:00
Koitharu
940d448e00 Fix local manga update on shelf 2023-07-17 14:13:16 +03:00
Koitharu
5ab48a7545 Fix scrobbling rating 2023-07-17 13:30:50 +03:00
Koitharu
cb2bdbdd9a Update parsers 2023-07-17 13:08:39 +03:00
Cookies
8fdaf92cc4 Translated using Weblate (Vietnamese)
Currently translated at 89.2% (399 of 447 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-07-17 12:39:46 +03:00
Shubham Niraula
0416077964 Translated using Weblate (Nepali)
Currently translated at 51.9% (232 of 447 strings)

Co-authored-by: Shubham Niraula <niraulas018@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-07-17 12:39:46 +03:00
javlon
a8176e6589 add settings backup 2023-07-15 21:22:16 +02:00
Zakhar Timoshenko
a2437dd27a Adjust tip view 2023-07-15 15:56:07 +03:00
Koitharu
9e56766e9e New sources tip 2023-07-15 14:59:54 +03:00
Koitharu
eec750789d Reader background option 2023-07-14 14:33:55 +03:00
Koitharu
44a2b6db11 Update explore fragment 2023-07-13 16:08:09 +03:00
Koitharu
55ca2b8d8d Description bottom sheet 2023-07-13 15:08:39 +03:00
Koitharu
2d670418c7 Application update indicator 2023-07-13 15:08:19 +03:00
Koitharu
4c201bf950 Merge branch 'devel' into next 2023-07-13 13:20:53 +03:00
Koitharu
7b60ed6bad Fix new sources dialog list 2023-07-13 13:12:21 +03:00
Cookies
619be69580 Translated using Weblate (Vietnamese)
Currently translated at 89.2% (399 of 447 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-07-12 11:00:13 +03:00
Shubham Niraula
9f3c3f8985 Translated using Weblate (Nepali)
Currently translated at 51.2% (229 of 447 strings)

Co-authored-by: Shubham Niraula <niraulas018@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-07-12 11:00:13 +03:00
Vítor Fernandes Almado
f345977858 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: Vítor Fernandes Almado <vfalmado@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2023-07-12 11:00:13 +03:00
Koitharu
9610caf002 Downloads scheduler fixes 2023-07-12 10:58:02 +03:00
Koitharu
b75220a1b7 Fix cover loading in details 2023-07-11 11:45:57 +03:00
Koitharu
ab2a6f5a17 Fix loading state 2023-07-11 11:34:47 +03:00
Koitharu
2aeefc607b Udpate dependencies 2023-07-11 11:09:45 +03:00
Shubham Niraula
9af769bc69 Translated using Weblate (Nepali)
Currently translated at 50.3% (225 of 447 strings)

Co-authored-by: Shubham Niraula <niraulas018@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Pluto
46b78cfcd7 Translated using Weblate (Czech)
Currently translated at 100.0% (6 of 6 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (447 of 447 strings)

Added translation using Weblate (Czech)

Added translation using Weblate (Czech)

Co-authored-by: Pluto <notemailprotected@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2023-07-11 09:54:06 +03:00
Nguyễn Mạnh Hùng
c24324de9a Translated using Weblate (Vietnamese)
Currently translated at 81.8% (366 of 447 strings)

Co-authored-by: Nguyễn Mạnh Hùng <hungmn13@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Hosted Weblate
48b9c1236d 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
2023-07-11 09:54:06 +03:00
plum7x
c69d293caa Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (447 of 447 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.5% (436 of 447 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (435 of 447 strings)

Co-authored-by: plum7x <plumgift@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Clxff H3r4ld0
0f4cca0e07 Translated using Weblate (Indonesian)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Luiz-bro
d6500b8fec Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.1% (443 of 447 strings)

Co-authored-by: Luiz-bro <luiznneto1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2023-07-11 09:54:06 +03:00
Detrimental God
86140cab1e Added translation using Weblate (Malayalam)
Co-authored-by: Detrimental God <judeliger1@gmail.com>
2023-07-11 09:54:06 +03:00
Isira Seneviratne
46ab5af905 Apply suggestions. 2023-07-11 09:53:42 +03:00
Isira Seneviratne
9a815f28fa Add Parceler implementations for Manga classes. 2023-07-11 09:53:42 +03:00
Koitharu
394479192b Refactor adapters 2023-07-06 06:53:23 +03:00
Koitharu
7908eb1441 Favourites container fixes 2023-07-04 13:44:01 +03:00
Koitharu
ed672feebe Show related manga 2023-07-04 09:09:56 +03:00
Koitharu
4739da2774 Reorganize navigation 2023-07-03 17:20:17 +03:00
Zakhar Timoshenko
fb674b6028 More M3 theming 2023-07-03 16:11:05 +03:00
Zakhar Timoshenko
df5f5ea737 Return rounded square for simple manga item 2023-07-03 14:57:57 +03:00
Zakhar Timoshenko
e6facd4e41 Material fade through preview 2023-07-03 14:39:37 +03:00
Koitharu
942d4fe5ab Refactor ListModel 2023-07-03 13:57:51 +03:00
Zakhar Timoshenko
80db817ff2 Single suggestion more button intent 2023-07-02 15:59:42 +03:00
Zakhar Timoshenko
b2817a2ce7 Recommendation item on explore screen (not finished) 2023-07-02 02:15:07 +03:00
Zakhar Timoshenko
e2835e3e95 Use FastOutSlowInInterpolator for segmented bar 2023-07-01 23:45:03 +03:00
Zakhar Timoshenko
91928d058b Pie chart test 2023-07-01 23:43:38 +03:00
Zakhar Timoshenko
425e8a49c4 Adjust two-line items to M3 guidelines 2023-07-01 22:54:45 +03:00
Zakhar Timoshenko
148dbfdf02 Set 16dp corners for tips 2023-07-01 22:54:00 +03:00
Koitharu
55fdd6b7b1 Update default favicon drawable 2023-07-01 17:44:36 +03:00
Zakhar Timoshenko
a726b4f499 Merge remote-tracking branch 'origin/next' into next 2023-07-01 17:07:26 +03:00
Koitharu
39cd199044 Tip view 2023-07-01 16:03:48 +03:00
Koitharu
98b8aa3f2d Update details fragment layout 2023-07-01 14:46:42 +03:00
Koitharu
f5b8d41a86 Merge branch 'devel' into next 2023-07-01 13:18:29 +03:00
Koitharu
90dfc84119 Update dependencies 2023-07-01 12:54:44 +03:00
Zakhar Timoshenko
f01fd18711 Main activity theming 2023-06-30 21:27:20 +03:00
Zakhar Timoshenko
0098bdd07e Initial starting of writing next major version 2023-06-30 17:01:29 +03:00
Koitharu
6a792f8ac3 Use CoroutineStart.ATOMIC in some cases 2023-06-30 14:04:22 +03:00
Koitharu
c81e8749b6 Update parsers and headers processing 2023-06-28 13:27:26 +03:00
ztimms73
5fa260a0c7 Update issue template 2023-06-28 03:14:30 +03:00
Koitharu
e0ba4e2686 Remove unused code 2023-06-27 12:52:41 +03:00
Koitharu
f188d1c0f3 Remove ongoing flag from background work notifications 2023-06-27 12:34:12 +03:00
CakesTwix
6de55afa27 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: CakesTwix <cakestwix1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
J. Lavoie
21dcb5b754 Translated using Weblate (French)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
gallegonovato
9b3ea57db1 Translated using Weblate (Spanish)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
kuragehime
032a8607ba Translated using Weblate (Japanese)
Currently translated at 100.0% (447 of 447 strings)

Co-authored-by: kuragehime <kuragehime641@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
Макар Разин
f7303c5957 Translated using Weblate (Serbian)
Currently translated at 29.3% (131 of 447 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (445 of 447 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (447 of 447 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (447 of 447 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/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-27 12:30:56 +03:00
Koitharu
d696606ef9 Misc fixes 2023-06-27 10:28:47 +03:00
Koitharu
0a6e106a1d Filter local manga files 2023-06-24 13:18:09 +03:00
Koitharu
de1a7f0ca8 Fix IndexOutOfBoundsException in RemoteViewsFactory 2023-06-24 09:38:13 +03:00
Koitharu
9d31e76cc7 Translated using Weblate (Russian)
Currently translated at 100.0% (443 of 443 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Cookies
20910ffb5d Translated using Weblate (Vietnamese)
Currently translated at 81.2% (360 of 443 strings)

Co-authored-by: Cookies <Nekop1845@proton.me>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Clxff H3r4ld0
7497ee6364 Translated using Weblate (Indonesian)
Currently translated at 100.0% (443 of 443 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Abay Emes
0f2ed50e18 Translated using Weblate (Kazakh)
Currently translated at 48.8% (213 of 436 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
BlackSpectrum
ba066b577b Translated using Weblate (Hindi)
Currently translated at 15.5% (68 of 436 strings)

Co-authored-by: BlackSpectrum <tittan5000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
CakesTwix
4496fe876f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (436 of 436 strings)

Co-authored-by: CakesTwix <cakestwix1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
gallegonovato
a9f5abebf0 Translated using Weblate (Spanish)
Currently translated at 100.0% (443 of 443 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (436 of 436 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
qrynill
bebee2ef27 Translated using Weblate (Norwegian Nynorsk)
Currently translated at 91.7% (399 of 435 strings)

Co-authored-by: qrynill <tryvseu@tuta.io>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Макар Разин
4ec2b0c8fe Translated using Weblate (Vietnamese)
Currently translated at 79.3% (345 of 435 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Korean)

Currently translated at 75.1% (327 of 435 strings)

Translated using Weblate (Greek)

Currently translated at 19.3% (84 of 435 strings)

Translated using Weblate (Serbian)

Currently translated at 28.2% (123 of 435 strings)

Translated using Weblate (Arabic)

Currently translated at 18.1% (79 of 435 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (435 of 435 strings)

Translated using Weblate (Italian)

Currently translated at 85.2% (371 of 435 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (435 of 435 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
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/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
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/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2023-06-23 16:23:37 +03:00
Koitharu
4a7be70898 Update queries in manifest 2023-06-23 16:14:41 +03:00
Koitharu
2bcba1eb21 Configure manga directories 2023-06-22 13:45:29 +03:00
Koitharu
feca7ba3fc Support for custom directories for manga 2023-06-22 10:11:11 +03:00
Koitharu
745b349e5e Ability to remove item from updates 2023-06-21 15:27:20 +03:00
Koitharu
13946783a5 Fix crashes 2023-06-21 15:06:01 +03:00
Koitharu
84e5400522 Download options dialog 2023-06-21 14:54:11 +03:00
Koitharu
02c9a933d2 Fix offline manga details 2023-06-20 17:06:18 +03:00
Koitharu
92af851d3b Option to clear single source cookies 2023-06-20 13:43:09 +03:00
Koitharu
009eb9fe44 Fix recursive sync 2023-06-17 18:34:08 +03:00
Koitharu
fc8a5ccd9f Fix Continue button in offline mode 2023-06-17 18:20:57 +03:00
Koitharu
91f46de547 Fix crashes 2023-06-17 18:11:09 +03:00
Koitharu
d548993e14 Move syncronization to main process 2023-06-17 17:36:12 +03:00
Koitharu
4f32664b33 Respect system PowerSave mode 2023-06-17 16:12:14 +03:00
Koitharu
71b14a3aa8 Refactor FilterOwner 2023-06-17 16:05:08 +03:00
Isira Seneviratne
183a61272e Use ParcelCompat methods. 2023-06-17 15:50:08 +03:00
666 changed files with 17944 additions and 11684 deletions

View File

@@ -13,6 +13,7 @@ disabled_rules = no-wildcard-imports, no-unused-imports
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
ij_continuation_indent_size = 4
ij_xml_attribute_wrap = on_every_item
[{*.kt,*.kts}]
ij_kotlin_allow_trailing_comma_on_call_site = true

View File

@@ -61,4 +61,6 @@ body:
label: Acknowledgements
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
required: true
- label: If this is an issue with a parser, I should be opening an issue in the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers/issues/new/choose).
required: true

1
.idea/.gitignore generated vendored
View File

@@ -1,3 +1,4 @@
# Default ignored files
/shelf/
/workspace.xml
/migrations.xml

2
.idea/gradle.xml generated
View File

@@ -5,7 +5,6 @@
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
@@ -14,6 +13,7 @@
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

11
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,11 @@
## Kotatsu contribution guidelines
- If you want to fix bug or implement a new feature, that already mention in the [issues](https://github.com/KotatsuApp/Kotatsu/issues), please, assign this issue to you and/or comment about it.
- Whether you have to implement new feature, please, open an issue or discussion regarding it to ensure it will be accepted.
- Translations have to be managed using the [Weblate](https://hosted.weblate.org/engage/kotatsu/) platform.
- In case you want to add a new manga source, refer to the [parsers repository](https://github.com/KotatsuApp/kotatsu-parsers).
Refactoring or some dev-faces improvements are also might be accepted, however please stick to the following principles:
- Performance matters. In the case of choosing between source code beauty and performance, performance should be a priority.
- Please, do not modify readme and other information files (except for typos).
- Avoid adding new dependencies unless required. APK size is important.

View File

@@ -39,6 +39,10 @@ Kotatsu is a free and open source manga reader for Android.
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/)
### Contributing
See [CONTRIBUTING.md](./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)

View File

@@ -2,28 +2,29 @@ plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'com.google.devtools.ksp'
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
}
android {
compileSdk = 33
buildToolsVersion = '33.0.2'
compileSdk = 34
buildToolsVersion = '34.0.0'
namespace = 'org.koitharu.kotatsu'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 554
versionName '5.2.2'
minSdk = 21
targetSdk = 34
versionCode = 573
versionName = '6.0'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
arguments {
arg 'room.schemaLocation', "$projectDir/schemas".toString()
}
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
androidResources {
generateLocaleConfig true
}
}
buildTypes {
@@ -39,6 +40,7 @@ android {
}
buildFeatures {
viewBinding true
buildConfig true
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
@@ -79,81 +81,80 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:86a82970fc') {
implementation('com.github.KotatsuApp:kotatsu-parsers:3a76504380') {
exclude group: 'org.json', module: 'json'
}
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.10'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.activity:activity-ktx:1.7.2'
implementation 'androidx.fragment:fragment-ktx:1.6.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-service:2.6.1'
implementation 'androidx.lifecycle:lifecycle-process:2.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.9.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.1'
// TODO https://issuetracker.google.com/issues/254846063
implementation 'androidx.work:work-runtime-ktx:2.8.1'
//noinspection GradleDependency
implementation('com.google.guava:guava:32.0.0-android') {
implementation('com.google.guava:guava:32.0.1-android') {
exclude group: 'com.google.guava', module: 'failureaccess'
exclude group: 'org.checkerframework', module: 'checker-qual'
exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
}
implementation 'androidx.room:room-runtime:2.5.1'
implementation 'androidx.room:room-ktx:2.5.1'
kapt 'androidx.room:room-compiler:2.5.1'
implementation 'androidx.room:room-runtime:2.5.2'
implementation 'androidx.room:room-ktx:2.5.2'
ksp 'androidx.room:room-compiler:2.5.2'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
implementation 'com.squareup.okio:okio:3.3.0'
implementation 'com.squareup.okio:okio:3.5.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.46.1'
kapt 'com.google.dagger:hilt-compiler:2.46.1'
implementation 'com.google.dagger:hilt-android:2.47'
kapt 'com.google.dagger:hilt-compiler:2.47'
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation 'io.coil-kt:coil-base:2.4.0'
implementation 'io.coil-kt:coil-svg:2.4.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:1b19231b2f'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:9b1d20be67'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'
implementation 'ch.acra:acra-http:5.9.7'
implementation 'ch.acra:acra-dialog:5.9.7'
implementation 'ch.acra:acra-http:5.11.1'
implementation 'ch.acra:acra-dialog:5.11.1'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20230227'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
testImplementation 'org.json:json:20230618'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.room:room-testing:2.5.1'
androidTestImplementation 'androidx.room:room-testing:2.5.2'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.46.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.46.1'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.47'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.47'
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}

View File

@@ -17,7 +17,7 @@ class MangaDatabaseTest {
MangaDatabase::class.java,
)
private val migrations = databaseMigrations
private val migrations = getDatabaseMigrations(InstrumentationRegistry.getInstrumentation().targetContext)
@Test
fun versions() {

View File

@@ -48,6 +48,7 @@ class AppShortcutManagerTest {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return@runTest
}
database.invalidationTracker.addObserver(appShortcutManager)
awaitUpdate()
assertTrue(getShortcuts().isEmpty())
historyRepository.addOrUpdate(

View File

@@ -1,37 +0,0 @@
package org.koitharu.kotatsu.core.util
import android.util.Log
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
class LoggingAdapterDataObserver(
private val tag: String,
) : AdapterDataObserver() {
override fun onChanged() {
Log.d(tag, "onChanged()")
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeChanged(positionStart: Int, itemCount: Int, payload: Any?) {
Log.d(tag, "onItemRangeChanged(positionStart=$positionStart, itemCount=$itemCount, payload=$payload)")
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeInserted(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
Log.d(tag, "onItemRangeRemoved(positionStart=$positionStart, itemCount=$itemCount)")
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
Log.d(tag, "onItemRangeMoved(fromPosition=$fromPosition, toPosition=$toPosition, itemCount=$itemCount)")
}
override fun onStateRestorationPolicyChanged() {
Log.d(tag, "onStateRestorationPolicyChanged()")
}
}

View File

@@ -0,0 +1,45 @@
package org.koitharu.kotatsu
import android.content.Context
import android.os.StrictMode
import androidx.fragment.app.strictmode.FragmentStrictMode
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
class KotatsuApp : BaseApp() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
enableStrictMode()
}
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.setClassInstanceLimit(PageLoader::class.java, 1)
.penaltyLog()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
// .detectWrongFragmentContainer() FIXME: migrate to ViewPager2
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.build()
}
}

View File

@@ -17,7 +17,7 @@ import java.util.EnumSet
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain()
get() = ConfigKey.Domain("")
override val sortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java)

View File

@@ -18,6 +18,23 @@
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<intent>
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
</intent>
</queries>
<application
android:name="org.koitharu.kotatsu.KotatsuApp"
@@ -30,8 +47,8 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:localeConfig="@xml/locales"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Kotatsu"
@@ -54,6 +71,17 @@
<intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="kotatsu.app" />
<data android:path="/manga" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
@@ -83,6 +111,9 @@
<activity
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
android:label="@string/suggestions" />
<activity
android:name="org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity"
android:label="@string/related_manga" />
<activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:exported="true"
@@ -98,6 +129,9 @@
<data android:host="sync-settings" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
android:label="@string/local_manga_directories" />
<activity
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
@@ -112,8 +146,7 @@
android:windowSoftInputMode="adjustResize" />
<activity
android:name="org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesActivity"
android:label="@string/favourites"
android:windowSoftInputMode="stateAlwaysHidden" />
android:label="@string/manage_categories" />
<activity
android:name="org.koitharu.kotatsu.widget.shelf.ShelfConfigActivity"
android:exported="true"
@@ -144,9 +177,6 @@
<activity
android:name="org.koitharu.kotatsu.reader.ui.colorfilter.ColorFilterConfigActivity"
android:label="@string/color_correction" />
<activity
android:name="org.koitharu.kotatsu.shelf.ui.config.ShelfSettingsActivity"
android:label="@string/settings" />
<activity
android:name="org.koitharu.kotatsu.scrobbling.common.ui.config.ScrobblerConfigActivity"
android:exported="true"
@@ -167,7 +197,13 @@
</activity>
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<service
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
android:foregroundServiceType="dataSync" />
<service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
@@ -188,8 +224,7 @@
<service
android:name="org.koitharu.kotatsu.sync.ui.favourites.FavouritesSyncService"
android:exported="false"
android:label="@string/favourites"
android:process=":sync">
android:label="@string/favourites">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
@@ -200,8 +235,7 @@
<service
android:name="org.koitharu.kotatsu.sync.ui.history.HistorySyncService"
android:exported="false"
android:label="@string/history"
android:process=":sync">
android:label="@string/history">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
@@ -282,6 +316,660 @@
android:name="com.samsung.android.icon_container.has_icon_container"
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
<activity-alias
android:name="org.koitharu.kotatsu.details.ui.DetailsBYLinkActivity"
android:exported="true"
android:targetActivity="org.koitharu.kotatsu.details.ui.DetailsActivity">
<intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="1stkissmanga.me" />
<data android:host="3asq.org" />
<data android:host="18porncomic.com" />
<data android:host="212.32.226.234" />
<data android:host="247manga.com" />
<data android:host="365manga.com" />
<data android:host="2023.allhen.online" />
<data android:host="adultwebtoon.com" />
<data android:host="afroditscans.com" />
<data android:host="ainzscans.site" />
<data android:host="aiyumanga.com" />
<data android:host="alceascan.my.id" />
<data android:host="allporncomic.com" />
<data android:host="anibel.net" />
<data android:host="anigliscans.com" />
<data android:host="anikiga.com" />
<data android:host="animaregia.net" />
<data android:host="anisamanga.com" />
<data android:host="anshscans.org" />
<data android:host="apenasmaisumyaoi.com" />
<data android:host="apollcomics.com" />
<data android:host="aquamanga.com" />
<data android:host="arabtoons.net" />
<data android:host="araznovel.com" />
<data android:host="arcanescans.com" />
<data android:host="arenascans.net" />
<data android:host="arthurscan.xyz" />
<data android:host="astral-manga.fr" />
<data android:host="astrallibrary.net" />
<data android:host="astrumscans.xyz" />
<data android:host="asura.nacm.xyz" />
<data android:host="asurascanstr.com" />
<data android:host="athenafansub.com" />
<data android:host="ayatoon.com" />
<data android:host="azoranov.com" />
<data android:host="azuremanga.com" />
<data android:host="babeltoon.com" />
<data android:host="bakai.org" />
<data android:host="bakaman.net" />
<data android:host="bakamh.com" />
<data android:host="banana-scan.com" />
<data android:host="bato.to" />
<data android:host="batocomic.com" />
<data android:host="batocomic.net" />
<data android:host="batocomic.org" />
<data android:host="batotoo.com" />
<data android:host="batotwo.com" />
<data android:host="battwo.com" />
<data android:host="beast-scans.com" />
<data android:host="beehentai.com" />
<data android:host="bentomanga.com" />
<data android:host="bestmanga.club" />
<data android:host="bestmanhua.com" />
<data android:host="bibimanga.com" />
<data android:host="birdmanga.com" />
<data android:host="birdtoon.net" />
<data android:host="blogmanga.net" />
<data android:host="blogtruyenmoi.com" />
<data android:host="bokugents.com" />
<data android:host="boosei.net" />
<data android:host="boyslove.me" />
<data android:host="br.atlantisscan.com" />
<data android:host="br.ninemanga.com" />
<data android:host="cabaredowatame.site" />
<data android:host="cafecomyaoi.com.br" />
<data android:host="carteldemanhwas.com" />
<data android:host="cat300.com" />
<data android:host="cerisescans.com" />
<data android:host="chap.mangairo.com" />
<data android:host="chapmanganato.com" />
<data android:host="chapmanganato.com" />
<data android:host="cizgiromanarsivi.com" />
<data android:host="cmreader.info" />
<data android:host="cocorip.net" />
<data android:host="coffeemanga.io" />
<data android:host="coloredmanga.com" />
<data android:host="comick.app" />
<data android:host="comiko.net" />
<data android:host="comiko.org" />
<data android:host="copypastescan.xyz" />
<data android:host="cosmicscans.com" />
<data android:host="daprob.com" />
<data android:host="darkscans.com" />
<data android:host="de.ninemanga.com" />
<data android:host="desu.me" />
<data android:host="diamondfansub.com" />
<data android:host="dojing.net" />
<data android:host="dokkomanga.com" />
<data android:host="dokkomanga.com" />
<data android:host="doujin69.com" />
<data android:host="doujindesu.rip" />
<data android:host="doujinhentai.net" />
<data android:host="dragontea.ink" />
<data android:host="dragontranslation.net" />
<data android:host="drakescans.com" />
<data android:host="dto.to" />
<data android:host="duckmanga.com" />
<data android:host="duniakomik.id" />
<data android:host="dynasty-scans.com" />
<data android:host="e-hentai.org" />
<data android:host="elarcpage.com" />
<data android:host="en.leviatanscans.com" />
<data android:host="epsilonscan.fr" />
<data android:host="es.ninemanga.com" />
<data android:host="esomanga.com" />
<data android:host="exhentai.org" />
<data android:host="falconmanga.com" />
<data android:host="fbsquads.com" />
<data android:host="finalscans.com" />
<data android:host="flamescans.org" />
<data android:host="foxwhite.com.br" />
<data android:host="fr-scan.cc" />
<data android:host="fr.ninemanga.com" />
<data android:host="franxxmangas.net" />
<data android:host="freakscans.com" />
<data android:host="freemanga.me" />
<data android:host="freemangatop.com" />
<data android:host="freewebtooncoins.com" />
<data android:host="frscans.com" />
<data android:host="furyosociety.com" />
<data android:host="galaxymanga.org" />
<data android:host="gatemanga.com" />
<data android:host="gdscans.com" />
<data android:host="gekkou.com.br" />
<data android:host="glorymanga.com" />
<data android:host="goldenmanga.top" />
<data android:host="golgebahcesi.com" />
<data android:host="gooffansub.com" />
<data android:host="gourmetscans.net" />
<data android:host="grabber.zone" />
<data android:host="gremorymangas.com" />
<data android:host="guimah.com" />
<data android:host="guncelmanga.net" />
<data android:host="h.mangabat.com" />
<data android:host="hachiraw.com" />
<data android:host="harimanga.com" />
<data android:host="hayalistic.com" />
<data android:host="hensekai.com" />
<data android:host="hentai3z.cc" />
<data android:host="hentai3z.xyz" />
<data android:host="hentai4free.net" />
<data android:host="hentai20.io" />
<data android:host="hentai.gekkouscans.com.br" />
<data android:host="hentai.scantrad-vf.cc" />
<data android:host="hentaichan.live" />
<data android:host="hentaichan.pro" />
<data android:host="hentaicube.net" />
<data android:host="hentailib.me" />
<data android:host="hentaimanga.me" />
<data android:host="hentaiteca.net" />
<data android:host="hentaivn.autos" />
<data android:host="hentaivn.tv" />
<data android:host="hentaiwebtoon.com" />
<data android:host="hentaixcomic.com" />
<data android:host="hentaixdickgirl.com" />
<data android:host="hentaixyuri.com" />
<data android:host="hentaizone.xyz" />
<data android:host="herenscan.com" />
<data android:host="hhentai.fr" />
<data android:host="hikariscan.com.br" />
<data android:host="hipercool.xyz" />
<data android:host="hmanhwa.com" />
<data android:host="hni-scantrad.com" />
<data android:host="honey-manga.com.ua" />
<data android:host="hscans.com" />
<data android:host="hto.to" />
<data android:host="id.gourmetscans.net" />
<data android:host="illusionscan.com" />
<data android:host="immortalupdates.com" />
<data android:host="immortalupdates.id" />
<data android:host="imperiodabritannia.com" />
<data android:host="imperioscans.com.br" />
<data android:host="indo18h.com" />
<data android:host="infrafandub.xyz" />
<data android:host="isekaiscan.top" />
<data android:host="it.ninemanga.com" />
<data android:host="itsyourightmanhua.com" />
<data android:host="jaiminisbox.net" />
<data android:host="japscan.ws" />
<data android:host="jiangzaitoon.co" />
<data android:host="jimanga.com" />
<data android:host="jpmangas.xyz" />
<data android:host="kanzenin.xyz" />
<data android:host="karatcam-scans.fr" />
<data android:host="katakomik.online" />
<data android:host="kiryuu.id" />
<data android:host="kissmanga.in" />
<data android:host="klikmanga.id" />
<data android:host="klz9.com" />
<data android:host="koinoboriscan.com" />
<data android:host="kolmanga.com" />
<data android:host="komikav.com" />
<data android:host="komikcast.io" />
<data android:host="komikdewasa.cfd" />
<data android:host="komikgo.org" />
<data android:host="komikhentai.co" />
<data android:host="komikid.com" />
<data android:host="komikindo.co" />
<data android:host="komikindo.info" />
<data android:host="komiklab.com" />
<data android:host="komiklokal.cfd" />
<data android:host="komikmama.co" />
<data android:host="komikmanhwa.me" />
<data android:host="komikmirror.art" />
<data android:host="komiksan.link" />
<data android:host="komiksay.site" />
<data android:host="komikstation.co" />
<data android:host="komiktap.in" />
<data android:host="komiku.com" />
<data android:host="komikzoid.xyz" />
<data android:host="ksgroupscans.com" />
<data android:host="kumascans.com" />
<data android:host="kunmanga.com" />
<data android:host="ladymanga.com" />
<data android:host="lectortmo.com" />
<data android:host="lectorunitoon.com" />
<data android:host="legacy-scans.com" />
<data android:host="legionscans.com" />
<data android:host="leitor.kamisama.com.br" />
<data android:host="leitorizakaya.net" />
<data android:host="lelscanvf.cc" />
<data android:host="leryaoi.com" />
<data android:host="lilymanga.net" />
<data android:host="limascans.xyz/v2" />
<data android:host="lkscanlation.com" />
<data android:host="lolicon.mobi" />
<data android:host="lugnica-scans.com" />
<data android:host="lunarscan.org" />
<data android:host="luxmanga.net" />
<data android:host="lxmanga.net" />
<data android:host="lynxscans.com" />
<data android:host="m.isekaiscan.to" />
<data android:host="mafia-manga.com" />
<data android:host="maidscan.com.br" />
<data android:host="manga1st.online" />
<data android:host="manga3s.com" />
<data android:host="manga18.club" />
<data android:host="manga68.com" />
<data android:host="manga689.com" />
<data android:host="manga-chan.me" />
<data android:host="manga-crab.com" />
<data android:host="manga-diyari.com" />
<data android:host="manga-fast.com" />
<data android:host="manga-fr.me" />
<data android:host="manga-mate.org" />
<data android:host="manga-moons.net" />
<data android:host="manga-scan.co" />
<data android:host="manga-scantrad.io" />
<data android:host="manga-tx.com" />
<data android:host="manga-uptocats.com" />
<data android:host="manga.clone-army.org" />
<data android:host="manga.in.ua" />
<data android:host="manga.mundodrama.site" />
<data android:host="mangaaction.com" />
<data android:host="mangaatrend.net" />
<data android:host="mangabaz.net" />
<data android:host="mangabob.com" />
<data android:host="mangabuddy.com" />
<data android:host="mangacim.com" />
<data android:host="mangaclash.com" />
<data android:host="mangacultivator.com" />
<data android:host="mangacute.com" />
<data android:host="mangacv.com" />
<data android:host="mangadass.com" />
<data android:host="mangadeemak.com" />
<data android:host="mangadex.org" />
<data android:host="mangadistrict.com" />
<data android:host="mangadna.com" />
<data android:host="mangadoor.com" />
<data android:host="mangaeffect.com" />
<data android:host="mangaforest.me" />
<data android:host="mangaforfree.com" />
<data android:host="mangafoxfull.com" />
<data android:host="mangafreak.online" />
<data android:host="mangagalaxy.me" />
<data android:host="mangagg.com" />
<data android:host="mangagoyaoi.com" />
<data android:host="mangagreat.com" />
<data android:host="mangahentai.me" />
<data android:host="mangahub.fr" />
<data android:host="mangaid.click" />
<data android:host="mangaindo.me" />
<data android:host="mangak2.com" />
<data android:host="mangakakalot.com" />
<data android:host="mangakeyfi.net" />
<data android:host="mangaking.net" />
<data android:host="mangakio.me" />
<data android:host="mangakiss.org" />
<data android:host="mangakita.net" />
<data android:host="mangakomi.io" />
<data android:host="mangakyo.org" />
<data android:host="mangalek.com" />
<data android:host="mangaleks.com" />
<data android:host="mangaleveling.com" />
<data android:host="mangalib.me" />
<data android:host="mangalike.me" />
<data android:host="mangalink.online" />
<data android:host="mangalionz.com" />
<data android:host="mangamammy.ru" />
<data android:host="mangamanhua.online" />
<data android:host="mangamaniacs.org" />
<data android:host="manganato.com" />
<data android:host="mangaokutr.com" />
<data android:host="mangaonelove.site" />
<data android:host="mangaonlineteam.com" />
<data android:host="mangaowl.to" />
<data android:host="mangaprotm.com" />
<data android:host="mangapt.com" />
<data android:host="mangapuma.com" />
<data android:host="mangaread.co" />
<data android:host="mangareaderpro.com" />
<data android:host="mangareading.org" />
<data android:host="mangarockteam.com" />
<data android:host="mangarocky.com" />
<data android:host="mangarolls.net" />
<data android:host="mangarosie.in" />
<data android:host="mangas-origines.fr" />
<data android:host="mangas-origines.xyz" />
<data android:host="mangaschan.com" />
<data android:host="mangasehri.com" />
<data android:host="mangaspark.com" />
<data android:host="mangastarz.com" />
<data android:host="mangastic.cc" />
<data android:host="mangastic.cc" />
<data android:host="mangasushi.org" />
<data android:host="mangasusuku.xyz" />
<data android:host="mangatale.co" />
<data android:host="mangatone.com" />
<data android:host="mangatoto.com" />
<data android:host="mangatoto.net" />
<data android:host="mangatoto.org" />
<data android:host="mangatx.com" />
<data android:host="mangaus.xyz" />
<data android:host="mangavisa.com" />
<data android:host="mangaweebs.in" />
<data android:host="mangawt.com" />
<data android:host="mangax1.com" />
<data android:host="mangaxyz.com" />
<data android:host="mangayaro.net" />
<data android:host="mangazavr.ru" />
<data android:host="mangazodiac.com" />
<data android:host="manhatic.com" />
<data android:host="manhuaes.com" />
<data android:host="manhuafast.com" />
<data android:host="manhuafast.net" />
<data android:host="manhuaga.com" />
<data android:host="manhuahot.com" />
<data android:host="manhuamix.com" />
<data android:host="manhuaplus.com" />
<data android:host="manhuascan.us" />
<data android:host="manhuaus.com" />
<data android:host="manhuazone.net" />
<data android:host="manhwa18.app" />
<data android:host="manhwa18.com" />
<data android:host="manhwa18.net" />
<data android:host="manhwa18.org" />
<data android:host="manhwa68.com" />
<data android:host="manhwa-latino.com" />
<data android:host="manhwaclan.com" />
<data android:host="manhwadesu.top" />
<data android:host="manhwafull.com" />
<data android:host="manhwahentai.me" />
<data android:host="manhwaindo.icu" />
<data android:host="manhwaindo.id" />
<data android:host="manhwakool.com" />
<data android:host="manhwalist.xyz" />
<data android:host="manhwalover.com" />
<data android:host="manhwaplus.pro" />
<data android:host="manhwasco.net" />
<data android:host="manhwatop.com" />
<data android:host="manhwaworld.com" />
<data android:host="manhwax.org" />
<data android:host="manhwaz.com" />
<data android:host="mantrazscan.com" />
<data android:host="manwe.pro" />
<data android:host="manycomic.com" />
<data android:host="manytoon.com" />
<data android:host="manytoon.me" />
<data android:host="masterkomik.com" />
<data android:host="melokomik.xyz" />
<data android:host="mgkomik.com" />
<data android:host="miauscans.com" />
<data android:host="milftoon.xxx" />
<data android:host="mintmanga.com" />
<data android:host="mintmanga.live" />
<data android:host="mirrordesu.ink" />
<data android:host="mm-scans.org" />
<data android:host="momonohanascan.com" />
<data android:host="monarcamanga.com" />
<data android:host="moonloversscan.com.br" />
<data android:host="moonwitchinlovescan.com" />
<data android:host="mortalsgroove.com" />
<data android:host="mto.to" />
<data android:host="mundomangakun.com.br" />
<data android:host="mundomanhwa.com" />
<data android:host="murimscan.run" />
<data android:host="neatmangas.com" />
<data android:host="neoxscans.net" />
<data android:host="nettruyenin.com" />
<data android:host="nettruyento.com" />
<data android:host="neumanga.net" />
<data android:host="neumanga.xyz" />
<data android:host="nhattruyenmin.com" />
<data android:host="nhentai.net" />
<data android:host="nicovideo.jp" />
<data android:host="nightscans.org" />
<data android:host="niji-translations.com" />
<data android:host="ninjascan.site" />
<data android:host="niverafansub.com" />
<data android:host="nocsummer.com.br" />
<data android:host="noindexscan.com" />
<data android:host="nonbiri.space" />
<data android:host="novelcrow.com" />
<data android:host="novelmic.com" />
<data android:host="novelstown.cyou" />
<data android:host="nude-moon.net" />
<data android:host="nude-moon.org" />
<data android:host="nyxmanga.com" />
<data android:host="origami-orpheans.com.br" />
<data android:host="otsugami.id" />
<data android:host="oxapk.com" />
<data android:host="ozulmanga.com" />
<data android:host="painfulnightz.com" />
<data android:host="pantheon-scan.com" />
<data android:host="papscan.com" />
<data android:host="paragonscans.com" />
<data android:host="peacescans.com" />
<data android:host="phantomscans.com" />
<data android:host="phenixscans.fr" />
<data android:host="pianmanga.me" />
<data android:host="pirulitorosa.site" />
<data android:host="piscans.in" />
<data android:host="platinumscans.com" />
<data android:host="pojokmanga.net" />
<data android:host="popsmanga.com" />
<data android:host="portalyaoi.com" />
<data android:host="prismahentai.com" />
<data android:host="prismascans.net" />
<data android:host="projetoscanlator.com" />
<data android:host="psunicorn.com" />
<data android:host="queenscans.com" />
<data android:host="ragnarokscan.com" />
<data android:host="ragnarokscanlation.com" />
<data android:host="raijinscans.fr" />
<data android:host="raikiscan.com" />
<data android:host="rainbowfairyscan.com" />
<data android:host="randomscans.com" />
<data android:host="ravenscans.com" />
<data android:host="rawdex.net" />
<data android:host="rawkuma.com" />
<data android:host="read-nifteam.info" />
<data android:host="read.babelwuxia.com" />
<data android:host="readcomicsonline.ru" />
<data android:host="reader.deathtollscans.net" />
<data android:host="reader.decadencescans.com" />
<data android:host="reader.evilflowers.com" />
<data android:host="reader.mangatellers.gr" />
<data android:host="reader.onepiecenakama.pl" />
<data android:host="reader.powermanga.org" />
<data android:host="reader.silentsky-scans.net" />
<data android:host="readfreecomics.com" />
<data android:host="readkomik.com" />
<data android:host="readmanga.io" />
<data android:host="readmanga.live" />
<data android:host="readmanga.me" />
<data android:host="readmangabat.com" />
<data android:host="readmanhua.net" />
<data android:host="readtoto.com" />
<data android:host="readtoto.net" />
<data android:host="readtoto.org" />
<data android:host="realmscans.xyz" />
<data android:host="reaperscans.fr" />
<data android:host="remanga.org" />
<data android:host="rightdark-scan.com" />
<data android:host="rio2manga.com" />
<data android:host="rio2manga.net" />
<data android:host="rogmangas.com" />
<data android:host="romantikmanga.com" />
<data android:host="ru.ninemanga.com" />
<data android:host="s2manga.com" />
<data android:host="samuraiscan.com" />
<data android:host="sawamics.com" />
<data android:host="saytruyenhay.com" />
<data android:host="scambertraslator.com" />
<data android:host="scan.hentai.menu" />
<data android:host="scanmanga-vf.ws" />
<data android:host="scansmangas.me" />
<data android:host="scansraw.com" />
<data android:host="scantrad-union.com" />
<data android:host="scantrad-vf.co" />
<data android:host="sekaikomik.pro" />
<data android:host="sektedoujin.cc" />
<data android:host="sektekomik.xyz" />
<data android:host="selfmanga.live" />
<data android:host="senpaiediciones.com" />
<data android:host="shadowmangas.com" />
<data android:host="shadowtrad.net" />
<data android:host="sheakomik.com" />
<data android:host="shibamanga.com" />
<data android:host="shinigami.id" />
<data android:host="shirodoujin.com" />
<data android:host="shootingstarscans.com" />
<data android:host="silencescan.com.br" />
<data android:host="sinensisscans.com" />
<data android:host="skanlacje-feniksy.pl" />
<data android:host="skymanga.work" />
<data android:host="skymangas.com" />
<data android:host="sleepytranslations.com" />
<data android:host="soulscans.my.id" />
<data android:host="spartanmanga.com.tr" />
<data android:host="sssscanlator.com" />
<data android:host="summanga.com" />
<data android:host="suryascans.com" />
<data android:host="sushiscan.fr" />
<data android:host="sushiscan.net" />
<data android:host="swatop.club" />
<data android:host="tankouhentai.com" />
<data android:host="tatakaescan.com" />
<data android:host="tecnoscann.com" />
<data android:host="teenmanhua.com" />
<data android:host="tempestfansub.com" />
<data android:host="templescan.net" />
<data android:host="templescanesp.com" />
<data android:host="tenkaiscan.net" />
<data android:host="theguildscans.com" />
<data android:host="thesugarscan.com" />
<data android:host="timenaight.com" />
<data android:host="todaymic.com" />
<data android:host="tonizutoon.com" />
<data android:host="toonchill.com" />
<data android:host="toonfr.com" />
<data android:host="toonhunter.com" />
<data android:host="toonily.com" />
<data android:host="toonily.me" />
<data android:host="toonily.net" />
<data android:host="toonitube.com" />
<data android:host="tortuga-ceviri.com" />
<data android:host="traduccionesmoonlight.com" />
<data android:host="treemanga.com" />
<data android:host="tritinia.org" />
<data android:host="truemanga.com" />
<data android:host="truyentranhlh.net" />
<data android:host="tsundoku.com.br" />
<data android:host="tukangkomik.id" />
<data android:host="tumanhwas.club" />
<data android:host="turktoon.com" />
<data android:host="v2.comiz.net" />
<data android:host="valkyriescan.com" />
<data android:host="vercomicsporno.com" />
<data android:host="vermangasporno.com" />
<data android:host="vermanhwa.es" />
<data android:host="viyafansub.com" />
<data android:host="void-scans.com" />
<data android:host="w.mangairo.com" />
<data android:host="wakamics.net" />
<data android:host="webcomic.me" />
<data android:host="webtoon-tr.com" />
<data android:host="webtoon.uk" />
<data android:host="webtoonempire.org" />
<data android:host="webtoonhatti.com" />
<data android:host="webtoons.top" />
<data android:host="webtoonscan.com" />
<data android:host="weloma.art" />
<data android:host="welovemanga.one" />
<data android:host="westmanga.info" />
<data android:host="wickedwitchscan.com" />
<data android:host="winterscan.com" />
<data android:host="wonderlandscan.com" />
<data android:host="woopread.com" />
<data android:host="worldmanhwas.bar" />
<data android:host="wto.to" />
<data android:host="www1.bluesolo.org" />
<data android:host="www.areascans.net" />
<data android:host="www.bentomanga.com" />
<data android:host="www.eromiau.com" />
<data android:host="www.inu-manga.com" />
<data android:host="www.japscan.lol" />
<data android:host="www.kuroimanga.com" />
<data android:host="www.lami-manga.com" />
<data android:host="www.lelmanga.com" />
<data android:host="www.lianscans.my.id" />
<data android:host="www.maid.my.id" />
<data android:host="www.majorscans.com" />
<data android:host="www.mangadods.com" />
<data android:host="www.mangaread.org" />
<data android:host="www.mangascantrad.fr" />
<data android:host="www.mangatown.com" />
<data android:host="www.manhuabug.com" />
<data android:host="www.manhuakey.com" />
<data android:host="www.manhuasy.com" />
<data android:host="www.menudo-fansub.com" />
<data android:host="www.nettruyenmax.com" />
<data android:host="www.nettruyento.com" />
<data android:host="www.nightcomic.com" />
<data android:host="www.ninemanga.com" />
<data android:host="www.noblessetranslations.com" />
<data android:host="www.pantheon-scan.fr" />
<data android:host="www.paritehaber.com" />
<data android:host="www.peachscan.com" />
<data android:host="www.petrotechsociety.org" />
<data android:host="www.petrotechsociety.org" />
<data android:host="www.ramareader.it" />
<data android:host="www.rh2plusmanga.com" />
<data android:host="www.ruyamanga.com" />
<data android:host="www.scan-fr.org" />
<data android:host="www.scan-vf.net" />
<data android:host="www.thaimanga.net" />
<data android:host="www.topmanhua.com" />
<data android:host="www.vfscan.com" />
<data android:host="www.walpurgiscan.it" />
<data android:host="www.webtoon.xyz" />
<data android:host="www.witcomics.net" />
<data android:host="www.xn--l3c0azab5a2gta.com" />
<data android:host="www.yaoitoshokan.net" />
<data android:host="xbato.com" />
<data android:host="xbato.net" />
<data android:host="xbato.org" />
<data android:host="xoxocomics.net" />
<data android:host="xx.hentaichan.live" />
<data android:host="xxx.hentaichan.live" />
<data android:host="y.hentaichan.live" />
<data android:host="yaoi-chan.me" />
<data android:host="yaoi.mobi" />
<data android:host="yaoilib.me" />
<data android:host="yaoiscan.com" />
<data android:host="ycscan.com" />
<data android:host="yugenmangas.com.br" />
<data android:host="yuri.live" />
<data android:host="zahard.xyz" />
<data android:host="zandynofansub.aishiteru.org" />
<data android:host="zbato.com" />
<data android:host="zbato.net" />
<data android:host="zbato.org" />
<data android:host="zeroscan.com.br" />
<data android:host="zinmanga.com" />
<data android:host="zinmanhwa.com" />
<data android:host="zuttomanga.com" />
<data android:host="реманга.орг" />
</intent-filter>
</activity-alias>
</application>
</manifest>

View File

@@ -5,6 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@@ -14,15 +15,24 @@ abstract class BookmarksDao {
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
abstract suspend fun find(pageId: Long): BookmarkEntity?
@Transaction
@Query(
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent",
)
abstract suspend fun findAll(): 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?>
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY created_at DESC")
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY percent")
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
@Transaction
@Query(
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY bookmarks.created_at",
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent",
)
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
@@ -35,6 +45,12 @@ abstract class BookmarksDao {
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
abstract suspend fun delete(pageId: Long): Int
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int
@Upsert
abstract suspend fun upsert(bookmarks: Collection<BookmarkEntity>)
}

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.bookmarks.data
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
import java.util.Date
fun BookmarkEntity.toBookmark(manga: Manga) = Bookmark(
manga = manga,
@@ -30,4 +30,5 @@ fun Collection<BookmarkEntity>.toBookmarks(manga: Manga) = map {
it.toBookmark(manga)
}
fun Collection<Bookmark>.ids() = map { it.pageId }
@JvmName("bookmarksIds")
fun Collection<Bookmark>.ids() = map { it.pageId }

View File

@@ -1,10 +1,12 @@
package org.koitharu.kotatsu.bookmarks.domain
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.local.data.ImageFileFilter
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import java.util.Date
class Bookmark(
data class Bookmark(
val manga: Manga,
val pageId: Long,
val chapterId: Long,
@@ -13,11 +15,21 @@ class Bookmark(
val imageUrl: String,
val createdAt: Date,
val percent: Float,
) {
) : ListModel {
val directImageUrl: String?
get() = if (isImageUrlDirect()) imageUrl else null
val imageLoadData: Any
get() = if (isImageUrlDirect()) imageUrl else toMangaPage()
override fun areItemsTheSame(other: ListModel): Boolean {
return other is Bookmark &&
manga.id == other.manga.id &&
chapterId == other.chapterId &&
page == other.page
}
fun toMangaPage() = MangaPage(
id = pageId,
url = imageUrl,
@@ -26,34 +38,7 @@ class Bookmark(
)
private fun isImageUrlDirect(): Boolean {
return imageUrl.substringAfterLast('.').length in 2..4
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Bookmark
if (manga != other.manga) return false
if (pageId != other.pageId) return false
if (chapterId != other.chapterId) return false
if (page != other.page) return false
if (scroll != other.scroll) return false
if (imageUrl != other.imageUrl) return false
if (createdAt != other.createdAt) return false
return percent == other.percent
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + pageId.hashCode()
result = 31 * result + chapterId.hashCode()
result = 31 * result + page
result = 31 * result + scroll
result = 31 * result + imageUrl.hashCode()
result = 31 * result + createdAt.hashCode()
result = 31 * result + percent.hashCode()
return result
val extension = imageUrl.substringAfterLast('.')
return extension.isNotEmpty() && ImageFileFilter().isExtensionValid(extension)
}
}

View File

@@ -52,6 +52,13 @@ class BookmarksRepository @Inject constructor(
}
}
suspend fun updateBookmark(bookmark: Bookmark, imageUrl: String) {
val entity = bookmark.toEntity().copy(
imageUrl = imageUrl,
)
db.bookmarksDao.upsert(listOf(entity))
}
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) {
"Bookmark not found"
@@ -62,18 +69,16 @@ class BookmarksRepository @Inject constructor(
removeBookmark(bookmark.manga.id, bookmark.chapterId, bookmark.page)
}
suspend fun removeBookmarks(ids: Map<Manga, Set<Long>>): ReversibleHandle {
suspend fun removeBookmarks(ids: Set<Long>): ReversibleHandle {
val entities = ArrayList<BookmarkEntity>(ids.size)
db.withTransaction {
val dao = db.bookmarksDao
for ((manga, idSet) in ids) {
for (pageId in idSet) {
val e = dao.find(manga.id, pageId)
if (e != null) {
entities.add(e)
}
dao.delete(manga.id, pageId)
for (pageId in ids) {
val e = dao.find(pageId)
if (e != null) {
entities.add(e)
}
dao.delete(pageId)
}
}
return BookmarksRestorer(entities)

View File

@@ -12,31 +12,31 @@ import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.data.ids
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksGroupAdapter
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
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.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.ui.util.reverseAsync
import org.koitharu.kotatsu.core.util.ext.invalidateNestedItemDecorations
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.model.ListModel
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.main.ui.owners.SnackbarOwner
import org.koitharu.kotatsu.parsers.model.Manga
@@ -48,15 +48,18 @@ class BookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener,
OnListItemClickListener<Bookmark>,
SectionedSelectionController.Callback<Manga>,
FastScroller.FastScrollListener {
ListSelectionController.Callback2,
FastScroller.FastScrollListener, ListHeaderClickListener {
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private val viewModel by viewModels<BookmarksViewModel>()
private var adapter: BookmarksGroupAdapter? = null
private var selectionController: SectionedSelectionController<Manga>? = null
private var bookmarksAdapter: BookmarksAdapter? = null
private var selectionController: ListSelectionController? = null
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentListSimpleBinding {
return FragmentListSimpleBinding.inflate(inflater, container, false)
@@ -64,48 +67,62 @@ class BookmarksFragment :
override fun onViewBindingCreated(binding: FragmentListSimpleBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
selectionController = SectionedSelectionController(
selectionController = ListSelectionController(
activity = requireActivity(),
owner = this,
decoration = BookmarksSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,
)
adapter = BookmarksGroupAdapter(
bookmarksAdapter = BookmarksAdapter(
lifecycleOwner = viewLifecycleOwner,
coil = coil,
listener = this,
selectionController = checkNotNull(selectionController),
bookmarkClickListener = this,
groupClickListener = OnGroupClickListener(),
clickListener = this,
headerClickListener = this,
)
binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true)
val spacingDecoration = SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration)
viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
val spanSizeLookup = SpanSizeLookup()
with(binding.recyclerView) {
setHasFixedSize(true)
val spanResolver = MangaListSpanResolver(resources)
addItemDecoration(TypedListSpacingDecoration(context, false))
adapter = bookmarksAdapter
addOnLayoutChangeListener(spanResolver)
spanResolver.setGridSize(settings.gridSize / 100f, this)
val lm = GridLayoutManager(context, spanResolver.spanCount)
lm.spanSizeLookup = spanSizeLookup
layoutManager = lm
selectionController?.attachToRecyclerView(this)
}
viewModel.content.observe(viewLifecycleOwner) {
bookmarksAdapter?.setItems(it, spanSizeLookup)
}
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ::onActionDone)
}
override fun onDestroyView() {
super.onDestroyView()
adapter = null
bookmarksAdapter = null
selectionController = null
}
override fun onItemClick(item: Bookmark, view: View) {
if (selectionController?.onItemClick(item.manga, item.pageId) != true) {
if (selectionController?.onItemClick(item.pageId) != true) {
val intent = ReaderActivity.IntentBuilder(view.context)
.bookmark(item)
.incognito(true)
.build()
startActivity(intent, scaleUpActivityOptionsOf(view))
startActivity(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))
}
override fun onItemLongClick(item: Bookmark, view: View): Boolean {
return selectionController?.onItemLongClick(item.manga, item.pageId) ?: false
return selectionController?.onItemLongClick(item.pageId) ?: false
}
override fun onRetryClick(error: Throwable) = Unit
@@ -118,24 +135,16 @@ class BookmarksFragment :
override fun onFastScrollStop(fastScroller: FastScroller) = Unit
override fun onSelectionChanged(controller: SectionedSelectionController<Manga>, count: Int) {
requireViewBinding().recyclerView.invalidateNestedItemDecorations()
override fun onSelectionChanged(controller: ListSelectionController, count: Int) {
requireViewBinding().recyclerView.invalidateItemDecorations()
}
override fun onCreateActionMode(
controller: SectionedSelectionController<Manga>,
mode: ActionMode,
menu: Menu,
): Boolean {
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
mode.menuInflater.inflate(R.menu.mode_bookmarks, menu)
return true
}
override fun onActionItemClicked(
controller: SectionedSelectionController<Manga>,
mode: ActionMode,
item: MenuItem,
): Boolean {
override fun onActionItemClicked(controller: ListSelectionController, mode: ActionMode, item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_remove -> {
val ids = selectionController?.snapshot() ?: return false
@@ -148,24 +157,16 @@ class BookmarksFragment :
}
}
override fun onCreateItemDecoration(
controller: SectionedSelectionController<Manga>,
section: Manga,
): AbstractSelectionItemDecoration = BookmarksSelectionDecoration(requireContext())
override fun onWindowInsetsChanged(insets: Insets) {
requireViewBinding().recyclerView.updatePadding(
bottom = insets.bottom,
val rv = requireViewBinding().recyclerView
rv.updatePadding(
bottom = insets.bottom + rv.paddingTop,
)
requireViewBinding().recyclerView.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rv.fastScroller.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = insets.bottom
}
}
private fun onListChanged(list: List<ListModel>) {
adapter?.items = list
}
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
@@ -176,24 +177,24 @@ class BookmarksFragment :
snackbar.show()
}
private inner class OnGroupClickListener : OnListItemClickListener<BookmarksGroup> {
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup(), Runnable {
override fun onItemClick(item: BookmarksGroup, view: View) {
val controller = selectionController
if (controller != null && controller.count > 0) {
if (controller.getSectionCount(item.manga) == item.bookmarks.size) {
controller.clearSelection(item.manga)
} else {
controller.addToSelection(item.manga, item.bookmarks.ids())
}
return
}
val intent = DetailsActivity.newIntent(view.context, item.manga)
startActivity(intent)
init {
isSpanIndexCacheEnabled = true
isSpanGroupIndexCacheEnabled = true
}
override fun onItemLongClick(item: BookmarksGroup, view: View): Boolean {
return selectionController?.addToSelection(item.manga, item.bookmarks.ids()) ?: false
override fun getSpanSize(position: Int): Int {
val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
return when (bookmarksAdapter?.getItemViewType(position)) {
ListItemType.PAGE_THUMB.ordinal -> 1
else -> total
}
}
override fun run() {
invalidateSpanGroupIndexCache()
invalidateSpanIndexCache()
}
}

View File

@@ -10,13 +10,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.list.ui.model.toErrorState
@@ -41,17 +42,26 @@ class BookmarksViewModel @Inject constructor(
actionStringRes = 0,
),
)
} else list.map { (manga, bookmarks) ->
BookmarksGroup(manga, bookmarks)
} else {
mapList(list)
}
}
.catch { e -> emit(listOf(e.toErrorState(canRetry = false))) }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState))
fun removeBookmarks(ids: Map<Manga, Set<Long>>) {
fun removeBookmarks(ids: Set<Long>) {
launchJob(Dispatchers.Default) {
val handle = repository.removeBookmarks(ids)
onActionDone.call(ReversibleAction(R.string.bookmarks_removed, handle))
}
}
private fun mapList(data: Map<Manga, List<Bookmark>>): List<ListModel> {
val result = ArrayList<ListModel>(data.values.sumOf { it.size + 1 })
for ((manga, bookmarks) in data) {
result.add(ListHeader(manga.title, R.string.more, manga))
result.addAll(bookmarks)
}
return result
}
}

View File

@@ -28,13 +28,13 @@ fun bookmarkListAD(
binding.root.setOnLongClickListener(listener)
bind {
val data: Any = item.directImageUrl ?: item.toMangaPage()
binding.imageViewThumb.newImageRequest(lifecycleOwner, data)?.run {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
tag(item)
decodeRegion(item.scroll)
source(item.manga.source)
enqueueWith(coil)

View File

@@ -1,32 +1,19 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
class BookmarksAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
) : AsyncListDifferDelegationAdapter<Bookmark>(
DiffCallback(),
bookmarkListAD(coil, lifecycleOwner, clickListener),
) {
private class DiffCallback : DiffUtil.ItemCallback<Bookmark>() {
override fun areItemsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
return oldItem.manga.id == newItem.manga.id &&
oldItem.chapterId == newItem.chapterId &&
oldItem.page == newItem.page
}
override fun areContentsTheSame(oldItem: Bookmark, newItem: Bookmark): Boolean {
return oldItem.imageUrl == newItem.imageUrl
}
) : BaseListAdapter<Bookmark>() {
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkListAD(coil, lifecycleOwner, clickListener))
}
}

View File

@@ -1,69 +0,0 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.util.ext.clearItemDecorations
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
fun bookmarksGroupAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
sharedPool: RecyclerView.RecycledViewPool,
selectionController: SectionedSelectionController<Manga>,
bookmarkClickListener: OnListItemClickListener<Bookmark>,
groupClickListener: OnListItemClickListener<BookmarksGroup>,
) = adapterDelegateViewBinding<BookmarksGroup, ListModel, ItemBookmarksGroupBinding>(
{ layoutInflater, parent -> ItemBookmarksGroupBinding.inflate(layoutInflater, parent, false) },
) {
val viewListenerAdapter = object : View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) = groupClickListener.onItemClick(item, v)
override fun onLongClick(v: View) = groupClickListener.onItemLongClick(item, v)
}
val adapter = BookmarksAdapter(coil, lifecycleOwner, bookmarkClickListener)
binding.recyclerView.setRecycledViewPool(sharedPool)
binding.recyclerView.adapter = adapter
val spacingDecoration = SpacingItemDecoration(context.resources.getDimensionPixelOffset(R.dimen.grid_spacing))
binding.recyclerView.addItemDecoration(spacingDecoration)
binding.root.setOnClickListener(viewListenerAdapter)
binding.root.setOnLongClickListener(viewListenerAdapter)
bind { payloads ->
if (payloads.isEmpty()) {
binding.recyclerView.clearItemDecorations()
binding.recyclerView.addItemDecoration(spacingDecoration)
selectionController.attachToRecyclerView(item.manga, binding.recyclerView)
}
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
size(CoverSizeResolver(binding.imageViewCover))
source(item.manga.source)
enqueueWith(coil)
}
binding.textViewTitle.text = item.manga.title
adapter.items = item.bookmarks
}
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}

View File

@@ -1,77 +0,0 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
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.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.jvm.internal.Intrinsics
class BookmarksGroupAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
selectionController: SectionedSelectionController<Manga>,
listener: ListStateHolderListener,
bookmarkClickListener: OnListItemClickListener<Bookmark>,
groupClickListener: OnListItemClickListener<BookmarksGroup>,
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
init {
val pool = RecyclerView.RecycledViewPool()
delegatesManager
.addDelegate(
bookmarksGroupAD(
coil = coil,
lifecycleOwner = lifecycleOwner,
sharedPool = pool,
selectionController = selectionController,
bookmarkClickListener = bookmarkClickListener,
groupClickListener = groupClickListener,
),
)
.addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD())
.addDelegate(emptyStateListAD(coil, lifecycleOwner, listener))
.addDelegate(errorStateListAD(listener))
}
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return when {
oldItem is BookmarksGroup && newItem is BookmarksGroup -> {
oldItem.manga.id == newItem.manga.id
}
oldItem is LoadingFooter && newItem is LoadingFooter -> {
oldItem.key == newItem.key
}
else -> oldItem.javaClass == newItem.javaClass
}
}
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
return Intrinsics.areEqual(oldItem, newItem)
}
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
return when {
oldItem is BookmarksGroup && newItem is BookmarksGroup -> Unit
else -> super.getChangePayload(oldItem, newItem)
}
}
}
}

View File

@@ -1,31 +0,0 @@
package org.koitharu.kotatsu.bookmarks.ui.model
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.areItemsEquals
class BookmarksGroup(
val manga: Manga,
val bookmarks: List<Bookmark>,
) : ListModel {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BookmarksGroup
if (manga != other.manga) return false
return bookmarks.areItemsEquals(other.bookmarks) { a, b ->
a.imageUrl == b.imageUrl
}
}
override fun hashCode(): Int {
var result = manga.hashCode()
result = 31 * result + bookmarks.sumOf { it.imageUrl.hashCode() }
return result
}
}

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.bookmarks.ui.sheet
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
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.decodeRegion
import org.koitharu.kotatsu.core.util.ext.disposeImageRequest
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemBookmarkLargeBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
fun bookmarkLargeAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
) = adapterDelegateViewBinding<Bookmark, ListModel, ItemBookmarkLargeBinding>(
{ inflater, parent -> ItemBookmarkLargeBinding.inflate(inflater, parent, false) },
) {
val listener = AdapterDelegateClickListenerAdapter(this, clickListener)
binding.root.setOnClickListener(listener)
binding.root.setOnLongClickListener(listener)
bind {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true)
tag(item)
decodeRegion(item.scroll)
source(item.manga.source)
enqueueWith(coil)
}
binding.progressView.percent = item.percent
}
onViewRecycled {
binding.imageViewThumb.disposeImageRequest()
}
}

View File

@@ -0,0 +1,44 @@
package org.koitharu.kotatsu.bookmarks.ui.sheet
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
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.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
class BookmarksAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>,
headerClickListener: ListHeaderClickListener?,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
val list = items
for (i in (0..position).reversed()) {
val item = list.getOrNull(i) ?: continue
if (item is ListHeader) {
return item.getText(context)
}
}
return null
}
}

View File

@@ -0,0 +1,169 @@
package org.koitharu.kotatsu.bookmarks.ui.sheet
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetBehavior
import org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetCallback
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.plus
import org.koitharu.kotatsu.core.util.ext.showDistinct
import org.koitharu.kotatsu.core.util.ext.withArgs
import org.koitharu.kotatsu.databinding.SheetPagesBinding
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.reader.ui.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
class BookmarksSheet :
BaseAdaptiveSheet<SheetPagesBinding>(),
AdaptiveSheetCallback,
OnListItemClickListener<Bookmark> {
private val viewModel by viewModels<BookmarksSheetViewModel>()
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var settings: AppSettings
private var bookmarksAdapter: BookmarksAdapter? = null
private var spanResolver: MangaListSpanResolver? = null
private val spanSizeLookup = SpanSizeLookup()
private val listCommitCallback = Runnable {
spanSizeLookup.invalidateCache()
}
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetPagesBinding {
return SheetPagesBinding.inflate(inflater, container, false)
}
override fun onViewBindingCreated(binding: SheetPagesBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
addSheetCallback(this)
spanResolver = MangaListSpanResolver(binding.root.resources)
bookmarksAdapter = BookmarksAdapter(
coil = coil,
lifecycleOwner = viewLifecycleOwner,
clickListener = this@BookmarksSheet,
headerClickListener = null,
)
viewBinding?.headerBar?.setTitle(R.string.bookmarks)
with(binding.recyclerView) {
addItemDecoration(TypedListSpacingDecoration(context, false))
adapter = bookmarksAdapter
addOnLayoutChangeListener(spanResolver)
spanResolver?.setGridSize(settings.gridSize / 100f, this)
(layoutManager as GridLayoutManager).spanSizeLookup = spanSizeLookup
}
viewModel.content.observe(viewLifecycleOwner, ::onThumbnailsChanged)
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
}
override fun onDestroyView() {
spanResolver = null
bookmarksAdapter = null
spanSizeLookup.invalidateCache()
super.onDestroyView()
}
override fun onItemClick(item: Bookmark, view: View) {
val listener = (parentFragment as? OnPageSelectListener) ?: (activity as? OnPageSelectListener)
if (listener != null) {
listener.onPageSelected(ReaderPage(item.toMangaPage(), item.page, item.chapterId))
} else {
val intent = IntentBuilder(view.context)
.manga(viewModel.manga)
.bookmark(item)
.incognito(true)
.build()
startActivity(intent)
}
dismiss()
}
override fun onStateChanged(sheet: View, newState: Int) {
viewBinding?.recyclerView?.isFastScrollerEnabled = newState == AdaptiveSheetBehavior.STATE_EXPANDED
}
private fun onThumbnailsChanged(list: List<ListModel>) {
val adapter = bookmarksAdapter ?: return
if (adapter.itemCount == 0) {
var position = list.indexOfFirst { it is PageThumbnail && it.isCurrent }
if (position > 0) {
val spanCount = spanResolver?.spanCount ?: 0
val offset = if (position > spanCount + 1) {
(resources.getDimensionPixelSize(R.dimen.manga_list_details_item_height) * 0.6).roundToInt()
} else {
position = 0
0
}
val scrollCallback = RecyclerViewScrollCallback(requireViewBinding().recyclerView, position, offset)
adapter.setItems(list, listCommitCallback + scrollCallback)
} else {
adapter.setItems(list, listCommitCallback)
}
} else {
adapter.setItems(list, listCommitCallback)
}
}
private inner class SpanSizeLookup : GridLayoutManager.SpanSizeLookup() {
init {
isSpanIndexCacheEnabled = true
isSpanGroupIndexCacheEnabled = true
}
override fun getSpanSize(position: Int): Int {
val total = (viewBinding?.recyclerView?.layoutManager as? GridLayoutManager)?.spanCount ?: return 1
return when (bookmarksAdapter?.getItemViewType(position)) {
ListItemType.PAGE_THUMB.ordinal -> 1
else -> total
}
}
fun invalidateCache() {
invalidateSpanGroupIndexCache()
invalidateSpanIndexCache()
}
}
companion object {
const val ARG_MANGA = "manga"
private const val TAG = "BookmarksSheet"
fun show(fm: FragmentManager, manga: Manga) {
BookmarksSheet().withArgs(1) {
putParcelable(ARG_MANGA, ParcelableManga(manga, withChapters = true))
}.showDistinct(fm, TAG)
}
}
}

View File

@@ -0,0 +1,54 @@
package org.koitharu.kotatsu.bookmarks.ui.sheet
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import javax.inject.Inject
@HiltViewModel
class BookmarksSheetViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
mangaRepositoryFactory: MangaRepository.Factory,
bookmarksRepository: BookmarksRepository,
) : BaseViewModel() {
val manga = savedStateHandle.require<ParcelableManga>(BookmarksSheet.ARG_MANGA).manga
private val chaptersLazy = SuspendLazy {
requireNotNull(manga.chapters ?: mangaRepositoryFactory.create(manga.source).getDetails(manga).chapters)
}
val content: StateFlow<List<ListModel>> = bookmarksRepository.observeBookmarks(manga)
.map { mapList(it) }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Lazily, listOf(LoadingFooter()))
private suspend fun mapList(bookmarks: List<Bookmark>): List<ListModel> {
val chapters = chaptersLazy.get()
val bookmarksMap = bookmarks.groupBy { it.chapterId }
val result = ArrayList<ListModel>(bookmarks.size + bookmarksMap.size)
for (chapter in chapters) {
val b = bookmarksMap[chapter.id]
if (b.isNullOrEmpty()) {
continue
}
result += ListHeader(chapter.name)
result.addAll(b)
}
return result
}
}

View File

@@ -8,14 +8,15 @@ 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 org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
@@ -34,8 +35,9 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
with(viewBinding.webView.settings) {
javaScriptEnabled = true
userAgentString = CommonHeadersInterceptor.userAgentChrome
userAgentString = UserAgents.CHROME_MOBILE
}
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView)

View File

@@ -0,0 +1,74 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.Context
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import coil.request.ErrorResult
import coil.request.ImageRequest
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.model.ContentType
class CaptchaNotifier(
private val context: Context,
) : ImageRequest.Listener {
@SuppressLint("MissingPermission")
fun notify(exception: CloudFlareProtectedException) {
val manager = NotificationManagerCompat.from(context)
if (!manager.areNotificationsEnabled()) {
return
}
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(context.getString(R.string.captcha_required))
.setShowBadge(true)
.setVibrationEnabled(false)
.setSound(null, null)
.setLightsEnabled(false)
.build()
manager.createNotificationChannel(channel)
val intent = CloudFlareActivity.newIntent(context, exception.url, exception.headers)
.setData(exception.url.toUri())
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(channel.name)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(NotificationCompat.DEFAULT_SOUND)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setAutoCancel(true)
.setVisibility(
if (exception.source?.contentType == ContentType.HENTAI) {
NotificationCompat.VISIBILITY_SECRET
} else {
NotificationCompat.VISIBILITY_PUBLIC
},
)
.setContentText(
context.getString(
R.string.captcha_required_summary,
exception.source?.title ?: context.getString(R.string.app_name),
),
)
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
.build()
manager.notify(TAG, exception.source.hashCode(), notification)
}
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
val e = result.throwable
if (e is CloudFlareProtectedException) {
notify(e)
}
}
private companion object {
private const val CHANNEL_ID = "captcha"
private const val TAG = CHANNEL_ID
}
}

View File

@@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.webkit.CookieManager
import android.webkit.WebSettings
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets
import androidx.core.net.toUri
@@ -18,12 +17,12 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -49,10 +48,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
val url = intent?.dataString.orEmpty()
with(viewBinding.webView.settings) {
javaScriptEnabled = true
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
userAgentString = intent?.getStringExtra(ARG_UA) ?: UserAgents.CHROME_MOBILE
}
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {

View File

@@ -6,12 +6,14 @@ import android.provider.SearchRecentSuggestions
import android.text.Html
import androidx.collection.arraySetOf
import androidx.room.InvalidationTracker
import androidx.work.WorkManager
import coil.ComponentRegistry
import coil.ImageLoader
import coil.decode.SvgDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -45,6 +47,7 @@ import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestorer
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
@@ -88,6 +91,7 @@ interface AppModule {
mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory,
coverRestorerProvider: Lazy<CoverRestorer>,
): ImageLoader {
val diskCacheFactory = {
val rootDir = context.externalCacheDir ?: context.cacheDir
@@ -104,6 +108,7 @@ interface AppModule {
.diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice())
.eventListenerFactory { coverRestorerProvider.get() }
.components(
ComponentRegistry.Builder()
.add(SvgDecoder.Factory())
@@ -172,5 +177,10 @@ interface AppModule {
fun provideLocalStorageChangesFlow(
@LocalStorageChanges flow: MutableSharedFlow<LocalManga?>,
): SharedFlow<LocalManga?> = flow.asSharedFlow()
@Provides
fun provideWorkManager(
@ApplicationContext context: Context,
): WorkManager = WorkManager.getInstance(context)
}
}

View File

@@ -1,14 +1,13 @@
package org.koitharu.kotatsu
package org.koitharu.kotatsu.core
import android.app.Application
import android.content.Context
import android.os.StrictMode
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.hilt.work.HiltWorkerFactory
import androidx.room.InvalidationTracker
import androidx.work.Configuration
import androidx.work.WorkManager
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -19,20 +18,19 @@ import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
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.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
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.settings.work.WorkScheduleManager
import javax.inject.Inject
import javax.inject.Provider
@HiltAndroidApp
class KotatsuApp : Application(), Configuration.Provider {
open class BaseApp : Application(), Configuration.Provider {
@Inject
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer>
@@ -55,12 +53,12 @@ class KotatsuApp : Application(), Configuration.Provider {
@Inject
lateinit var workScheduleManager: WorkScheduleManager
@Inject
lateinit var workManagerProvider: Provider<WorkManager>
override fun onCreate() {
super.onCreate()
ACRA.errorReporter.putCustomData("isOriginalApp", appValidator.isOriginalApp.toString())
if (BuildConfig.DEBUG) {
enableStrictMode()
}
AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setApplicationLocales(settings.appLocales)
setupActivityLifecycleCallbacks()
@@ -68,7 +66,7 @@ class KotatsuApp : Application(), Configuration.Provider {
setupDatabaseObservers()
}
workScheduleManager.init()
WorkServiceStopHelper(applicationContext).setup()
WorkServiceStopHelper(workManagerProvider).setup()
}
override fun attachBaseContext(base: Context?) {
@@ -131,31 +129,4 @@ class KotatsuApp : Application(), Configuration.Provider {
registerActivityLifecycleCallbacks(it)
}
}
private fun enableStrictMode() {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.setClassInstanceLimit(PageLoader::class.java, 1)
.penaltyLog()
.build(),
)
FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder()
.penaltyDeath()
.detectFragmentReuse()
.detectWrongFragmentContainer()
.detectRetainInstanceUsage()
.detectSetUserVisibleHint()
.detectFragmentTagUsage()
.build()
}
}

View File

@@ -13,5 +13,7 @@ class BackupEntry(
const val HISTORY = "history"
const val CATEGORIES = "categories"
const val FAVOURITES = "favourites"
const val SETTINGS = "settings"
const val BOOKMARKS = "bookmarks"
}
}
}

View File

@@ -5,6 +5,7 @@ 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.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -12,7 +13,10 @@ import javax.inject.Inject
private const val PAGE_SIZE = 10
class BackupRepository @Inject constructor(private val db: MangaDatabase) {
class BackupRepository @Inject constructor(
private val db: MangaDatabase,
private val settings: AppSettings,
) {
suspend fun dumpHistory(): BackupEntry {
var offset = 0
@@ -67,6 +71,36 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
return entry
}
suspend fun dumpBookmarks(): BackupEntry {
val entry = BackupEntry(BackupEntry.BOOKMARKS, JSONArray())
val all = db.bookmarksDao.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)
}
return entry
}
fun dumpSettings(): BackupEntry {
val entry = BackupEntry(BackupEntry.SETTINGS, JSONArray())
val settingsDump = settings.getAllValues().toMutableMap()
settingsDump.remove(AppSettings.KEY_APP_PASSWORD)
settingsDump.remove(AppSettings.KEY_PROXY_PASSWORD)
settingsDump.remove(AppSettings.KEY_PROXY_LOGIN)
settingsDump.remove(AppSettings.KEY_INCOGNITO_MODE)
val json = JsonSerializer(settingsDump).toJson()
entry.data.put(json)
return entry
}
fun createIndex(): BackupEntry {
val entry = BackupEntry(BackupEntry.INDEX, JSONArray())
val json = JSONObject()
@@ -127,4 +161,36 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
}
return result
}
suspend fun restoreBookmarks(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
val mangaJson = item.getJSONObject("manga")
val manga = JsonDeserializer(mangaJson).toMangaEntity()
val tags = item.getJSONArray("tags").mapJSON {
JsonDeserializer(it).toTagEntity()
}
val bookmarks = item.getJSONArray("bookmarks").mapJSON {
JsonDeserializer(it).toBookmarkEntity()
}
result += runCatchingCancellable {
db.withTransaction {
db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags)
db.bookmarksDao.upsert(bookmarks)
}
}
}
return result
}
fun restoreSettings(entry: BackupEntry): CompositeResult {
val result = CompositeResult()
for (item in entry.data.JSONIterator()) {
result += runCatchingCancellable {
settings.upsertAll(JsonDeserializer(item).toMap())
}
}
return result
}
}

View File

@@ -11,8 +11,8 @@ class BackupZipInput(val file: File) : Closeable {
private val zipFile = ZipFile(file)
suspend fun getEntry(name: String): BackupEntry = runInterruptible(Dispatchers.IO) {
val entry = zipFile.getEntry(name)
suspend fun getEntry(name: String): BackupEntry? = runInterruptible(Dispatchers.IO) {
val entry = zipFile.getEntry(name) ?: return@runInterruptible null
val json = zipFile.getInputStream(entry).use {
JSONArray(it.bufferedReader().readText())
}
@@ -22,4 +22,4 @@ class BackupZipInput(val file: File) : Closeable {
override fun close() {
zipFile.close()
}
}
}

View File

@@ -11,6 +11,9 @@ class CompositeResult {
val failures: List<Throwable>
get() = errors.filterNotNull()
val isEmpty: Boolean
get() = errors.isEmpty() && successCount == 0
val isAllSuccess: Boolean
get() = errors.none { it != null }
@@ -36,4 +39,4 @@ class CompositeResult {
result.errors.addAll(other.errors)
return result
}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
@@ -34,14 +35,14 @@ class JsonDeserializer(private val json: JSONObject) {
largeCoverUrl = json.getStringOrNull("large_cover_url"),
state = json.getStringOrNull("state"),
author = json.getStringOrNull("author"),
source = json.getString("source")
source = json.getString("source"),
)
fun toTagEntity() = TagEntity(
id = json.getLong("id"),
title = json.getString("title"),
key = json.getString("key"),
source = json.getString("source")
source = json.getString("source"),
)
fun toHistoryEntity() = HistoryEntity(
@@ -65,4 +66,28 @@ class JsonDeserializer(private val json: JSONObject) {
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
deletedAt = 0L,
)
fun toBookmarkEntity() = BookmarkEntity(
mangaId = json.getLong("manga_id"),
pageId = json.getLong("page_id"),
chapterId = json.getLong("chapter_id"),
page = json.getInt("page"),
scroll = json.getInt("scroll"),
imageUrl = json.getString("image_url"),
createdAt = json.getLong("created_at"),
percent = json.getDouble("percent").toFloat(),
)
fun toMap(): Map<String, Any?> {
val map = mutableMapOf<String, Any?>()
val keys = json.keys()
while (keys.hasNext()) {
val key = keys.next()
val value = json.get(key)
map[key] = value
}
return map
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.backup
import org.json.JSONObject
import org.koitharu.kotatsu.bookmarks.data.BookmarkEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
@@ -15,7 +16,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("category_id", e.categoryId)
put("sort_key", e.sortKey)
put("created_at", e.createdAt)
}
},
)
constructor(e: FavouriteCategoryEntity) : this(
@@ -27,7 +28,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("order", e.order)
put("track", e.track)
put("show_in_lib", e.isVisibleInLibrary)
}
},
)
constructor(e: HistoryEntity) : this(
@@ -39,7 +40,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("page", e.page)
put("scroll", e.scroll)
put("percent", e.percent)
}
},
)
constructor(e: TagEntity) : this(
@@ -48,7 +49,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("title", e.title)
put("key", e.key)
put("source", e.source)
}
},
)
constructor(e: MangaEntity) : this(
@@ -65,8 +66,25 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("state", e.state)
put("author", e.author)
put("source", e.source)
}
},
)
constructor(e: BookmarkEntity) : this(
JSONObject().apply {
put("manga_id", e.mangaId)
put("page_id", e.pageId)
put("chapter_id", e.chapterId)
put("page", e.page)
put("scroll", e.scroll)
put("image_url", e.imageUrl)
put("created_at", e.createdAt)
put("percent", e.percent)
},
)
constructor(m: Map<String, *>) : this(
JSONObject(m),
)
fun toJson(): JSONObject = json
}
}

View File

@@ -16,8 +16,29 @@ interface ContentCache {
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
data class Key(
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
class Key(
val source: MangaSource,
val url: String,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Key
if (source != other.source) return false
return url == other.url
}
override fun hashCode(): Int {
var result = source.hashCode()
result = 31 * result + url.hashCode()
return result
}
}
}

View File

@@ -16,6 +16,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
override val isCachingEnabled: Boolean = true
@@ -35,6 +36,14 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
pagesCache[ContentCache.Key(source, url)] = pages
}
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
}
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
relatedMangaCache[ContentCache.Key(source, url)] = related
}
override fun onConfigurationChanged(newConfig: Configuration) = Unit
override fun onLowMemory() = Unit
@@ -42,6 +51,7 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
override fun onTrimMemory(level: Int) {
trimCache(detailsCache, level)
trimCache(pagesCache, level)
trimCache(relatedMangaCache, level)
}
private fun trimCache(cache: ExpiringLruCache<*>, level: Int) {

View File

@@ -15,4 +15,8 @@ class StubContentCache : ContentCache {
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? = null
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) = Unit
}

View File

@@ -12,11 +12,13 @@ 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.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.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.db.migrations.Migration10To11
@@ -25,6 +27,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration12To13
import org.koitharu.kotatsu.core.db.migrations.Migration13To14
import org.koitharu.kotatsu.core.db.migrations.Migration14To15
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
import org.koitharu.kotatsu.core.db.migrations.Migration16To17
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
@@ -49,14 +52,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 = 16
const val DATABASE_VERSION = 17
@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,
ScrobblingEntity::class, MangaSourceEntity::class,
],
version = DATABASE_VERSION,
)
@@ -83,30 +86,32 @@ abstract class MangaDatabase : RoomDatabase() {
abstract val bookmarksDao: BookmarksDao
abstract val scrobblingDao: ScrobblingDao
abstract val sourcesDao: MangaSourcesDao
}
val databaseMigrations: Array<Migration>
get() = arrayOf(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
Migration9To10(),
Migration10To11(),
Migration11To12(),
Migration12To13(),
Migration13To14(),
Migration14To15(),
Migration15To16(),
)
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration1To2(),
Migration2To3(),
Migration3To4(),
Migration4To5(),
Migration5To6(),
Migration6To7(),
Migration7To8(),
Migration8To9(),
Migration9To10(),
Migration10To11(),
Migration11To12(),
Migration12To13(),
Migration13To14(),
Migration14To15(),
Migration15To16(),
Migration16To17(context),
)
fun MangaDatabase(context: Context): MangaDatabase = Room
.databaseBuilder(context, MangaDatabase::class.java, "kotatsu-db")
.addMigrations(*databaseMigrations)
.addMigrations(*getDatabaseMigrations(context))
.addCallback(DatabasePrePopulateCallback(context.resources))
.build()

View File

@@ -1,6 +1,12 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.Upsert
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@@ -13,6 +19,10 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE manga_id = :id")
abstract suspend fun find(id: Long): MangaWithTags?
@Transaction
@Query("SELECT * FROM manga WHERE public_url = :publicUrl")
abstract suspend fun findByPublicUrl(publicUrl: String): MangaWithTags?
@Transaction
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
@@ -21,8 +31,8 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND source = :source AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(manga: MangaEntity): Long
@Upsert
abstract suspend fun upsert(manga: MangaEntity)
@Update(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun update(manga: MangaEntity): Int
@@ -35,15 +45,13 @@ abstract class MangaDao {
@Transaction
open suspend fun upsert(manga: MangaEntity, tags: Iterable<TagEntity>? = null) {
if (update(manga) <= 0) {
insert(manga)
if (tags != null) {
clearTagRelation(manga.id)
tags.map {
MangaTagsEntity(manga.id, it.id)
}.forEach {
insertTagRelation(it)
}
upsert(manga)
if (tags != null) {
clearTagRelation(manga.id)
tags.map {
MangaTagsEntity(manga.id, it.id)
}.forEach {
insertTagRelation(it)
}
}
}

View File

@@ -0,0 +1,57 @@
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 androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.core.db.entity.MangaSourceEntity
@Dao
abstract class MangaSourcesDao {
@Query("SELECT * FROM sources ORDER BY sort_key")
abstract suspend fun findAll(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
abstract suspend fun findAllEnabled(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 1 ORDER BY sort_key")
abstract fun observeEnabled(): Flow<List<MangaSourceEntity>>
@Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
@Query("SELECT IFNULL(MAX(sort_key),0) FROM sources")
abstract suspend fun getMaxSortKey(): Int
@Query("UPDATE sources SET enabled = 0")
abstract suspend fun disableAllSources()
@Query("UPDATE sources SET sort_key = :sortKey WHERE source = :source")
abstract suspend fun setSortKey(source: String, sortKey: Int)
@Insert(onConflict = OnConflictStrategy.IGNORE)
@Transaction
abstract suspend fun insertIfAbsent(entries: Collection<MangaSourceEntity>)
@Upsert
abstract suspend fun upsert(entry: MangaSourceEntity)
@Transaction
open suspend fun setEnabled(source: String, isEnabled: Boolean) {
if (updateIsEnabled(source, isEnabled) == 0) {
val entity = MangaSourceEntity(
source = source,
isEnabled = isEnabled,
sortKey = getMaxSortKey() + 1,
)
upsert(entity)
}
}
@Query("UPDATE sources SET enabled = :isEnabled WHERE source = :source")
protected abstract suspend fun updateIsEnabled(source: String, isEnabled: Boolean): Int
}

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.core.db.dao
import androidx.room.*
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
@@ -12,6 +14,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE manga_tags.manga_id IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",
@@ -21,7 +24,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source
WHERE tags.source = :source
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",
@@ -31,7 +34,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE tags.source = :source AND title LIKE :query
WHERE tags.source = :source AND title LIKE :query
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",
@@ -41,7 +44,7 @@ abstract class TagsDao {
@Query(
"""SELECT tags.* FROM tags
LEFT JOIN manga_tags ON tags.tag_id = manga_tags.tag_id
WHERE title LIKE :query
WHERE title LIKE :query AND manga_tags.manga_id IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)
GROUP BY tags.title
ORDER BY COUNT(manga_id) DESC
LIMIT :limit""",

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(
tableName = "sources",
)
data class MangaSourceEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "source")
val source: String,
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
)

View File

@@ -21,9 +21,7 @@ class MangaWithTags(
other as MangaWithTags
if (manga != other.manga) return false
if (tags != other.tags) return false
return true
return tags == other.tags
}
override fun hashCode(): Int {
@@ -31,4 +29,4 @@ class MangaWithTags(
result = 31 * result + tags.hashCode()
return result
}
}
}

View File

@@ -9,5 +9,7 @@ class Migration13To14 : Migration(13, 14) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE favourites ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE history ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
}
}

View File

@@ -5,8 +5,5 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration14To15 : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
}
override fun migrate(database: SupportSQLiteDatabase) = Unit
}

View File

@@ -0,0 +1,41 @@
package org.koitharu.kotatsu.core.db.migrations
import android.content.Context
import androidx.preference.PreferenceManager
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.parsers.model.MangaSource
class Migration16To17(context: Context) : Migration(16, 17) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `sources` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, PRIMARY KEY(`source`))")
database.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
val sources = MangaSource.entries
for (source in sources) {
if (source == MangaSource.LOCAL) {
continue
}
val name = source.name
val isHidden = name in hiddenSources
var sortKey = order.indexOf(name)
if (sortKey == -1) {
if (isHidden) {
sortKey = order.size + source.ordinal
} else {
continue
}
}
database.execSQL(
"INSERT INTO `sources` (`source`, `enabled`, `sort_key`) VALUES (?, ?, ?)",
arrayOf(name, (!isHidden).toInt(), sortKey),
)
}
}
private fun Boolean.toInt() = if (this) 1 else 0
}

View File

@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.core.exceptions
import okhttp3.Headers
import okio.IOException
import org.koitharu.kotatsu.parsers.model.MangaSource
class CloudFlareProtectedException(
val url: String,
val source: MangaSource?,
@Transient val headers: Headers,
) : IOException("Protected by CloudFlare")

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.exceptions
import okio.IOException
import java.util.Date
class TooManyRequestExceptions(
val url: String,
val retryAt: Date?,
) : IOException()

View File

@@ -33,11 +33,21 @@ abstract class ErrorObserver(
return resolver != null && ExceptionResolver.canResolve(error)
}
private fun isAlive(): Boolean {
return when {
fragment != null -> fragment.view != null
activity != null -> !activity.isDestroyed
else -> true
}
}
protected fun resolve(error: Throwable) {
lifecycleScope.launch {
val isResolved = resolver?.resolve(error) ?: false
if (isActive) {
onResolved?.accept(isResolved)
if (isAlive()) {
lifecycleScope.launch {
val isResolved = resolver?.resolve(error) ?: false
if (isActive) {
onResolved?.accept(isResolved)
}
}
}
}

View File

@@ -0,0 +1,17 @@
package org.koitharu.kotatsu.core.exceptions.resolve
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
class ToastErrorObserver(
host: View,
fragment: Fragment?,
) : ErrorObserver(host, fragment, null, null) {
override suspend fun emit(value: Throwable) {
val toast = Toast.makeText(host.context, value.getDisplayMessage(host.context.resources), Toast.LENGTH_SHORT)
toast.show()
}
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.core.fs
import android.os.Build
import org.koitharu.kotatsu.core.util.iterator.CloseableIterator
import org.koitharu.kotatsu.core.util.iterator.MappingIterator
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
class FileSequence(private val dir: File) : Sequence<File> {
override fun iterator(): Iterator<File> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val stream = Files.newDirectoryStream(dir.toPath())
CloseableIterator(MappingIterator(stream.iterator(), Path::toFile), stream)
} else {
dir.listFiles().orEmpty().iterator()
}
}
}

View File

@@ -40,9 +40,7 @@ class VersionId(
if (minor != other.minor) return false
if (build != other.build) return false
if (variantType != other.variantType) return false
if (variantNumber != other.variantNumber) return false
return true
return variantNumber == other.variantNumber
}
override fun hashCode(): Int {

View File

@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.*
import java.util.Date
@Parcelize
data class FavouriteCategory(
@@ -14,4 +16,20 @@ data class FavouriteCategory(
val createdAt: Date,
val isTrackingEnabled: Boolean,
val isVisibleInLibrary: Boolean,
) : Parcelable
) : Parcelable, ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is FavouriteCategory && id == other.id
}
override fun getChangePayload(previousState: ListModel): Any? {
if (previousState !is FavouriteCategory) {
return null
}
return if (isTrackingEnabled != previousState.isTrackingEnabled || isVisibleInLibrary != previousState.isVisibleInLibrary) {
ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
} else {
null
}
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.model
import android.net.Uri
import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.core.util.ext.iterator
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
@@ -8,10 +9,14 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapToSet
@JvmName("mangaIds")
fun Collection<Manga>.ids() = mapToSet { it.id }
fun Collection<Manga>.distinctById() = distinctBy { it.id }
@JvmName("chaptersIds")
fun Collection<MangaChapter>.ids() = mapToSet { it.id }
fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
if (size <= 1) {
return size
@@ -62,3 +67,10 @@ fun Manga.getPreferredBranch(history: MangaHistory?): String? {
val Manga.isLocal: Boolean
get() = source == MangaSource.LOCAL
val Manga.appUrl: Uri
get() = Uri.parse("https://kotatsu.app/manga").buildUpon()
.appendQueryParameter("source", source.name)
.appendQueryParameter("name", title)
.appendQueryParameter("url", url)
.build()

View File

@@ -10,7 +10,7 @@ fun MangaSource.getLocaleTitle(): String? {
}
fun MangaSource(name: String): MangaSource {
MangaSource.values().forEach {
MangaSource.entries.forEach {
if (it.name == name) return it
}
return MangaSource.DUMMY

View File

@@ -1,98 +0,0 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaTag
fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
out.writeLong(id)
out.writeString(title)
out.writeString(altTitle)
out.writeString(url)
out.writeString(publicUrl)
out.writeFloat(rating)
ParcelCompat.writeBoolean(out, isNsfw)
out.writeString(coverUrl)
out.writeString(largeCoverUrl)
out.writeString(description)
out.writeParcelable(ParcelableMangaTags(tags), flags)
out.writeSerializable(state)
out.writeString(author)
if (withChapters) {
out.writeParcelable(chapters?.let(::ParcelableMangaChapters), flags)
} else {
out.writeString(null)
}
out.writeSerializable(source)
}
fun Parcel.readManga() = Manga(
id = readLong(),
title = requireNotNull(readString()),
altTitle = readString(),
url = requireNotNull(readString()),
publicUrl = requireNotNull(readString()),
rating = readFloat(),
isNsfw = ParcelCompat.readBoolean(this),
coverUrl = requireNotNull(readString()),
largeCoverUrl = readString(),
description = readString(),
tags = requireNotNull(readParcelableCompat<ParcelableMangaTags>()).tags,
state = readSerializableCompat(),
author = readString(),
chapters = readParcelableCompat<ParcelableMangaChapters>()?.chapters,
source = checkNotNull(readSerializableCompat()),
)
fun MangaPage.writeToParcel(out: Parcel) {
out.writeLong(id)
out.writeString(url)
out.writeString(preview)
out.writeSerializable(source)
}
fun Parcel.readMangaPage() = MangaPage(
id = readLong(),
url = requireNotNull(readString()),
preview = readString(),
source = checkNotNull(readSerializableCompat()),
)
fun MangaChapter.writeToParcel(out: Parcel) {
out.writeLong(id)
out.writeString(name)
out.writeInt(number)
out.writeString(url)
out.writeString(scanlator)
out.writeLong(uploadDate)
out.writeString(branch)
out.writeSerializable(source)
}
fun Parcel.readMangaChapter() = MangaChapter(
id = readLong(),
name = requireNotNull(readString()),
number = readInt(),
url = requireNotNull(readString()),
scanlator = readString(),
uploadDate = readLong(),
branch = readString(),
source = checkNotNull(readSerializableCompat()),
)
fun MangaTag.writeToParcel(out: Parcel) {
out.writeString(title)
out.writeString(key)
out.writeSerializable(source)
}
fun Parcel.readMangaTag() = MangaTag(
title = requireNotNull(readString()),
key = requireNotNull(readString()),
source = checkNotNull(readSerializableCompat()),
)

View File

@@ -2,56 +2,83 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ParcelCompat
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import org.koitharu.kotatsu.core.util.ext.readParcelableCompat
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.Manga
// Limits to avoid TransactionTooLargeException
private const val MAX_SAFE_SIZE = 1024 * 100 // Assume that 100 kb is safe parcel size
private const val MAX_SAFE_CHAPTERS_COUNT = 24 // this is 100% safe
class ParcelableManga(
@Parcelize
data class ParcelableManga(
val manga: Manga,
private val withChapters: Boolean,
) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readManga(), true)
override fun writeToParcel(parcel: Parcel, flags: Int) {
val chapters = manga.chapters
if (!withChapters || chapters == null) {
manga.writeToParcel(parcel, flags, withChapters = false)
return
}
if (chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
// fast path
manga.writeToParcel(parcel, flags, withChapters = true)
return
}
val tempParcel = Parcel.obtain()
manga.writeToParcel(tempParcel, flags, withChapters = true)
val size = tempParcel.dataSize()
if (size < MAX_SAFE_SIZE) {
parcel.appendFrom(tempParcel, 0, size)
} else {
manga.writeToParcel(parcel, flags, withChapters = false)
}
tempParcel.recycle()
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "ParcelableManga(manga=$manga, withChapters=$withChapters)"
}
companion object CREATOR : Parcelable.Creator<ParcelableManga> {
override fun createFromParcel(parcel: Parcel): ParcelableManga {
return ParcelableManga(parcel)
companion object : Parceler<ParcelableManga> {
private fun Manga.writeToParcel(out: Parcel, flags: Int, withChapters: Boolean) {
out.writeLong(id)
out.writeString(title)
out.writeString(altTitle)
out.writeString(url)
out.writeString(publicUrl)
out.writeFloat(rating)
ParcelCompat.writeBoolean(out, isNsfw)
out.writeString(coverUrl)
out.writeString(largeCoverUrl)
out.writeString(description)
out.writeParcelable(ParcelableMangaTags(tags), flags)
out.writeSerializable(state)
out.writeString(author)
val parcelableChapters = if (withChapters) null else chapters?.let(::ParcelableMangaChapters)
out.writeParcelable(parcelableChapters, flags)
out.writeSerializable(source)
}
override fun newArray(size: Int): Array<ParcelableManga?> {
return arrayOfNulls(size)
override fun ParcelableManga.write(parcel: Parcel, flags: Int) {
val chapters = manga.chapters
if (!withChapters || chapters == null) {
manga.writeToParcel(parcel, flags, withChapters = false)
return
}
if (chapters.size <= MAX_SAFE_CHAPTERS_COUNT) {
// fast path
manga.writeToParcel(parcel, flags, withChapters = true)
return
}
val tempParcel = Parcel.obtain()
manga.writeToParcel(tempParcel, flags, withChapters = true)
val size = tempParcel.dataSize()
if (size < MAX_SAFE_SIZE) {
parcel.appendFrom(tempParcel, 0, size)
} else {
manga.writeToParcel(parcel, flags, withChapters = false)
}
tempParcel.recycle()
}
override fun create(parcel: Parcel) = ParcelableManga(
Manga(
id = parcel.readLong(),
title = requireNotNull(parcel.readString()),
altTitle = parcel.readString(),
url = requireNotNull(parcel.readString()),
publicUrl = requireNotNull(parcel.readString()),
rating = parcel.readFloat(),
isNsfw = ParcelCompat.readBoolean(parcel),
coverUrl = requireNotNull(parcel.readString()),
largeCoverUrl = parcel.readString(),
description = parcel.readString(),
tags = requireNotNull(parcel.readParcelableCompat<ParcelableMangaTags>()).tags,
state = parcel.readSerializableCompat(),
author = parcel.readString(),
chapters = parcel.readParcelableCompat<ParcelableMangaChapters>()?.chapters,
source = requireNotNull(parcel.readSerializableCompat()),
),
withChapters = true
)
}
}

View File

@@ -2,38 +2,36 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.MangaChapter
class ParcelableMangaChapters(
val chapters: List<MangaChapter>,
) : Parcelable {
constructor(parcel: Parcel) : this(
List(parcel.readInt()) { parcel.readMangaChapter() }
object MangaChapterParceler : Parceler<MangaChapter> {
override fun create(parcel: Parcel) = MangaChapter(
id = parcel.readLong(),
name = requireNotNull(parcel.readString()),
number = parcel.readInt(),
url = requireNotNull(parcel.readString()),
scanlator = parcel.readString(),
uploadDate = parcel.readLong(),
branch = parcel.readString(),
source = requireNotNull(parcel.readSerializableCompat()),
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(chapters.size)
for (chapter in chapters) {
chapter.writeToParcel(parcel)
}
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "ParcelableMangaChapters(chapters=$chapters)"
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaChapters> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaChapters {
return ParcelableMangaChapters(parcel)
}
override fun newArray(size: Int): Array<ParcelableMangaChapters?> {
return arrayOfNulls(size)
}
override fun MangaChapter.write(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeString(name)
parcel.writeInt(number)
parcel.writeString(url)
parcel.writeString(scanlator)
parcel.writeLong(uploadDate)
parcel.writeString(branch)
parcel.writeSerializable(source)
}
}
@Parcelize
@TypeParceler<MangaChapter, MangaChapterParceler>
data class ParcelableMangaChapters(val chapters: List<MangaChapter>) : Parcelable

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.MangaPage
object MangaPageParceler : Parceler<MangaPage> {
override fun create(parcel: Parcel) = MangaPage(
id = parcel.readLong(),
url = requireNotNull(parcel.readString()),
preview = parcel.readString(),
source = requireNotNull(parcel.readSerializableCompat()),
)
override fun MangaPage.write(parcel: Parcel, flags: Int) {
parcel.writeLong(id)
parcel.writeString(url)
parcel.writeString(preview)
parcel.writeSerializable(source)
}
}
@Parcelize
@TypeParceler<MangaPage, MangaPageParceler>
class ParcelableMangaPage(val page: MangaPage) : Parcelable

View File

@@ -1,39 +0,0 @@
package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.parsers.model.MangaPage
class ParcelableMangaPages(
val pages: List<MangaPage>,
) : Parcelable {
constructor(parcel: Parcel) : this(
List(parcel.readInt()) { parcel.readMangaPage() }
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(pages.size)
for (page in pages) {
page.writeToParcel(parcel)
}
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "ParcelableMangaPages(pages=$pages)"
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaPages> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaPages {
return ParcelableMangaPages(parcel)
}
override fun newArray(size: Int): Array<ParcelableMangaPages?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -2,39 +2,26 @@ package org.koitharu.kotatsu.core.model.parcelable
import android.os.Parcel
import android.os.Parcelable
import org.koitharu.kotatsu.core.util.ext.Set
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler
import org.koitharu.kotatsu.core.util.ext.readSerializableCompat
import org.koitharu.kotatsu.parsers.model.MangaTag
class ParcelableMangaTags(
val tags: Set<MangaTag>,
) : Parcelable {
constructor(parcel: Parcel) : this(
Set(parcel.readInt()) { parcel.readMangaTag() },
object MangaTagParceler : Parceler<MangaTag> {
override fun create(parcel: Parcel) = MangaTag(
title = requireNotNull(parcel.readString()),
key = requireNotNull(parcel.readString()),
source = requireNotNull(parcel.readSerializableCompat()),
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(tags.size)
for (tag in tags) {
tag.writeToParcel(parcel)
}
}
override fun describeContents(): Int {
return 0
}
override fun toString(): String {
return "ParcelableMangaTags(tags=$tags)"
}
companion object CREATOR : Parcelable.Creator<ParcelableMangaTags> {
override fun createFromParcel(parcel: Parcel): ParcelableMangaTags {
return ParcelableMangaTags(parcel)
}
override fun newArray(size: Int): Array<ParcelableMangaTags?> {
return arrayOfNulls(size)
}
override fun MangaTag.write(parcel: Parcel, flags: Int) {
parcel.writeString(title)
parcel.writeString(key)
parcel.writeSerializable(source)
}
}
@Parcelize
@TypeParceler<MangaTag, MangaTagParceler>
data class ParcelableMangaTags(val tags: Set<MangaTag>) : Parcelable

View File

@@ -3,23 +3,26 @@ package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.internal.closeQuietly
import org.jsoup.Jsoup
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.net.HttpURLConnection.HTTP_FORBIDDEN
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
private const val HEADER_SERVER = "Server"
private const val SERVER_CLOUDFLARE = "cloudflare"
class CloudFlareInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
val content = response.body?.source()?.peek()?.use {
Jsoup.parse(it.inputStream(), Charsets.UTF_8.name(), response.request.url.toString())
} ?: return response
if (content.getElementById("challenge-error-title") != null) {
val request = response.request
response.closeQuietly()
throw CloudFlareProtectedException(
url = request.url.toString(),
source = request.tag(MangaSource::class.java),
headers = request.headers,
)
}

View File

@@ -15,6 +15,7 @@ object CommonHeaders {
const val AUTHORIZATION = "Authorization"
const val CACHE_CONTROL = "Cache-Control"
const val PROXY_AUTHORIZATION = "Proxy-Authorization"
const val RETRY_AFTER = "Retry-After"
val CACHE_CONTROL_NO_STORE: CacheControl
get() = CacheControl.Builder().noStore().build()

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.core.network
import android.os.Build
import android.util.Log
import dagger.Lazy
import okhttp3.Headers
@@ -10,11 +9,11 @@ import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mergeWith
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.mergeWith
import java.net.IDN
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -39,7 +38,7 @@ class CommonHeadersInterceptor @Inject constructor(
headersBuilder.mergeWith(it, replaceExisting = false)
}
if (headersBuilder[CommonHeaders.USER_AGENT] == null) {
headersBuilder[CommonHeaders.USER_AGENT] = userAgentFallback
headersBuilder[CommonHeaders.USER_AGENT] = UserAgents.CHROME_MOBILE
}
if (headersBuilder[CommonHeaders.REFERER] == null && repository != null) {
val idn = IDN.toASCII(repository.domain)
@@ -62,26 +61,4 @@ class CommonHeadersInterceptor @Inject constructor(
override fun request(): Request = request
}
companion object {
val userAgentFallback
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language,
)
val userAgentChrome
get() = (
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/100.0.4896.127 Mobile Safari/537.36"
).format(
Build.VERSION.RELEASE,
Build.MODEL,
)
}
}

View File

@@ -67,6 +67,7 @@ interface NetworkModule {
cache(cache)
addInterceptor(GZipInterceptor())
addInterceptor(CloudFlareInterceptor())
addInterceptor(RateLimitInterceptor())
if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor())
}

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
class RateLimitInterceptor : Interceptor {
private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ZZZ", Locale.ENGLISH)
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.code == 429) {
val retryDate = response.header(CommonHeaders.RETRY_AFTER)?.parseRetryDate()
val request = response.request
response.closeQuietly()
throw TooManyRequestExceptions(
url = request.url.toString(),
retryAt = retryDate,
)
}
return response
}
private fun String.parseRetryDate(): Date? {
toIntOrNull()?.let {
return Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(it.toLong()))
}
return dateFormat.parse(this)
}
}

View File

@@ -4,6 +4,7 @@ import android.webkit.CookieManager
import androidx.annotation.WorkerThread
import okhttp3.Cookie
import okhttp3.HttpUrl
import org.koitharu.kotatsu.core.util.ext.newBuilder
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
@@ -30,6 +31,21 @@ class AndroidCookieJar : MutableCookieJar {
}
}
override fun removeCookies(url: HttpUrl) {
val cookies = loadForRequest(url)
if (cookies.isEmpty()) {
return
}
val urlString = url.toString()
for (c in cookies) {
val nc = c.newBuilder()
.expiresAt(System.currentTimeMillis() - 100000)
.build()
cookieManager.setCookie(urlString, nc.toString())
}
check(loadForRequest(url).isEmpty())
}
override suspend fun clear() = suspendCoroutine<Boolean> { continuation ->
cookieManager.removeAllCookies(continuation::resume)
}

View File

@@ -73,9 +73,7 @@ class CookieWrapper(
other as CookieWrapper
if (cookie != other.cookie) return false
return true
return cookie == other.cookie
}
override fun hashCode(): Int {

View File

@@ -13,5 +13,8 @@ interface MutableCookieJar : CookieJar {
@WorkerThread
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>)
@WorkerThread
fun removeCookies(url: HttpUrl)
suspend fun clear(): Boolean
}

View File

@@ -21,6 +21,7 @@ class PreferencesCookieJar(
private var isLoaded = false
@WorkerThread
@Synchronized
override fun loadForRequest(url: HttpUrl): List<Cookie> {
loadPersistent()
val expired = HashSet<String>()
@@ -40,6 +41,7 @@ class PreferencesCookieJar(
}
@WorkerThread
@Synchronized
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val wrapped = cookies.map { CookieWrapper(it) }
prefs.edit(commit = true) {
@@ -53,6 +55,22 @@ class PreferencesCookieJar(
}
}
@Synchronized
@WorkerThread
override fun removeCookies(url: HttpUrl) {
loadPersistent()
val toRemove = HashSet<String>()
for ((key, cookie) in cache) {
if (cookie.isExpired() || cookie.cookie.matches(url)) {
toRemove += key
}
}
if (toRemove.isNotEmpty()) {
cache.removeAll(toRemove)
removePersistent(toRemove)
}
}
override suspend fun clear(): Boolean {
cache.clear()
withContext(Dispatchers.IO) {

View File

@@ -1,12 +1,9 @@
package org.koitharu.kotatsu.core.os
import android.app.ActivityManager
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ShortcutManager
import android.os.Build
import android.util.Size
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
@@ -16,6 +13,7 @@ import androidx.room.InvalidationTracker
import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.Size
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -28,6 +26,7 @@ import org.koitharu.kotatsu.core.ui.image.ThumbnailTransformation
import org.koitharu.kotatsu.core.util.ext.getDrawableOrThrow
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -44,7 +43,9 @@ class AppShortcutManager @Inject constructor(
private val settings: AppSettings,
) : InvalidationTracker.Observer(TABLE_HISTORY), SharedPreferences.OnSharedPreferenceChangeListener {
private val iconSize by lazy { getIconSize(context) }
private val iconSize by lazy {
Size(ShortcutManagerCompat.getIconMaxWidth(context), ShortcutManagerCompat.getIconMaxHeight(context))
}
private var shortcutsUpdateJob: Job? = null
init {
@@ -52,7 +53,7 @@ class AppShortcutManager @Inject constructor(
}
override fun onInvalidated(tables: Set<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || !settings.isDynamicShortcutsEnabled) {
if (!settings.isDynamicShortcutsEnabled) {
return
}
val prevJob = shortcutsUpdateJob
@@ -63,7 +64,7 @@ class AppShortcutManager @Inject constructor(
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && key == AppSettings.KEY_SHORTCUTS) {
if (key == AppSettings.KEY_SHORTCUTS) {
if (settings.isDynamicShortcutsEnabled) {
onInvalidated(emptySet())
} else {
@@ -73,11 +74,7 @@ class AppShortcutManager @Inject constructor(
}
suspend fun requestPinShortcut(manga: Manga): Boolean {
return ShortcutManagerCompat.requestPinShortcut(
context,
buildShortcutInfo(manga).build(),
null,
)
return ShortcutManagerCompat.requestPinShortcut(context, buildShortcutInfo(manga), null)
}
@VisibleForTesting
@@ -85,49 +82,34 @@ class AppShortcutManager @Inject constructor(
return shortcutsUpdateJob?.join() != null
}
fun isDynamicShortcutsAvailable(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return false
}
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
return manager.maxShortcutCountPerActivity > 0
}
fun notifyMangaOpened(mangaId: Long) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
return
}
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
manager.reportShortcutUsed(mangaId.toString())
ShortcutManagerCompat.reportShortcutUsed(context, mangaId.toString())
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private suspend fun updateShortcutsImpl() = runCatchingCancellable {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
val maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context).coerceAtLeast(5)
val shortcuts = historyRepository.getList(0, maxShortcuts)
.filter { x -> x.title.isNotEmpty() }
.map { buildShortcutInfo(it).build().toShortcutInfo() }
manager.dynamicShortcuts = shortcuts
.map { buildShortcutInfo(it) }
ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts)
}.onFailure {
it.printStackTraceDebug()
}
@RequiresApi(Build.VERSION_CODES.N_MR1)
private fun clearShortcuts() {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
try {
manager.removeAllDynamicShortcuts()
ShortcutManagerCompat.removeAllDynamicShortcuts(context)
} catch (_: IllegalStateException) {
}
}
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat {
val icon = runCatchingCancellable {
coil.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.size(iconSize.width, iconSize.height)
.tag(manga.source)
.size(iconSize)
.source(manga.source)
.scale(Scale.FILL)
.transformations(ThumbnailTransformation())
.build(),
@@ -141,22 +123,17 @@ class AppShortcutManager @Inject constructor(
.setShortLabel(manga.title)
.setLongLabel(manga.title)
.setIcon(icon)
.setLongLived(true)
.setIntent(
ReaderActivity.IntentBuilder(context)
.mangaId(manga.id)
.build(),
)
.build()
}
private fun getIconSize(context: Context): Size {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
(context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager).let {
Size(it.iconMaxWidth, it.iconMaxHeight)
}
} else {
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).launcherLargeIconSize.let {
Size(it, it)
}
}
fun isDynamicShortcutsAvailable(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 &&
context.getSystemService(ShortcutManager::class.java).maxShortcutCountPerActivity > 0
}
}

View File

@@ -0,0 +1,15 @@
package org.koitharu.kotatsu.core.os
import android.content.Intent
import android.os.Build
import android.provider.Settings
@Suppress("FunctionName")
fun NetworkManageIntent(): Intent {
val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Settings.Panel.ACTION_INTERNET_CONNECTIVITY
} else {
Settings.ACTION_WIRELESS_SETTINGS
}
return Intent(action)
}

View File

@@ -17,10 +17,12 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import javax.inject.Inject
import javax.inject.Provider
@Reusable
class MangaDataRepository @Inject constructor(
private val db: MangaDatabase,
private val resolverProvider: Provider<MangaLinkResolver>,
) {
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
@@ -63,10 +65,15 @@ class MangaDataRepository @Inject constructor(
return db.mangaDao.find(mangaId)?.toManga()
}
suspend fun findMangaByPublicUrl(publicUrl: String): Manga? {
return db.mangaDao.findByPublicUrl(publicUrl)?.toManga()
}
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
intent.manga != null -> intent.manga
intent.mangaId != 0L -> findMangaById(intent.mangaId)
else -> null // TODO resolve uri
intent.uri != null -> resolverProvider.get().resolve(intent.uri)
else -> null
}
suspend fun storeManga(manga: Manga) {

View File

@@ -0,0 +1,119 @@
package org.koitharu.kotatsu.core.parser
import android.net.Uri
import dagger.Reusable
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.almostEquals
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.parsers.util.toRelativeUrl
import javax.inject.Inject
@Reusable
class MangaLinkResolver @Inject constructor(
private val repositoryFactory: MangaRepository.Factory,
private val sourcesRepository: MangaSourcesRepository,
private val dataRepository: MangaDataRepository,
) {
suspend fun resolve(uri: Uri): Manga {
return if (uri.host == "kotatsu.app") {
resolveAppLink(uri)
} else {
resolveExternalLink(uri)
} ?: throw NotFoundException("Manga not found", uri.toString())
}
suspend fun resolveAppLink(uri: Uri): Manga? {
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
val source = MangaSource(sourceName)
require(source != MangaSource.DUMMY) { "Manga source $sourceName is not supported" }
val repo = repositoryFactory.create(source)
return repo.findExact(
url = uri.getQueryParameter("url"),
title = uri.getQueryParameter("name"),
)
}
suspend fun resolveExternalLink(uri: Uri): Manga? {
dataRepository.findMangaByPublicUrl(uri.toString())?.let {
return it
}
val host = uri.host ?: return null
val repo = sourcesRepository.allMangaSources.asSequence()
.map { source ->
repositoryFactory.create(source) as RemoteMangaRepository
}.find { repo ->
host in repo.domains
} ?: return null
return repo.findExact(uri.toString().toRelativeUrl(host), null)
}
private suspend fun MangaRepository.findExact(url: String?, title: String?): Manga? {
if (!title.isNullOrEmpty()) {
val list = getList(0, title)
if (url != null) {
list.find { it.url == url }?.let {
return it
}
}
list.minByOrNull { it.title.levenshteinDistance(title) }
?.takeIf { it.title.almostEquals(title, 0.2f) }
?.let { return it }
}
val seed = getDetailsNoCache(
getSeedManga(source, url ?: return null, title),
)
return runCatchingCancellable {
val seedTitle = seed.title.ifEmpty {
seed.altTitle
}.ifNullOrEmpty {
seed.author
} ?: return@runCatchingCancellable null
val seedList = getList(0, seedTitle)
seedList.first { x -> x.url == url }
}.getOrThrow()
}
private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga {
return if (this is RemoteMangaRepository) {
getDetails(manga, withCache = false)
} else {
getDetails(manga)
}
}
private fun getSeedManga(source: MangaSource, url: String, title: String?) = Manga(
id = run {
var h = 1125899906842597L
source.name.forEach { c ->
h = 31 * h + c.code
}
url.forEach { c ->
h = 31 * h + c.code
}
h
},
title = title.orEmpty(),
altTitle = null,
url = url,
publicUrl = "",
rating = 0.0f,
isNsfw = source.contentType == ContentType.HENTAI,
coverUrl = "",
tags = emptySet(),
state = null,
author = null,
largeCoverUrl = null,
description = null,
chapters = null,
source = source,
)
}

View File

@@ -3,12 +3,11 @@ package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.newParser
fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
return if (source == MangaSource.DUMMY) {
DummyParser(loaderContext)
} else {
source.newParser(loaderContext)
loaderContext.newParserInstance(source)
}
}
}

View File

@@ -36,6 +36,8 @@ interface MangaRepository {
suspend fun getTags(): Set<MangaTag>
suspend fun getRelated(seed: Manga): List<Manga>
@Singleton
class Factory @Inject constructor(
private val localMangaRepository: LocalMangaRepository,

View File

@@ -51,7 +51,10 @@ class RemoteMangaRepository(
getConfig()[parser.configKeyDomain] = value
}
val headers: Headers?
val domains: Array<out String>
get() = parser.configKeyDomain.presetValues
val headers: Headers
get() = parser.headers
override fun intercept(chain: Interceptor.Chain): Response {
@@ -70,14 +73,7 @@ class RemoteMangaRepository(
return parser.getList(offset, tags, sortOrder)
}
override suspend fun getDetails(manga: Manga): Manga {
cache.getDetails(source, manga.url)?.let { return it }
val details = asyncSafe {
parser.getDetails(manga)
}
cache.putDetails(source, manga.url, details)
return details.await()
}
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, withCache = true)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
cache.getPages(source, chapter.url)?.let { return it }
@@ -94,6 +90,32 @@ class RemoteMangaRepository(
suspend fun getFavicons(): Favicons = parser.getFavicons()
override suspend fun getRelated(seed: Manga): List<Manga> {
cache.getRelatedManga(source, seed.url)?.let { return it }
val related = asyncSafe {
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
}
cache.putRelatedManga(source, seed.url, related)
return related.await()
}
suspend fun getDetails(manga: Manga, withCache: Boolean): Manga {
if (!withCache) {
return parser.getDetails(manga)
}
cache.getDetails(source, manga.url)?.let { return it }
val details = asyncSafe {
parser.getDetails(manga)
}
cache.putDetails(source, manga.url, details)
return details.await()
}
suspend fun find(manga: Manga): Manga? {
val list = getList(0, manga.title)
return list.find { x -> x.id == manga.id }
}
fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider
fun getConfigKeys(): List<ConfigKey<*>> = ArrayList<ConfigKey<*>>().also {

View File

@@ -21,6 +21,7 @@ import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly
import okio.Closeable
import okio.buffer
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
@@ -49,22 +50,32 @@ class FaviconFetcher(
override suspend fun fetch(): FetchResult {
getCached(options)?.let { return it }
val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository
val favicons = repo.getFavicons()
val sizePx = maxOf(
options.size.width.pxOrElse { FALLBACK_SIZE },
options.size.height.pxOrElse { FALLBACK_SIZE },
)
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
val response = loadIcon(icon.url, mangaSource)
val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
response.closeQuietly()
} ?: responseBody.toImageSource(response)
return SourceResult(
source = source,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
dataSource = response.toDataSource(),
)
var favicons = repo.getFavicons()
while (favicons.isNotEmpty()) {
val icon = favicons.find(sizePx) ?: throwNSEE()
val response = try {
loadIcon(icon.url, mangaSource)
} catch (e: CloudFlareProtectedException) {
throw e
} catch (e: HttpException) {
favicons -= icon
continue
}
val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource()?.also {
response.closeQuietly()
} ?: responseBody.toImageSource(response)
return SourceResult(
source = source,
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(icon.type),
dataSource = response.toDataSource(),
)
}
throwNSEE()
}
private suspend fun loadIcon(url: String, source: MangaSource): Response {
@@ -143,6 +154,8 @@ class FaviconFetcher(
append(height.toString())
}
private fun throwNSEE(): Nothing = throw NoSuchElementException("No favicons found")
class Factory(
context: Context,
private val okHttpClient: OkHttpClient,

View File

@@ -8,28 +8,26 @@ import android.os.Build
import android.provider.Settings
import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.arraySetOf
import androidx.collection.ArraySet
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.BuildConfig
import org.json.JSONArray
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.filterToSet
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.takeIfReadable
import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.history.domain.model.HistoryOrder
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.shelf.domain.model.ShelfSection
import java.io.File
import java.net.Proxy
import java.util.Collections
import java.util.EnumSet
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -40,32 +38,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val connectivityManager = context.connectivityManager
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
remove(MangaSource.LOCAL)
if (!BuildConfig.DEBUG) {
remove(MangaSource.DUMMY)
}
}
val remoteMangaSources: Set<MangaSource>
get() = Collections.unmodifiableSet(remoteSources)
var shelfSections: List<ShelfSection>
get() {
val raw = prefs.getString(KEY_SHELF_SECTIONS, null)
val values = enumValues<ShelfSection>()
if (raw.isNullOrEmpty()) {
return values.toList()
}
return raw.split('|')
.mapNotNull { values.getOrNull(it.toIntOrNull() ?: -1) }
.distinct()
}
set(value) {
val raw = value.joinToString("|") { it.ordinal.toString() }
prefs.edit { putString(KEY_SHELF_SECTIONS, raw) }
}
var listMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE, ListMode.GRID)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE, value) }
@@ -79,10 +51,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isAmoledTheme: Boolean
get() = prefs.getBoolean(KEY_THEME_AMOLED, false)
val isFavoritesNavItemFirst: Boolean
get() = (prefs.getString(KEY_FIRST_NAV_ITEM, null)?.toIntOrNull() ?: 0) == 1
var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
var isNsfwContentDisabled: Boolean
get() = prefs.getBoolean(KEY_DISABLE_NSFW, false)
set(value) = prefs.edit { putBoolean(KEY_DISABLE_NSFW, value) }
var appLocales: LocaleListCompat
get() {
val raw = prefs.getString(KEY_APP_LOCALE, null)
@@ -111,6 +90,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isTrackerEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true)
val isTrackerWifiOnly: Boolean
get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false)
val isTrackerNotificationsEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
@@ -125,8 +107,11 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val notificationLight: Boolean
get() = prefs.getBoolean(KEY_NOTIFICATIONS_LIGHT, true)
val readerAnimation: Boolean
get() = prefs.getBoolean(KEY_READER_ANIMATION, false)
val readerAnimation: ReaderAnimation
get() = prefs.getEnumValue(KEY_READER_ANIMATION, ReaderAnimation.DEFAULT)
val readerBackground: ReaderBackground
get() = prefs.getEnumValue(KEY_READER_BACKGROUND, ReaderBackground.DEFAULT)
val defaultReaderMode: ReaderMode
get() = prefs.getEnumValue(KEY_READER_MODE, ReaderMode.STANDARD)
@@ -156,7 +141,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER)
val trackSources: Set<String>
get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: arraySetOf(TRACK_FAVOURITES, TRACK_HISTORY)
get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: setOf(TRACK_FAVOURITES)
var appPassword: String?
get() = prefs.getString(KEY_APP_PASSWORD, null)
@@ -190,37 +175,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
return policy.isNetworkAllowed(connectivityManager)
}
var sourcesOrder: List<String>
get() = prefs.getString(KEY_SOURCES_ORDER, null)
?.split('|')
.orEmpty()
set(value) = prefs.edit {
putString(KEY_SOURCES_ORDER, value.joinToString("|"))
}
var hiddenSources: Set<String>
get() = prefs.getStringSet(KEY_SOURCES_HIDDEN, null)?.filterToSet { name ->
remoteSources.any { it.name == name }
}.orEmpty()
set(value) = prefs.edit { putStringSet(KEY_SOURCES_HIDDEN, value) }
val isSourcesSelected: Boolean
get() = KEY_SOURCES_HIDDEN in prefs
val newSources: Set<MangaSource>
get() {
val known = sourcesOrder.toSet()
val hidden = hiddenSources
return remoteMangaSources
.filterNotTo(EnumSet.noneOf(MangaSource::class.java)) { x ->
x.name in known || x.name in hidden
}
}
fun markKnownSources(sources: Collection<MangaSource>) {
sourcesOrder = (sourcesOrder + sources.map { it.name }).distinct()
}
var isSourcesGridMode: Boolean
get() = prefs.getBoolean(KEY_SOURCES_GRID, false)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
@@ -234,14 +188,28 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
if (key == null) ScreenshotsPolicy.ALLOW else ScreenshotsPolicy.valueOf(key)
}.getOrDefault(ScreenshotsPolicy.ALLOW)
var userSpecifiedMangaDirectories: Set<File>
get() {
val set = prefs.getStringSet(KEY_LOCAL_MANGA_DIRS, emptySet()).orEmpty()
return set.mapNotNullToSet { File(it).takeIfReadable() }
}
set(value) {
val set = value.mapToSet { it.absolutePath }
prefs.edit { putStringSet(KEY_LOCAL_MANGA_DIRS, set) }
}
var mangaStorageDir: File?
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
File(it)
}?.takeIf { it.exists() }
}?.takeIf { it.exists() && it in userSpecifiedMangaDirectories }
set(value) = prefs.edit {
if (value == null) {
remove(KEY_LOCAL_STORAGE)
} else {
val userDirs = userSpecifiedMangaDirectories
if (value !in userDirs) {
userSpecifiedMangaDirectories = userDirs + value
}
putString(KEY_LOCAL_STORAGE, value.path)
}
}
@@ -256,11 +224,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) }
val isSuggestionsWiFiOnly: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_WIFI_ONLY, false)
val isSuggestionsExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
val isSuggestionsNotificationAvailable: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, true)
get() = prefs.getBoolean(KEY_SUGGESTIONS_NOTIFICATIONS, false)
val suggestionsTagsBlacklist: Set<String>
get() {
@@ -308,6 +279,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getEnumValue(KEY_LOCAL_LIST_ORDER, SortOrder.NEWEST)
set(value) = prefs.edit { putEnumValue(KEY_LOCAL_LIST_ORDER, value) }
var historySortOrder: HistoryOrder
get() = prefs.getEnumValue(KEY_HISTORY_ORDER, HistoryOrder.UPDATED)
set(value) = prefs.edit { putEnumValue(KEY_HISTORY_ORDER, value) }
val isRelatedMangaEnabled: Boolean
get() = prefs.getBoolean(KEY_RELATED_MANGA, true)
val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
@@ -325,20 +303,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
return policy.isNetworkAllowed(connectivityManager)
}
fun getMangaSources(includeHidden: Boolean): List<MangaSource> {
val list = remoteSources.toMutableList()
val order = sourcesOrder
list.sortBy { x ->
val e = order.indexOf(x.name)
if (e == -1) order.size + x.ordinal else e
}
if (!includeHidden) {
val hidden = hiddenSources
list.removeAll { x -> x.name in hidden }
}
return list
}
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
@@ -361,6 +325,23 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
fun observe() = prefs.observe()
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())
}
}
}
}
private fun isBackgroundNetworkRestricted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
connectivityManager.restrictBackgroundStatus == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
@@ -369,6 +350,15 @@ 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 PAGE_SWITCH_TAPS = "taps"
@@ -381,8 +371,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_THEME = "theme"
const val KEY_COLOR_THEME = "color_theme"
const val KEY_THEME_AMOLED = "amoled_theme"
const val KEY_SOURCES_ORDER = "sources_order_2"
const val KEY_SOURCES_HIDDEN = "sources_hidden"
const val KEY_TRAFFIC_WARNING = "traffic_warning"
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
@@ -395,6 +383,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_LOCAL_STORAGE = "local_storage"
const val KEY_READER_SWITCHERS = "reader_switchers"
const val KEY_TRACKER_ENABLED = "tracker_enabled"
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
const val KEY_TRACK_SOURCES = "track_sources"
const val KEY_TRACK_CATEGORIES = "track_categories"
const val KEY_TRACK_WARNING = "track_warning"
@@ -404,7 +393,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_NOTIFICATIONS_VIBRATE = "notifications_vibrate"
const val KEY_NOTIFICATIONS_LIGHT = "notifications_light"
const val KEY_NOTIFICATIONS_INFO = "tracker_notifications_info"
const val KEY_READER_ANIMATION = "reader_animation"
const val KEY_READER_ANIMATION = "reader_animation2"
const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_APP_PASSWORD = "app_password"
@@ -422,6 +411,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SCREENSHOTS_POLICY = "screenshots_policy"
const val KEY_PAGES_PRELOAD = "pages_preload"
const val KEY_SUGGESTIONS = "suggestions"
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_NOTIFICATIONS = "suggestions_notifications"
@@ -438,11 +428,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
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_BACKGROUND = "reader_background"
const val KEY_SHORTCUTS = "dynamic_shortcuts"
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_LOCAL_LIST_ORDER = "local_order"
const val KEY_HISTORY_ORDER = "history_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_SHELF_SECTIONS = "shelf_sections_2"
const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"
@@ -461,6 +452,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PROXY_LOGIN = "proxy_login"
const val KEY_PROXY_PASSWORD = "proxy_password"
const val KEY_IMAGES_PROXY = "images_proxy"
const val KEY_LOCAL_MANGA_DIRS = "local_manga_dirs"
const val KEY_DISABLE_NSFW = "no_nsfw"
const val KEY_RELATED_MANGA = "related_manga"
const val KEY_FIRST_NAV_ITEM = "nav_first"
// About
const val KEY_APP_UPDATE = "app_update"

View File

@@ -4,6 +4,7 @@ import androidx.annotation.StringRes
import androidx.annotation.StyleRes
import com.google.android.material.color.DynamicColors
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.find
enum class ColorScheme(
@StyleRes val styleResId: Int,
@@ -31,7 +32,7 @@ enum class ColorScheme(
}
fun getAvailableList(): List<ColorScheme> {
val list = enumValues<ColorScheme>().toMutableList()
val list = ColorScheme.entries.toMutableList()
if (!DynamicColors.isDynamicColorAvailable()) {
list.remove(MONET)
}
@@ -39,7 +40,7 @@ enum class ColorScheme(
}
fun safeValueOf(name: String): ColorScheme? {
return enumValues<ColorScheme>().find { it.name == name }
return ColorScheme.entries.find(name)
}
}
}

View File

@@ -20,7 +20,7 @@ enum class NetworkPolicy(
fun from(key: String?, default: NetworkPolicy): NetworkPolicy {
val intKey = key?.toIntOrNull() ?: return default
return enumValues<NetworkPolicy>().find { it.key == intKey } ?: default
return NetworkPolicy.entries.find { it.key == intKey } ?: default
}
}
}

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.core.prefs
enum class ReaderAnimation {
// Do not rename this
NONE, DEFAULT, ADVANCED;
}

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.core.prefs
import android.content.Context
import android.view.ContextThemeWrapper
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import org.koitharu.kotatsu.core.util.ext.getThemeDrawable
import com.google.android.material.R as materialR
enum class ReaderBackground {
DEFAULT, LIGHT, DARK, WHITE, BLACK;
fun resolve(context: Context) = when (this) {
DEFAULT -> context.getThemeDrawable(android.R.attr.windowBackground)
LIGHT -> ContextThemeWrapper(context, materialR.style.ThemeOverlay_Material3_Light)
.getThemeDrawable(android.R.attr.windowBackground)
DARK -> ContextThemeWrapper(context, materialR.style.ThemeOverlay_Material3_Dark)
.getThemeDrawable(android.R.attr.windowBackground)
WHITE -> ContextCompat.getColor(context, android.R.color.white).toDrawable()
BLACK -> ContextCompat.getColor(context, android.R.color.black).toDrawable()
}
}

View File

@@ -8,6 +8,6 @@ enum class ReaderMode(val id: Int) {
companion object {
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
fun valueOf(id: Int) = entries.firstOrNull { it.id == id }
}
}
}

View File

@@ -14,6 +14,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
@@ -96,15 +97,13 @@ abstract class BaseActivity<B : ViewBinding> :
}
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
@Suppress("DEPRECATION")
onBackPressed()
true
} else super.onOptionsItemSelected(item)
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
// ActivityCompat.recreate(this)
error("Test")
ActivityCompat.recreate(this)
return true
}
return super.onKeyDown(keyCode, event)

View File

@@ -1,106 +0,0 @@
package org.koitharu.kotatsu.core.ui
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.activity.OnBackPressedDispatcher
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.AppBottomSheetDialog
import org.koitharu.kotatsu.core.util.ext.findActivity
import org.koitharu.kotatsu.core.util.ext.getDisplaySize
import com.google.android.material.R as materialR
@Deprecated(
"Use BaseAdaptiveSheet",
replaceWith = ReplaceWith("BaseAdaptiveSheet<B>", "org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet"),
)
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
var viewBinding: B? = null
private set
@Deprecated("", ReplaceWith("requireViewBinding()"))
protected val binding: B
get() = requireViewBinding()
protected val behavior: BottomSheetBehavior<*>?
get() = (dialog as? BottomSheetDialog)?.behavior
val isExpanded: Boolean
get() = behavior?.state == BottomSheetBehavior.STATE_EXPANDED
val onBackPressedDispatcher: OnBackPressedDispatcher
get() = (requireDialog() as AppBottomSheetDialog).onBackPressedDispatcher
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
val binding = onCreateViewBinding(inflater, container)
viewBinding = binding
return binding.root
}
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = requireViewBinding()
// Enforce max width for tablets
val width = resources.getDimensionPixelSize(R.dimen.bottom_sheet_width)
if (width > 0) {
behavior?.maxWidth = width
}
// Set peek height to 40% display height
binding.root.context.findActivity()?.getDisplaySize()?.let {
behavior?.peekHeight = (it.height() * 0.4).toInt()
}
onViewBindingCreated(binding, savedInstanceState)
}
override fun onDestroyView() {
viewBinding = null
super.onDestroyView()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AppBottomSheetDialog(requireContext(), theme)
}
fun addBottomSheetCallback(callback: BottomSheetBehavior.BottomSheetCallback) {
val b = behavior ?: return
b.addBottomSheetCallback(callback)
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
if (rootView != null) {
callback.onStateChanged(rootView, b.state)
}
}
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
val b = behavior ?: return
if (isExpanded) {
b.state = BottomSheetBehavior.STATE_EXPANDED
}
b.isFitToContents = !isExpanded
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet)
rootView?.updateLayoutParams {
height = if (isExpanded) LayoutParams.MATCH_PARENT else LayoutParams.WRAP_CONTENT
}
b.isDraggable = !isLocked
}
fun requireViewBinding(): B = checkNotNull(viewBinding) {
"Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()."
}
}

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.core.ui
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.coroutines.suspendCoroutine
open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
AsyncDifferConfig.Builder(ListModelDiffCallback<T>())
.setBackgroundThreadExecutor(Dispatchers.Default.limitedParallelism(2).asExecutor())
.build(),
), FlowCollector<List<T>?> {
override suspend fun emit(value: List<T>?) = suspendCoroutine { cont ->
setItems(value.orEmpty(), ContinuationResumeRunnable(cont))
}
fun addDelegate(type: ListItemType, delegate: AdapterDelegate<List<T>>): BaseListAdapter<T> {
delegatesManager.addDelegate(type.ordinal, delegate)
return this
}
fun addListListener(listListener: ListListener<T>) {
differ.addListListener(listListener)
}
fun removeListListener(listListener: ListListener<T>) {
differ.removeListListener(listListener)
}
}

View File

@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.settings.SettingsActivity
import javax.inject.Inject
@@ -33,7 +34,8 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.setBackgroundColor(view.context.getThemeColor(android.R.attr.colorBackground))
val themedContext = (view.parentView ?: view).context
view.setBackgroundColor(themedContext.getThemeColor(android.R.attr.colorBackground))
listView.clipToPadding = false
insetsDelegate.onViewCreated(view)
insetsDelegate.addInsetsListener(this)

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