Compare commits

...

213 Commits

Author SHA1 Message Date
Koitharu
e4e4f18066 Move read button to bottom 2024-05-18 18:20:58 +03:00
Koitharu
b1ab48e912 Option to disable connectivity check 2024-05-17 11:36:42 +03:00
Koitharu
a71e2dd289 Update settings ui and fix crash 2024-05-17 10:31:15 +03:00
Koitharu
bbdf1c756e Update dependencies 2024-05-16 11:14:40 +03:00
Koitharu
283878879b Update parsers 2024-05-16 10:44:08 +03:00
Koitharu
b74ec98d68 Merge branch 'devel' of https://hosted.weblate.org/git/kotatsu/strings into devel 2024-05-16 10:13:07 +03:00
Koitharu
3691db8e8e App udpate activity #880 2024-05-15 18:20:58 +03:00
Paing Frow
e25ccf6b25 Added translation using Weblate (Burmese)
Co-authored-by: Paing Frow <paingphyoe66@gmail.com>
2024-05-14 14:48:40 +03:00
Infy's Tagalog Translations
ffebdb0c49 Translated using Weblate (Filipino)
Currently translated at 99.8% (637 of 638 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (637 of 638 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Scrambled777
6accdbced5 Translated using Weblate (Hindi)
Currently translated at 100.0% (638 of 638 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Oğuz Ersen
2fcb94e1d7 Translated using Weblate (Turkish)
Currently translated at 100.0% (638 of 638 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Carlota-gif
6211ef974d Translated using Weblate (Portuguese)
Currently translated at 99.3% (634 of 638 strings)

Co-authored-by: Carlota-gif <gamefox1407@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Eduardo Malaspina
0eacf7bb98 Translated using Weblate (Spanish)
Currently translated at 99.8% (637 of 638 strings)

Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Anon
c9b7d650a8 Translated using Weblate (Serbian)
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (638 of 638 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Anonymous
a29f7d6533 Translated using Weblate (Hungarian)
Currently translated at 96.7% (617 of 638 strings)

Translated using Weblate (Malay)

Currently translated at 48.4% (309 of 638 strings)

Translated using Weblate (Estonian)

Currently translated at 66.1% (422 of 638 strings)

Translated using Weblate (Kazakh)

Currently translated at 81.5% (520 of 638 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 59.2% (378 of 638 strings)

Translated using Weblate (Hindi)

Currently translated at 99.5% (635 of 638 strings)

Translated using Weblate (Korean)

Currently translated at 52.8% (337 of 638 strings)

Translated using Weblate (Greek)

Currently translated at 84.1% (537 of 638 strings)

Translated using Weblate (Arabic)

Currently translated at 52.6% (336 of 638 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (622 of 638 strings)

Translated using Weblate (Japanese)

Currently translated at 72.1% (460 of 638 strings)

Translated using Weblate (Portuguese)

Currently translated at 96.7% (617 of 638 strings)

Translated using Weblate (Italian)

Currently translated at 84.6% (540 of 638 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/et/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Kristian de Frutos
72f8c626d7 Translated using Weblate (Czech)
Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Czech)

Currently translated at 83.0% (528 of 636 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Kristian de Frutos <kristiandef@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-05-14 14:48:40 +03:00
gekka
f05ef5125d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Макар Разин
40b3d8e6fd Translated using Weblate (Belarusian)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2024-05-14 14:48:40 +03:00
Paing Frow
a695bdc565 Translated using Weblate (Burmese)
Currently translated at 88.8% (8 of 9 strings)

Added translation using Weblate (Burmese)

Co-authored-by: Paing Frow <paingphyoe66@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/my/
Translation: Kotatsu/plurals
2024-05-14 11:48:01 +00:00
Infy's Tagalog Translations
9700fabd9a Translated using Weblate (Filipino)
Currently translated at 99.8% (637 of 638 strings)

Translated using Weblate (Filipino)

Currently translated at 99.8% (637 of 638 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-05-14 11:47:59 +00:00
Scrambled777
4877db42f9 Translated using Weblate (Hindi)
Currently translated at 100.0% (638 of 638 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-05-14 11:47:58 +00:00
Oğuz Ersen
9b418fd63b Translated using Weblate (Turkish)
Currently translated at 100.0% (638 of 638 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-05-14 11:47:57 +00:00
Carlota-gif
b2eef0df11 Translated using Weblate (Portuguese)
Currently translated at 99.3% (634 of 638 strings)

Co-authored-by: Carlota-gif <gamefox1407@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-05-14 11:47:56 +00:00
Eduardo Malaspina
34462829ff Translated using Weblate (Spanish)
Currently translated at 99.8% (637 of 638 strings)

Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-05-14 11:47:55 +00:00
Anon
2afcbef8d0 Translated using Weblate (Serbian)
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (638 of 638 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-05-14 11:47:53 +00:00
Anonymous
695becbda0 Translated using Weblate (Hungarian)
Currently translated at 96.7% (617 of 638 strings)

Translated using Weblate (Malay)

Currently translated at 48.4% (309 of 638 strings)

Translated using Weblate (Estonian)

Currently translated at 66.1% (422 of 638 strings)

Translated using Weblate (Kazakh)

Currently translated at 81.5% (520 of 638 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 59.2% (378 of 638 strings)

Translated using Weblate (Hindi)

Currently translated at 99.5% (635 of 638 strings)

Translated using Weblate (Korean)

Currently translated at 52.8% (337 of 638 strings)

Translated using Weblate (Greek)

Currently translated at 84.1% (537 of 638 strings)

Translated using Weblate (Arabic)

Currently translated at 52.6% (336 of 638 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (622 of 638 strings)

Translated using Weblate (Japanese)

Currently translated at 72.1% (460 of 638 strings)

Translated using Weblate (Portuguese)

Currently translated at 96.7% (617 of 638 strings)

Translated using Weblate (Italian)

Currently translated at 84.6% (540 of 638 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/et/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/nn/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-05-14 11:47:52 +00:00
Kristian de Frutos
5877d8215d Translated using Weblate (Czech)
Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Czech)

Currently translated at 83.0% (528 of 636 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Kristian de Frutos <kristiandef@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-05-14 11:47:50 +00:00
gekka
48b357dfef Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-05-14 11:47:49 +00:00
Макар Разин
b20cc7c0d9 Translated using Weblate (Belarusian)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2024-05-14 11:47:48 +00:00
Koitharu
0f43f02fad Update parsers 2024-05-14 14:47:18 +03:00
Koitharu
9b658cf0b8 Fix offline details loading 2024-05-14 14:47:18 +03:00
MrChocolatine
ce705e12a8 Use format yyyyMMdd for backups
Resolves #791 .
2024-05-12 18:58:55 +03:00
Koitharu
28dede0d3e Fix displaying long author name 2024-05-12 16:52:26 +03:00
Koitharu
d66e61f845 Update parsers 2024-05-12 16:19:32 +03:00
Koitharu
b246575486 Fix main navigation bar behavior 2024-05-11 18:05:29 +03:00
Koitharu
18dd205051 Hide widgets content when app protected 2024-05-11 17:53:45 +03:00
Koitharu
0e10fdaf36 Code cleanup and refactor 2024-05-11 11:51:59 +03:00
Koitharu
7c82b4effb Multiple sources selection 2024-05-10 17:39:00 +03:00
Koitharu
82684601b7 Code cleanup and refactor 2024-05-10 15:37:34 +03:00
Koitharu
77ad21bd7a Details activity ui fixes 2024-05-10 08:15:43 +03:00
Koitharu
e6c8591bf8 Fix crash if animations disabled 2024-05-08 16:38:16 +03:00
Koitharu
e330be5d13 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
Ismail Özcan
6a4cd9643a Translated using Weblate (German)
Currently translated at 96.3% (613 of 636 strings)

Co-authored-by: Ismail Özcan <me+weblate@ismailoezcan.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
ngocanhtve
d98cb9a577 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
abc0922001
ac455527ef Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
Nayuki
7e37345dea Translated using Weblate (Thai)
Currently translated at 63.9% (407 of 636 strings)

Translated using Weblate (Thai)

Currently translated at 100.0% (9 of 9 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
2024-05-08 14:34:40 +03:00
Oğuz Ersen
6e810179a7 Translated using Weblate (Turkish)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
Infy's Tagalog Translations
7715aff953 Translated using Weblate (Filipino)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
Anon
63e6b9f026 Translated using Weblate (Serbian)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
Naga
b6f136fb71 Translated using Weblate (French)
Currently translated at 99.5% (633 of 636 strings)

Co-authored-by: Naga <yz2000.pro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
Макар Разин
de0327a00a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (636 of 636 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
2024-05-08 14:34:40 +03:00
maryush
e5f09ae4c9 Translated using Weblate (Polish)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
gekka
f10d9b54d8 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
gallegonovato
619d672e49 Translated using Weblate (Spanish)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-05-08 14:34:40 +03:00
Koitharu
db519701bc Show single branch in details 2024-05-08 14:31:19 +03:00
Koitharu
e42aeb857f Fix details read/continue state 2024-05-08 14:23:04 +03:00
Koitharu
4f82495cfc Optimize app initialization 2024-05-08 13:58:25 +03:00
Koitharu
311c36b7c0 Builtin ssl certificates for old devices 2024-05-08 13:16:10 +03:00
Koitharu
002ce25d7e Notification settings actions in notifications 2024-05-08 08:46:21 +03:00
Koitharu
d9cf13d3fb Fix tracking and progress 2024-05-08 08:34:34 +03:00
Koitharu
ed5b1306b8 UI fixes 2024-05-07 12:10:39 +03:00
Koitharu
227fe86cf9 Allow to add readonly manga directories 2024-05-07 10:46:50 +03:00
Koitharu
1905482b06 UI fixes 2024-05-06 17:10:08 +03:00
Koitharu
46ded4af0d Bookmarks selection 2024-05-06 15:05:25 +03:00
Koitharu
6676ab82b4 Fix ChaptersPagesSheet nested scrolling 2024-05-06 14:47:55 +03:00
Koitharu
1a60df6d98 Fix images memory caching 2024-05-06 13:42:17 +03:00
Koitharu
5ef1b4ac9c Fix restoring bookmarks 2024-05-04 17:20:42 +03:00
Koitharu
17828ae755 Fix crashes 2024-05-04 17:06:28 +03:00
Koitharu
d8ac4d6738 Fix default ChaptersPagesSheet tab 2024-05-04 12:21:57 +03:00
Koitharu
0a10cb509c Details activity fixes 2024-05-03 10:01:47 +03:00
Koitharu
7a3fd20dfa Update dependencies 2024-05-03 08:57:48 +03:00
Koitharu
ab20e50dc1 Merge branch 'devel' of https://hosted.weblate.org/git/kotatsu/strings into devel 2024-04-29 19:51:35 +03:00
Scrambled777
f783ffef11 Translated using Weblate (Hindi)
Currently translated at 100.0% (636 of 636 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
2024-04-29 18:09:46 +02:00
Andrius
d62ecdc177 Added translation using Weblate (Lithuanian)
Added translation using Weblate (Lithuanian)

Co-authored-by: Andrius <sndriuss@gmail.com>
2024-04-29 19:09:07 +03:00
Infy's Tagalog Translations
77cd7dda5f Translated using Weblate (Filipino)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
Scrambled777
bd7099e97c Translated using Weblate (Hindi)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
gekka
b9457a35b9 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
Celysia
53918ddddd Translated using Weblate (Indonesian)
Currently translated at 91.6% (582 of 635 strings)

Co-authored-by: Celysia <celysiasyantik@neko2.net>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
Макар Разин
84f0da0871 Translated using Weblate (Polish)
Currently translated at 99.8% (634 of 635 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (635 of 635 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
Koitharu
11c2e2e3bc Translated using Weblate (Russian)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
Oğuz Ersen
622c8d1c18 Translated using Weblate (Turkish)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
gallegonovato
10ffae7d4e Translated using Weblate (Spanish)
Currently translated at 99.8% (634 of 635 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
Макар Разин
15b48fd902 Translated using Weblate (Belarusian)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2024-04-29 19:09:07 +03:00
Koitharu
e2d7f2890d Tune ui 2024-04-29 19:06:35 +03:00
Nayuki
e01c485949 Translated using Weblate (Thai)
Currently translated at 63.7% (405 of 635 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2024-04-29 11:28:24 +02:00
maryush
3672c84e8f Translated using Weblate (Polish)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-04-29 11:28:23 +02:00
Anon
55c5a07c8b Translated using Weblate (Serbian)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-04-29 11:28:21 +02:00
Andrius
a3cf32aefb Translated using Weblate (Lithuanian)
Currently translated at 5.8% (37 of 635 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (9 of 9 strings)

Added translation using Weblate (Lithuanian)

Added translation using Weblate (Lithuanian)

Co-authored-by: Andrius <sndriuss@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/lt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/lt/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-04-29 11:28:20 +02:00
Infy's Tagalog Translations
c21bf30e91 Translated using Weblate (Filipino)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-04-29 11:28:19 +02:00
Scrambled777
1719547ce0 Translated using Weblate (Hindi)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-04-29 11:28:17 +02:00
gekka
22186825a0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (635 of 635 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-04-29 11:28:16 +02:00
Celysia
b9c83ad5cc Translated using Weblate (Indonesian)
Currently translated at 91.6% (582 of 635 strings)

Co-authored-by: Celysia <celysiasyantik@neko2.net>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-04-29 11:28:14 +02:00
Макар Разин
1359689b23 Translated using Weblate (Polish)
Currently translated at 99.8% (634 of 635 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (635 of 635 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-04-29 11:28:13 +02:00
Koitharu
7bad6ad077 Translated using Weblate (Russian)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-04-29 11:28:12 +02:00
Oğuz Ersen
b9097fa077 Translated using Weblate (Turkish)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-04-29 11:28:10 +02:00
gallegonovato
0b03806ccd Translated using Weblate (Spanish)
Currently translated at 100.0% (635 of 635 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (634 of 635 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-04-29 11:28:09 +02:00
Макар Разин
db9c1279ac Translated using Weblate (Belarusian)
Currently translated at 100.0% (635 of 635 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2024-04-29 11:28:07 +02:00
Koitharu
af510beb7b Detect CloudFlare blocks 2024-04-29 12:27:29 +03:00
Koitharu
8cf0203b42 Add "Open in browser" action in lists 2024-04-29 11:32:42 +03:00
Koitharu
ea4a81c6ec Fixes 2024-04-29 11:14:28 +03:00
Koitharu
63b53d2244 Non-modal bottom sheet on details activity 2024-04-29 11:00:56 +03:00
Koitharu
aba6b64074 Scrobbling fixes 2024-04-27 16:41:10 +03:00
Koitharu
324bfc733b Fix bookmark action in reader 2024-04-25 08:56:37 +03:00
Koitharu
fcfb3c9808 Fix feed layoutmanager 2024-04-24 11:26:42 +03:00
Koitharu
4ab77064ee Merge branch 'devel' of https://hosted.weblate.org/git/kotatsu/strings into devel 2024-04-24 10:53:18 +03:00
N. Hao
ca2182d588 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (631 of 631 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
2024-04-24 09:48:15 +02:00
Scrambled777
5ba410acd5 Translated using Weblate (Hindi)
Currently translated at 100.0% (631 of 631 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
2024-04-24 09:48:15 +02:00
gekka
06382649c4 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (631 of 631 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
2024-04-24 09:48:13 +02:00
Oğuz Ersen
4f50e905af Translated using Weblate (Turkish)
Currently translated at 100.0% (631 of 631 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
2024-04-24 09:48:12 +02:00
Oğuz Ersen
822cdab6ee Translated using Weblate (English)
Currently translated at 100.0% (631 of 631 strings)

Translation: Kotatsu/Strings
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
2024-04-24 09:48:11 +02:00
Koitharu
8fad307c9a Fix margins for pinned navbar 2024-04-24 10:47:58 +03:00
Koitharu
daa545f3db Add authors suggestion and update search suggestion ui 2024-04-24 10:41:44 +03:00
Koitharu
56892aea3c Fix alert dialog style 2024-04-24 10:07:19 +03:00
Koitharu
73e768def0 Configure search suggestions 2024-04-24 09:31:40 +03:00
Koitharu
19da2267d6 Pin navigation ui option #851 2024-04-23 11:35:44 +03:00
Koitharu
3affec0f88 Group tracker notifications 2024-04-23 11:10:29 +03:00
Koitharu
448c688629 Tracker frequency preference 2024-04-23 09:48:39 +03:00
Koitharu
fc2ab3f795 Bring back reader slider 2024-04-23 09:23:19 +03:00
Koitharu
e520e695f9 Fix list selector 2024-04-23 07:35:54 +03:00
Koitharu
b34f438430 Remove redundant translations 2024-04-21 18:50:26 +03:00
Koitharu
72bfe15728 Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2024-04-21 18:31:39 +03:00
maryush
60198bc878 Translated using Weblate (Polish)
Currently translated at 100.0% (625 of 625 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
Anon
631c09badb Translated using Weblate (Serbian)
Currently translated at 100.0% (625 of 625 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
abc0922001
2bf6eb6f0e Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (625 of 625 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
gallegonovato
1ee8b65ff7 Translated using Weblate (Spanish)
Currently translated at 100.0% (625 of 625 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
N. Hao
d367750331 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (625 of 625 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: N. Hao <nguyenviethao2002@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/vi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/vi/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-04-21 17:45:48 +03:00
Akhil Raj
6d1bc5b1fd Translated using Weblate (Malayalam)
Currently translated at 1.7% (11 of 625 strings)

Added translation using Weblate (Malayalam)

Co-authored-by: Akhil Raj <akhilakae07@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ml/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
Infy's Tagalog Translations
45771adef0 Translated using Weblate (Filipino)
Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
Scrambled777
d91f613c28 Translated using Weblate (Hindi)
Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
gekka
988dd767d8 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (625 of 625 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
Oğuz Ersen
d715c175b8 Translated using Weblate (Turkish)
Currently translated at 100.0% (650 of 650 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
Макар Разин
a114605be1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (625 of 625 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (625 of 625 strings)

Translated using Weblate (Polish)

Currently translated at 99.5% (647 of 650 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (650 of 650 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (650 of 650 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (650 of 650 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/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
Renn
f7a9e2ef89 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (648 of 648 strings)

Co-authored-by: Renn <mcperenan134@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-04-21 17:45:48 +03:00
Koitharu
2aa3133c52 Update parsers and fix build 2024-04-21 17:44:39 +03:00
Koitharu
2f15ea213d Fix notifications 2024-04-21 15:11:01 +03:00
Koitharu
19a3f14190 Fix chapters sheet interaction 2024-04-21 14:38:51 +03:00
Koitharu
fb716d300e Chapters list grouping 2024-04-21 10:01:57 +03:00
Koitharu
1fe5095654 Update dependencies 2024-04-21 08:14:26 +03:00
Koitharu
820d3f2413 Update placeholders 2024-04-17 19:16:23 +03:00
Koitharu
34903fc951 Hide search bar when ActionMode is active 2024-04-17 08:56:05 +03:00
Koitharu
7ec2e0c5cc Tracker improvements 2024-04-17 08:53:41 +03:00
Koitharu
846c346a86 Add unread field to feed items 2024-04-16 10:03:01 +03:00
Koitharu
f685ed6932 Fix track worker scheduling 2024-04-16 07:45:52 +03:00
Koitharu
98b8ec5c89 Remove unused resources 2024-04-16 07:45:14 +03:00
Koitharu
0e20bf4afe Gaps between pages in webtoon mode #833 2024-04-15 08:19:26 +03:00
Zakhar Timoshenko
fe588c08e2 UI adjust part 2 2024-04-14 15:13:39 +03:00
Koitharu
3ee6ac605d Fix current chapter download #840 2024-04-14 08:55:35 +03:00
Koitharu
535feb424c UI fixes 2024-04-14 08:55:34 +03:00
Макар Разин
2cca696808 Translated using Weblate (Ukrainian)
Currently translated at 99.0% (641 of 647 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (647 of 647 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
abc0922001
b5ea0ec7fa Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (647 of 647 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Raphael Terrance Fernandez
1e3d2595cf Translated using Weblate (Malayalam)
Currently translated at 0.0% (0 of 9 strings)

Co-authored-by: Raphael Terrance Fernandez <raphaeltfernandez@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ml/
Translation: Kotatsu/plurals
2024-04-14 08:44:18 +03:00
kaajjo
960b960726 Translated using Weblate (Russian)
Currently translated at 99.5% (643 of 646 strings)

Co-authored-by: kaajjo <claymanoff@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Blackiezin
cd29760836 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.5% (643 of 646 strings)

Co-authored-by: Blackiezin <mcperenan134@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
gallegonovato
27a2883f0a Translated using Weblate (Spanish)
Currently translated at 100.0% (647 of 647 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (646 of 646 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Boum Boum
326bca2273 Translated using Weblate (French)
Currently translated at 97.9% (633 of 646 strings)

Translated using Weblate (French)

Currently translated at 97.3% (629 of 646 strings)

Co-authored-by: Boum Boum <bboum184@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
bedo david
b32487fcb8 Translated using Weblate (Hungarian)
Currently translated at 99.8% (643 of 644 strings)

Co-authored-by: bedo david <bedo.david7676@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Eduardo Malaspina
105bdff9ab Translated using Weblate (Spanish)
Currently translated at 99.5% (641 of 644 strings)

Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Roger VC
6b767523a9 Translated using Weblate (Catalan)
Currently translated at 13.1% (85 of 644 strings)

Added translation using Weblate (Catalan)

Added translation using Weblate (Catalan)

Co-authored-by: Roger VC <rogervilarasau@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ca/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Milena Vitoria Carvalho Barbosa
396050c051 Translated using Weblate (Portuguese)
Currently translated at 94.0% (603 of 641 strings)

Co-authored-by: Milena Vitoria Carvalho Barbosa <mv8365597@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Infy's Tagalog Translations
c32d1877ff Translated using Weblate (Filipino)
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (647 of 647 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (644 of 644 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Anon
df78d9bf4c Translated using Weblate (Serbian)
Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (644 of 644 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Макар Разин
cc3bea3b2c Translated using Weblate (Belarusian)
Currently translated at 100.0% (647 of 647 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (639 of 639 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (639 of 639 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
2024-04-14 08:44:18 +03:00
Scrambled777
87aa38b4e8 Translated using Weblate (Hindi)
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (647 of 647 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (644 of 644 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
maryush
ee0215511a Translated using Weblate (Polish)
Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
gekka
bd0056394e Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (647 of 647 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (644 of 644 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Oğuz Ersen
76ea7ab046 Translated using Weblate (Turkish)
Currently translated at 100.0% (648 of 648 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (647 of 647 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (646 of 646 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (644 of 644 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
gallegonovato
3d7ea1637f Translated using Weblate (Spanish)
Currently translated at 100.0% (641 of 641 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (639 of 639 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-04-14 08:44:18 +03:00
Koitharu
4b30905f9c Tracker improvements 2024-04-13 13:57:11 +03:00
Koitharu
bddb8431c5 Update reader ui 2024-04-13 13:56:47 +03:00
Koitharu
61e02dd827 Update incognito indicator and PreviewFragment 2024-04-13 11:26:31 +03:00
Koitharu
ff4eac8269 Improve updated manga screen 2024-04-13 09:13:07 +03:00
Koitharu
32eba77639 Add Updated navigation section 2024-04-11 18:56:47 +03:00
Koitharu
09eb82ca2e Move import to service 2024-04-11 18:01:28 +03:00
Koitharu
4d7ff5f6cc Tracker debug info 2024-04-11 10:39:39 +03:00
Koitharu
59dd53c025 Cleanup 2024-04-11 08:12:29 +03:00
Koitharu
c98d7561b8 Update suggestions section on explore screen 2024-04-10 14:14:21 +03:00
Koitharu
5c8157b81f Update DotsIndicator style 2024-04-09 08:22:13 +03:00
Koitharu
7f5ff1ab14 Update recommendations item in explore section 2024-04-08 19:26:42 +03:00
Koitharu
018c84b6af Add Last used default tab in details 2024-04-08 17:54:34 +03:00
Koitharu
b95174727a Update details activity 2024-04-08 17:32:24 +03:00
Koitharu
0aec2359cf Merge branch 'ui' of github.com:KotatsuApp/Kotatsu into devel 2024-04-08 10:41:01 +03:00
Koitharu
62bd5008fd ProgressButton fixes 2024-04-08 10:40:48 +03:00
Koitharu
89dd7beafe Updated sort order for history and favorites 2024-04-08 10:13:54 +03:00
Koitharu
cecf3617af Adaptive tracker interval 2024-04-08 10:13:53 +03:00
Zakhar Timoshenko
f4c52654a7 UI adjust part 1 2024-04-08 01:31:22 +03:00
Zakhar Timoshenko
44b71460ee Fix read button coloring 2024-04-07 23:52:19 +03:00
Koitharu
265fbc9f63 UI improvements 2024-04-07 18:47:04 +03:00
Koitharu
7c4b254f08 UI improvements 2024-04-06 19:58:54 +03:00
Koitharu
1bf01ca240 Improve tracker part 2 2024-04-06 17:12:27 +03:00
Koitharu
54ff63dbc7 Improve tracker part 1 2024-04-06 16:12:59 +03:00
Koitharu
61ddee0bba New details activity and chapters sheet improvements 2024-04-04 11:16:51 +03:00
Koitharu
8174d236f6 Imrpove new chapters sheet 2024-04-03 11:23:53 +03:00
Koitharu
b27d5607ac New details activity 2024-04-03 07:40:01 +03:00
Koitharu
905f565766 Check backup format before restoring 2024-04-01 13:33:34 +03:00
Koitharu
b33c93290b Disable password saving for protect activity 2024-04-01 10:24:25 +03:00
Koitharu
5abb07fda2 Fix crash in BrowserActivity #835 2024-03-30 15:25:47 +02:00
Koitharu
b57069c55f Merge remote-tracking branch 'weblate/devel' into devel 2024-03-30 09:18:41 +02:00
Koitharu
5b1a4d3ff5 Update dependencies 2024-03-30 09:15:20 +02:00
Koitharu
2b26f944d0 Fix background color in webttoon mode #832 2024-03-30 09:02:12 +02:00
Koitharu
a15197f69d Update suggestions after config changes #831 2024-03-30 08:47:08 +02:00
Koitharu
41f64b2e36 Handle NoDataReceivedException 2024-03-30 08:21:47 +02:00
Koitharu
bec032c7dc Fix TransactionTooLargeException when using WebView 2024-03-30 08:13:02 +02:00
maryush
061eaa2a56 Translated using Weblate (Polish)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
Anton Prevrhal
bc6e29b562 Translated using Weblate (German)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Anton Prevrhal <anton.prevrhal@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
jonathan | ヨナタン
d8c1dcef29 Translated using Weblate (German)
Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: jonathan | ヨナタン <jonathan.evertz@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
Anon
ca281afba1 Translated using Weblate (Serbian)
Currently translated at 99.8% (635 of 636 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
ReksaTresna
cde07a60d7 Translated using Weblate (Indonesian)
Currently translated at 95.1% (605 of 636 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: ReksaTresna <ilham151096@gmail.com>
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
2024-03-23 15:36:15 +01:00
Infy's Tagalog Translations
e31af0f43f Translated using Weblate (Filipino)
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
Scrambled777
15dd0f38e7 Translated using Weblate (Hindi)
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
gekka
d93647e889 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
Oğuz Ersen
509d9a2fba Translated using Weblate (Turkish)
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
gallegonovato
879d05f1a6 Translated using Weblate (Spanish)
Currently translated at 100.0% (638 of 638 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (636 of 636 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-03-23 15:36:15 +01:00
Макар Разин
ecf6bbfb66 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (636 of 636 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (636 of 636 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
2024-03-23 15:36:15 +01:00
468 changed files with 8644 additions and 6959 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
/.idea/deviceManager.xml

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 631 versionCode = 642
versionName = '6.8.1' versionName = '7.0.1'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner' testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp { ksp {
@@ -82,31 +82,31 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:9821e93d25') { implementation('com.github.KotatsuApp:kotatsu-parsers:078b59b1e2') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.24'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.activity:activity-ktx:1.8.2' implementation 'androidx.activity:activity-ktx:1.9.0'
implementation 'androidx.fragment:fragment-ktx:1.6.2' implementation 'androidx.fragment:fragment-ktx:1.7.1'
implementation 'androidx.collection:collection-ktx:1.4.0' implementation 'androidx.collection:collection-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0'
implementation 'androidx.lifecycle:lifecycle-service:2.7.0' implementation 'androidx.lifecycle:lifecycle-service:2.8.0'
implementation 'androidx.lifecycle:lifecycle-process:2.7.0' implementation 'androidx.lifecycle:lifecycle-process:2.8.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.12.0-beta01' implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0' implementation 'androidx.lifecycle:lifecycle-common-java8:2.8.0'
implementation 'androidx.webkit:webkit:1.10.0' implementation 'androidx.webkit:webkit:1.11.0'
implementation 'androidx.work:work-runtime:2.9.0' implementation 'androidx.work:work-runtime:2.9.0'
//noinspection GradleDependency //noinspection GradleDependency
@@ -121,14 +121,15 @@ dependencies {
ksp 'androidx.room:room-compiler:2.6.1' ksp 'androidx.room:room-compiler:2.6.1'
implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:okhttp-tls:4.12.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
implementation 'com.squareup.okio:okio:3.9.0' implementation 'com.squareup.okio:okio:3.9.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.51' implementation 'com.google.dagger:hilt-android:2.51.1'
kapt 'com.google.dagger:hilt-compiler:2.51' kapt 'com.google.dagger:hilt-compiler:2.51.1'
implementation 'androidx.hilt:hilt-work:1.2.0' implementation 'androidx.hilt:hilt-work:1.2.0'
kapt 'androidx.hilt:hilt-compiler:1.2.0' kapt 'androidx.hilt:hilt-compiler:1.2.0'
@@ -145,22 +146,23 @@ dependencies {
implementation 'org.conscrypt:conscrypt-android:2.5.2' implementation 'org.conscrypt:conscrypt-android:2.5.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
debugImplementation 'com.github.Koitharu:WorkInspector:5778dd1747'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20240303' testImplementation 'org.json:json:20240303'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0' androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5' androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1'
androidTestImplementation 'androidx.room:room-testing:2.6.1' androidTestImplementation 'androidx.room:room-testing:2.6.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51' androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.51' kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.51.1'
} }

View File

@@ -174,12 +174,12 @@ class TrackerTest {
assertEquals(3, repository.getNewChaptersCount(mangaFirst.id)) assertEquals(3, repository.getNewChaptersCount(mangaFirst.id))
var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) } var chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex - 1) }
repository.syncWithHistory(mangaFull, chapter.id) tracker.syncWithHistory(mangaFull, chapter.id)
assertEquals(1, repository.getNewChaptersCount(mangaFirst.id)) assertEquals(1, repository.getNewChaptersCount(mangaFirst.id))
chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) } chapter = requireNotNull(mangaFull.chapters).run { get(lastIndex) }
repository.syncWithHistory(mangaFull, chapter.id) tracker.syncWithHistory(mangaFull, chapter.id)
assertEquals(0, repository.getNewChaptersCount(mangaFirst.id)) assertEquals(0, repository.getNewChaptersCount(mangaFirst.id))

View File

@@ -0,0 +1,12 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".tracker.ui.debug.TrackerDebugActivity"
android:label="@string/check_for_new_chapters" />
</application>
</manifest>

View File

@@ -11,7 +11,7 @@ import org.koitharu.kotatsu.reader.domain.PageLoader
class KotatsuApp : BaseApp() { class KotatsuApp : BaseApp() {
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(base) super.attachBaseContext(base)
enableStrictMode() enableStrictMode()
} }

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.util.ext
import android.os.Looper
fun Throwable.printStackTraceDebug() = printStackTrace()
fun assertNotInMainThread() = check(Looper.myLooper() != Looper.getMainLooper()) {
"Calling this from the main thread is prohibited"
}

View File

@@ -1,3 +0,0 @@
package org.koitharu.kotatsu.core.util.ext
fun Throwable.printStackTraceDebug() = printStackTrace()

View File

@@ -0,0 +1,72 @@
package org.koitharu.kotatsu.tracker.ui.debug
import android.graphics.Color
import android.text.format.DateUtils
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemTrackDebugBinding
import org.koitharu.kotatsu.tracker.data.TrackEntity
import com.google.android.material.R as materialR
fun trackDebugAD(
lifecycleOwner: LifecycleOwner,
coil: ImageLoader,
clickListener: OnListItemClickListener<TrackDebugItem>,
) = adapterDelegateViewBinding<TrackDebugItem, TrackDebugItem, ItemTrackDebugBinding>(
{ layoutInflater, parent -> ItemTrackDebugBinding.inflate(layoutInflater, parent, false) },
) {
val indicatorNew = ContextCompat.getDrawable(context, R.drawable.ic_new)
itemView.setOnClickListener { v ->
clickListener.onItemClick(item, v)
}
bind {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
defaultPlaceholders(context)
allowRgb565(true)
source(item.manga.source)
enqueueWith(coil)
}
binding.textViewTitle.text = item.manga.title
binding.textViewSummary.text = buildSpannedString {
item.lastCheckTime?.let {
append(
DateUtils.getRelativeDateTimeString(
context,
it.toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.WEEK_IN_MILLIS,
0,
),
)
}
if (item.lastResult == TrackEntity.RESULT_FAILED) {
append(" - ")
bold {
color(context.getThemeColor(materialR.attr.colorError, Color.RED)) {
append(getString(R.string.error))
}
}
}
}
binding.textViewTitle.drawableStart = if (item.newChapters > 0) {
indicatorNew
} else {
null
}
}
}

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.tracker.ui.debug
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import java.time.Instant
data class TrackDebugItem(
val manga: Manga,
val lastChapterId: Long,
val newChapters: Int,
val lastCheckTime: Instant?,
val lastChapterDate: Instant?,
val lastResult: Int,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is TrackDebugItem && other.manga.id == manga.id
}
}

View File

@@ -0,0 +1,57 @@
package org.koitharu.kotatsu.tracker.ui.debug
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.ActivityTrackerDebugBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import javax.inject.Inject
@AndroidEntryPoint
class TrackerDebugActivity : BaseActivity<ActivityTrackerDebugBinding>(), OnListItemClickListener<TrackDebugItem> {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<TrackerDebugViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityTrackerDebugBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val tracksAdapter = BaseListAdapter<TrackDebugItem>()
.addDelegate(ListItemType.FEED, trackDebugAD(this, coil, this))
with(viewBinding.recyclerView) {
adapter = tracksAdapter
addItemDecoration(TypedListSpacingDecoration(context, false))
}
viewModel.content.observe(this, tracksAdapter)
}
override fun onWindowInsetsChanged(insets: Insets) {
val rv = viewBinding.recyclerView
rv.updatePadding(
left = insets.left + rv.paddingTop,
right = insets.right + rv.paddingTop,
bottom = insets.bottom,
)
viewBinding.toolbar.updatePadding(
left = insets.left,
right = insets.right,
)
}
override fun onItemClick(item: TrackDebugItem, view: View) {
startActivity(DetailsActivity.newIntent(this, item.manga))
}
}

View File

@@ -0,0 +1,36 @@
package org.koitharu.kotatsu.tracker.ui.debug
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toManga
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.toInstantOrNull
import org.koitharu.kotatsu.tracker.data.TrackWithManga
import javax.inject.Inject
@HiltViewModel
class TrackerDebugViewModel @Inject constructor(
private val db: MangaDatabase
) : BaseViewModel() {
val content = db.getTracksDao().observeAll()
.map { it.toUiList() }
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
private fun List<TrackWithManga>.toUiList(): List<TrackDebugItem> = map {
TrackDebugItem(
manga = it.manga.toManga(emptySet()),
lastChapterId = it.track.lastChapterId,
newChapters = it.track.newChapters,
lastCheckTime = it.track.lastCheckTime.toInstantOrNull(),
lastChapterDate = it.track.lastChapterDate.toInstantOrNull(),
lastResult = it.track.lastResult,
)
}
}

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbarLayout"
android:layout_width="match_parent"
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:toolbarId="@id/toolbar">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="@dimen/list_spacing_normal"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
tools:listitem="@layout/item_track_debug" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -4,7 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="72dp"
android:background="@drawable/list_selector" android:background="@drawable/list_selector"
android:clipChildren="false"> android:clipChildren="false">
@@ -14,7 +14,9 @@
android:layout_height="40dp" android:layout_height="40dp"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Small"
@@ -26,42 +28,29 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:drawablePadding="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyLarge" android:textAppearance="?attr/textAppearanceTitleSmall"
app:layout_constraintBottom_toTopOf="@+id/textView_subtitle" app:layout_constraintBottom_toTopOf="@+id/textView_summary"
app:layout_constraintEnd_toStartOf="@id/button_more" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover" app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toTopOf="@+id/imageView_cover" app:layout_constraintTop_toTopOf="@+id/imageView_cover"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
<TextView <TextView
android:id="@+id/textView_subtitle" android:id="@+id/textView_summary"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodySmall"
app:layout_constraintBottom_toBottomOf="@+id/imageView_cover" app:layout_constraintBottom_toBottomOf="@+id/imageView_cover"
app:layout_constraintEnd_toStartOf="@id/button_more" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView_cover" app:layout_constraintStart_toEndOf="@+id/imageView_cover"
app:layout_constraintTop_toBottomOf="@+id/textView_title" app:layout_constraintTop_toBottomOf="@+id/textView_title"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />
<Button
android:id="@+id/button_more"
style="@style/Widget.Kotatsu.ExploreButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:gravity="center"
android:minWidth="120dp"
android:text="@string/more"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -8,4 +8,14 @@
android:title="@string/leak_canary_display_activity_label" android:title="@string/leak_canary_display_activity_label"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> <item
android:id="@id/action_tracker"
android:title="@string/check_for_new_chapters"
app:showAsAction="never" />
<item
android:id="@id/action_works"
android:title="Works"
app:showAsAction="never" />
</menu>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool> <bool name="leak_canary_add_launcher_icon" tools:node="replace">false</bool>
<bool name="wi_launcher_icon_enabled" tools:node="replace">false</bool>
</resources> </resources>

View File

@@ -122,7 +122,7 @@
android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity" android:name="org.koitharu.kotatsu.favourites.ui.FavouritesActivity"
android:label="@string/favourites" /> android:label="@string/favourites" />
<activity <activity
android:name="org.koitharu.kotatsu.bookmarks.ui.BookmarksActivity" android:name="org.koitharu.kotatsu.bookmarks.ui.AllBookmarksActivity"
android:label="@string/bookmarks" /> android:label="@string/bookmarks" />
<activity <activity
android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity" android:name="org.koitharu.kotatsu.suggestions.ui.SuggestionsActivity"
@@ -245,6 +245,9 @@
<activity <activity
android:name="org.koitharu.kotatsu.alternatives.ui.AlternativesActivity" android:name="org.koitharu.kotatsu.alternatives.ui.AlternativesActivity"
android:label="@string/alternatives" /> android:label="@string/alternatives" />
<activity
android:name="org.koitharu.kotatsu.settings.about.AppUpdateActivity"
android:label="@string/app_update_available" />
<service <service
android:name="androidx.work.impl.foreground.SystemForegroundService" android:name="androidx.work.impl.foreground.SystemForegroundService"
@@ -253,6 +256,9 @@
<service <service
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
android:name="org.koitharu.kotatsu.local.ui.ImportService"
android:foregroundServiceType="dataSync" />
<service <service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService" android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
@@ -354,13 +360,6 @@
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/widget_recent" /> android:resource="@xml/widget_recent" />
</receiver> </receiver>
<receiver
android:name="org.koitharu.kotatsu.settings.about.UpdateDownloadReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<receiver <receiver
android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver" android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver"
android:exported="false"> android:exported="false">

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

View File

@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.history.data.toMangaHistory
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.tracker.data.TrackEntity
import javax.inject.Inject import javax.inject.Inject
class MigrateUseCase @Inject constructor( class MigrateUseCase @Inject constructor(
@@ -56,6 +57,22 @@ class MigrateUseCase @Inject constructor(
historyDao.delete(oldDetails.id) historyDao.delete(oldDetails.id)
historyDao.upsert(newHistory) historyDao.upsert(newHistory)
} }
// track
val tracksDao = database.getTracksDao()
val oldTrack = tracksDao.find(oldDetails.id)
if (oldTrack != null) {
val lastChapter = newDetails.chapters?.lastOrNull()
val newTrack = TrackEntity(
mangaId = newDetails.id,
lastChapterId = lastChapter?.id ?: 0L,
newChapters = 0,
lastCheckTime = System.currentTimeMillis(),
lastChapterDate = lastChapter?.uploadDate ?: 0L,
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
)
tracksDao.delete(oldDetails.id)
tracksDao.upsert(newTrack)
}
} }
progressUpdateUseCase(newManga) progressUpdateUseCase(newManga)
} }

View File

@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -79,9 +80,7 @@ fun alternativeAD(
} }
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run { binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover)) size(CoverSizeResolver(binding.imageViewCover))
placeholder(R.drawable.ic_placeholder) defaultPlaceholders(context)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
transformations(TrimTransformation()) transformations(TrimTransformation())
allowRgb565(true) allowRgb565(true)
tag(item.manga) tag(item.manga)

View File

@@ -12,9 +12,6 @@ import org.koitharu.kotatsu.core.db.entity.MangaWithTags
@Dao @Dao
abstract class BookmarksDao { 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 page_id = :pageId") @Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
abstract suspend fun find(pageId: Long): BookmarkEntity? abstract suspend fun find(pageId: Long): BookmarkEntity?
@@ -42,9 +39,6 @@ abstract class BookmarksDao {
@Delete @Delete
abstract suspend fun delete(entity: BookmarkEntity) abstract suspend fun delete(entity: BookmarkEntity)
@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") @Query("DELETE FROM bookmarks WHERE page_id = :pageId")
abstract suspend fun delete(pageId: Long): Int abstract suspend fun delete(pageId: Long): Int

View File

@@ -16,7 +16,7 @@ import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner import org.koitharu.kotatsu.main.ui.owners.SnackbarOwner
@AndroidEntryPoint @AndroidEntryPoint
class BookmarksActivity : class AllBookmarksActivity :
BaseActivity<ActivityContainerBinding>(), BaseActivity<ActivityContainerBinding>(),
AppBarOwner, AppBarOwner,
SnackbarOwner { SnackbarOwner {
@@ -35,7 +35,7 @@ class BookmarksActivity :
if (fm.findFragmentById(R.id.container) == null) { if (fm.findFragmentById(R.id.container) == null) {
fm.commit { fm.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
replace(R.id.container, BookmarksFragment::class.java, null) replace(R.id.container, AllBookmarksFragment::class.java, null)
} }
} }
} }
@@ -49,6 +49,6 @@ class BookmarksActivity :
companion object { companion object {
fun newIntent(context: Context) = Intent(context, BookmarksActivity::class.java) fun newIntent(context: Context) = Intent(context, AllBookmarksActivity::class.java)
} }
} }

View File

@@ -17,7 +17,7 @@ import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.ui.sheet.BookmarksAdapter import org.koitharu.kotatsu.bookmarks.ui.adapter.BookmarksAdapter
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseFragment import org.koitharu.kotatsu.core.ui.BaseFragment
@@ -25,11 +25,12 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.MangaListSpanResolver import org.koitharu.kotatsu.list.ui.GridSpanResolver
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener import org.koitharu.kotatsu.list.ui.adapter.ListStateHolderListener
@@ -41,7 +42,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class BookmarksFragment : class AllBookmarksFragment :
BaseFragment<FragmentListSimpleBinding>(), BaseFragment<FragmentListSimpleBinding>(),
ListStateHolderListener, ListStateHolderListener,
OnListItemClickListener<Bookmark>, OnListItemClickListener<Bookmark>,
@@ -54,7 +55,7 @@ class BookmarksFragment :
@Inject @Inject
lateinit var settings: AppSettings lateinit var settings: AppSettings
private val viewModel by viewModels<BookmarksViewModel>() private val viewModel by viewModels<AllBookmarksViewModel>()
private var bookmarksAdapter: BookmarksAdapter? = null private var bookmarksAdapter: BookmarksAdapter? = null
private var selectionController: ListSelectionController? = null private var selectionController: ListSelectionController? = null
@@ -71,7 +72,7 @@ class BookmarksFragment :
) { ) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
selectionController = ListSelectionController( selectionController = ListSelectionController(
activity = requireActivity(), appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = BookmarksSelectionDecoration(binding.root.context), decoration = BookmarksSelectionDecoration(binding.root.context),
registryOwner = this, registryOwner = this,
callback = this, callback = this,
@@ -85,7 +86,7 @@ class BookmarksFragment :
val spanSizeLookup = SpanSizeLookup() val spanSizeLookup = SpanSizeLookup()
with(binding.recyclerView) { with(binding.recyclerView) {
setHasFixedSize(true) setHasFixedSize(true)
val spanResolver = MangaListSpanResolver(resources) val spanResolver = GridSpanResolver(resources)
addItemDecoration(TypedListSpacingDecoration(context, false)) addItemDecoration(TypedListSpacingDecoration(context, false))
adapter = bookmarksAdapter adapter = bookmarksAdapter
addOnLayoutChangeListener(spanResolver) addOnLayoutChangeListener(spanResolver)
@@ -100,7 +101,7 @@ class BookmarksFragment :
} }
viewModel.onError.observeEvent( viewModel.onError.observeEvent(
viewLifecycleOwner, viewLifecycleOwner,
SnackbarErrorObserver(binding.recyclerView, this) SnackbarErrorObserver(binding.recyclerView, this),
) )
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView)) viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
} }
@@ -206,11 +207,12 @@ class BookmarksFragment :
companion object { companion object {
@Deprecated( @Deprecated(
"", ReplaceWith( "",
ReplaceWith(
"BookmarksFragment()", "BookmarksFragment()",
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment" "org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
) ),
) )
fun newInstance() = BookmarksFragment() fun newInstance() = AllBookmarksFragment()
} }
} }

View File

@@ -25,7 +25,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BookmarksViewModel @Inject constructor( class AllBookmarksViewModel @Inject constructor(
private val repository: BookmarksRepository, private val repository: BookmarksRepository,
) : BaseViewModel() { ) : BaseViewModel() {

View File

@@ -1,14 +1,14 @@
package org.koitharu.kotatsu.bookmarks.ui.sheet package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -30,9 +30,7 @@ fun bookmarkLargeAD(
bind { bind {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run { binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb)) size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder) defaultPlaceholders(context)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
tag(item) tag(item)
decodeRegion(item.scroll) decodeRegion(item.scroll)

View File

@@ -3,12 +3,12 @@ package org.koitharu.kotatsu.bookmarks.ui.adapter
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.decodeRegion import org.koitharu.kotatsu.core.util.ext.decodeRegion
import org.koitharu.kotatsu.core.util.ext.defaultPlaceholders
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
@@ -29,9 +29,7 @@ fun bookmarkListAD(
bind { bind {
binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run { binding.imageViewThumb.newImageRequest(lifecycleOwner, item.imageLoadData)?.run {
size(CoverSizeResolver(binding.imageViewThumb)) size(CoverSizeResolver(binding.imageViewThumb))
placeholder(R.drawable.ic_placeholder) defaultPlaceholders(context)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
tag(item) tag(item)
decodeRegion(item.scroll) decodeRegion(item.scroll)

View File

@@ -1,19 +1,36 @@
package org.koitharu.kotatsu.bookmarks.ui.adapter package org.koitharu.kotatsu.bookmarks.ui.adapter
import android.content.Context
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener 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.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.ListModel
class BookmarksAdapter( class BookmarksAdapter(
coil: ImageLoader, coil: ImageLoader,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
clickListener: OnListItemClickListener<Bookmark>, clickListener: OnListItemClickListener<Bookmark>,
) : BaseListAdapter<Bookmark>() { headerClickListener: ListHeaderClickListener?,
) : BaseListAdapter<ListModel>(), FastScroller.SectionIndexer {
init { init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkListAD(coil, lifecycleOwner, clickListener)) 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? {
return findHeader(position)?.getText(context)
} }
} }

View File

@@ -1,36 +0,0 @@
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.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? {
return findHeader(position)?.getText(context)
}
}

View File

@@ -1,169 +0,0 @@
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))
}.showDistinct(fm, TAG)
}
}
}

View File

@@ -1,55 +0,0 @@
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) }
.withErrorHandling()
.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

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.browser package org.koitharu.kotatsu.browser
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -12,18 +11,28 @@ import android.webkit.CookieManager
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled") @AndroidEntryPoint
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback { class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
private lateinit var onBackPressedCallback: WebViewBackPressedCallback private lateinit var onBackPressedCallback: WebViewBackPressedCallback
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
@@ -33,7 +42,11 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
setDisplayHomeAsUpEnabled(true) setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
} }
viewBinding.webView.configureForParser(null) val userAgent = intent?.getSerializableExtraCompat<MangaSource>(EXTRA_SOURCE)?.let { source ->
val repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository
repository?.headers?.get(CommonHeaders.USER_AGENT)
}
viewBinding.webView.configureForParser(userAgent)
CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true) CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
viewBinding.webView.webViewClient = BrowserClient(this) viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
@@ -54,16 +67,6 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
} }
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
viewBinding.webView.saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
viewBinding.webView.restoreState(savedInstanceState)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_browser, menu) menuInflater.inflate(R.menu.opt_browser, menu)
@@ -136,11 +139,13 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
companion object { companion object {
private const val EXTRA_TITLE = "title" private const val EXTRA_TITLE = "title"
private const val EXTRA_SOURCE = "source"
fun newIntent(context: Context, url: String, title: String?): Intent { fun newIntent(context: Context, url: String, source: MangaSource?, title: String?): Intent {
return Intent(context, BrowserActivity::class.java) return Intent(context, BrowserActivity::class.java)
.setData(Uri.parse(url)) .setData(Uri.parse(url))
.putExtra(EXTRA_TITLE, title) .putExtra(EXTRA_TITLE, title)
.putExtra(EXTRA_SOURCE, source)
} }
} }
} }

View File

@@ -1,6 +1,9 @@
package org.koitharu.kotatsu.browser.cloudflare package org.koitharu.kotatsu.browser.cloudflare
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -20,7 +23,7 @@ class CaptchaNotifier(
) : EventListener { ) : EventListener {
fun notify(exception: CloudFlareProtectedException) { fun notify(exception: CloudFlareProtectedException) {
if (!context.checkNotificationPermission()) { if (!context.checkNotificationPermission(CHANNEL_ID)) {
return return
} }
val manager = NotificationManagerCompat.from(context) val manager = NotificationManagerCompat.from(context)
@@ -33,13 +36,14 @@ class CaptchaNotifier(
.build() .build()
manager.createNotificationChannel(channel) manager.createNotificationChannel(channel)
val intent = CloudFlareActivity.newIntent(context, exception.url, exception.headers) val intent = CloudFlareActivity.newIntent(context, exception)
.setData(exception.url.toUri()) .setData(exception.url.toUri())
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(channel.name) .setContentTitle(channel.name)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(NotificationCompat.DEFAULT_SOUND) .setDefaults(NotificationCompat.DEFAULT_SOUND)
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
.setGroup(GROUP_CAPTCHA)
.setAutoCancel(true) .setAutoCancel(true)
.setVisibility( .setVisibility(
if (exception.source?.contentType == ContentType.HENTAI) { if (exception.source?.contentType == ContentType.HENTAI) {
@@ -55,8 +59,21 @@ class CaptchaNotifier(
), ),
) )
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false)) .setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
.build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.notify(TAG, exception.source.hashCode(), notification) val actionIntent = PendingIntentCompat.getActivity(
context, SETTINGS_ACTION_CODE,
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
.putExtra(Settings.EXTRA_CHANNEL_ID, CHANNEL_ID),
0, false,
)
notification.addAction(
R.drawable.ic_settings,
context.getString(R.string.notifications_settings),
actionIntent,
)
}
manager.notify(TAG, exception.source.hashCode(), notification.build())
} }
fun dismiss(source: MangaSource) { fun dismiss(source: MangaSource) {
@@ -82,5 +99,7 @@ class CaptchaNotifier(
private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha" private const val PARAM_IGNORE_CAPTCHA = "ignore_captcha"
private const val CHANNEL_ID = "captcha" private const val CHANNEL_ID = "captcha"
private const val TAG = CHANNEL_ID private const val TAG = CHANNEL_ID
private const val GROUP_CAPTCHA = "org.koitharu.kotatsu.CAPTCHA"
private const val SETTINGS_ACTION_CODE = 3
} }
} }

View File

@@ -23,12 +23,15 @@ import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.WebViewBackPressedCallback import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.configureForParser import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject import javax.inject.Inject
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@@ -81,16 +84,6 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
super.onDestroy() super.onDestroy()
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
viewBinding.webView.saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
viewBinding.webView.restoreState(savedInstanceState)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_captcha, menu) menuInflater.inflate(R.menu.opt_captcha, menu)
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
@@ -147,6 +140,10 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
override fun onCheckPassed() { override fun onCheckPassed() {
pendingResult = RESULT_OK pendingResult = RESULT_OK
val source = intent?.getStringExtra(ARG_SOURCE)
if (source != null) {
CaptchaNotifier(this).dismiss(MangaSource(source))
}
finishAfterTransition() finishAfterTransition()
} }
@@ -184,9 +181,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
} }
} }
class Contract : ActivityResultContract<Pair<String, Headers?>, TaggedActivityResult>() { class Contract : ActivityResultContract<CloudFlareProtectedException, TaggedActivityResult>() {
override fun createIntent(context: Context, input: Pair<String, Headers?>): Intent { override fun createIntent(context: Context, input: CloudFlareProtectedException): Intent {
return newIntent(context, input.first, input.second) return newIntent(context, input)
} }
override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult { override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
@@ -198,13 +195,23 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
const val TAG = "CloudFlareActivity" const val TAG = "CloudFlareActivity"
private const val ARG_UA = "ua" private const val ARG_UA = "ua"
private const val ARG_SOURCE = "_source"
fun newIntent( fun newIntent(context: Context, exception: CloudFlareProtectedException) = newIntent(
context = context,
url = exception.url,
source = exception.source,
headers = exception.headers,
)
private fun newIntent(
context: Context, context: Context,
url: String, url: String,
source: MangaSource?,
headers: Headers?, headers: Headers?,
) = Intent(context, CloudFlareActivity::class.java).apply { ) = Intent(context, CloudFlareActivity::class.java).apply {
data = url.toUri() data = url.toUri()
putExtra(ARG_SOURCE, source?.name)
headers?.get(CommonHeaders.USER_AGENT)?.let { headers?.get(CommonHeaders.USER_AGENT)?.let {
putExtra(ARG_UA, it) putExtra(ARG_UA, it)
} }

View File

@@ -26,9 +26,6 @@ import kotlinx.coroutines.flow.asSharedFlow
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.StubContentCache
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
import org.koitharu.kotatsu.core.network.MangaHttpClient import org.koitharu.kotatsu.core.network.MangaHttpClient
@@ -37,12 +34,14 @@ import org.koitharu.kotatsu.core.os.NetworkState
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher import org.koitharu.kotatsu.core.parser.favicon.FaviconFetcher
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.image.CoilImageGetter import org.koitharu.kotatsu.core.ui.image.CoilImageGetter
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.AcraScreenLogger import org.koitharu.kotatsu.core.util.AcraScreenLogger
import org.koitharu.kotatsu.core.util.IncognitoModeIndicator
import org.koitharu.kotatsu.core.util.ext.connectivityManager import org.koitharu.kotatsu.core.util.ext.connectivityManager
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageFetcher
import org.koitharu.kotatsu.details.ui.pager.pages.MangaPageKeyer
import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.CbzFetcher
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
@@ -50,11 +49,11 @@ import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.settings.backup.BackupObserver import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.widget.WidgetUpdater import org.koitharu.kotatsu.widget.WidgetUpdater
import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -72,8 +71,9 @@ interface AppModule {
@Provides @Provides
@Singleton @Singleton
fun provideNetworkState( fun provideNetworkState(
@ApplicationContext context: Context @ApplicationContext context: Context,
) = NetworkState(context.connectivityManager) settings: AppSettings,
) = NetworkState(context.connectivityManager, settings)
@Provides @Provides
@Singleton @Singleton
@@ -87,7 +87,7 @@ interface AppModule {
@Singleton @Singleton
fun provideCoil( fun provideCoil(
@ApplicationContext context: Context, @ApplicationContext context: Context,
@MangaHttpClient okHttpClient: OkHttpClient, @MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
mangaRepositoryFactory: MangaRepository.Factory, mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor, imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory, pageFetcherFactory: MangaPageFetcher.Factory,
@@ -99,11 +99,14 @@ interface AppModule {
.directory(rootDir.resolve(CacheDir.THUMBS.dir)) .directory(rootDir.resolve(CacheDir.THUMBS.dir))
.build() .build()
} }
val okHttpClientLazy = lazy {
okHttpClientProvider.get().newBuilder().cache(null).build()
}
return ImageLoader.Builder(context) return ImageLoader.Builder(context)
.okHttpClient(okHttpClient.newBuilder().cache(null).build()) .okHttpClient { okHttpClientLazy.value }
.interceptorDispatcher(Dispatchers.Default) .interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO) .fetcherDispatcher(Dispatchers.Default)
.decoderDispatcher(Dispatchers.Default) .decoderDispatcher(Dispatchers.IO)
.transformationDispatcher(Dispatchers.Default) .transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory) .diskCache(diskCacheFactory)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null) .logger(if (BuildConfig.DEBUG) DebugLogger() else null)
@@ -113,7 +116,8 @@ interface AppModule {
ComponentRegistry.Builder() ComponentRegistry.Builder()
.add(SvgDecoder.Factory()) .add(SvgDecoder.Factory())
.add(CbzFetcher.Factory()) .add(CbzFetcher.Factory())
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory)) .add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
.add(MangaPageKeyer())
.add(pageFetcherFactory) .add(pageFetcherFactory)
.add(imageProxyInterceptor) .add(imageProxyInterceptor)
.add(coverRestoreInterceptor) .add(coverRestoreInterceptor)
@@ -147,27 +151,13 @@ interface AppModule {
fun provideActivityLifecycleCallbacks( fun provideActivityLifecycleCallbacks(
appProtectHelper: AppProtectHelper, appProtectHelper: AppProtectHelper,
activityRecreationHandle: ActivityRecreationHandle, activityRecreationHandle: ActivityRecreationHandle,
incognitoModeIndicator: IncognitoModeIndicator,
acraScreenLogger: AcraScreenLogger, acraScreenLogger: AcraScreenLogger,
): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf( ): Set<@JvmSuppressWildcards Application.ActivityLifecycleCallbacks> = arraySetOf(
appProtectHelper, appProtectHelper,
activityRecreationHandle, activityRecreationHandle,
incognitoModeIndicator,
acraScreenLogger, acraScreenLogger,
) )
@Provides
@Singleton
fun provideContentCache(
application: Application,
): ContentCache {
return if (application.isLowRamDevice()) {
StubContentCache()
} else {
MemoryContentCache(application)
}
}
@Provides @Provides
@Singleton @Singleton
@LocalStorageChanges @LocalStorageChanges

View File

@@ -37,7 +37,7 @@ import javax.inject.Provider
open class BaseApp : Application(), Configuration.Provider { open class BaseApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var databaseObservers: Set<@JvmSuppressWildcards InvalidationTracker.Observer> lateinit var databaseObserversProvider: Provider<Set<@JvmSuppressWildcards InvalidationTracker.Observer>>
@Inject @Inject
lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks> lateinit var activityLifecycleCallbacks: Set<@JvmSuppressWildcards ActivityLifecycleCallbacks>
@@ -87,7 +87,7 @@ open class BaseApp : Application(), Configuration.Provider {
WorkServiceStopHelper(workManagerProvider).setup() WorkServiceStopHelper(workManagerProvider).setup()
} }
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(base) super.attachBaseContext(base)
initAcra { initAcra {
buildConfigClass = BuildConfig::class.java buildConfigClass = BuildConfig::class.java
@@ -123,7 +123,7 @@ open class BaseApp : Application(), Configuration.Provider {
@WorkerThread @WorkerThread
private fun setupDatabaseObservers() { private fun setupDatabaseObservers() {
val tracker = database.get().invalidationTracker val tracker = database.get().invalidationTracker
databaseObservers.forEach { databaseObserversProvider.get().forEach {
tracker.addObserver(it) tracker.addObserver(it)
} }
} }

View File

@@ -6,12 +6,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import okio.Closeable import okio.Closeable
import org.json.JSONArray import org.json.JSONArray
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import java.io.File import java.io.File
import java.util.EnumSet import java.util.EnumSet
import java.util.zip.ZipException
import java.util.zip.ZipFile import java.util.zip.ZipFile
class BackupZipInput(val file: File) : Closeable { class BackupZipInput private constructor(val file: File) : Closeable {
private val zipFile = ZipFile(file) private val zipFile = ZipFile(file)
@@ -41,4 +43,17 @@ class BackupZipInput(val file: File) : Closeable {
} }
} }
} }
companion object {
fun from(file: File): BackupZipInput = try {
val res = BackupZipInput(file)
if (res.zipFile.getEntry("index") == null) {
throw BadBackupFormatException(null)
}
res
} catch (e: ZipException) {
throw BadBackupFormatException(e)
}
}
} }

View File

@@ -39,7 +39,7 @@ suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptibl
val filename = buildString { val filename = buildString {
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT)) append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
append('_') append('_')
append(LocalDate.now().format(DateTimeFormatter.ofPattern("ddMMyyyy"))) append(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")))
append(".bk.zip") append(".bk.zip")
} }
BackupZipOutput(File(dir, filename)) BackupZipOutput(File(dir, filename))

View File

@@ -1,29 +0,0 @@
package org.koitharu.kotatsu.core.cache
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
interface ContentCache {
val isCachingEnabled: Boolean
suspend fun getDetails(source: MangaSource, url: String): Manga?
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
fun clear(source: MangaSource)
data class Key(
val source: MangaSource,
val url: String,
)
}

View File

@@ -2,18 +2,19 @@ package org.koitharu.kotatsu.core.cache
import androidx.collection.LruCache import androidx.collection.LruCache
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import org.koitharu.kotatsu.core.cache.MemoryContentCache.Key as CacheKey
class ExpiringLruCache<T>( class ExpiringLruCache<T>(
val maxSize: Int, val maxSize: Int,
private val lifetime: Long, private val lifetime: Long,
private val timeUnit: TimeUnit, private val timeUnit: TimeUnit,
) : Iterable<ContentCache.Key> { ) : Iterable<CacheKey> {
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize) private val cache = LruCache<CacheKey, ExpiringValue<T>>(maxSize)
override fun iterator(): Iterator<ContentCache.Key> = cache.snapshot().keys.iterator() override fun iterator(): Iterator<CacheKey> = cache.snapshot().keys.iterator()
operator fun get(key: ContentCache.Key): T? { operator fun get(key: CacheKey): T? {
val value = cache[key] ?: return null val value = cache[key] ?: return null
if (value.isExpired) { if (value.isExpired) {
cache.remove(key) cache.remove(key)
@@ -21,7 +22,7 @@ class ExpiringLruCache<T>(
return value.get() return value.get()
} }
operator fun set(key: ContentCache.Key, value: T) { operator fun set(key: CacheKey, value: T) {
cache.put(key, ExpiringValue(value, lifetime, timeUnit)) cache.put(key, ExpiringValue(value, lifetime, timeUnit))
} }
@@ -33,7 +34,7 @@ class ExpiringLruCache<T>(
cache.trimToSize(size) cache.trimToSize(size)
} }
fun remove(key: ContentCache.Key) { fun remove(key: CacheKey) {
cache.remove(key) cache.remove(key)
} }
} }

View File

@@ -3,48 +3,54 @@ package org.koitharu.kotatsu.core.cache
import android.app.Application import android.app.Application
import android.content.ComponentCallbacks2 import android.content.ComponentCallbacks2
import android.content.res.Configuration import android.content.res.Configuration
import org.koitharu.kotatsu.core.util.ext.isLowRamDevice
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 { @Singleton
class MemoryContentCache @Inject constructor(application: Application) : ComponentCallbacks2 {
private val isLowRam = application.isLowRamDevice()
init { init {
application.registerComponentCallbacks(this) application.registerComponentCallbacks(this)
} }
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES) private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES) private val pagesCache =
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES) ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
private val relatedMangaCache =
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
override val isCachingEnabled: Boolean = true suspend fun getDetails(source: MangaSource, url: String): Manga? {
return detailsCache[Key(source, url)]?.awaitOrNull()
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
} }
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) { fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
detailsCache[ContentCache.Key(source, url)] = details detailsCache[Key(source, url)] = details
} }
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? { suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull() return pagesCache[Key(source, url)]?.awaitOrNull()
} }
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) { fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
pagesCache[ContentCache.Key(source, url)] = pages pagesCache[Key(source, url)] = pages
} }
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? { suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull() return relatedMangaCache[Key(source, url)]?.awaitOrNull()
} }
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) { fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
relatedMangaCache[ContentCache.Key(source, url)] = related relatedMangaCache[Key(source, url)] = related
} }
override fun clear(source: MangaSource) { fun clear(source: MangaSource) {
clearCache(detailsCache, source) clearCache(detailsCache, source)
clearCache(pagesCache, source) clearCache(pagesCache, source)
clearCache(relatedMangaCache, source) clearCache(relatedMangaCache, source)
@@ -81,4 +87,9 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
} }
} }
} }
data class Key(
val source: MangaSource,
val url: String,
)
} }

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.core.cache
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
class StubContentCache : ContentCache {
override val isCachingEnabled: Boolean = false
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
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
override fun clear(source: MangaSource) = Unit
}

View File

@@ -31,6 +31,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration15To16
import org.koitharu.kotatsu.core.db.migrations.Migration16To17 import org.koitharu.kotatsu.core.db.migrations.Migration16To17
import org.koitharu.kotatsu.core.db.migrations.Migration17To18 import org.koitharu.kotatsu.core.db.migrations.Migration17To18
import org.koitharu.kotatsu.core.db.migrations.Migration18To19 import org.koitharu.kotatsu.core.db.migrations.Migration18To19
import org.koitharu.kotatsu.core.db.migrations.Migration19To20
import org.koitharu.kotatsu.core.db.migrations.Migration1To2 import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration3To4
@@ -57,7 +58,7 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 19 const val DATABASE_VERSION = 20
@Database( @Database(
entities = [ entities = [
@@ -116,6 +117,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration16To17(context), Migration16To17(context),
Migration17To18(), Migration17To18(),
Migration18To19(), Migration18To19(),
Migration19To20(),
) )
fun MangaDatabase(context: Context): MangaDatabase = Room fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -28,6 +28,9 @@ abstract class MangaDao {
@Query("SELECT * FROM manga WHERE source = :source") @Query("SELECT * FROM manga WHERE source = :source")
abstract suspend fun findAllBySource(source: String): List<MangaWithTags> abstract suspend fun findAllBySource(source: String): List<MangaWithTags>
@Query("SELECT author FROM manga WHERE author LIKE :query GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
abstract suspend fun findAuthors(query: String, limit: Int): List<String>
@Transaction @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") @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> abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
@@ -37,7 +40,7 @@ abstract class MangaDao {
abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags> abstract suspend fun searchByTitle(query: String, source: String, limit: Int): List<MangaWithTags>
@Upsert @Upsert
abstract suspend fun upsert(manga: MangaEntity) protected abstract suspend fun upsert(manga: MangaEntity)
@Update(onConflict = OnConflictStrategy.IGNORE) @Update(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun update(manga: MangaEntity): Int abstract suspend fun update(manga: MangaEntity): Int

View File

@@ -20,11 +20,8 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources ORDER BY sort_key") @Query("SELECT * FROM sources ORDER BY sort_key")
abstract suspend fun findAll(): List<MangaSourceEntity> abstract suspend fun findAll(): List<MangaSourceEntity>
@Query("SELECT * FROM sources WHERE enabled = 0 ORDER BY sort_key") @Query("SELECT source FROM sources WHERE enabled = 1")
abstract suspend fun findAllDisabled(): List<MangaSourceEntity> abstract suspend fun findAllEnabledNames(): List<String>
@Query("SELECT * FROM sources WHERE enabled = 0")
abstract fun observeDisabled(): Flow<List<MangaSourceEntity>>
@Query("SELECT * FROM sources ORDER BY sort_key") @Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>> abstract fun observeAll(): Flow<List<MangaSourceEntity>>

View File

@@ -1,6 +1,10 @@
package org.koitharu.kotatsu.core.db.dao 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 kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
@@ -12,18 +16,24 @@ interface TrackLogsDao {
@Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0") @Query("SELECT * FROM track_logs ORDER BY created_at DESC LIMIT :limit OFFSET 0")
fun observeAll(limit: Int): Flow<List<TrackLogWithManga>> fun observeAll(limit: Int): Flow<List<TrackLogWithManga>>
@Query("SELECT COUNT(*) FROM track_logs WHERE unread = 1")
fun observeUnreadCount(): Flow<Int>
@Query("DELETE FROM track_logs") @Query("DELETE FROM track_logs")
suspend fun clear() suspend fun clear()
@Query("UPDATE track_logs SET unread = 0 WHERE id = :id")
suspend fun markAsRead(id: Long)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entity: TrackLogEntity): Long suspend fun insert(entity: TrackLogEntity): Long
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)") @Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun gc() suspend fun gc()
@Query("DELETE FROM track_logs WHERE id IN (SELECT id FROM track_logs ORDER BY created_at DESC LIMIT 0 OFFSET :size)")
suspend fun trim(size: Int)
@Query("SELECT COUNT(*) FROM track_logs") @Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int suspend fun count(): Int
} }

View File

@@ -0,0 +1,18 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration19To20 : Migration(19, 20) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("CREATE TABLE tracks_bk (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id))")
db.execSQL("INSERT INTO tracks_bk SELECT manga_id, chapters_total, last_chapter_id, chapters_new, last_check, last_notified_id FROM tracks")
db.execSQL("DROP TABLE tracks")
db.execSQL("CREATE TABLE tracks (`manga_id` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check_time` INTEGER NOT NULL, `last_chapter_date` INTEGER NOT NULL, `last_result` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
db.execSQL("INSERT INTO tracks SELECT manga_id, last_chapter_id, chapters_new, last_check AS last_check_time, 0 AS last_chapter_date, 0 AS last_result FROM tracks_bk")
db.execSQL("DROP TABLE tracks_bk")
db.execSQL("ALTER TABLE track_logs ADD COLUMN `unread` INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -0,0 +1,5 @@
package org.koitharu.kotatsu.core.exceptions
import java.io.IOException
class BadBackupFormatException(cause: Throwable?) : IOException(cause)

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.core.exceptions
import okhttp3.Headers
import okio.IOException
import org.koitharu.kotatsu.parsers.model.MangaSource
class CloudFlareBlockedException(
val url: String,
val source: MangaSource?,
) : IOException("Blocked by CloudFlare")

View File

@@ -0,0 +1,7 @@
package org.koitharu.kotatsu.core.exceptions
import okio.IOException
class NoDataReceivedException(
private val url: String,
) : IOException("No data has been received from $url")

View File

@@ -6,7 +6,6 @@ import androidx.annotation.StringRes
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import okhttp3.Headers
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
@@ -30,7 +29,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
private val activity: FragmentActivity? private val activity: FragmentActivity?
private val fragment: Fragment? private val fragment: Fragment?
private val sourceAuthContract: ActivityResultLauncher<MangaSource> private val sourceAuthContract: ActivityResultLauncher<MangaSource>
private val cloudflareContract: ActivityResultLauncher<Pair<String, Headers?>> private val cloudflareContract: ActivityResultLauncher<CloudFlareProtectedException>
constructor(activity: FragmentActivity) { constructor(activity: FragmentActivity) {
this.activity = activity this.activity = activity
@@ -55,7 +54,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
} }
suspend fun resolve(e: Throwable): Boolean = when (e) { suspend fun resolve(e: Throwable): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e.url, e.headers) is CloudFlareProtectedException -> resolveCF(e)
is AuthRequiredException -> resolveAuthException(e.source) is AuthRequiredException -> resolveAuthException(e.source)
is NotFoundException -> { is NotFoundException -> {
openInBrowser(e.url) openInBrowser(e.url)
@@ -70,9 +69,9 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
else -> false else -> false
} }
private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont -> private suspend fun resolveCF(e: CloudFlareProtectedException): Boolean = suspendCoroutine { cont ->
continuations[CloudFlareActivity.TAG] = cont continuations[CloudFlareActivity.TAG] = cont
cloudflareContract.launch(url to headers) cloudflareContract.launch(e)
} }
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont -> private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
@@ -82,7 +81,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
private fun openInBrowser(url: String) { private fun openInBrowser(url: String) {
val context = activity ?: fragment?.activity ?: return val context = activity ?: fragment?.activity ?: return
context.startActivity(BrowserActivity.newIntent(context, url, null)) context.startActivity(BrowserActivity.newIntent(context, url, null, null))
} }
private fun openAlternatives(manga: Manga) { private fun openAlternatives(manga: Manga) {

View File

@@ -13,9 +13,8 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.formatSimple
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
@JvmName("mangaIds") @JvmName("mangaIds")
@@ -119,17 +118,10 @@ val Manga.appUrl: Uri
.appendQueryParameter("url", url) .appendQueryParameter("url", url)
.build() .build()
private val chaptersNumberFormat = DecimalFormat("#.#").also { f -> fun MangaChapter.formatNumber(): String? = if (number > 0f) {
f.decimalFormatSymbols = DecimalFormatSymbols.getInstance().also { number.formatSimple()
it.decimalSeparator = '.' } else {
} null
}
fun MangaChapter.formatNumber(): String? {
if (number <= 0f) {
return null
}
return chaptersNumberFormat.format(number.toDouble())
} }
fun Manga.chaptersCount(): Int { fun Manga.chaptersCount(): Int {

View File

@@ -4,6 +4,7 @@ import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import java.net.HttpURLConnection.HTTP_FORBIDDEN import java.net.HttpURLConnection.HTTP_FORBIDDEN
@@ -17,14 +18,23 @@ class CloudFlareInterceptor : Interceptor {
val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use { val content = response.body?.let { response.peekBody(Long.MAX_VALUE) }?.byteStream()?.use {
Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString()) Jsoup.parse(it, Charsets.UTF_8.name(), response.request.url.toString())
} ?: return response } ?: return response
if (content.getElementById("challenge-error-title") != null) { val hasCaptcha = content.getElementById("challenge-error-title") != null
val isBlocked = content.selectFirst("h2[data-translate=\"blocked_why_headline\"]") != null
if (hasCaptcha || isBlocked) {
val request = response.request val request = response.request
response.closeQuietly() response.closeQuietly()
throw CloudFlareProtectedException( if (isBlocked) {
url = request.url.toString(), throw CloudFlareBlockedException(
source = request.tag(MangaSource::class.java), url = request.url.toString(),
headers = request.headers, source = request.tag(MangaSource::class.java),
) )
} else {
throw CloudFlareProtectedException(
url = request.url.toString(),
source = request.tag(MangaSource::class.java),
headers = request.headers,
)
}
} }
} }
return response return response

View File

@@ -16,8 +16,10 @@ import org.koitharu.kotatsu.core.network.cookies.AndroidCookieJar
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -50,10 +52,12 @@ interface NetworkModule {
@Singleton @Singleton
@BaseHttpClient @BaseHttpClient
fun provideBaseHttpClient( fun provideBaseHttpClient(
@ApplicationContext contextProvider: Provider<Context>,
cache: Cache, cache: Cache,
cookieJar: CookieJar, cookieJar: CookieJar,
settings: AppSettings, settings: AppSettings,
): OkHttpClient = OkHttpClient.Builder().apply { ): OkHttpClient = OkHttpClient.Builder().apply {
assertNotInMainThread()
connectTimeout(20, TimeUnit.SECONDS) connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS) readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS) writeTimeout(20, TimeUnit.SECONDS)
@@ -62,7 +66,9 @@ interface NetworkModule {
proxyAuthenticator(ProxyAuthenticator(settings)) proxyAuthenticator(ProxyAuthenticator(settings))
dns(DoHManager(cache, settings)) dns(DoHManager(cache, settings))
if (settings.isSSLBypassEnabled) { if (settings.isSSLBypassEnabled) {
bypassSSLErrors() disableCertificateVerification()
} else {
installExtraCertsificates(contextProvider.get())
} }
cache(cache) cache(cache)
addInterceptor(GZipInterceptor()) addInterceptor(GZipInterceptor())

View File

@@ -1,30 +0,0 @@
package org.koitharu.kotatsu.core.network
import android.annotation.SuppressLint
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.security.SecureRandom
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@SuppressLint("CustomX509TrustManager")
fun OkHttpClient.Builder.bypassSSLErrors() = also { builder ->
runCatching {
val trustAllCerts = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf(trustAllCerts), SecureRandom())
val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory
builder.sslSocketFactory(sslSocketFactory, trustAllCerts)
builder.hostnameVerifier { _, _ -> true }
}.onFailure {
it.printStackTraceDebug()
}
}

View File

@@ -0,0 +1,63 @@
package org.koitharu.kotatsu.core.network
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.AssetManager
import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.tls.HandshakeCertificates
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import java.security.SecureRandom
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
@SuppressLint("CustomX509TrustManager")
fun OkHttpClient.Builder.disableCertificateVerification() = also { builder ->
runCatching {
val trustAllCerts = object : X509TrustManager {
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) = Unit
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
}
val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf(trustAllCerts), SecureRandom())
val sslSocketFactory: SSLSocketFactory = sslContext.socketFactory
builder.sslSocketFactory(sslSocketFactory, trustAllCerts)
builder.hostnameVerifier { _, _ -> true }
}.onFailure {
it.printStackTraceDebug()
}
}
fun OkHttpClient.Builder.installExtraCertsificates(context: Context) = also { builder ->
val certificatesBuilder = HandshakeCertificates.Builder()
.addPlatformTrustedCertificates()
val assets = context.assets.list("").orEmpty()
for (path in assets) {
if (path.endsWith(".pem")) {
val cert = loadCert(context, path) ?: continue
certificatesBuilder.addTrustedCertificate(cert)
}
}
val certificates = certificatesBuilder.build()
builder.sslSocketFactory(certificates.sslSocketFactory(), certificates.trustManager)
}
private fun loadCert(context: Context, path: String): X509Certificate? = runCatching {
val cf = CertificateFactory.getInstance("X.509")
context.assets.open(path, AssetManager.ACCESS_STREAMING).use {
cf.generateCertificate(it)
} as X509Certificate
}.onFailure { e ->
e.printStackTraceDebug()
}.onSuccess {
if (BuildConfig.DEBUG) {
Log.i("ExtraCerts", "Loaded cert $path")
}
}.getOrNull()

View File

@@ -18,6 +18,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
@@ -31,6 +32,7 @@ import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.search.ui.MangaListActivity import org.koitharu.kotatsu.search.ui.MangaListActivity
@@ -90,6 +92,14 @@ class AppShortcutManager @Inject constructor(
false false
} }
fun getMangaShortcuts(): Set<Long> {
val shortcuts = ShortcutManagerCompat.getShortcuts(
context,
ShortcutManagerCompat.FLAG_MATCH_CACHED or ShortcutManagerCompat.FLAG_MATCH_PINNED or ShortcutManagerCompat.FLAG_MATCH_DYNAMIC,
)
return shortcuts.mapNotNullToSet { it.id.toLongOrNull() }
}
@VisibleForTesting @VisibleForTesting
suspend fun await(): Boolean { suspend fun await(): Boolean {
return shortcutsUpdateJob?.join() != null return shortcutsUpdateJob?.join() != null
@@ -150,7 +160,7 @@ class AppShortcutManager @Inject constructor(
.build() .build()
} }
private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat { private suspend fun buildShortcutInfo(source: MangaSource): ShortcutInfoCompat = withContext(Dispatchers.Default) {
val icon = runCatchingCancellable { val icon = runCatchingCancellable {
coil.execute( coil.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
@@ -163,7 +173,7 @@ class AppShortcutManager @Inject constructor(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }, onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) },
) )
return ShortcutInfoCompat.Builder(context, source.name) ShortcutInfoCompat.Builder(context, source.name)
.setShortLabel(source.title) .setShortLabel(source.title)
.setLongLabel(source.title) .setLongLabel(source.title)
.setIcon(icon) .setIcon(icon)

View File

@@ -5,13 +5,15 @@ import android.net.ConnectivityManager.NetworkCallback
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.os.Build
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.MediatorStateFlow import org.koitharu.kotatsu.core.util.MediatorStateFlow
import org.koitharu.kotatsu.core.util.ext.isOnline
class NetworkState( class NetworkState(
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
) : MediatorStateFlow<Boolean>(connectivityManager.isOnline()) { private val settings: AppSettings,
) : MediatorStateFlow<Boolean>(connectivityManager.isOnline(settings)) {
private val callback = NetworkCallbackImpl() private val callback = NetworkCallbackImpl()
@@ -19,7 +21,10 @@ class NetworkState(
override fun onActive() { override fun onActive() {
invalidate() invalidate()
val request = NetworkRequest.Builder() val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
.build() .build()
connectivityManager.registerNetworkCallback(request, callback) connectivityManager.registerNetworkCallback(request, callback)
} }
@@ -37,7 +42,7 @@ class NetworkState(
} }
private fun invalidate() { private fun invalidate() {
publishValue(connectivityManager.isOnline()) publishValue(connectivityManager.isOnline(settings))
} }
private inner class NetworkCallbackImpl : NetworkCallback() { private inner class NetworkCallbackImpl : NetworkCallback() {
@@ -48,4 +53,27 @@ class NetworkState(
override fun onUnavailable() = invalidate() override fun onUnavailable() = invalidate()
} }
private companion object {
fun ConnectivityManager.isOnline(settings: AppSettings): Boolean {
if (settings.isOfflineCheckDisabled) {
return true
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activeNetwork?.let { isOnline(it) } ?: false
} else {
@Suppress("DEPRECATION")
activeNetworkInfo?.isConnected == true
}
}
private fun ConnectivityManager.isOnline(network: Network): Boolean {
val capabilities = getNetworkCapabilities(network) ?: return false
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
|| capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
}
}
} }

View File

@@ -22,11 +22,11 @@ import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -38,15 +38,10 @@ class MangaLoaderContextImpl @Inject constructor(
) : MangaLoaderContext() { ) : MangaLoaderContext() {
private var webViewCached: WeakReference<WebView>? = null private var webViewCached: WeakReference<WebView>? = null
private val webViewUserAgent by lazy { obtainWebViewUserAgent() }
private val userAgentLazy = SuspendLazy {
withContext(Dispatchers.Main) {
obtainWebView().settings.userAgentString
}.sanitizeHeaderValue()
}
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main) { override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main.immediate) {
val webView = obtainWebView() val webView = obtainWebView()
suspendCoroutine { cont -> suspendCoroutine { cont ->
webView.evaluateJavascript(script) { result -> webView.evaluateJavascript(script) { result ->
@@ -55,13 +50,7 @@ class MangaLoaderContextImpl @Inject constructor(
} }
} }
override fun getDefaultUserAgent(): String = runCatching { override fun getDefaultUserAgent(): String = webViewUserAgent
runBlocking {
userAgentLazy.get()
}
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
override fun getConfig(source: MangaSource): MangaSourceConfig { override fun getConfig(source: MangaSource): MangaSourceConfig {
return SourceSettings(androidContext, source) return SourceSettings(androidContext, source)
@@ -86,4 +75,22 @@ class MangaLoaderContextImpl @Inject constructor(
webViewCached = WeakReference(it) webViewCached = WeakReference(it)
} }
} }
private fun obtainWebViewUserAgent(): String {
val mainDispatcher = Dispatchers.Main.immediate
return if (!mainDispatcher.isDispatchNeeded(EmptyCoroutineContext)) {
obtainWebViewUserAgentImpl()
} else {
runBlocking(mainDispatcher) {
obtainWebViewUserAgentImpl()
}
}
}
@MainThread
private fun obtainWebViewUserAgentImpl() = runCatching {
obtainWebView().settings.userAgentString.sanitizeHeaderValue()
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
} }

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser package org.koitharu.kotatsu.core.parser
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
@@ -57,7 +57,7 @@ interface MangaRepository {
class Factory @Inject constructor( class Factory @Inject constructor(
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
private val loaderContext: MangaLoaderContext, private val loaderContext: MangaLoaderContext,
private val contentCache: ContentCache, private val contentCache: MemoryContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor, private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) { ) {

View File

@@ -13,15 +13,15 @@ import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.MemoryContentCache
import org.koitharu.kotatsu.core.cache.SafeDeferred import org.koitharu.kotatsu.core.cache.SafeDeferred
import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor import org.koitharu.kotatsu.core.network.MirrorSwitchInterceptor
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.MultiMutex
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.exception.ParseException
import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.ContentRating
import org.koitharu.kotatsu.parsers.model.Favicons import org.koitharu.kotatsu.parsers.model.Favicons
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@@ -38,10 +38,14 @@ import java.util.Locale
class RemoteMangaRepository( class RemoteMangaRepository(
private val parser: MangaParser, private val parser: MangaParser,
private val cache: ContentCache, private val cache: MemoryContentCache,
private val mirrorSwitchInterceptor: MirrorSwitchInterceptor, private val mirrorSwitchInterceptor: MirrorSwitchInterceptor,
) : MangaRepository, Interceptor { ) : MangaRepository, Interceptor {
private val detailsMutex = MultiMutex<Long>()
private val relatedMangaMutex = MultiMutex<Long>()
private val pagesMutex = MultiMutex<Long>()
override val source: MangaSource override val source: MangaSource
get() = parser.source get() = parser.source
@@ -97,7 +101,7 @@ class RemoteMangaRepository(
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED) override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
cache.getPages(source, chapter.url)?.let { return it } cache.getPages(source, chapter.url)?.let { return it }
val pages = asyncSafe { val pages = asyncSafe {
mirrorSwitchInterceptor.withMirrorSwitching { mirrorSwitchInterceptor.withMirrorSwitching {
@@ -105,8 +109,8 @@ class RemoteMangaRepository(
} }
} }
cache.putPages(source, chapter.url, pages) cache.putPages(source, chapter.url, pages)
return pages.await() pages
} }.await()
override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching { override suspend fun getPageUrl(page: MangaPage): String = mirrorSwitchInterceptor.withMirrorSwitching {
parser.getPageUrl(page) parser.getPageUrl(page)
@@ -124,16 +128,16 @@ class RemoteMangaRepository(
parser.getFavicons() parser.getFavicons()
} }
override suspend fun getRelated(seed: Manga): List<Manga> { override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
cache.getRelatedManga(source, seed.url)?.let { return it } cache.getRelatedManga(source, seed.url)?.let { return it }
val related = asyncSafe { val related = asyncSafe {
parser.getRelatedManga(seed).filterNot { it.id == seed.id } parser.getRelatedManga(seed).filterNot { it.id == seed.id }
} }
cache.putRelatedManga(source, seed.url, related) cache.putRelatedManga(source, seed.url, related)
return related.await() related
} }.await()
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga { suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
if (cachePolicy.readEnabled) { if (cachePolicy.readEnabled) {
cache.getDetails(source, manga.url)?.let { return it } cache.getDetails(source, manga.url)?.let { return it }
} }
@@ -145,8 +149,8 @@ class RemoteMangaRepository(
if (cachePolicy.writeEnabled) { if (cachePolicy.writeEnabled) {
cache.putDetails(source, manga.url, details) cache.putDetails(source, manga.url, details)
} }
return details.await() details
} }.await()
suspend fun peekDetails(manga: Manga): Manga? { suspend fun peekDetails(manga: Manga): Manga? {
return cache.getDetails(source, manga.url) return cache.getDetails(source, manga.url)

View File

@@ -170,10 +170,11 @@ class FaviconFetcher(
class Factory( class Factory(
context: Context, context: Context,
private val okHttpClient: OkHttpClient, okHttpClientLazy: Lazy<OkHttpClient>,
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
) : Fetcher.Factory<Uri> { ) : Fetcher.Factory<Uri> {
private val okHttpClient by okHttpClientLazy
private val diskCache = lazy { private val diskCache = lazy {
val rootDir = context.externalCacheDir ?: context.cacheDir val rootDir = context.externalCacheDir ?: context.cacheDir
DiskCache.Builder() DiskCache.Builder()

View File

@@ -32,6 +32,7 @@ import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import java.io.File import java.io.File
import java.net.Proxy import java.net.Proxy
import java.util.EnumSet
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -74,10 +75,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isNavLabelsVisible: Boolean val isNavLabelsVisible: Boolean
get() = prefs.getBoolean(KEY_NAV_LABELS, true) get() = prefs.getBoolean(KEY_NAV_LABELS, true)
val isNavBarPinned: Boolean
get() = prefs.getBoolean(KEY_NAV_PINNED, false)
var gridSize: Int var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100) get() = prefs.getInt(KEY_GRID_SIZE, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) } set(value) = prefs.edit { putInt(KEY_GRID_SIZE, value) }
var gridSizePages: Int
get() = prefs.getInt(KEY_GRID_SIZE_PAGES, 100)
set(value) = prefs.edit { putInt(KEY_GRID_SIZE_PAGES, value) }
var historyListMode: ListMode var historyListMode: ListMode
get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode) get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_HISTORY, value) } set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_HISTORY, value) }
@@ -128,6 +136,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true) get() = prefs.getBoolean(KEY_TRAFFIC_WARNING, true)
set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) } set(value) = prefs.edit { putBoolean(KEY_TRAFFIC_WARNING, value) }
val isOfflineCheckDisabled: Boolean
get() = prefs.getBoolean(KEY_OFFLINE_DISABLED, false)
var isAllFavouritesVisible: Boolean var isAllFavouritesVisible: Boolean
get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true) get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true)
set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) } set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) }
@@ -138,6 +149,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isTrackerWifiOnly: Boolean val isTrackerWifiOnly: Boolean
get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false) get() = prefs.getBoolean(KEY_TRACKER_WIFI_ONLY, false)
val trackerFrequencyFactor: Float
get() = prefs.getString(KEY_TRACKER_FREQUENCY, null)?.toFloatOrNull() ?: 1f
val isTrackerNotificationsEnabled: Boolean val isTrackerNotificationsEnabled: Boolean
get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true) get() = prefs.getBoolean(KEY_TRACKER_NOTIFICATIONS, true)
@@ -168,6 +182,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true) get() = prefs.getBoolean(KEY_HISTORY_GROUPING, true)
set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) } set(value) = prefs.edit { putBoolean(KEY_HISTORY_GROUPING, value) }
var isUpdatedGroupingEnabled: Boolean
get() = prefs.getBoolean(KEY_UPDATED_GROUPING, true)
set(value) = prefs.edit { putBoolean(KEY_UPDATED_GROUPING, value) }
var isFeedHeaderVisible: Boolean
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
val isReadingIndicatorsEnabled: Boolean val isReadingIndicatorsEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_INDICATORS, true) get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
@@ -202,6 +224,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false) get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) } set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
val searchSuggestionTypes: Set<SearchSuggestionType>
get() = prefs.getStringSet(KEY_SEARCH_SUGGESTION_TYPES, null)?.let { stringSet ->
stringSet.mapNotNullTo(EnumSet.noneOf(SearchSuggestionType::class.java)) { x ->
enumValueOf<SearchSuggestionType>(x)
}
} ?: EnumSet.allOf(SearchSuggestionType::class.java)
val isLoggingEnabled: Boolean val isLoggingEnabled: Boolean
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false) get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
@@ -226,11 +255,20 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val defaultDetailsTab: Int val defaultDetailsTab: Int
get() = if (isPagesTabEnabled) { get() = if (isPagesTabEnabled) {
prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull()?.coerceIn(0, 1) ?: 0 val raw = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull() ?: -1
if (raw == -1) {
lastDetailsTab
} else {
raw
}.coerceIn(0, 2)
} else { } else {
0 0
} }
var lastDetailsTab: Int
get() = prefs.getInt(KEY_DETAILS_LAST_TAB, 0)
set(value) = prefs.edit { putInt(KEY_DETAILS_LAST_TAB, value) }
val isContentPrefetchEnabled: Boolean val isContentPrefetchEnabled: Boolean
get() { get() {
if (isBackgroundNetworkRestricted()) { if (isBackgroundNetworkRestricted()) {
@@ -246,7 +284,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
set(value) = prefs.edit { putEnumValue(KEY_SOURCES_ORDER, value) } set(value) = prefs.edit { putEnumValue(KEY_SOURCES_ORDER, value) }
var isSourcesGridMode: Boolean var isSourcesGridMode: Boolean
get() = prefs.getBoolean(KEY_SOURCES_GRID, false) get() = prefs.getBoolean(KEY_SOURCES_GRID, true)
set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) } set(value) = prefs.edit { putBoolean(KEY_SOURCES_GRID, value) }
val isNewSourcesTipEnabled: Boolean val isNewSourcesTipEnabled: Boolean
@@ -384,9 +422,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isRelatedMangaEnabled: Boolean val isRelatedMangaEnabled: Boolean
get() = prefs.getBoolean(KEY_RELATED_MANGA, true) get() = prefs.getBoolean(KEY_RELATED_MANGA, true)
val isWebtoonZoomEnable: Boolean val isWebtoonZoomEnabled: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true) get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
var isWebtoonGapsEnabled: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_GAPS, false)
set(value) = prefs.edit { putBoolean(KEY_WEBTOON_GAPS, value) }
@get:FloatRange(from = 0.0, to = 0.5) @get:FloatRange(from = 0.0, to = 0.5)
val defaultWebtoonZoomOut: Float val defaultWebtoonZoomOut: Float
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
@@ -518,6 +560,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_COLOR_THEME = "color_theme" const val KEY_COLOR_THEME = "color_theme"
const val KEY_THEME_AMOLED = "amoled_theme" const val KEY_THEME_AMOLED = "amoled_theme"
const val KEY_TRAFFIC_WARNING = "traffic_warning" const val KEY_TRAFFIC_WARNING = "traffic_warning"
const val KEY_OFFLINE_DISABLED = "no_offline"
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear" const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear" const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
const val KEY_COOKIES_CLEAR = "cookies_clear" const val KEY_COOKIES_CLEAR = "cookies_clear"
@@ -527,6 +570,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear" const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear" const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear"
const val KEY_GRID_SIZE = "grid_size" const val KEY_GRID_SIZE = "grid_size"
const val KEY_GRID_SIZE_PAGES = "grid_size_pages"
const val KEY_REMOTE_SOURCES = "remote_sources" const val KEY_REMOTE_SOURCES = "remote_sources"
const val KEY_LOCAL_STORAGE = "local_storage" const val KEY_LOCAL_STORAGE = "local_storage"
const val KEY_READER_DOUBLE_PAGES = "reader_double_pages" const val KEY_READER_DOUBLE_PAGES = "reader_double_pages"
@@ -536,6 +580,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons" const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons"
const val KEY_TRACKER_ENABLED = "tracker_enabled" const val KEY_TRACKER_ENABLED = "tracker_enabled"
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi" const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
const val KEY_TRACKER_FREQUENCY = "tracker_freq"
const val KEY_TRACK_SOURCES = "track_sources" const val KEY_TRACK_SOURCES = "track_sources"
const val KEY_TRACK_CATEGORIES = "track_categories" const val KEY_TRACK_CATEGORIES = "track_categories"
const val KEY_TRACK_WARNING = "track_warning" const val KEY_TRACK_WARNING = "track_warning"
@@ -561,6 +606,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output" const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output"
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last" const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
const val KEY_HISTORY_GROUPING = "history_grouping" const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_UPDATED_GROUPING = "updated_grouping"
const val KEY_READING_INDICATORS = "reading_indicators" const val KEY_READING_INDICATORS = "reading_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters" const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters" const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
@@ -595,6 +641,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_LOCAL_LIST_ORDER = "local_order" const val KEY_LOCAL_LIST_ORDER = "local_order"
const val KEY_HISTORY_ORDER = "history_order" const val KEY_HISTORY_ORDER = "history_order"
const val KEY_FAVORITES_ORDER = "fav_order" const val KEY_FAVORITES_ORDER = "fav_order"
const val KEY_WEBTOON_GAPS = "webtoon_gaps"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom" const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out" const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
const val KEY_PREFETCH_CONTENT = "prefetch_content" const val KEY_PREFETCH_CONTENT = "prefetch_content"
@@ -621,6 +668,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_RELATED_MANGA = "related_manga" const val KEY_RELATED_MANGA = "related_manga"
const val KEY_NAV_MAIN = "nav_main" const val KEY_NAV_MAIN = "nav_main"
const val KEY_NAV_LABELS = "nav_labels" const val KEY_NAV_LABELS = "nav_labels"
const val KEY_NAV_PINNED = "nav_pinned"
const val KEY_32BIT_COLOR = "enhanced_colors" const val KEY_32BIT_COLOR = "enhanced_colors"
const val KEY_SOURCES_ORDER = "sources_sort_order" const val KEY_SOURCES_ORDER = "sources_sort_order"
const val KEY_SOURCES_CATALOG = "sources_catalog" const val KEY_SOURCES_CATALOG = "sources_catalog"
@@ -631,11 +679,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_IGNORE_DOZE = "ignore_dose" const val KEY_IGNORE_DOZE = "ignore_dose"
const val KEY_PAGES_TAB = "pages_tab" const val KEY_PAGES_TAB = "pages_tab"
const val KEY_DETAILS_TAB = "details_tab" const val KEY_DETAILS_TAB = "details_tab"
const val KEY_DETAILS_LAST_TAB = "details_last_tab"
const val KEY_READING_TIME = "reading_time" const val KEY_READING_TIME = "reading_time"
const val KEY_PAGES_SAVE_DIR = "pages_dir" const val KEY_PAGES_SAVE_DIR = "pages_dir"
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask" const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
const val KEY_STATS_ENABLED = "stats_on" const val KEY_STATS_ENABLED = "stats_on"
const val KEY_APP_UPDATE = "app_update" const val KEY_APP_UPDATE = "app_update"
const val KEY_APP_TRANSLATION = "about_app_translation" const val KEY_APP_TRANSLATION = "about_app_translation"
const val KEY_FEED_HEADER = "feed_header"
const val KEY_SEARCH_SUGGESTION_TYPES = "search_suggest_types"
} }
} }

View File

@@ -17,12 +17,13 @@ enum class NavItem(
EXPLORE(R.id.nav_explore, R.string.explore, R.drawable.ic_explore_selector), EXPLORE(R.id.nav_explore, R.string.explore, R.drawable.ic_explore_selector),
SUGGESTIONS(R.id.nav_suggestions, R.string.suggestions, R.drawable.ic_suggestion_selector), SUGGESTIONS(R.id.nav_suggestions, R.string.suggestions, R.drawable.ic_suggestion_selector),
FEED(R.id.nav_feed, R.string.feed, R.drawable.ic_feed_selector), FEED(R.id.nav_feed, R.string.feed, R.drawable.ic_feed_selector),
UPDATED(R.id.nav_updated, R.string.updated, R.drawable.ic_updated_selector),
BOOKMARKS(R.id.nav_bookmarks, R.string.bookmarks, R.drawable.ic_bookmark_selector), BOOKMARKS(R.id.nav_bookmarks, R.string.bookmarks, R.drawable.ic_bookmark_selector),
; ;
fun isAvailable(settings: AppSettings): Boolean = when (this) { fun isAvailable(settings: AppSettings): Boolean = when (this) {
SUGGESTIONS -> settings.isSuggestionsEnabled SUGGESTIONS -> settings.isSuggestionsEnabled
FEED -> settings.isTrackerEnabled UPDATED, FEED -> settings.isTrackerEnabled
else -> true else -> true
} }
} }

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.core.prefs
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
enum class SearchSuggestionType(
@StringRes val titleResId: Int,
) {
GENRES(R.string.genres),
QUERIES_RECENT(R.string.recent_queries),
QUERIES_SUGGEST(R.string.suggested_queries),
MANGA(R.string.content_type_manga),
SOURCES(R.string.remote_sources),
AUTHORS(R.string.authors),
}

View File

@@ -3,33 +3,27 @@ package org.koitharu.kotatsu.core.ui
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.ui.util.BaseActivityEntryPoint
import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
@Suppress("LeakingThis") @Suppress("LeakingThis")
@@ -72,7 +66,7 @@ abstract class BaseActivity<B : ViewBinding> :
onBackPressedDispatcher.addCallback(actionModeDelegate) onBackPressedDispatcher.addCallback(actionModeDelegate)
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent) {
putDataToExtras(intent) putDataToExtras(intent)
super.onNewIntent(intent) super.onNewIntent(intent)
} }
@@ -98,6 +92,9 @@ abstract class BaseActivity<B : ViewBinding> :
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
if (supportFragmentManager.popBackStackImmediate()) {
return false
}
dispatchNavigateUp() dispatchNavigateUp()
return true return true
} }
@@ -123,32 +120,13 @@ abstract class BaseActivity<B : ViewBinding> :
@CallSuper @CallSuper
override fun onSupportActionModeStarted(mode: ActionMode) { override fun onSupportActionModeStarted(mode: ActionMode) {
super.onSupportActionModeStarted(mode) super.onSupportActionModeStarted(mode)
actionModeDelegate.onSupportActionModeStarted(mode) actionModeDelegate.onSupportActionModeStarted(mode, window)
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors(
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
getThemeColor(R.attr.m3ColorBackground),
)
} else {
ContextCompat.getColor(this, R.color.kotatsu_m3_background)
}
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar).apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
} }
@CallSuper @CallSuper
override fun onSupportActionModeFinished(mode: ActionMode) { override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode) super.onSupportActionModeFinished(mode)
actionModeDelegate.onSupportActionModeFinished(mode) actionModeDelegate.onSupportActionModeFinished(mode, window)
window.statusBarColor = defaultStatusBarColor
} }
protected open fun dispatchNavigateUp() { protected open fun dispatchNavigateUp() {
@@ -181,6 +159,12 @@ abstract class BaseActivity<B : ViewBinding> :
} }
} }
@EntryPoint
@InstallIn(SingletonComponent::class)
interface BaseActivityEntryPoint {
val settings: AppSettings
}
companion object { companion object {
const val EXTRA_DATA = "data" const val EXTRA_DATA = "data"

View File

@@ -19,7 +19,7 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
with(window) { with(window) {
systemUiController = SystemUiController(this) systemUiController = SystemUiController(this)
statusBarColor = Color.TRANSPARENT statusBarColor = Color.TRANSPARENT
navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim) ContextCompat.getColor(this@BaseFullscreenActivity, R.color.dim)
} else { } else {
Color.TRANSPARENT Color.TRANSPARENT

View File

@@ -6,7 +6,12 @@ import com.hannesdorfmann.adapterdelegates4.AdapterDelegate
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
@@ -48,4 +53,14 @@ open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
} }
return null return null
} }
fun observeItems(): Flow<List<T>> = callbackFlow {
val listListener = ListListener<T> { _, list ->
trySendBlocking(list)
}
addListListener(listListener)
awaitClose { removeListListener(listListener) }
}.onStart {
emit(items)
}
} }

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.core.ui package org.koitharu.kotatsu.core.ui
import android.content.Intent import android.content.Intent
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
@@ -39,8 +41,10 @@ abstract class CoroutineIntentService : BaseService() {
} }
} }
@WorkerThread
protected abstract suspend fun processIntent(startId: Int, intent: Intent) protected abstract suspend fun processIntent(startId: Int, intent: Intent)
@AnyThread
protected abstract fun onError(startId: Int, error: Throwable) protected abstract fun onError(startId: Int, error: Throwable)
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable -> private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->

View File

@@ -0,0 +1,78 @@
package org.koitharu.kotatsu.core.ui.image
import android.animation.TimeAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Animatable
import android.graphics.drawable.Drawable
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import com.google.android.material.animation.ArgbEvaluatorCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import kotlin.math.abs
import com.google.android.material.R as materialR
class AnimatedPlaceholderDrawable(context: Context) : Drawable(), Animatable, TimeAnimator.TimeListener {
private val colorLow = context.getThemeColor(materialR.attr.colorSurfaceContainerLowest)
private val colorHigh = context.getThemeColor(materialR.attr.colorSurfaceContainerHighest)
private var currentColor: Int = colorLow
private val interpolator = FastOutSlowInInterpolator()
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
private val timeAnimator = TimeAnimator()
init {
timeAnimator.setTimeListener(this)
updateColor()
}
override fun draw(canvas: Canvas) {
if (!isRunning && period > 0) {
updateColor()
start()
}
canvas.drawColor(currentColor)
}
override fun setAlpha(alpha: Int) {
// this.alpha = alpha FIXME coil's crossfade
}
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
@Suppress("DeprecatedCallableAddReplaceWith")
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int = PixelFormat.OPAQUE
override fun getAlpha(): Int = 255
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
callback?.also {
updateColor()
it.invalidateDrawable(this)
} ?: stop()
}
override fun start() {
timeAnimator.start()
}
override fun stop() {
timeAnimator.end()
}
override fun isRunning(): Boolean = timeAnimator.isStarted
private fun updateColor() {
if (period <= 0f) {
return
}
val ph = period / 2
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
currentColor = ArgbEvaluatorCompat.getInstance()
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
}
}

View File

@@ -6,7 +6,7 @@ import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import coil.size.Dimension import coil.size.Dimension
import coil.size.Size import coil.size.Size
import coil.size.SizeResolver import coil.size.ViewSizeResolver
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume import kotlin.coroutines.resume
@@ -16,24 +16,24 @@ private const val ASPECT_RATIO_HEIGHT = 18f
private const val ASPECT_RATIO_WIDTH = 13f private const val ASPECT_RATIO_WIDTH = 13f
class CoverSizeResolver( class CoverSizeResolver(
private val imageView: ImageView, override val view: ImageView,
) : SizeResolver { ) : ViewSizeResolver<ImageView> {
override suspend fun size(): Size { override suspend fun size(): Size {
getSize()?.let { return it } getSize()?.let { return it }
return suspendCancellableCoroutine { cont -> return suspendCancellableCoroutine { cont ->
val layoutListener = LayoutListener(cont) val layoutListener = LayoutListener(cont)
imageView.addOnLayoutChangeListener(layoutListener) view.addOnLayoutChangeListener(layoutListener)
cont.invokeOnCancellation { cont.invokeOnCancellation {
imageView.removeOnLayoutChangeListener(layoutListener) view.removeOnLayoutChangeListener(layoutListener)
} }
} }
} }
private fun getSize(): Size? { private fun getSize(): Size? {
val lp = imageView.layoutParams val lp = view.layoutParams
var width = getDimension(lp.width, imageView.width, imageView.paddingLeft + imageView.paddingRight) var width = getDimension(lp.width, view.width, view.paddingLeft + view.paddingRight)
var height = getDimension(lp.height, imageView.height, imageView.paddingTop + imageView.paddingBottom) var height = getDimension(lp.height, view.height, view.paddingTop + view.paddingBottom)
if (width == null && height == null) { if (width == null && height == null) {
return null return null
} }

View File

@@ -29,7 +29,7 @@ class FitHeightGridLayoutManager : GridLayoutManager {
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) { if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
val parentBottom = height - paddingBottom val parentBottom = height - paddingBottom
val offset = parentBottom - bottom val offset = parentBottom - bottom
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset) super.layoutDecoratedWithMargins(child, left, top, right, bottom + offset)
} else { } else {
super.layoutDecoratedWithMargins(child, left, top, right, bottom) super.layoutDecoratedWithMargins(child, left, top, right, bottom)
} }

View File

@@ -29,7 +29,7 @@ class FitHeightLinearLayoutManager : LinearLayoutManager {
if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) { if (orientation == RecyclerView.VERTICAL && child.layoutParams.height == LayoutParams.MATCH_PARENT) {
val parentBottom = height - paddingBottom val parentBottom = height - paddingBottom
val offset = parentBottom - bottom val offset = parentBottom - bottom
super.layoutDecoratedWithMargins(child, left, top + offset, right, bottom + offset) super.layoutDecoratedWithMargins(child, left, top, right, bottom + offset)
} else { } else {
super.layoutDecoratedWithMargins(child, left, top, right, bottom) super.layoutDecoratedWithMargins(child, left, top, right, bottom)
} }

View File

@@ -1,10 +1,10 @@
package org.koitharu.kotatsu.core.ui.list package org.koitharu.kotatsu.core.ui.list
import android.app.Activity import android.app.Notification.Action
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@@ -20,7 +20,7 @@ private const val KEY_SELECTION = "selection"
private const val PROVIDER_NAME = "selection_decoration" private const val PROVIDER_NAME = "selection_decoration"
class ListSelectionController( class ListSelectionController(
private val activity: Activity, private val appCompatDelegate: AppCompatDelegate,
private val decoration: AbstractSelectionItemDecoration, private val decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner, private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback2, private val callback: Callback2,
@@ -81,8 +81,7 @@ class ListSelectionController(
} }
fun onItemLongClick(id: Long): Boolean { fun onItemLongClick(id: Long): Boolean {
startActionMode() return startActionMode()?.also {
return actionMode?.also {
decoration.setItemIsChecked(id, true) decoration.setItemIsChecked(id, true)
notifySelectionChanged() notifySelectionChanged()
} != null } != null
@@ -106,9 +105,9 @@ class ListSelectionController(
actionMode = null actionMode = null
} }
private fun startActionMode() { private fun startActionMode(): ActionMode? {
if (actionMode == null) { return actionMode ?: appCompatDelegate.startSupportActionMode(this).also {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this) actionMode = it
} }
} }

View File

@@ -67,7 +67,7 @@ class FastScroller @JvmOverloads constructor(
private var hideScrollbar = true private var hideScrollbar = true
private var showBubble = true private var showBubble = true
private var showBubbleAlways = false private var showBubbleAlways = false
private var bubbleSize = BubbleSize.NORMAL private var bubbleSize = BubbleSize.SMALL
private var bubbleImage: Drawable? = null private var bubbleImage: Drawable? = null
private var handleImage: Drawable? = null private var handleImage: Drawable? = null
private var trackImage: Drawable? = null private var trackImage: Drawable? = null
@@ -91,7 +91,7 @@ class FastScroller @JvmOverloads constructor(
if (showBubbleAlways) { if (showBubbleAlways) {
val targetPos = getRecyclerViewTargetPosition(y) val targetPos = getRecyclerViewTargetPosition(y)
sectionIndexer?.let { binding.bubble.text = it.getSectionText(recyclerView.context, targetPos) } sectionIndexer?.let { bindBubble(it.getSectionText(recyclerView.context, targetPos)) }
} }
} }
} }
@@ -145,7 +145,7 @@ class FastScroller @JvmOverloads constructor(
showBubble = getBoolean(R.styleable.FastScrollRecyclerView_showBubble, showBubble) showBubble = getBoolean(R.styleable.FastScrollRecyclerView_showBubble, showBubble)
showBubbleAlways = getBoolean(R.styleable.FastScrollRecyclerView_showBubbleAlways, showBubbleAlways) showBubbleAlways = getBoolean(R.styleable.FastScrollRecyclerView_showBubbleAlways, showBubbleAlways)
showTrack = getBoolean(R.styleable.FastScrollRecyclerView_showTrack, showTrack) showTrack = getBoolean(R.styleable.FastScrollRecyclerView_showTrack, showTrack)
bubbleSize = getBubbleSize(R.styleable.FastScrollRecyclerView_bubbleSize, BubbleSize.NORMAL) bubbleSize = getBubbleSize(R.styleable.FastScrollRecyclerView_bubbleSize, bubbleSize)
val textSize = getDimension(R.styleable.FastScrollRecyclerView_bubbleTextSize, bubbleSize.textSize) val textSize = getDimension(R.styleable.FastScrollRecyclerView_bubbleTextSize, bubbleSize.textSize)
binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize) binding.bubble.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
offset = getDimensionPixelOffset(R.styleable.FastScrollRecyclerView_scrollerOffset, offset) offset = getDimensionPixelOffset(R.styleable.FastScrollRecyclerView_scrollerOffset, offset)
@@ -473,7 +473,7 @@ class FastScroller @JvmOverloads constructor(
val layoutManager = recyclerView?.layoutManager ?: return val layoutManager = recyclerView?.layoutManager ?: return
val targetPos = getRecyclerViewTargetPosition(y) val targetPos = getRecyclerViewTargetPosition(y)
layoutManager.scrollToPosition(targetPos) layoutManager.scrollToPosition(targetPos)
if (showBubble) sectionIndexer?.let { binding.bubble.text = it.getSectionText(context, targetPos) } if (showBubble) sectionIndexer?.let { bindBubble(it.getSectionText(context, targetPos)) }
} }
private fun setViewPositions(y: Float) { private fun setViewPositions(y: Float) {
@@ -535,6 +535,11 @@ class FastScroller @JvmOverloads constructor(
} }
} }
private fun bindBubble(text: CharSequence?) {
binding.bubble.text = text
binding.bubble.alpha = if (text.isNullOrEmpty()) 0f else 1f
}
private val BubbleSize.textSize private val BubbleSize.textSize
@Px get() = resources.getDimension(textSizeId) @Px get() = resources.getDimension(textSizeId)

View File

@@ -2,7 +2,10 @@ package org.koitharu.kotatsu.core.ui.sheet
import android.app.Dialog import android.app.Dialog
import android.view.View import android.view.View
import android.view.ViewParent
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ancestors
import androidx.fragment.app.DialogFragment
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
@@ -109,7 +112,16 @@ sealed class AdaptiveSheetBehavior {
const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING const val STATE_DRAGGING = SideSheetBehavior.STATE_DRAGGING
const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN const val STATE_HIDDEN = SideSheetBehavior.STATE_HIDDEN
fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) { fun from(fragment: DialogFragment): AdaptiveSheetBehavior? {
from(fragment.dialog)?.let { return it }
val rootView = fragment.view ?: return null
for (parent in rootView.ancestors) {
from(parent)?.let { return it }
}
return null
}
private fun from(dialog: Dialog?): AdaptiveSheetBehavior? = when (dialog) {
is BottomSheetDialog -> Bottom(dialog.behavior) is BottomSheetDialog -> Bottom(dialog.behavior)
is SideSheetDialog -> Side(dialog.behavior) is SideSheetDialog -> Side(dialog.behavior)
else -> null else -> null
@@ -121,5 +133,10 @@ sealed class AdaptiveSheetBehavior {
is SideSheetBehavior<*> -> Side(behavior) is SideSheetBehavior<*> -> Side(behavior)
else -> null else -> null
} }
private fun from(parent: ViewParent): AdaptiveSheetBehavior? {
val lp = ((parent as? View)?.layoutParams as? CoordinatorLayout.LayoutParams) ?: return null
return from(lp)
}
} }
} }

View File

@@ -1,19 +1,29 @@
package org.koitharu.kotatsu.core.ui.sheet package org.koitharu.kotatsu.core.ui.sheet
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import androidx.activity.ComponentDialog
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.app.AppCompatDialogFragment import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ActionMode
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.sidesheet.SideSheetDialog import com.google.android.material.sidesheet.SideSheetDialog
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() { abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
@@ -29,13 +39,20 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
get() = requireViewBinding() get() = requireViewBinding()
protected val behavior: AdaptiveSheetBehavior? protected val behavior: AdaptiveSheetBehavior?
get() = AdaptiveSheetBehavior.from(dialog) get() = AdaptiveSheetBehavior.from(this)
var actionModeDelegate: ActionModeDelegate? = null
private set
val isExpanded: Boolean val isExpanded: Boolean
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
val onBackPressedDispatcher: OnBackPressedDispatcher val onBackPressedDispatcher: OnBackPressedDispatcher
get() = requireComponentDialog().onBackPressedDispatcher get() = (dialog as? ComponentDialog)?.onBackPressedDispatcher ?: requireActivity().onBackPressedDispatcher
var isLocked = false
private set
private var lockCounter = 0
final override fun onCreateView( final override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -50,38 +67,69 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val binding = requireViewBinding() val binding = requireViewBinding()
if (actionModeDelegate == null) {
actionModeDelegate = (activity as? BaseActivity<*>)?.actionModeDelegate
}
onViewBindingCreated(binding, savedInstanceState) onViewBindingCreated(binding, savedInstanceState)
} }
override fun onDestroyView() { override fun onDestroyView() {
viewBinding = null viewBinding = null
actionModeDelegate = null
super.onDestroyView() super.onDestroyView()
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext() val context = requireContext()
return if (context.resources.getBoolean(R.bool.is_tablet)) { val dialog = if (context.resources.getBoolean(R.bool.is_tablet)) {
SideSheetDialog(context, theme) SideSheetDialogImpl(context, theme)
} else { } else {
BottomSheetDialog(context, theme) BottomSheetDialogImpl(context, theme)
} }
actionModeDelegate = ActionModeDelegate().also {
dialog.onBackPressedDispatcher.addCallback(it)
}
return dialog
} }
fun addSheetCallback(callback: AdaptiveSheetCallback) { @CallSuper
val b = behavior ?: return protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
actionModeDelegate?.onSupportActionModeStarted(mode, dialog?.window)
}
@CallSuper
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
actionModeDelegate?.onSupportActionModeFinished(mode, dialog?.window)
}
fun addSheetCallback(callback: AdaptiveSheetCallback, lifecycleOwner: LifecycleOwner): Boolean {
val b = behavior ?: return false
b.addCallback(callback) b.addCallback(callback)
val rootView = dialog?.findViewById<View>(materialR.id.design_bottom_sheet) val rootView = dialog?.findViewById(materialR.id.design_bottom_sheet)
?: dialog?.findViewById(materialR.id.coordinator) ?: dialog?.findViewById(materialR.id.coordinator)
?: view
if (rootView != null) { if (rootView != null) {
callback.onStateChanged(rootView, b.state) callback.onStateChanged(rootView, b.state)
} }
lifecycleOwner.lifecycle.addObserver(CallbackRemoveObserver(b, callback))
return true
} }
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? {
val delegate =
(dialog as? AppCompatDialog)?.delegate ?: (activity as? AppCompatActivity)?.delegate ?: return null
return delegate.startSupportActionMode(callback)
}
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) { protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
this.isLocked = isLocked
if (!isLocked) {
lockCounter = 0
}
val b = behavior ?: return val b = behavior ?: return
if (isExpanded) { if (isExpanded) {
b.state = BottomSheetBehavior.STATE_EXPANDED b.state = BottomSheetBehavior.STATE_EXPANDED
@@ -109,6 +157,20 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
} }
} }
@CallSuper
open fun expandAndLock() {
lockCounter++
setExpanded(isExpanded = true, isLocked = true)
}
@CallSuper
open fun unlock() {
lockCounter--
if (lockCounter <= 0) {
setExpanded(isExpanded, false)
}
}
fun requireViewBinding(): B = checkNotNull(viewBinding) { fun requireViewBinding(): B = checkNotNull(viewBinding) {
"Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()." "Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()."
} }
@@ -171,4 +233,50 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
override fun onSlide(sheet: View, slideOffset: Float) {} override fun onSlide(sheet: View, slideOffset: Float) {}
} }
private inner class SideSheetDialogImpl(context: Context, theme: Int) : SideSheetDialog(context, theme) {
override fun onSupportActionModeStarted(mode: ActionMode?) {
super.onSupportActionModeStarted(mode)
if (mode != null) {
dispatchSupportActionModeStarted(mode)
}
}
override fun onSupportActionModeFinished(mode: ActionMode?) {
super.onSupportActionModeFinished(mode)
if (mode != null) {
dispatchSupportActionModeFinished(mode)
}
}
}
private inner class BottomSheetDialogImpl(context: Context, theme: Int) : BottomSheetDialog(context, theme) {
override fun onSupportActionModeStarted(mode: ActionMode?) {
super.onSupportActionModeStarted(mode)
if (mode != null) {
dispatchSupportActionModeStarted(mode)
}
}
override fun onSupportActionModeFinished(mode: ActionMode?) {
super.onSupportActionModeFinished(mode)
if (mode != null) {
dispatchSupportActionModeFinished(mode)
}
}
}
private class CallbackRemoveObserver(
private val behavior: AdaptiveSheetBehavior,
private val callback: AdaptiveSheetCallback,
) : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
owner.lifecycle.removeObserver(this)
behavior.removeCallback(callback)
}
}
} }

View File

@@ -0,0 +1,30 @@
package org.koitharu.kotatsu.core.ui.sheet
import android.view.View
import androidx.activity.OnBackPressedCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED
class BottomSheetCollapseCallback(
private val behavior: BottomSheetBehavior<*>,
) : OnBackPressedCallback(behavior.state == STATE_EXPANDED) {
init {
behavior.addBottomSheetCallback(
object : BottomSheetBehavior.BottomSheetCallback() {
override fun onStateChanged(view: View, state: Int) {
isEnabled = state == STATE_EXPANDED || state == STATE_HALF_EXPANDED
}
override fun onSlide(p0: View, p1: Float) = Unit
},
)
}
override fun handleOnBackPressed() {
behavior.state = STATE_COLLAPSED
}
}

View File

@@ -1,14 +1,28 @@
package org.koitharu.kotatsu.core.ui.util package org.koitharu.kotatsu.core.ui.util
import android.graphics.Color
import android.os.Build
import android.view.ViewGroup
import android.view.Window
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.ActionBarContextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import com.google.android.material.R as materialR
class ActionModeDelegate : OnBackPressedCallback(false) { class ActionModeDelegate : OnBackPressedCallback(false) {
private var activeActionMode: ActionMode? = null private var activeActionMode: ActionMode? = null
private var listeners: MutableList<ActionModeListener>? = null private var listeners: MutableList<ActionModeListener>? = null
private var defaultStatusBarColor = Color.TRANSPARENT
val isActionModeStarted: Boolean val isActionModeStarted: Boolean
get() = activeActionMode != null get() = activeActionMode != null
@@ -17,16 +31,40 @@ class ActionModeDelegate : OnBackPressedCallback(false) {
finishActionMode() finishActionMode()
} }
fun onSupportActionModeStarted(mode: ActionMode) { fun onSupportActionModeStarted(mode: ActionMode, window: Window?) {
activeActionMode = mode activeActionMode = mode
isEnabled = true isEnabled = true
listeners?.forEach { it.onActionModeStarted(mode) } listeners?.forEach { it.onActionModeStarted(mode) }
if (window != null) {
val ctx = window.context
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors(
ContextCompat.getColor(ctx, materialR.color.m3_appbar_overlay_color),
ctx.getThemeColor(materialR.attr.colorSurface),
)
} else {
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
val insets = ViewCompat.getRootWindowInsets(window.decorView)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
window.decorView.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
}
} }
fun onSupportActionModeFinished(mode: ActionMode) { fun onSupportActionModeFinished(mode: ActionMode, window: Window?) {
activeActionMode = null activeActionMode = null
isEnabled = false isEnabled = false
listeners?.forEach { it.onActionModeFinished(mode) } listeners?.forEach { it.onActionModeFinished(mode) }
if (window != null) {
window.statusBarColor = defaultStatusBarColor
}
} }
fun addListener(listener: ActionModeListener) { fun addListener(listener: ActionModeListener) {

View File

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

View File

@@ -0,0 +1,50 @@
package org.koitharu.kotatsu.core.ui.util
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ancestors
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
class PagerNestedScrollHelper(
private val recyclerView: RecyclerView,
) : DefaultLifecycleObserver {
fun bind(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycle.addObserver(this)
recyclerView.isNestedScrollingEnabled = lifecycleOwner.lifecycle.currentState.isAtLeast(RESUMED)
}
override fun onPause(owner: LifecycleOwner) {
recyclerView.isNestedScrollingEnabled = false
invalidateBottomSheetScrollTarget()
}
override fun onResume(owner: LifecycleOwner) {
recyclerView.isNestedScrollingEnabled = true
}
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
}
/**
* Here we need to invalidate the `nestedScrollingChildRef` of the [BottomSheetBehavior]
*/
private fun invalidateBottomSheetScrollTarget() {
var handleCoordinator = false
for (parent in recyclerView.ancestors) {
if (handleCoordinator && parent is CoordinatorLayout) {
parent.requestLayout()
break
}
val lp = (parent as? View)?.layoutParams ?: continue
if (lp is CoordinatorLayout.LayoutParams && lp.behavior is BottomSheetBehavior<*>) {
handleCoordinator = true
}
}
}
}

View File

@@ -1,42 +0,0 @@
package org.koitharu.kotatsu.core.ui.util
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.shape.MaterialShapeDrawable
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import com.google.android.material.R as materialR
class StatusBarDimHelper : AppBarLayout.OnOffsetChangedListener {
private var animator: ValueAnimator? = null
private val interpolator = AccelerateDecelerateInterpolator()
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
val foreground = appBarLayout.statusBarForeground ?: return
val start = foreground.alpha
val collapsed = verticalOffset != 0
val end = if (collapsed) 255 else 0
animator?.cancel()
if (start == end) {
animator = null
return
}
animator = ValueAnimator.ofInt(start, end).apply {
duration = appBarLayout.context.getAnimationDuration(materialR.integer.app_bar_elevation_anim_duration)
interpolator = this@StatusBarDimHelper.interpolator
addUpdateListener {
foreground.alpha = it.animatedValue as Int
}
start()
}
}
fun attachToAppBar(appBarLayout: AppBarLayout) {
appBarLayout.addOnOffsetChangedListener(this)
appBarLayout.statusBarForeground =
MaterialShapeDrawable.createWithElevationOverlay(appBarLayout.context).apply {
alpha = 0
}
}
}

View File

@@ -33,23 +33,30 @@ sealed class SystemUiController(
private class LegacyImpl(window: Window) : SystemUiController(window) { private class LegacyImpl(window: Window) : SystemUiController(window) {
override fun setSystemUiVisible(value: Boolean) { override fun setSystemUiVisible(value: Boolean) {
val flags = window.decorView.systemUiVisibility
window.decorView.systemUiVisibility = if (value) { window.decorView.systemUiVisibility = if (value) {
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or (flags and LEGACY_FLAGS_HIDDEN.inv()) or LEGACY_FLAGS_VISIBLE
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
} else { } else {
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or (flags and LEGACY_FLAGS_VISIBLE.inv()) or LEGACY_FLAGS_HIDDEN
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
} }
} }
} }
companion object { companion object {
@Suppress("DEPRECATION")
private const val LEGACY_FLAGS_VISIBLE = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
@Suppress("DEPRECATION")
private const val LEGACY_FLAGS_HIDDEN = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
operator fun invoke(window: Window): SystemUiController = operator fun invoke(window: Window): SystemUiController =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Api30Impl(window) Api30Impl(window)

View File

@@ -110,7 +110,7 @@ class ChipsView @JvmOverloads constructor(
chip.isChipIconVisible = false chip.isChipIconVisible = false
chip.isCloseIconVisible = onChipCloseClickListener != null chip.isCloseIconVisible = onChipCloseClickListener != null
chip.setOnCloseIconClickListener(chipOnCloseListener) chip.setOnCloseIconClickListener(chipOnCloseListener)
chip.setEnsureMinTouchTargetSize(false) // TODO remove chip.setEnsureMinTouchTargetSize(false)
chip.setOnClickListener(chipOnClickListener) chip.setOnClickListener(chipOnClickListener)
addView(chip) addView(chip)
return chip return chip

View File

@@ -10,8 +10,8 @@ import com.google.android.material.imageview.ShapeableImageView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import kotlin.math.roundToInt import kotlin.math.roundToInt
private const val ASPECT_RATIO_HEIGHT = 18f private const val ASPECT_RATIO_HEIGHT = 3f
private const val ASPECT_RATIO_WIDTH = 13f private const val ASPECT_RATIO_WIDTH = 2f
class CoverImageView @JvmOverloads constructor( class CoverImageView @JvmOverloads constructor(
context: Context, context: Context,

View File

@@ -0,0 +1,144 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.core.content.withStyledAttributes
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import androidx.viewpager2.widget.ViewPager2
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.measureDimension
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.parsers.util.toIntUp
import com.google.android.material.R as materialR
class DotsIndicator @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.dotIndicatorStyle,
) : View(context, attrs, defStyleAttr) {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var indicatorSize = context.resources.resolveDp(12f)
private var dotSpacing = 0f
private var smallDotScale = 0.33f
private var smallDotAlpha = 0.6f
private var positionOffset: Float = 0f
private var position: Int = 0
private val inset = context.resources.resolveDp(1f)
var max: Int = 6
set(value) {
if (field != value) {
field = value
requestLayout()
invalidate()
}
}
var progress: Int
get() = position
set(value) {
if (position != value) {
position = value
invalidate()
}
}
init {
paint.style = Paint.Style.FILL
context.withStyledAttributes(attrs, R.styleable.DotsIndicator, defStyleAttr) {
paint.color = getColor(
R.styleable.DotsIndicator_dotColor,
context.getThemeColor(materialR.attr.colorOnBackground, Color.DKGRAY),
)
indicatorSize = getDimension(R.styleable.DotsIndicator_dotSize, indicatorSize)
dotSpacing = getDimension(R.styleable.DotsIndicator_dotSpacing, dotSpacing)
smallDotScale = getFloat(R.styleable.DotsIndicator_dotScale, smallDotScale).coerceIn(0f, 1f)
smallDotAlpha = getFloat(R.styleable.DotsIndicator_dotAlpha, smallDotAlpha).coerceIn(0f, 1f)
max = getInt(R.styleable.DotsIndicator_android_max, max)
position = getInt(R.styleable.DotsIndicator_android_progress, position)
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val dotSize = getDotSize()
val y = paddingTop + (height - paddingTop - paddingBottom) / 2f
var x = paddingLeft + dotSize / 2f
val radius = dotSize / 2f - inset
val spacing = (width - paddingLeft - paddingRight) / max.toFloat() - dotSize
x += spacing / 2f
for (i in 0 until max) {
val scale = when (i) {
position -> (1f - smallDotScale) * (1f - positionOffset) + smallDotScale
position + 1 -> (1f - smallDotScale) * positionOffset + smallDotScale
else -> smallDotScale
}
paint.alpha = (255 * when (i) {
position -> (1f - smallDotAlpha) * (1f - positionOffset) + smallDotAlpha
position + 1 -> (1f - smallDotAlpha) * positionOffset + smallDotAlpha
else -> smallDotAlpha
}).toInt()
canvas.drawCircle(x, y, radius * scale, paint)
x += spacing + dotSize
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val dotSize = getDotSize()
val desiredHeight = (dotSize + paddingTop + paddingBottom).toIntUp()
val desiredWidth = ((dotSize + dotSpacing) * max).toIntUp() + paddingLeft + paddingRight
setMeasuredDimension(
measureDimension(desiredWidth, widthMeasureSpec),
measureDimension(desiredHeight, heightMeasureSpec),
)
}
fun bindToViewPager(pager: ViewPager2) {
pager.registerOnPageChangeCallback(ViewPagerCallback())
pager.adapter?.let {
it.registerAdapterDataObserver(AdapterObserver(it))
}
}
private fun getDotSize() = if (indicatorSize <= 0) {
(height - paddingTop - paddingBottom).toFloat()
} else {
indicatorSize
}
private inner class ViewPagerCallback : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
this@DotsIndicator.position = position
this@DotsIndicator.positionOffset = positionOffset
invalidate()
}
}
private inner class AdapterObserver(
private val adapter: RecyclerView.Adapter<*>,
) : AdapterDataObserver() {
override fun onChanged() {
super.onChanged()
max = adapter.itemCount
}
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
super.onItemRangeInserted(positionStart, itemCount)
max = adapter.itemCount
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
max = adapter.itemCount
}
}
}

View File

@@ -1,36 +0,0 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
@SuppressLint("ClickableViewAccessibility")
class EnhancedViewPager @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
) : ViewPager(context, attrs) {
var isUserInputEnabled: Boolean = true
set(value) {
field = value
if (!value) {
cancelPendingInputEvents()
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
return isUserInputEnabled && super.onTouchEvent(event)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return try {
isUserInputEnabled && super.onInterceptTouchEvent(event)
} catch (e: IllegalArgumentException) {
e.printStackTraceDebug()
false
}
}
}

View File

@@ -25,6 +25,15 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
private var dyRatio = 1F private var dyRatio = 1F
var isPinned: Boolean = false
set(value) {
field = value
if (value) {
offsetAnimator?.cancel()
offsetAnimator = null
}
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean { override fun layoutDependsOn(parent: CoordinatorLayout, child: BottomNavigationView, dependency: View): Boolean {
return dependency is AppBarLayout return dependency is AppBarLayout
} }
@@ -51,7 +60,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
axes: Int, axes: Int,
type: Int, type: Int,
): Boolean { ): Boolean {
if (axes != ViewCompat.SCROLL_AXIS_VERTICAL) { if (isPinned || axes != ViewCompat.SCROLL_AXIS_VERTICAL) {
return false return false
} }
lastStartedType = type lastStartedType = type
@@ -69,7 +78,9 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
type: Int, type: Int,
) { ) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type)
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat()) if (!isPinned) {
child.translationY = (child.translationY + (dy * dyRatio)).coerceIn(0F, child.height.toFloat())
}
} }
override fun onStopNestedScroll( override fun onStopNestedScroll(
@@ -78,7 +89,7 @@ class HideBottomNavigationOnScrollBehavior @JvmOverloads constructor(
target: View, target: View,
type: Int, type: Int,
) { ) {
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) { if (!isPinned && (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH)) {
animateBottomNavigationVisibility(child, child.translationY < child.height / 2) animateBottomNavigationVisibility(child, child.translationY < child.height / 2)
} }
} }

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.AttrRes
import com.google.android.material.textview.MaterialTextView
class MultilineEllipsizeTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
@AttrRes defStyleAttr: Int = android.R.attr.textViewStyle,
) : MaterialTextView(context, attrs, defStyleAttr) {
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val lh = lineHeight
maxLines = if (lh > 0) h / lh else 1
}
}

View File

@@ -0,0 +1,182 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.animation.ValueAnimator
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Outline
import android.graphics.Paint
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewOutlineProvider
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.view.children
import androidx.core.widget.TextViewCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import com.google.android.material.R as materialR
class ProgressButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : LinearLayoutCompat(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener {
private val textViewTitle = TextView(context)
private val textViewSubtitle = TextView(context)
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private var progress = 0f
private var targetProgress = 0f
private var colorBase: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
private var colorProgress: ColorStateList = ColorStateList.valueOf(Color.TRANSPARENT)
private var progressAnimator: ValueAnimator? = null
private var colorBaseCurrent = colorProgress.defaultColor
private var colorProgressCurrent = colorProgress.defaultColor
var title: CharSequence?
get() = textViewTitle.textAndVisible
set(value) {
textViewTitle.textAndVisible = value
}
var subtitle: CharSequence?
get() = textViewSubtitle.textAndVisible
set(value) {
textViewSubtitle.textAndVisible = value
}
init {
orientation = VERTICAL
outlineProvider = OutlineProvider()
clipToOutline = true
context.withStyledAttributes(attrs, R.styleable.ProgressButton, defStyleAttr) {
val textAppearanceFallback = androidx.appcompat.R.style.TextAppearance_AppCompat
TextViewCompat.setTextAppearance(
textViewTitle,
getResourceId(R.styleable.ProgressButton_titleTextAppearance, textAppearanceFallback),
)
TextViewCompat.setTextAppearance(
textViewSubtitle,
getResourceId(R.styleable.ProgressButton_subtitleTextAppearance, textAppearanceFallback),
)
textViewTitle.text = getText(R.styleable.ProgressButton_title)
textViewSubtitle.text = getText(R.styleable.ProgressButton_subtitle)
colorBase = getColorStateList(R.styleable.ProgressButton_baseColor)
?: context.getThemeColorStateList(materialR.attr.colorPrimaryContainer) ?: colorBase
colorProgress = getColorStateList(R.styleable.ProgressButton_progressColor)
?: context.getThemeColorStateList(materialR.attr.colorPrimary) ?: colorProgress
getColorStateList(R.styleable.ProgressButton_android_textColor)?.let { colorText ->
textViewTitle.setTextColor(colorText)
textViewSubtitle.setTextColor(colorText)
}
progress = getInt(R.styleable.ProgressButton_android_progress, 0).toFloat() /
getInt(R.styleable.ProgressButton_android_max, 100).toFloat()
}
addView(textViewTitle, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT))
addView(
textViewSubtitle,
LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).also { lp ->
lp.topMargin = context.resources.resolveDp(2)
},
)
paint.style = Paint.Style.FILL
applyGravity()
setWillNotDraw(false)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(colorBaseCurrent)
if (progress > 0f) {
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
}
}
override fun drawableStateChanged() {
super.drawableStateChanged()
val state = drawableState
colorBaseCurrent = colorBase.getColorForState(state, colorBase.defaultColor)
colorProgressCurrent = colorProgress.getColorForState(state, colorProgress.defaultColor)
colorProgressCurrent = ColorUtils.setAlphaComponent(colorProgressCurrent, 84 /* 255 * 0.33F */)
paint.color = colorProgressCurrent
}
override fun setGravity(gravity: Int) {
super.setGravity(gravity)
if (childCount != 0) {
applyGravity()
}
}
override fun setEnabled(enabled: Boolean) {
super.setEnabled(enabled)
children.forEach { it.isEnabled = enabled }
}
override fun onAnimationUpdate(animation: ValueAnimator) {
if (animation === progressAnimator) {
progress = animation.animatedValue as Float
invalidate()
}
}
fun setTitle(@StringRes titleResId: Int) {
textViewTitle.setTextAndVisible(titleResId)
}
fun setSubtitle(@StringRes titleResId: Int) {
textViewSubtitle.setTextAndVisible(titleResId)
}
fun setProgress(value: Float, animate: Boolean) {
val prevAnimator = progressAnimator
if (animate && context.isAnimationsEnabled) {
if (value == targetProgress) {
return
}
targetProgress = value
progressAnimator = ValueAnimator.ofFloat(progress, value).apply {
duration = context.getAnimationDuration(android.R.integer.config_mediumAnimTime)
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener(this@ProgressButton)
}
progressAnimator?.start()
} else {
progressAnimator = null
progress = value
targetProgress = value
invalidate()
}
prevAnimator?.cancel()
}
private fun applyGravity() {
val value = (gravity and Gravity.HORIZONTAL_GRAVITY_MASK) or Gravity.CENTER_VERTICAL
textViewTitle.gravity = value
textViewSubtitle.gravity = value
}
private class OutlineProvider : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f)
}
}
}

View File

@@ -11,6 +11,7 @@ import android.view.ViewPropertyAnimator
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.customview.view.AbsSavedState import androidx.customview.view.AbsSavedState
import androidx.interpolator.view.animation.FastOutLinearInInterpolator import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
@@ -38,6 +39,18 @@ class SlidingBottomNavigationView @JvmOverloads constructor(
private var currentState = STATE_UP private var currentState = STATE_UP
private var behavior = HideBottomNavigationOnScrollBehavior() private var behavior = HideBottomNavigationOnScrollBehavior()
var isPinned: Boolean
get() = behavior.isPinned
set(value) {
behavior.isPinned = value
if (value) {
translationX = 0f
}
}
val isShownOrShowing: Boolean
get() = isVisible && currentState == STATE_UP
override fun getBehavior(): CoordinatorLayout.Behavior<*> { override fun getBehavior(): CoordinatorLayout.Behavior<*> {
return behavior return behavior
} }

View File

@@ -12,6 +12,7 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import androidx.core.view.isVisible
import androidx.core.view.setPadding import androidx.core.view.setPadding
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.shape.ShapeAppearanceModel
@@ -75,7 +76,7 @@ class TipView @JvmOverloads constructor(
val shapeAppearanceModel = ShapeAppearanceModel.builder(context, attrs, defStyleAttr, 0).build() val shapeAppearanceModel = ShapeAppearanceModel.builder(context, attrs, defStyleAttr, 0).build()
background = MaterialShapeDrawable(shapeAppearanceModel).also { background = MaterialShapeDrawable(shapeAppearanceModel).also {
it.fillColor = getColorStateList(R.styleable.TipView_cardBackgroundColor) it.fillColor = getColorStateList(R.styleable.TipView_cardBackgroundColor)
?: context.getThemeColorStateList(R.attr.m3ColorExploreButton) ?: context.getThemeColorStateList(com.google.android.material.R.attr.colorSurfaceContainerHigh)
it.strokeWidth = getDimension(R.styleable.TipView_strokeWidth, 0f) it.strokeWidth = getDimension(R.styleable.TipView_strokeWidth, 0f)
it.strokeColor = getColorStateList(R.styleable.TipView_strokeColor) it.strokeColor = getColorStateList(R.styleable.TipView_strokeColor)
it.elevation = getDimension(R.styleable.TipView_elevation, 0f) it.elevation = getDimension(R.styleable.TipView_elevation, 0f)
@@ -103,16 +104,22 @@ class TipView @JvmOverloads constructor(
fun setPrimaryButtonText(@StringRes resId: Int) { fun setPrimaryButtonText(@StringRes resId: Int) {
binding.buttonPrimary.setTextAndVisible(resId) binding.buttonPrimary.setTextAndVisible(resId)
updateButtonsLayout()
} }
fun setSecondaryButtonText(@StringRes resId: Int) { fun setSecondaryButtonText(@StringRes resId: Int) {
binding.buttonSecondary.setTextAndVisible(resId) binding.buttonSecondary.setTextAndVisible(resId)
updateButtonsLayout()
} }
fun setIcon(@DrawableRes resId: Int) { fun setIcon(@DrawableRes resId: Int) {
icon = ContextCompat.getDrawable(context, resId) icon = ContextCompat.getDrawable(context, resId)
} }
private fun updateButtonsLayout() {
binding.layoutButtons.isVisible = binding.buttonPrimary.isVisible || binding.buttonSecondary.isVisible
}
interface OnButtonClickListener { interface OnButtonClickListener {
fun onPrimaryButtonClick(tipView: TipView) fun onPrimaryButtonClick(tipView: TipView)

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.core.util
import kotlinx.coroutines.CoroutineExceptionHandler
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.report
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
class AcraCoroutineErrorHandler : AbstractCoroutineContextElement(CoroutineExceptionHandler),
CoroutineExceptionHandler {
override fun handleException(context: CoroutineContext, exception: Throwable) {
exception.printStackTraceDebug()
exception.report()
}
}

View File

@@ -1,58 +0,0 @@
package org.koitharu.kotatsu.core.util
import androidx.collection.ArrayMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.coroutineContext
@Deprecated("", replaceWith = ReplaceWith("CompositeMutex2"))
class CompositeMutex<T : Any> : Set<T> {
private val state = ArrayMap<T, MutableStateFlow<Boolean>>()
private val mutex = Mutex()
override val size: Int
get() = state.size
override fun contains(element: T): Boolean {
return state.containsKey(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return elements.all { x -> state.containsKey(x) }
}
override fun isEmpty(): Boolean {
return state.isEmpty()
}
override fun iterator(): Iterator<T> {
return state.keys.iterator()
}
suspend fun lock(element: T) {
while (coroutineContext.isActive) {
waitForRemoval(element)
mutex.withLock {
if (state[element] == null) {
state[element] = MutableStateFlow(false)
return
}
}
}
}
fun unlock(element: T) {
checkNotNull(state.remove(element)) {
"CompositeMutex is not locked for $element"
}.value = true
}
private suspend fun waitForRemoval(element: T) {
val flow = state[element] ?: return
flow.first { it }
}
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.core.util
class CompositeRunnable(
private val children: List<Runnable>,
) : Runnable, Collection<Runnable> by children {
override fun run() {
for (child in children) {
child.run()
}
}
}

View File

@@ -9,8 +9,8 @@ class Event<T>(
suspend fun consume(collector: FlowCollector<T>) { suspend fun consume(collector: FlowCollector<T>) {
if (!isConsumed) { if (!isConsumed) {
collector.emit(data)
isConsumed = true isConsumed = true
collector.emit(data)
} }
} }

View File

@@ -1,46 +0,0 @@
package org.koitharu.kotatsu.core.util
import android.app.Activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.ui.DefaultActivityLifecycleCallbacks
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class IncognitoModeIndicator @Inject constructor(
private val settings: AppSettings,
) : DefaultActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activity !is AppCompatActivity) {
return
}
settings.observeAsFlow(
key = AppSettings.KEY_INCOGNITO_MODE,
valueProducer = { isIncognitoModeEnabled },
).flowOn(Dispatchers.IO)
.flowWithLifecycle(activity.lifecycle)
.onEach { updateStatusBar(activity, it) }
.launchIn(activity.lifecycleScope)
}
private fun updateStatusBar(activity: AppCompatActivity, isIncognitoModeEnabled: Boolean) {
activity.window.statusBarColor = if (isIncognitoModeEnabled) {
ContextCompat.getColor(activity, R.color.status_bar_incognito)
} else {
activity.getThemeColor(android.R.attr.statusBarColor)
}
}
}

View File

@@ -2,20 +2,22 @@ package org.koitharu.kotatsu.core.util
import androidx.collection.ArrayMap import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
class CompositeMutex2<T : Any> : Set<T> { class MultiMutex<T : Any> : Set<T> {
private val delegates = ArrayMap<T, Mutex>() private val delegates = ArrayMap<T, Mutex>()
override val size: Int override val size: Int
get() = delegates.size get() = delegates.size
override fun contains(element: T): Boolean { override fun contains(element: T): Boolean = synchronized(delegates) {
return delegates.containsKey(element) delegates.containsKey(element)
} }
override fun containsAll(elements: Collection<T>): Boolean { override fun containsAll(elements: Collection<T>): Boolean = synchronized(delegates) {
return elements.all { x -> delegates.containsKey(x) } elements.all { x -> delegates.containsKey(x) }
} }
override fun isEmpty(): Boolean { override fun isEmpty(): Boolean {
@@ -40,4 +42,16 @@ class CompositeMutex2<T : Any> : Set<T> {
delegates.remove(element)?.unlock() delegates.remove(element)?.unlock()
} }
} }
suspend inline fun <R> withLock(element: T, block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return try {
lock(element)
block()
} finally {
unlock(element)
}
}
} }

View File

@@ -14,11 +14,12 @@ import android.content.ContextWrapper
import android.content.OperationApplicationException import android.content.OperationApplicationException
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SyncResult import android.content.SyncResult
import android.content.pm.PackageManager import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.database.SQLException import android.database.SQLException
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -31,12 +32,20 @@ import android.webkit.WebView
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes import androidx.annotation.IntegerRes
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDialog
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.elevation.ElevationOverlayProvider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -54,12 +63,12 @@ import okio.use
import org.json.JSONException import org.json.JSONException
import org.jsoup.internal.StringUtil.StringJoiner import org.jsoup.internal.StringUtil.StringJoiner
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import java.io.File import java.io.File
import kotlin.math.roundToLong import kotlin.math.roundToLong
import com.google.android.material.R as materialR
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager get() = getSystemService(ACTIVITY_SERVICE) as? ActivityManager
@@ -67,6 +76,9 @@ val Context.activityManager: ActivityManager?
val Context.powerManager: PowerManager? val Context.powerManager: PowerManager?
get() = getSystemService(POWER_SERVICE) as? PowerManager get() = getSystemService(POWER_SERVICE) as? PowerManager
val Context.connectivityManager: ConnectivityManager
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
@@ -133,10 +145,13 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
!context.getSystemBoolean("config_navBarNeedsScrim", true) !context.getSystemBoolean("config_navBarNeedsScrim", true)
) { ) {
Color.TRANSPARENT Color.TRANSPARENT
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) {
val baseColor = context.getThemeColor(android.R.attr.navigationBarColor)
ColorUtils.setAlphaComponent(baseColor, (Color.alpha(baseColor) * alphaFactor).toInt())
} else { } else {
// Set navbar scrim 70% of navigationBarColor // Set navbar scrim 70% of navigationBarColor
ElevationOverlayProvider(context).compositeOverlayIfNeeded( ElevationOverlayProvider(context).compositeOverlayIfNeeded(
context.getThemeColor(R.attr.m3ColorBottomMenuBackground, alphaFactor), context.getThemeColor(materialR.attr.colorSurfaceContainer, alphaFactor),
elevation, elevation,
) )
} }
@@ -216,10 +231,26 @@ fun Context.findActivity(): Activity? = when (this) {
else -> null else -> null
} }
fun Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { fun Fragment.findAppCompatDelegate(): AppCompatDelegate? {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED ((this as? DialogFragment)?.dialog as? AppCompatDialog)?.run {
} else { return delegate
NotificationManagerCompat.from(this).areNotificationsEnabled() }
return parentFragment?.findAppCompatDelegate() ?: (activity as? AppCompatActivity)?.delegate
}
fun Context.checkNotificationPermission(channelId: String?): Boolean {
val hasPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(this).areNotificationsEnabled()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasPermission && channelId != null) {
val channel = NotificationManagerCompat.from(this).getNotificationChannel(channelId)
if (channel != null && channel.importance == NotificationManagerCompat.IMPORTANCE_NONE) {
return false
}
}
return hasPermission
} }
@WorkerThread @WorkerThread
@@ -241,6 +272,9 @@ fun WebView.configureForParser(userAgentOverride: String?) = with(settings) {
javaScriptEnabled = true javaScriptEnabled = true
domStorageEnabled = true domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false mediaPlaybackRequiresUserGesture = false
if (WebViewFeature.isFeatureSupported(WebViewFeature.MUTE_AUDIO)) {
WebViewCompat.setAudioMuted(this@configureForParser, true)
}
databaseEnabled = true databaseEnabled = true
if (userAgentOverride != null) { if (userAgentOverride != null) {
userAgentString = userAgentOverride userAgentString = userAgentOverride

View File

@@ -1,7 +1,9 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.content.Context import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.widget.ImageView import android.widget.ImageView
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
@@ -12,9 +14,11 @@ import coil.request.SuccessResult
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.progressindicator.BaseProgressIndicator import com.google.android.material.progressindicator.BaseProgressIndicator
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.image.AnimatedPlaceholderDrawable
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import com.google.android.material.R as materialR
fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): ImageRequest.Builder? { fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): ImageRequest.Builder? {
val current = CoilUtils.result(this) val current = CoilUtils.result(this)
@@ -25,7 +29,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
} }
// disposeImageRequest() // disposeImageRequest()
return ImageRequest.Builder(context) return ImageRequest.Builder(context)
.data(data?.takeUnless { it == "" }) .data(data?.takeUnless { it == "" || it == 0 })
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.crossfade(context) .crossfade(context)
.target(this) .target(this)
@@ -43,15 +47,6 @@ fun ImageResult.getDrawableOrThrow() = when (this) {
is ErrorResult -> throw throwable is ErrorResult -> throw throwable
} }
@Deprecated(
"",
ReplaceWith(
"getDrawableOrThrow().toBitmap()",
"androidx.core.graphics.drawable.toBitmap",
),
)
fun ImageResult.requireBitmap() = getDrawableOrThrow().toBitmap()
fun ImageResult.toBitmapOrNull() = when (this) { fun ImageResult.toBitmapOrNull() = when (this) {
is SuccessResult -> try { is SuccessResult -> try {
drawable.toBitmap() drawable.toBitmap()
@@ -85,6 +80,17 @@ fun ImageRequest.Builder.source(source: MangaSource?): ImageRequest.Builder {
return tag(MangaSource::class.java, source) return tag(MangaSource::class.java, source)
} }
fun ImageRequest.Builder.defaultPlaceholders(context: Context): ImageRequest.Builder {
val errorColor = ColorUtils.blendARGB(
context.getThemeColor(materialR.attr.colorErrorContainer),
context.getThemeColor(materialR.attr.colorBackgroundFloating),
0.25f,
)
return placeholder(AnimatedPlaceholderDrawable(context))
.fallback(ColorDrawable(context.getThemeColor(materialR.attr.colorSurfaceContainer)))
.error(ColorDrawable(errorColor))
}
fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder { fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder {
val existing = build().listener val existing = build().listener
return listener( return listener(

View File

@@ -68,3 +68,5 @@ fun <T> Iterable<T>.sortedWithSafe(comparator: Comparator<in T>): List<T> = try
toList() toList()
} }
} }
fun Collection<*>?.sizeOrZero() = if (this == null) 0 else size

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleDestroyedException import androidx.lifecycle.LifecycleDestroyedException
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
@@ -10,17 +9,20 @@ import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.lifecycle.RetainedLifecycle import dagger.hilt.android.lifecycle.RetainedLifecycle
import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import org.koitharu.kotatsu.core.util.AcraCoroutineErrorHandler
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
val processLifecycleScope: LifecycleCoroutineScope val processLifecycleScope: CoroutineScope
inline get() = ProcessLifecycleOwner.get().lifecycleScope get() = ProcessLifecycleOwner.get().lifecycleScope + AcraCoroutineErrorHandler()
val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope val RetainedLifecycle.lifecycleScope: RetainedLifecycleCoroutineScope
inline get() = RetainedLifecycleCoroutineScope(this) inline get() = RetainedLifecycleCoroutineScope(this)

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