Compare commits

..

359 Commits
v6.6.6 ... ui

Author SHA1 Message Date
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
0ffefddb86 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 16:38:21 +02:00
Anton Prevrhal
09b154c997 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 16:38:21 +02:00
jonathan | ヨナタン
d9f3b4f76e 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 16:38:21 +02:00
Anon
8ebb3ef804 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 16:38:21 +02:00
ReksaTresna
b03682a81f 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 16:38:21 +02:00
Infy's Tagalog Translations
5dd54be06c 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 16:38:21 +02:00
Scrambled777
98c0b60207 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 16:38:21 +02:00
gekka
10a0009532 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 16:38:21 +02:00
Oğuz Ersen
5e203f0b27 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 16:38:21 +02:00
gallegonovato
46fc48cfd7 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 16:38:21 +02:00
Макар Разин
e8a17708d2 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 16:38:21 +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
Koitharu
bc42fda786 Update parsers 2024-03-23 16:36:01 +02:00
Koitharu
d3590372f3 Disable reporting of ParseException 2024-03-23 16:22:25 +02:00
Koitharu
88f55997fa Fix stats chart color 2024-03-23 16:18:01 +02:00
Koitharu
0a1bc6716b Fix crashes 2024-03-23 15:59:49 +02:00
Koitharu
559e546462 Fix favorites migration 2024-03-23 10:05:01 +02:00
Koitharu
6c5775a2ed Option to disable Pages tab on details screen 2024-03-23 09:54:10 +02:00
Koitharu
4858adbbe7 Fix chapters selection 2024-03-20 07:23:17 +02:00
Koitharu
cae07b2798 Update parsers 2024-03-19 13:38:48 +02:00
Koitharu
b14603c384 Fix history migration 2024-03-18 14:28:29 +02:00
Koitharu
2f21d0f0f8 Merge remote-tracking branch 'weblate/devel' into devel 2024-03-18 13:32:13 +02:00
Koitharu
7e182cb0ad Fix passing CloudFlare protection #819 #820 2024-03-18 13:18:24 +02:00
maryush
f79d2cb733 Translated using Weblate (Polish)
Currently translated at 100.0% (626 of 626 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-03-15 16:09:07 +02:00
大王叫我来巡山
ce296900c5 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (616 of 626 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-03-15 16:09:07 +02:00
Oğuz Ersen
0156ae86eb Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (626 of 626 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-15 16:09:07 +02:00
Scrambled777
efd82b6d96 Translated using Weblate (Hindi)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (626 of 626 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (621 of 621 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-03-15 16:09:07 +02:00
Abay Emes
b4371d2cd2 Translated using Weblate (Kazakh)
Currently translated at 87.8% (541 of 616 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2024-03-15 16:09:07 +02:00
gallegonovato
676c94d759 Translated using Weblate (Spanish)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (616 of 616 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (621 of 621 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (621 of 621 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (616 of 616 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-15 16:09:07 +02:00
Anon
5f79d37506 Translated using Weblate (Serbian)
Currently translated at 99.8% (620 of 621 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Serbian)

Currently translated at 99.6% (614 of 616 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-03-15 16:09:07 +02:00
gekka
2e074573c0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (627 of 629 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.3% (612 of 616 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.3% (612 of 616 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-03-15 16:09:07 +02:00
maryush
82281312fb Translated using Weblate (Polish)
Currently translated at 100.0% (626 of 626 strings)

Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
2024-03-15 14:52:35 +01:00
大王叫我来巡山
ed6a906459 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (616 of 626 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-03-15 14:52:35 +01:00
Oğuz Ersen
00b01f298d Translated using Weblate (Turkish)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (626 of 626 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-15 14:52:35 +01:00
Scrambled777
aa99ea1245 Translated using Weblate (Hindi)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (626 of 626 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (621 of 621 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-03-15 14:52:35 +01:00
Abay Emes
732c614aad Translated using Weblate (Kazakh)
Currently translated at 87.8% (541 of 616 strings)

Co-authored-by: Abay Emes <abayemes@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/kk/
Translation: Kotatsu/Strings
2024-03-15 14:52:35 +01:00
gallegonovato
afe16859d4 Translated using Weblate (Spanish)
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (628 of 628 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (616 of 616 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (621 of 621 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (621 of 621 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (621 of 621 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (616 of 616 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-15 14:52:35 +01:00
Anon
630cece4f5 Translated using Weblate (Serbian)
Currently translated at 99.8% (620 of 621 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Serbian)

Currently translated at 99.6% (614 of 616 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-03-15 14:52:35 +01:00
gekka
f0101bc183 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (634 of 634 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (629 of 629 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (627 of 629 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.3% (612 of 616 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.3% (612 of 616 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-03-15 14:52:35 +01:00
Koitharu
7c2829226d Improve alternatives search 2024-03-15 15:52:15 +02:00
Koitharu
83bd390c2a Add enable toggle to source settings 2024-03-15 11:34:10 +02:00
Koitharu
b090652007 UI updates 2024-03-15 11:02:39 +02:00
Koitharu
569edd91c9 Fix manga migration 2024-03-15 08:09:51 +02:00
Koitharu
2e81684652 Fix chapters grid view 2024-03-14 14:08:22 +02:00
Koitharu
2573d150f9 Invalidate manga cache on config changes 2024-03-14 13:44:13 +02:00
Koitharu
24fe83aa5c Added more sort orders for local lists #707 #467 2024-03-14 12:51:50 +02:00
Koitharu
bbc39becc3 Update dependencies 2024-03-14 10:08:02 +02:00
Koitharu
65077c1fba Update chapters grid ui 2024-03-13 15:51:33 +02:00
Koitharu
bec0ce2c96 Refactor chapters grid mode 2024-03-13 14:46:47 +02:00
Koitharu
256f0a31bc Merge branch 'feature/grid-chapters' of github.com:jsericksk/Kotatsu into jsericksk-feature/grid-chapters 2024-03-13 13:02:29 +02:00
Koitharu
b8e48d8b8a Option to automatically clean read chapters 2024-03-13 10:06:39 +02:00
Koitharu
8313d6966f Action to remove read local chapters 2024-03-12 18:04:45 +02:00
Koitharu
7e581a5ed7 Fix local chapters deletion 2024-03-12 15:02:39 +02:00
Koitharu
16027e3295 Manga migration feature 2024-03-12 13:32:50 +02:00
Koitharu
e4e14214d9 Check if has stats on details screen 2024-03-11 12:49:20 +02:00
Koitharu
e40a39ca28 Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2024-03-11 10:22:10 +02:00
Koitharu
82e711d619 Merge branch 'master' into devel 2024-03-11 10:21:23 +02:00
Koitharu
8c2bff78f7 Fix chapters duplication 2024-03-09 14:27:12 +02:00
Koitharu
4f2c38d4ee Update parsers 2024-03-09 13:05:02 +02:00
Koitharu
3c54fe4217 Fix crash on text selection
(cherry picked from commit 65abfc3a49)
2024-03-09 12:34:52 +02:00
Koitharu
750bf11fdc Update app/src/main/res/color-v23/selector_overlay.xml
(cherry picked from commit ba88ca8234)
2024-03-09 12:34:47 +02:00
Kaorun
b2c5ec5082 Fixed hover color
(cherry picked from commit 63470db6f5)
2024-03-09 12:34:39 +02:00
Koitharu
f97d4d452f Fix null url crash
(cherry picked from commit 1e39ae48ec)
2024-03-09 12:34:33 +02:00
Koitharu
640fe272c8 Fix passing intent data to ViewModels
(cherry picked from commit 094b0f694c)
2024-03-09 12:34:21 +02:00
Koitharu
f730e80bb7 Use numeric keyboard if app password is numeric
(cherry picked from commit f98bb87d6e)
2024-03-09 12:33:53 +02:00
ines Djimli
d975e92991 Translated using Weblate (Arabic)
Currently translated at 88.8% (8 of 9 strings)

Co-authored-by: ines Djimli <djimliines124@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ar/
Translation: Kotatsu/plurals
2024-03-09 12:23:22 +02:00
Deivinni Silva
0d29190bd1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (616 of 616 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
Макар Разин
76c07b1567 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (616 of 616 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-09 12:23:22 +02:00
Hosted Weblate
55c82a6f5c Update translation files
Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
Nayuki
f81d298315 Translated using Weblate (Thai)
Currently translated at 68.8% (414 of 601 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
maryush
fd17e1ea20 Translated using Weblate (Polish)
Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Polish)

Currently translated at 99.0% (610 of 616 strings)

Translated using Weblate (Polish)

Currently translated at 91.5% (550 of 601 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Polish)

Currently translated at 88.3% (531 of 601 strings)

Co-authored-by: Mariusz <maryush@gmail.com>
Co-authored-by: maryush <maryush@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pl/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pl/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-03-09 12:23:22 +02:00
Infy's Tagalog Translations
a1dc401eee Translated using Weblate (Filipino)
Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (601 of 601 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-09 12:23:22 +02:00
Anon
b5ee465cde Translated using Weblate (Serbian)
Currently translated at 99.5% (613 of 616 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (601 of 601 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
Scrambled777
956b04e974 Translated using Weblate (Hindi)
Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (601 of 601 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
abc0922001
f65c213e2d Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (601 of 601 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
gekka
813ce2e195 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.3% (612 of 616 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (615 of 616 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (615 of 616 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (601 of 601 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (601 of 601 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
Madaraki
0eb320ec76 Translated using Weblate (Russian)
Currently translated at 99.6% (614 of 616 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (601 of 601 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (601 of 601 strings)

Co-authored-by: Madaraki <115705267+Madaraki-chan@users.noreply.github.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-03-09 12:23:22 +02:00
Oğuz Ersen
b17aa6c031 Translated using Weblate (Turkish)
Currently translated at 100.0% (616 of 616 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (601 of 601 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-09 12:23:22 +02:00
a
97b5102e6c Translated using Weblate (Portuguese)
Currently translated at 100.0% (601 of 601 strings)

Co-authored-by: a <cooki3yt2004@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
INeido
fe408c0832 Translated using Weblate (Hungarian)
Currently translated at 100.0% (601 of 601 strings)

Translated using Weblate (German)

Currently translated at 99.1% (596 of 601 strings)

Translated using Weblate (Hungarian)

Currently translated at 51.9% (312 of 601 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Hungarian)

Currently translated at 29.1% (175 of 601 strings)

Translated using Weblate (German)

Currently translated at 97.0% (583 of 601 strings)

Co-authored-by: INeido <reg@neido.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/hu/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-03-09 12:23:22 +02:00
gallegonovato
4f3d1a9814 Translated using Weblate (Spanish)
Currently translated at 100.0% (601 of 601 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-03-09 12:23:22 +02:00
Koitharu
65abfc3a49 Fix crash on text selection 2024-03-06 16:18:40 +02:00
Koitharu
ba88ca8234 Update app/src/main/res/color-v23/selector_overlay.xml 2024-03-06 16:16:47 +02:00
Kaorun
63470db6f5 Fixed hover color 2024-03-06 16:16:47 +02:00
Koitharu
1e39ae48ec Fix null url crash 2024-03-04 20:03:52 +02:00
Koitharu
6fcc45d554 Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2024-03-04 19:59:45 +02:00
Koitharu
094b0f694c Fix passing intent data to ViewModels 2024-03-04 19:58:46 +02:00
Koitharu
5d1a2fcf77 Statistics filters 2024-03-04 16:31:39 +02:00
Koitharu
876675445d Stats chart for single manga 2024-03-04 14:42:31 +02:00
ztimms73
51362e6cce Remove LICENSE appendix that should be removed initially 2024-03-02 15:07:01 +03:00
Koitharu
f7a70680bd Timeline stats per manga 2024-03-01 15:00:38 +02:00
Koitharu
8e82db441c Empty stats state 2024-03-01 10:34:31 +02:00
Koitharu
f2626c668d Switch and click preference 2024-02-29 16:15:44 +02:00
Koitharu
4694215ccc Statistics periods 2024-02-29 15:28:57 +02:00
Koitharu
096f5b15dc Clearing stats 2024-02-29 14:27:52 +02:00
Koitharu
101d357eff Stats activity 2024-02-29 14:01:31 +02:00
Koitharu
11cd5609bb Use stats for reading time estimation 2024-02-29 12:12:09 +02:00
Koitharu
fda59996aa Improve stats ui 2024-02-29 12:01:09 +02:00
Koitharu
20461112d2 Merge branch 'devel' into feature/stats 2024-02-29 11:20:31 +02:00
Koitharu
f98bb87d6e Use numeric keyboard if app password is numeric 2024-02-29 11:20:10 +02:00
Koitharu
c451952a1e Merge branch 'devel' into feature/stats 2024-02-29 10:00:49 +02:00
Koitharu
f8cbc9692f Fix local manga directories chapters 2024-02-28 16:12:49 +02:00
Koitharu
9f3113363b Merge remote-tracking branch 'weblate/devel' into devel 2024-02-28 14:40:02 +02:00
Koitharu
dba36838d4 Download format preference 2024-02-28 14:28:59 +02:00
Koitharu
f6de1b02d7 Fix download item ui 2024-02-28 14:06:08 +02:00
abc0922001
d6b8e2fd9e Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: abc0922001 <abc0922001@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
a
5227240478 Translated using Weblate (Portuguese)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: a <cooki3yt2004@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Infy's Tagalog Translations
8f65ea6535 Translated using Weblate (Filipino)
Currently translated at 99.8% (596 of 597 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-02-28 12:59:46 +01:00
Kyoya
7d7a6eadd2 Translated using Weblate (Turkish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Kyoya <thelol9181@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Scrambled777
40f1ad3181 Translated using Weblate (Hindi)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Anon
a28c9447d7 Translated using Weblate (Serbian)
Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-28 12:59:46 +01:00
gallegonovato
a84cf97982 Translated using Weblate (Spanish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Lokmane Abdelhakim Djilani
3a8eb58fd1 Translated using Weblate (Arabic)
Currently translated at 58.1% (347 of 597 strings)

Co-authored-by: Lokmane Abdelhakim Djilani <lokdabdo@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
gekka
5d75e9af4a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Oğuz Ersen
d4684e7462 Translated using Weblate (Turkish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Çınar
c0a2f0b533 Translated using Weblate (Turkish)
Currently translated at 100.0% (597 of 597 strings)

Co-authored-by: Çınar <cinardogan110@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-28 12:59:46 +01:00
Макар Разин
40867dd2b6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (597 of 597 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-02-28 12:59:46 +01:00
Koitharu
c3294e6459 Fix double pages mode enabling 2024-02-28 13:58:23 +02:00
Koitharu
5139feb51a Fix pages saving 2024-02-28 13:55:02 +02:00
Koitharu
6b1240fccb Fix crashes 2024-02-24 14:26:31 +02:00
Koitharu
e00a5b7505 Fix open Kitsu auth #773 2024-02-24 13:50:48 +02:00
Koitharu
2c07d2c8e1 Increase Kitsu password max length #774 2024-02-24 13:17:03 +02:00
Koitharu
45c3c05f01 Fix updating history in incognito mode #783 2024-02-24 13:13:35 +02:00
Koitharu
e97a745713 Fix filter ui issue #779 2024-02-24 12:50:59 +02:00
Koitharu
2dc4de0a3c Update dependencies 2024-02-24 12:25:44 +02:00
Koitharu
3cf2c58058 Local manga info dialog 2024-02-24 12:16:26 +02:00
Scrambled777
1e19f32fc5 Translated using Weblate (Hindi)
Currently translated at 22.6% (135 of 596 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
Naga
99e4359523 Translated using Weblate (English)
Currently translated at 100.0% (596 of 596 strings)

Co-authored-by: Naga <yz2000.pro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
Alex Georgiou
04868488cc Translated using Weblate (English)
Currently translated at 100.0% (596 of 596 strings)

Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
Naga
2b3b406b84 Translated using Weblate (French)
Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (French)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (French)

Currently translated at 99.4% (593 of 596 strings)

Co-authored-by: Naga <yz2000.pro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-24 12:14:04 +02:00
Infy's Tagalog Translations
7ab3c75232 Translated using Weblate (Filipino)
Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Infy's Tagalog Translations <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fil/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-24 12:14:04 +02:00
Макар Разин
61f7755465 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (596 of 596 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-02-24 12:14:04 +02:00
GpixeL
9389015ab9 Translated using Weblate (Indonesian)
Currently translated at 97.6% (582 of 596 strings)

Co-authored-by: GpixeL <gamesfire313@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
Сергій
bc56a94aa6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (596 of 596 strings)

Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
Madaraki
7cfcaec6dd Translated using Weblate (Russian)
Currently translated at 100.0% (596 of 596 strings)

Co-authored-by: Madaraki <115705267+Madaraki-chan@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
LaFouine-38
39c7ae31cd Translated using Weblate (French)
Currently translated at 92.6% (552 of 596 strings)

Co-authored-by: LaFouine-38 <thomasjb0208@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
Sergio Varela
9349eccc0c Translated using Weblate (Spanish)
Currently translated at 100.0% (596 of 596 strings)

Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
gekka
8204934359 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (595 of 596 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.4% (592 of 595 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (596 of 596 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (595 of 595 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-24 12:14:04 +02:00
Koitharu
35a2ac4b04 Simple reading stats display 2024-02-21 09:49:47 +02:00
Koitharu
b4d52f1367 Fix track worker notification behavior 2024-02-20 12:46:12 +02:00
jsericksk
81d4a3cf68 Fix grid view in ChaptersSheet 2024-02-19 17:32:02 -03:00
jsericksk
c2e30b3009 Update changes 2024-02-18 23:20:43 -03:00
jsericksk
0c823f1056 Add feature to toggle between list and grid view for chapters 2024-02-18 23:19:35 -03:00
jsericksk
44adbde536 Create chapterGridItemAD and its associated item_chapter_grid 2024-02-18 23:19:35 -03:00
jsericksk
ae0b405ba5 Add grid view option to chapters menu 2024-02-18 23:19:27 -03:00
Zakhar Timoshenko
325a8be484 Fix wrong sources count if NSFW sources are disabled 2024-02-18 23:56:19 +03:00
Koitharu
f39ccb6223 Stats settings 2024-02-18 13:38:46 +02:00
Koitharu
6cb6c891dd Collecting reading stats 2024-02-18 13:11:41 +02:00
Koitharu
8cc04b0f7a Add Remove from history action on details activity 2024-02-18 10:07:42 +02:00
Koitharu
258dbf3dc3 Fix reader info bar text size 2024-02-18 09:26:57 +02:00
Koitharu
e7af4e8450 Fix backup restore dialog layout #770 2024-02-18 09:20:34 +02:00
Koitharu
0c25c61858 Fix filtering pages by branches 2024-02-18 09:14:49 +02:00
Koitharu
abc3e45907 Fix reader state on changed 2024-02-18 09:08:22 +02:00
Koitharu
bd98d8eded Fix onboarding sources selection 2024-02-18 08:44:13 +02:00
Koitharu
2e81f41073 Update dependencies 2024-02-18 08:44:13 +02:00
Oğuz Ersen
5cccebc416 Translated using Weblate (Turkish)
Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-18 08:43:59 +02:00
Макар Разин
c668ffd555 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (591 of 591 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-02-18 08:43:59 +02:00
gekka
a0f77b715f Translated using Weblate (Chinese (Simplified))
Currently translated at 99.6% (589 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (590 of 591 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-18 08:43:59 +02:00
Anonymous
2831843a25 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.8% (590 of 591 strings)

Co-authored-by: Anonymous <noreply@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-18 08:43:59 +02:00
Koitharu
86c1aa11b0 Fix crash 2024-02-17 12:45:02 +02:00
Koitharu
d71514ec7a Configurable dir for silent pages saving 2024-02-17 12:31:12 +02:00
Koitharu
92ed320f57 Add compact navbar option 2024-02-17 10:41:55 +02:00
Koitharu
2de1fe8b77 Fix icon tint 2024-02-16 12:29:52 +02:00
vianh
cebc3cd9e8 Adjust search screen ui 2024-02-16 10:40:23 +02:00
vianh
6c0e2e2b90 Fix mirror domain switching 2024-02-16 10:40:23 +02:00
vianh
b4bd923ce8 Improve webtoon zoom fling 2024-02-16 10:39:17 +02:00
Koitharu
813561fd3b Merge remote-tracking branch 'weblate/devel' into devel 2024-02-16 10:37:43 +02:00
Jordan \"Tes\" Michel
4107336132 Translated using Weblate (French)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (French)

Currently translated at 93.4% (552 of 591 strings)

Co-authored-by: Jordan \"Tes\" Michel <jordan.michel13@yahoo.fr>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-16 10:35:52 +02:00
Anon
30d9d87c17 Translated using Weblate (Serbian)
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Serbian)

Currently translated at 99.6% (589 of 591 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-02-16 10:35:52 +02:00
gekka
c4b5be657d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-16 10:35:51 +02:00
Koitharu
8a763b2b9f Bring back adaptive reader control preference 2024-02-16 10:35:27 +02:00
Koitharu
c783378022 Update app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/edit/FavouritesCategoryEditActivity.kt 2024-02-15 10:26:22 +02:00
Isira Seneviratne
c4355f16e8 Improve serializable extra extension 2024-02-15 10:26:22 +02:00
Isira Seneviratne
522dfc2418 Use SoftwareKeyboardControllerCompat 2024-02-15 10:22:28 +02:00
Koitharu
06d03e3ddd Fix SyncSettings lifecycle 2024-02-14 12:49:46 +02:00
Koitharu
9dc8c7959d Update parsers 2024-02-14 12:19:08 +02:00
Davi Silveira
db219020ca Translated using Weblate (Portuguese)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: Davi Silveira <davilego10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-14 12:00:37 +02:00
Aiman Sara
c04edcb76c Translated using Weblate (Malay)
Currently translated at 53.9% (319 of 591 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Aiman Sara <aimansara21@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ms/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-14 12:00:37 +02:00
Anon
936fc2e4ae Translated using Weblate (Serbian)
Currently translated at 99.6% (589 of 591 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-02-14 12:00:37 +02:00
gekka
cbed866665 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-14 12:00:37 +02:00
Madaraki
ac568b6361 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: Madaraki <115705267+Madaraki-chan@users.noreply.github.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-02-14 12:00:37 +02:00
Oğuz Ersen
84157f988d Translated using Weblate (Turkish)
Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-14 12:00:37 +02:00
gallegonovato
6f6339f0f8 Translated using Weblate (Spanish)
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (589 of 589 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-02-14 12:00:37 +02:00
Jordan \"Tes\" Michel
a7019b9096 Translated using Weblate (French)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (French)

Currently translated at 93.4% (552 of 591 strings)

Co-authored-by: Jordan \"Tes\" Michel <jordan.michel13@yahoo.fr>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-14 10:33:12 +01:00
Davi Silveira
867e3f10ca Translated using Weblate (Portuguese)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: Davi Silveira <davilego10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-14 10:33:12 +01:00
Aiman Sara
fb2cf04d75 Translated using Weblate (Malay)
Currently translated at 53.9% (319 of 591 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Aiman Sara <aimansara21@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ms/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ms/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-14 10:33:12 +01:00
Anon
3ed44ba0d6 Translated using Weblate (Serbian)
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Serbian)

Currently translated at 99.6% (589 of 591 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
2024-02-14 10:33:12 +01:00
gekka
b78104a0f1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-14 10:33:12 +01:00
Madaraki
e4ee93f77c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (591 of 591 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (591 of 591 strings)

Co-authored-by: Madaraki <115705267+Madaraki-chan@users.noreply.github.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-02-14 10:33:11 +01:00
Oğuz Ersen
c6e8da5f23 Translated using Weblate (Turkish)
Currently translated at 100.0% (591 of 591 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (589 of 589 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-02-14 10:33:11 +01:00
Koitharu
bec2195971 Fix processing tap actions in reader when not resumed 2024-02-13 08:03:09 +02:00
Zakhar Timoshenko
722ac4ecc7 Tweak chapter item 2024-02-12 19:02:23 +03:00
Koitharu
516c1c02a6 Improve chapters list ui accessibility #752 2024-02-12 14:26:20 +02:00
Koitharu
0cb7e71781 Fix chapters selection decoration 2024-02-12 09:07:32 +02:00
Koitharu
36a74f32df Show hint if navigation section is unavailable #751 2024-02-10 16:52:49 +02:00
Koitharu
0e4ef32642 Show battery percentage in reader bar 2024-02-10 16:33:18 +02:00
Koitharu
3125cac4c8 Use constant textSize for reader info bar #748 2024-02-10 15:16:08 +02:00
Koitharu
5d9016d1bc Do not query suggestions by blacklisted tags #749 2024-02-10 14:59:25 +02:00
Koitharu
c5eeb89d10 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (589 of 589 strings)

Co-authored-by: Koitharu <nvasya95@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-02-10 14:43:16 +02:00
Davi Silveira
4f8f43cab1 Translated using Weblate (Portuguese)
Currently translated at 100.0% (586 of 586 strings)

Co-authored-by: Davi Silveira <davilego10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
2024-02-10 14:43:16 +02:00
Diegofmar
4cbff308ce Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (586 of 586 strings)

Co-authored-by: Diegofmar <diego2771@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-02-10 14:43:16 +02:00
Макар Разин
d786ab7deb Translated using Weblate (Polish)
Currently translated at 83.7% (491 of 586 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (586 of 586 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-02-10 14:43:16 +02:00
Mirkó Attila
c823d402ff Translated using Weblate (Hungarian)
Currently translated at 11.2% (66 of 586 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (9 of 9 strings)

Added translation using Weblate (Hungarian)

Co-authored-by: Mirkó Attila <kisatti007hun@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/hu/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hu/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-10 14:43:16 +02:00
gekka
12e68db41f Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (586 of 586 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (586 of 586 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-10 14:43:16 +02:00
Madaraki
96717321d2 Translated using Weblate (Ukrainian)
Currently translated at 99.8% (585 of 586 strings)

Co-authored-by: Madaraki <115705267+Madaraki-chan@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2024-02-10 14:43:16 +02:00
Oğuz Ersen
044b5590ef Translated using Weblate (Turkish)
Currently translated at 100.0% (586 of 586 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-10 14:43:16 +02:00
Eduardo Malaspina
00112ebb44 Translated using Weblate (Spanish)
Currently translated at 100.0% (586 of 586 strings)

Co-authored-by: Eduardo Malaspina <vaio0@swismail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-02-10 14:43:16 +02:00
Koitharu
00dde80fdf Update parsers 2024-02-10 14:42:12 +02:00
Koitharu
f1dfc4ebd6 Fix strings 2024-02-10 14:33:57 +02:00
Koitharu
5426edd83a Option to disable reading time estimator 2024-02-10 14:10:38 +02:00
Koitharu
2a500eb2cb Improve background works notifications 2024-02-10 13:54:36 +02:00
Koitharu
2310ed06c1 Translated using Weblate (Russian)
Currently translated at 99.8% (585 of 586 strings)

Co-authored-by: Koitharu <nvasya95@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
gallegonovato
68ed7a09d6 Translated using Weblate (Spanish)
Currently translated at 100.0% (584 of 584 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
Scrambled777
6cdb56e740 Translated using Weblate (Hindi)
Currently translated at 22.9% (134 of 583 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Scrambled777 <weblate.scrambled777@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/hi/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/hi/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-07 14:01:25 +02:00
大王叫我来巡山
35fb78c924 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
Davi Silveira
8c3b5d7f53 Translated using Weblate (Portuguese)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (582 of 583 strings)

Co-authored-by: Davi Silveira <davilego10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-07 14:01:25 +02:00
Макар Разин
af6592a8df Translated using Weblate (Belarusian)
Currently translated at 99.8% (582 of 583 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
Deivinni Silva
9efb82d887 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (583 of 583 strings)

Translated using Weblate (English)

Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
gekka
f8722ddc73 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (583 of 583 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
Nayuki
656ac97153 Translated using Weblate (Thai)
Currently translated at 69.8% (407 of 583 strings)

Co-authored-by: Nayuki <me@nayuki.cyou>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/th/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
LL Magical
4fc23f8f54 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.8% (582 of 583 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (582 of 583 strings)

Co-authored-by: LL Magical <lolayami2004@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-02-07 14:01:25 +02:00
Joshua “Josh”
e1f325993f Translated using Weblate (German)
Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: Joshua “Josh” <22joshua.mueller22@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/de/
Translation: Kotatsu/Strings
2024-02-07 14:01:25 +02:00
Koitharu
4c3938a1fd Reader fullscreen option 2024-02-07 13:53:45 +02:00
Koitharu
530dfa8cde Fix workers scheduling 2024-02-07 13:19:51 +02:00
Koitharu
58d1c3de26 Default webtoon zoom out option 2024-02-07 12:55:51 +02:00
Koitharu
ba2ed6a2ef Webtoon reader improvements 2024-02-07 10:25:23 +02:00
Koitharu
2d909854fb Update parsers 2024-02-07 09:40:30 +02:00
Koitharu
cba694bedd Merge branch 'master' into devel 2024-02-07 09:08:09 +02:00
Koitharu
e5cf1be91a Fix build signature validation 2024-02-05 12:54:14 +02:00
Koitharu
72a1dd8227 Increase versionCode 2024-02-03 16:58:24 +02:00
Koitharu
8558b00dca Fix chapter number for saved chapters 2024-02-03 16:53:26 +02:00
Koitharu
8e9175d5f0 Update parsers 2024-02-03 16:52:15 +02:00
Koitharu
eae40d9b90 Double reader fixes 2024-02-03 16:52:15 +02:00
Koitharu
2d61209696 Double page reader for reversed mode 2024-02-03 16:52:15 +02:00
Koitharu
d24754f2a0 Handle MAL errors in html 2024-02-03 16:52:15 +02:00
Koitharu
54ef02ad88 Fix downloading 2024-02-03 16:52:14 +02:00
gekka
e2a82920b6 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-02-03 16:37:45 +02:00
ALi.w
d494030d50 Translated using Weblate (Persian)
Currently translated at 44.5% (260 of 583 strings)

Co-authored-by: ALi.w <aminnimaj@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
2024-02-03 16:37:45 +02:00
Anon
73369f9a6d Translated using Weblate (Serbian)
Currently translated at 99.6% (581 of 583 strings)

Translated using Weblate (English)

Currently translated at 99.8% (582 of 583 strings)

Translated using Weblate (Serbian)

Currently translated at 99.3% (579 of 583 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Anon <anonymousprivate76@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/sr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/en/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/sr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-02-03 16:37:45 +02:00
Madaraki
cc1da6e8da Translated using Weblate (Ukrainian)
Currently translated at 100.0% (583 of 583 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: Madaraki <115705267+Madaraki-chan@users.noreply.github.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-02-03 16:37:45 +02:00
Oğuz Ersen
668a5bd040 Translated using Weblate (Turkish)
Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2024-02-03 16:37:45 +02:00
Koitharu
8efa8bc0d2 Handle MAL errors in html 2024-02-01 10:31:23 +02:00
Koitharu
6e6c70a770 Fix downloading 2024-02-01 10:12:20 +02:00
Sup Kelelawar
413605b520 Translated using Weblate (Indonesian)
Currently translated at 100.0% (583 of 583 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (583 of 583 strings)

Co-authored-by: Sup Kelelawar <apkfile007@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
Anna
bdf23a0d62 Translated using Weblate (German)
Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Anna <scheffler.anna@gmx.net>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/de/
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
Eji-san
4c5d26d4b4 Translated using Weblate (Indonesian)
Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Eji-san <ejierubani@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-01-31 16:37:37 +02:00
Wrld-Lain
3b7ad7f28d Translated using Weblate (Spanish)
Currently translated at 99.8% (578 of 579 strings)

Co-authored-by: Wrld-Lain <anzonx@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
gekka
331af45a29 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (583 of 583 strings)

Added translation using Weblate (Chinese (Literary))

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (579 of 579 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (579 of 579 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
Макар Разин
d349bd30c9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (579 of 579 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (579 of 579 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-01-31 16:37:37 +02:00
ZerOriSama
3349e3abc5 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (579 of 579 strings)

Co-authored-by: ZerOriSama <godarms2010@live.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
gekka
4aa31ead67 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (579 of 579 strings)

Co-authored-by: gekka <1778962971@qq.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
Clxff H3r4ld0
113da3b6c1 Translated using Weblate (Indonesian)
Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.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-01-31 16:37:37 +02:00
何意挽秋風
8b027e2f45 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (565 of 565 strings)

Added translation using Weblate (Chinese (Traditional))

Co-authored-by: 何意挽秋風 <9120518@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
FateXBlood
9c462b1a3a Translated using Weblate (Nepali)
Currently translated at 41.5% (235 of 565 strings)

Translated using Weblate (Nepali)

Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ne/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ne/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
Madaraki
a5bc8c1e9e Translated using Weblate (Ukrainian)
Currently translated at 98.9% (573 of 579 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (577 of 579 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (564 of 564 strings)

Co-authored-by: Madaraki <115705267+Madaraki-chan@users.noreply.github.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-01-31 16:37:37 +02:00
Deivinni Silva
ebb77c68cc Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (564 of 564 strings)

Co-authored-by: Deivinni Silva <deivinnimds3656@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
Madaraki
74ddf86ebe Translated using Weblate (Russian)
Currently translated at 100.0% (563 of 563 strings)

Co-authored-by: Madaraki <Madaraki@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
nobSEIFO
12d2fdaf3e Translated using Weblate (Arabic)
Currently translated at 59.6% (335 of 562 strings)

Co-authored-by: nobSEIFO <BEZAI2002@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ar/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
D
8cfc97c795 Translated using Weblate (Italian)
Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (562 of 562 strings)

Co-authored-by: D <dogger56@hotmail.it>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/it/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/it/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
Сергій
3855ca802e Translated using Weblate (Ukrainian)
Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (560 of 560 strings)

Co-authored-by: Сергій <sergiy.goncharuk.1@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/uk/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
Макар Разин
9db427275f Translated using Weblate (Belarusian)
Currently translated at 100.0% (579 of 579 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.2% (556 of 560 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (560 of 560 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/ru/
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
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
yearn
3a38644089 Added translation using Weblate (Chinese (Literary))
Co-authored-by: yearn <13676164923@163.com>
2024-01-31 16:37:37 +02:00
Anibal Dams
60a34ec092 Translated using Weblate (Spanish)
Currently translated at 100.0% (9 of 9 strings)

Co-authored-by: Anibal Dams <elnini120@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
shehuaizong
acd79f12e3 Added translation using Weblate (Franco-Provençal)
Co-authored-by: shehuaizong <1511985899@qq.com>
2024-01-31 16:37:37 +02:00
Infy's Tagalog Translations
461d7ed578 Translated using Weblate (Filipino)
Currently translated at 100.0% (558 of 558 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-01-31 16:37:37 +02:00
Alex Georgiou
5374ac390c Translated using Weblate (Greek)
Currently translated at 100.0% (557 of 557 strings)

Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/el/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
plum7x
913a67a652 Translated using Weblate (Chinese (Traditional))
Currently translated at 94.0% (524 of 557 strings)

Co-authored-by: plum7x <plumgift@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hant/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
大王叫我来巡山
e7a920e43a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (562 of 562 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (560 of 560 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (558 of 558 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (557 of 557 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
scaledzdn
9668b3ef5f Translated using Weblate (Indonesian)
Currently translated at 99.6% (555 of 557 strings)

Co-authored-by: scaledzdn <zaidanmovic0512@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
Ridhoardhiansyah7
9581f937de Translated using Weblate (Indonesian)
Currently translated at 99.6% (555 of 557 strings)

Co-authored-by: Ridhoardhiansyah7 <Zxx97607@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/id/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
Platiplus
44ef6f6dbf Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.4% (554 of 557 strings)

Co-authored-by: Platiplus <quazarweb@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/pt_BR/
Translation: Kotatsu/Strings
2024-01-31 16:37:37 +02:00
Oğuz Ersen
af11697133 Translated using Weblate (Turkish)
Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (579 of 579 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (562 of 562 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (557 of 557 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/tr/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
gallegonovato
09ff356790 Translated using Weblate (Spanish)
Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (560 of 560 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (9 of 9 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (558 of 558 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (557 of 557 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/es/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
ALi.w
92ea50d6b6 Translated using Weblate (Persian)
Currently translated at 41.9% (235 of 560 strings)

Translated using Weblate (Persian)

Currently translated at 41.9% (234 of 558 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (7 of 7 strings)

Translated using Weblate (Persian)

Currently translated at 40.0% (223 of 557 strings)

Added translation using Weblate (Persian)

Co-authored-by: ALi.w <aminnimaj@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/plurals/fa/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fa/
Translation: Kotatsu/Strings
Translation: Kotatsu/plurals
2024-01-31 16:37:37 +02:00
Koitharu
077107e9a7 Merge branch 'master' into devel 2024-01-31 16:30:30 +02:00
Koitharu
ae57561591 Update parsers 2024-01-31 16:16:04 +02:00
Zakhar Timoshenko
2379efc191 TLS 1.3 support for Android < 10
(cherry picked from commit 889b799d8d)
2024-01-31 15:59:04 +02:00
Koitharu
edca0e5334 Partially migrate to new collections 2024-01-31 15:55:33 +02:00
Koitharu
a4e2675d61 Update dependencies 2024-01-31 15:23:52 +02:00
Koitharu
892f95a7a6 Fix scrobbling chapter index 2024-01-31 13:56:48 +02:00
Koitharu
95aaa967a8 Scrobbling using Kitsu #360 2024-01-31 13:29:18 +02:00
Koitharu
5687ca6e96 Kitsu auth implementation 2024-01-30 15:41:03 +02:00
Koitharu
d0ee185d2e Merge branch 'feature/kitsu' of github.com:KotatsuApp/Kotatsu into devel 2024-01-30 13:59:34 +02:00
Koitharu
21a3ac0902 Use crop parameter for wsrv.nl #721 2024-01-30 13:33:59 +02:00
Koitharu
1382ab7933 Improve reader actions editor 2024-01-30 09:43:52 +02:00
Koitharu
aabdd281f3 Fix imports 2024-01-30 09:14:25 +02:00
Koitharu
131a0ffcaa Double reader integration 2024-01-29 18:57:18 +02:00
Koitharu
4194609929 Editor actions fixes 2024-01-29 11:54:21 +02:00
Zakhar Timoshenko
889b799d8d TLS 1.3 support for Android < 10 2024-01-28 17:58:38 +03:00
Koitharu
6f7f3dc5e2 Configurable reader tap actions 2024-01-27 18:06:51 +02:00
Koitharu
72187e7da0 Double reader integration 2024-01-27 12:25:39 +02:00
Koitharu
f881cc439a Draft double reader implementation 2024-01-27 12:25:34 +02:00
Zakhar Timoshenko
ccdebf6789 Initial support of long press in reader 2024-01-27 13:19:22 +03:00
Koitharu
4db61d3c04 Merge branch 'master' into devel 2024-01-24 12:32:04 +02:00
Koitharu
e7c9d1943d Disable autofill for protect password fields #702 2024-01-24 11:56:04 +02:00
Koitharu
b1240e7efa Last read order in favorites #705 2024-01-24 11:53:53 +02:00
Koitharu
a0a72b1192 Fix webtoon scroll dispatching 2024-01-24 11:36:48 +02:00
Koitharu
5d9a59d577 Update parsers and migrate to float chapter numbers 2024-01-22 17:11:00 +02:00
Koitharu
83cb35fe6e Fix pages numbers 2024-01-22 13:13:14 +02:00
Koitharu
0fff53ae47 Readers refactor 2024-01-22 13:00:56 +02:00
Koitharu
a95017a5f0 Update reader mode icons 2024-01-22 12:02:25 +02:00
Koitharu
9251823d9a Vertical reader mode 2024-01-21 11:37:22 +02:00
Koitharu
ce8f87272b Refactor ReadActivity menus 2024-01-21 11:02:53 +02:00
Koitharu
db1ddf539c Incognito mode indicator in reader 2024-01-21 10:10:20 +02:00
Koitharu
d56fc674ab Fix favorites counters 2024-01-20 15:54:15 +02:00
Isira Seneviratne
c9fcc0f0f8 Improve signature check 2024-01-20 09:23:33 +02:00
Koitharu
da2ad40adf Update parsers 2024-01-20 09:19:29 +02:00
Koitharu
af5716a8ce Fix import dialog injection 2024-01-20 09:16:06 +02:00
Koitharu
a98202e15e Update supported links domains 2024-01-20 09:08:26 +02:00
Koitharu
d6887e2d75 Skip unsupported sources in global search 2024-01-20 09:07:27 +02:00
Koitharu
ba6afd44dd Fix reading time format 2024-01-19 16:37:25 +02:00
Koitharu
0b55c4d037 Add volume headers to chapters list 2024-01-19 16:36:36 +02:00
Koitharu
2a5300a634 Show all favorites on categories screen 2024-01-18 16:16:12 +02:00
Koitharu
59bfa929fd Current branch indicator 2024-01-18 14:54:30 +02:00
Koitharu
c5d88f8700 Refactor reading time estimator 2024-01-18 13:38:25 +02:00
Koitharu
a1120ea709 Fix pages list scrolling 2024-01-18 12:19:34 +02:00
Koitharu
796af6b811 Refactor chapters description 2024-01-18 10:58:11 +02:00
Zakhar Timoshenko
eafd878413 Approximate reading time preview 2024-01-17 22:22:37 +03:00
Zakhar Timoshenko
9baf2bfcd9 Update Material lib 2024-01-17 22:20:13 +03:00
Zakhar Timoshenko
0b4dd5beef Enhance chapter items 2024-01-17 16:09:55 +03:00
Zakhar Timoshenko
fd01367601 Move Kitsu scrobbler to kotlin dir 2023-06-05 17:33:12 +03:00
Zakhar Timoshenko
cb64740349 Merge branch 'devel' into feature/kitsu
# Conflicts:
#	app/src/main/res/values/strings.xml
2023-06-05 17:21:56 +03:00
Zakhar Timoshenko
6fdcaf0d02 Merge branch 'devel' into feature/kitsu
# Conflicts:
#	app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt
#	app/src/main/res/values/strings.xml
2023-05-29 17:29:05 +03:00
Koitharu
56de725cf1 Open Kitsu auth activity 2023-05-06 18:15:24 +03:00
Koitharu
7a2ad47405 Merge branch 'devel' into feature/kitsu 2023-05-06 18:07:02 +03:00
Zakhar Timoshenko
41551451b0 Part 1 2023-05-06 16:15:17 +03:00
Zakhar Timoshenko
d5c24cd5c8 Initial adding of Kitsu scrobbler 2023-05-03 13:54:19 +03:00
466 changed files with 15233 additions and 4218 deletions

3
.idea/gradle.xml generated
View File

@@ -4,9 +4,8 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

53
LICENSE
View File

@@ -619,56 +619,3 @@ Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -19,7 +19,7 @@ Kotatsu is a free and open source manga reader for Android.
* Tablet-optimized Material You UI
* Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
* Password/fingerprint protect access to the app
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices

View File

@@ -16,8 +16,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 34
versionCode = 616
versionName = '6.6.6'
versionCode = 633
versionName = '6.8.3'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -82,18 +82,19 @@ afterEvaluate {
}
dependencies {
//noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:7c871edbca') {
implementation('com.github.KotatsuApp:kotatsu-parsers:44ea9fe709') {
exclude group: 'org.json', module: 'json'
}
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.22'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.9.23'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.activity:activity-ktx:1.8.2'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.collection:collection-ktx:1.4.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-service:2.7.0'
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
@@ -103,8 +104,9 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.android.material:material:1.12.0-beta01'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.7.0'
implementation 'androidx.webkit:webkit:1.10.0'
implementation 'androidx.work:work-runtime:2.9.0'
//noinspection GradleDependency
@@ -120,18 +122,18 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0'
implementation 'com.squareup.okio:okio:3.7.0'
implementation 'com.squareup.okio:okio:3.9.0'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.2'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.3.2'
implementation 'com.google.dagger:hilt-android:2.50'
kapt 'com.google.dagger:hilt-compiler:2.50'
implementation 'androidx.hilt:hilt-work:1.1.0'
kapt 'androidx.hilt:hilt-compiler:1.1.0'
implementation 'com.google.dagger:hilt-android:2.51.1'
kapt 'com.google.dagger:hilt-compiler:2.51.1'
implementation 'androidx.hilt:hilt-work:1.2.0'
kapt 'androidx.hilt:hilt-compiler:1.2.0'
implementation 'io.coil-kt:coil-base:2.5.0'
implementation 'io.coil-kt:coil-svg:2.5.0'
implementation 'io.coil-kt:coil-base:2.6.0'
implementation 'io.coil-kt:coil-svg:2.6.0'
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:02e6d6cfe9'
implementation 'com.github.solkin:disk-lru-cache:1.4'
implementation 'io.noties.markwon:core:4.6.2'
@@ -141,22 +143,24 @@ dependencies {
compileOnly 'com.google.auto.service:auto-service-annotations:1.1.1'
ksp 'dev.zacsweers.autoservice:auto-service-ksp:1.1.0'
implementation 'org.conscrypt:conscrypt-android:2.5.2'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20231013'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation 'org.json:json:20240303'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.5'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0'
androidTestImplementation 'androidx.room:room-testing:2.6.1'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.1'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.50'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.50'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.51.1'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.51.1'
}

View File

@@ -57,6 +57,7 @@ class AppShortcutManagerTest {
page = 4,
scroll = 2,
percent = 0.3f,
force = false,
)
awaitUpdate()

View File

@@ -7,7 +7,9 @@ import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -61,6 +63,7 @@ class AppBackupAgentTest {
page = 3,
scroll = 40,
percent = 0.2f,
force = false,
)
val history = checkNotNull(historyRepository.getOne(SampleData.manga))

View File

@@ -10,6 +10,8 @@ class CurlLoggingInterceptor(
private val curlOptions: String? = null
) : Interceptor {
private val escapeRegex = Regex("([\\[\\]\"])")
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
var isCompressed = false
@@ -40,7 +42,7 @@ class CurlLoggingInterceptor(
if (isCompressed) {
curlCmd.append(" --compressed")
}
curlCmd.append(" \"").append(request.url).append('"')
curlCmd.append(" \"").append(request.url.toString().escape()).append('"')
log("---cURL (" + request.url + ")")
log(curlCmd.toString())
@@ -48,7 +50,12 @@ class CurlLoggingInterceptor(
return chain.proceed(request)
}
private fun String.escape() = replace("\"", "\\\"")
private fun String.escape() = replace(escapeRegex) { match ->
"\\" + match.value
}
// .replace("\"", "\\\"")
// .replace("[", "\\[")
// .replace("]", "\\]")
private fun log(msg: String) {
Log.d("CURL", msg)

View File

@@ -94,6 +94,34 @@
<data android:host="kotatsu.app" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.details.ui.DetailsActivity2"
android:exported="true">
<intent-filter>
<action android:name="${applicationId}.action.VIEW_MANGA" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="kotatsu.app" />
<data android:path="/manga" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kotatsu" />
<data android:host="manga" />
<data android:host="kotatsu.app" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
android:exported="true">
@@ -145,6 +173,9 @@
<data android:host="sync-settings" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.settings.reader.ReaderTapGridConfigActivity"
android:label="@string/reader_actions" />
<activity
android:name="org.koitharu.kotatsu.settings.storage.directories.MangaDirectoriesActivity"
android:label="@string/local_manga_directories" />
@@ -218,12 +249,30 @@
<data android:host="shikimori-auth" />
<data android:host="anilist-auth" />
<data android:host="mal-auth" />
<data android:host="kitsu-auth" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.settings.sources.catalog.SourcesCatalogActivity"
android:label="@string/sources_catalog" />
<activity
android:name="org.koitharu.kotatsu.scrobbling.kitsu.ui.KitsuAuthActivity"
android:exported="false"
android:label="@string/kitsu"
tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="kotatsu+kitsu" />
</intent-filter>
</activity>
<activity
android:name="org.koitharu.kotatsu.stats.ui.StatsActivity"
android:label="@string/reading_stats" />
<activity
android:name="org.koitharu.kotatsu.alternatives.ui.AlternativesActivity"
android:label="@string/alternatives" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"

View File

@@ -0,0 +1,85 @@
package org.koitharu.kotatsu.alternatives.domain
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.util.ext.almostEquals
import org.koitharu.kotatsu.explore.data.MangaSourcesRepository
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaListFilter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
private const val MAX_PARALLELISM = 4
private const val MATCH_THRESHOLD = 0.2f
class AlternativesUseCase @Inject constructor(
private val sourcesRepository: MangaSourcesRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) {
suspend operator fun invoke(manga: Manga): Flow<Manga> {
val sources = getSources(manga.source)
if (sources.isEmpty()) {
return emptyFlow()
}
val semaphore = Semaphore(MAX_PARALLELISM)
return channelFlow {
for (source in sources) {
val repository = mangaRepositoryFactory.create(source)
if (!repository.isSearchSupported) {
continue
}
launch {
val list = runCatchingCancellable {
semaphore.withPermit {
repository.getList(offset = 0, filter = MangaListFilter.Search(manga.title))
}
}.getOrDefault(emptyList())
for (item in list) {
if (item.matches(manga)) {
send(item)
}
}
}
}
}.map {
runCatchingCancellable {
mangaRepositoryFactory.create(it.source).getDetails(it)
}.getOrDefault(it)
}
}
private suspend fun getSources(ref: MangaSource): List<MangaSource> {
val result = ArrayList<MangaSource>(MangaSource.entries.size - 2)
result.addAll(sourcesRepository.getEnabledSources())
result.sortByDescending { it.priority(ref) }
result.addAll(sourcesRepository.getDisabledSources().sortedByDescending { it.priority(ref) })
return result
}
private fun Manga.matches(ref: Manga): Boolean {
return matchesTitles(title, ref.title) ||
matchesTitles(title, ref.altTitle) ||
matchesTitles(altTitle, ref.title) ||
matchesTitles(altTitle, ref.altTitle)
}
private fun matchesTitles(a: String?, b: String?): Boolean {
return !a.isNullOrEmpty() && !b.isNullOrEmpty() && a.almostEquals(b, MATCH_THRESHOLD)
}
private fun MangaSource.priority(ref: MangaSource): Int {
var res = 0
if (locale == ref.locale) res += 2
if (contentType == ref.contentType) res++
return res
}
}

View File

@@ -0,0 +1,129 @@
package org.koitharu.kotatsu.alternatives.domain
import androidx.room.withTransaction
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.history.data.toMangaHistory
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
class MigrateUseCase @Inject constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository,
private val database: MangaDatabase,
private val progressUpdateUseCase: ProgressUpdateUseCase,
) {
suspend operator fun invoke(oldManga: Manga, newManga: Manga) {
val oldDetails = if (oldManga.chapters.isNullOrEmpty()) {
runCatchingCancellable {
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
}.getOrDefault(oldManga)
} else {
oldManga
}
val newDetails = if (newManga.chapters.isNullOrEmpty()) {
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} else {
newManga
}
mangaDataRepository.storeManga(newDetails)
database.withTransaction {
// replace favorites
val favoritesDao = database.getFavouritesDao()
val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
if (oldFavourites.isNotEmpty()) {
favoritesDao.delete(oldManga.id)
for (f in oldFavourites) {
val e = f.copy(
mangaId = newManga.id,
)
favoritesDao.upsert(e)
}
}
// replace history
val historyDao = database.getHistoryDao()
val oldHistory = historyDao.find(oldDetails.id)
if (oldHistory != null) {
val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
historyDao.delete(oldDetails.id)
historyDao.upsert(newHistory)
}
}
progressUpdateUseCase(newManga)
}
private fun makeNewHistory(
oldManga: Manga,
newManga: Manga,
history: HistoryEntity,
): HistoryEntity {
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
val branch = newManga.getPreferredBranch(null)
val chapters = checkNotNull(newManga.getChapters(branch))
val currentChapter = if (history.percent in 0f..1f) {
chapters[(chapters.lastIndex * history.percent).toInt()]
} else {
chapters.first()
}
return HistoryEntity(
mangaId = newManga.id,
createdAt = history.createdAt,
updatedAt = System.currentTimeMillis(),
chapterId = currentChapter.id,
page = history.page,
scroll = history.scroll,
percent = history.percent,
deletedAt = 0,
chaptersCount = chapters.size,
)
}
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
val oldChapters = checkNotNull(oldManga.getChapters(branch))
var index = oldChapters.indexOfFirst { it.id == history.chapterId }
if (index < 0) {
index = if (history.percent in 0f..1f) {
(oldChapters.lastIndex * history.percent).toInt()
} else {
0
}
}
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
val newBranch = if (newChapters.containsKey(branch)) {
branch
} else {
newManga.getPreferredBranch(null)
}
val newChapterId = checkNotNull(newChapters[newBranch]).let {
val oldChapter = oldChapters[index]
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
}.id
return HistoryEntity(
mangaId = newManga.id,
createdAt = history.createdAt,
updatedAt = System.currentTimeMillis(),
chapterId = newChapterId,
page = history.page,
scroll = history.scroll,
percent = PROGRESS_NONE,
deletedAt = 0,
chaptersCount = checkNotNull(newChapters[newBranch]).size,
)
}
private fun List<MangaChapter>.findByNumber(volume: Int, number: Float): MangaChapter? {
return if (number <= 0f) {
null
} else {
firstOrNull { it.volume == volume && it.number == number }
}
}
}

View File

@@ -0,0 +1,92 @@
package org.koitharu.kotatsu.alternatives.ui
import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import coil.request.ImageRequest
import coil.transform.CircleCropTransformation
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaAlternativeBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.math.sign
import com.google.android.material.R as materialR
fun alternativeAD(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: OnListItemClickListener<MangaAlternativeModel>,
) = adapterDelegateViewBinding<MangaAlternativeModel, ListModel, ItemMangaAlternativeBinding>(
{ inflater, parent -> ItemMangaAlternativeBinding.inflate(inflater, parent, false) },
) {
val colorGreen = ContextCompat.getColor(context, R.color.common_green)
val colorRed = ContextCompat.getColor(context, R.color.common_red)
val clickListener = AdapterDelegateClickListenerAdapter(this, listener)
itemView.setOnClickListener(clickListener)
binding.buttonMigrate.setOnClickListener(clickListener)
binding.chipSource.setOnClickListener(clickListener)
bind { payloads ->
binding.textViewTitle.text = item.manga.title
binding.textViewSubtitle.text = buildSpannedString {
if (item.chaptersCount > 0) {
append(context.resources.getQuantityString(R.plurals.chapters, item.chaptersCount, item.chaptersCount))
} else {
append(context.getString(R.string.no_chapters))
}
when (item.chaptersDiff.sign) {
-1 -> inSpans(ForegroundColorSpan(colorRed)) {
append("")
append(item.chaptersDiff.toString())
}
1 -> inSpans(ForegroundColorSpan(colorGreen)) {
append(" ▲ +")
append(item.chaptersDiff.toString())
}
}
}
binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
binding.chipSource.also { chip ->
chip.text = item.manga.source.title
ImageRequest.Builder(context)
.data(item.manga.source.faviconUri())
.lifecycle(lifecycleOwner)
.crossfade(false)
.size(context.resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size))
.target(ChipIconTarget(chip))
.placeholder(R.drawable.ic_web)
.fallback(R.drawable.ic_web)
.error(R.drawable.ic_web)
.source(item.manga.source)
.transformations(CircleCropTransformation())
.allowRgb565(true)
.enqueueWith(coil)
}
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover))
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
transformations(TrimTransformation())
allowRgb565(true)
tag(item.manga)
source(item.manga.source)
enqueueWith(coil)
}
}
}

View File

@@ -0,0 +1,114 @@
package org.koitharu.kotatsu.alternatives.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import coil.ImageLoader
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
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.DIALOG_THEME_CENTERED
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.ActivityAlternativesBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.search.ui.SearchActivity
import javax.inject.Inject
@AndroidEntryPoint
class AlternativesActivity : BaseActivity<ActivityAlternativesBinding>(),
OnListItemClickListener<MangaAlternativeModel> {
@Inject
lateinit var coil: ImageLoader
private val viewModel by viewModels<AlternativesViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityAlternativesBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
subtitle = viewModel.manga.title
}
val listAdapter = BaseListAdapter<ListModel>()
.addDelegate(ListItemType.MANGA_LIST_DETAILED, alternativeAD(coil, this, this))
.addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, this, null))
.addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
.addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
with(viewBinding.recyclerView) {
setHasFixedSize(true)
addItemDecoration(TypedListSpacingDecoration(context, addHorizontalPadding = false))
adapter = listAdapter
}
viewModel.onError.observeEvent(this, SnackbarErrorObserver(viewBinding.recyclerView, null))
viewModel.content.observe(this, listAdapter)
viewModel.onMigrated.observeEvent(this) {
Toast.makeText(this, R.string.migration_completed, Toast.LENGTH_SHORT).show()
startActivity(DetailsActivity.newIntent(this, it))
finishAfterTransition()
}
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
)
viewBinding.recyclerView.updatePadding(
bottom = insets.bottom + viewBinding.recyclerView.paddingTop,
)
}
override fun onItemClick(item: MangaAlternativeModel, view: View) {
when (view.id) {
R.id.chip_source -> startActivity(SearchActivity.newIntent(this, item.manga.source, viewModel.manga.title))
R.id.button_migrate -> confirmMigration(item.manga)
else -> startActivity(DetailsActivity.newIntent(this, item.manga))
}
}
private fun confirmMigration(target: Manga) {
MaterialAlertDialogBuilder(this, DIALOG_THEME_CENTERED)
.setIcon(R.drawable.ic_replace)
.setTitle(R.string.manga_migration)
.setMessage(
getString(
R.string.migrate_confirmation,
viewModel.manga.title,
viewModel.manga.source.title,
target.title,
target.source.title,
),
)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.migrate) { _, _ ->
viewModel.migrate(target)
}.show()
}
companion object {
fun newIntent(context: Context, manga: Manga) = Intent(context, AlternativesActivity::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
}
}

View File

@@ -0,0 +1,98 @@
package org.koitharu.kotatsu.alternatives.ui
import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.runningFold
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.domain.AlternativesUseCase
import org.koitharu.kotatsu.alternatives.domain.MigrateUseCase
import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
@HiltViewModel
class AlternativesViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val alternativesUseCase: AlternativesUseCase,
private val migrateUseCase: MigrateUseCase,
private val extraProvider: ListExtraProvider,
) : BaseViewModel() {
val manga = savedStateHandle.require<ParcelableManga>(MangaIntent.KEY_MANGA).manga
val onMigrated = MutableEventFlow<Manga>()
val content = MutableStateFlow<List<ListModel>>(listOf(LoadingState))
private var migrationJob: Job? = null
init {
launchJob(Dispatchers.Default) {
val ref = runCatchingCancellable {
mangaRepositoryFactory.create(manga.source).getDetails(manga)
}.getOrDefault(manga)
val refCount = ref.chaptersCount()
alternativesUseCase(ref)
.map {
MangaAlternativeModel(
manga = it,
progress = extraProvider.getProgress(it.id),
referenceChapters = refCount,
)
}.runningFold<MangaAlternativeModel, List<ListModel>>(listOf(LoadingState)) { acc, item ->
acc.filterIsInstance<MangaAlternativeModel>() + item + LoadingFooter()
}.onEmpty {
emit(
listOf(
EmptyState(
icon = R.drawable.ic_empty_common,
textPrimary = R.string.nothing_found,
textSecondary = R.string.text_search_holder_secondary,
actionStringRes = 0,
),
),
)
}.collect {
content.value = it
}
content.value = content.value.filterNot { it is LoadingFooter }
}
}
fun migrate(target: Manga) {
if (migrationJob?.isActive == true) {
return
}
migrationJob = launchLoadingJob(Dispatchers.Default) {
migrateUseCase(manga, target)
onMigrated.call(target)
}
}
private suspend fun mapList(list: List<Manga>, refCount: Int): List<MangaAlternativeModel> {
return list.map {
MangaAlternativeModel(
manga = it,
progress = extraProvider.getProgress(it.id),
referenceChapters = refCount,
)
}
}
}

View File

@@ -0,0 +1,21 @@
package org.koitharu.kotatsu.alternatives.ui
import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
data class MangaAlternativeModel(
val manga: Manga,
val progress: Float,
private val referenceChapters: Int,
) : ListModel {
val chaptersCount = manga.chaptersCount()
val chaptersDiff: Int
get() = if (referenceChapters == 0 || chaptersCount == 0) 0 else chaptersCount - referenceChapters
override fun areItemsTheSame(other: ListModel): Boolean {
return other is MangaAlternativeModel && other.manga.id == manga.id
}
}

View File

@@ -25,6 +25,7 @@ import org.koitharu.kotatsu.core.ui.list.ListSelectionController
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.databinding.FragmentListSimpleBinding
@@ -71,7 +72,7 @@ class BookmarksFragment :
) {
super.onViewBindingCreated(binding, savedInstanceState)
selectionController = ListSelectionController(
activity = requireActivity(),
appCompatDelegate = checkNotNull(findAppCompatDelegate()),
decoration = BookmarksSelectionDecoration(binding.root.context),
registryOwner = this,
callback = this,
@@ -100,7 +101,7 @@ class BookmarksFragment :
}
viewModel.onError.observeEvent(
viewLifecycleOwner,
SnackbarErrorObserver(binding.recyclerView, this)
SnackbarErrorObserver(binding.recyclerView, this),
)
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(binding.recyclerView))
}
@@ -206,10 +207,11 @@ class BookmarksFragment :
companion object {
@Deprecated(
"", ReplaceWith(
"",
ReplaceWith(
"BookmarksFragment()",
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment"
)
"org.koitharu.kotatsu.bookmarks.ui.BookmarksFragment",
),
)
fun newInstance() = BookmarksFragment()
}

View File

@@ -13,7 +13,6 @@ import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
class BookmarksAdapter(
@@ -32,13 +31,6 @@ class BookmarksAdapter(
}
override fun getSectionText(context: Context, position: Int): CharSequence? {
val list = items
for (i in (0..position).reversed()) {
val item = list.getOrNull(i) ?: continue
if (item is ListHeader) {
return item.getText(context)
}
}
return null
return findHeader(position)?.getText(context)
}
}

View File

@@ -37,6 +37,7 @@ import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import javax.inject.Inject
import kotlin.math.roundToInt
@Deprecated("")
@AndroidEntryPoint
class BookmarksSheet :
BaseAdaptiveSheet<SheetPagesBinding>(),

View File

@@ -21,6 +21,7 @@ import org.koitharu.kotatsu.list.ui.model.LoadingFooter
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import javax.inject.Inject
@Deprecated("")
@HiltViewModel
class BookmarksSheetViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.browser
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@@ -12,31 +11,42 @@ import android.webkit.CookieManager
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import dagger.hilt.android.AndroidEntryPoint
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.util.ext.catchingWebViewUnavailability
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.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.model.MangaSource
import javax.inject.Inject
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled")
@AndroidEntryPoint
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
private lateinit var onBackPressedCallback: WebViewBackPressedCallback
@Inject
lateinit var mangaRepositoryFactory: MangaRepository.Factory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
with(viewBinding.webView.settings) {
javaScriptEnabled = true
userAgentString = UserAgents.CHROME_MOBILE
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)
viewBinding.webView.webViewClient = BrowserClient(this)
viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar)
@@ -57,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 {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_browser, menu)
@@ -81,11 +81,14 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
R.id.action_browser -> {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(viewBinding.webView.url)
try {
startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) {
val url = viewBinding.webView.url?.toUriOrNull()
if (url != null) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = url
try {
startActivity(Intent.createChooser(intent, item.title))
} catch (_: ActivityNotFoundException) {
}
}
true
}
@@ -136,11 +139,13 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
companion object {
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)
.setData(Uri.parse(url))
.putExtra(EXTRA_TITLE, title)
.putExtra(EXTRA_SOURCE, source)
}
}
}

View File

@@ -20,7 +20,7 @@ class CaptchaNotifier(
) : EventListener {
fun notify(exception: CloudFlareProtectedException) {
if (!context.checkNotificationPermission()) {
if (!context.checkNotificationPermission(CHANNEL_ID)) {
return
}
val manager = NotificationManagerCompat.from(context)

View File

@@ -27,9 +27,8 @@ import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.network.UserAgents
import javax.inject.Inject
import com.google.android.material.R as materialR
@@ -41,17 +40,12 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
@Inject
lateinit var cookieJar: MutableCookieJar
private lateinit var cfClient: CloudFlareClient
private var onBackPressedCallback: WebViewBackPressedCallback? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!catchingWebViewUnavailability {
setContentView(
ActivityBrowserBinding.inflate(
layoutInflater,
),
)
}) {
if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) {
return
}
supportActionBar?.run {
@@ -59,13 +53,9 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
}
val url = intent?.dataString.orEmpty()
with(viewBinding.webView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
userAgentString = intent?.getStringExtra(ARG_UA) ?: UserAgents.CHROME_MOBILE
}
viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
cfClient = CloudFlareClient(cookieJar, this, url)
viewBinding.webView.configureForParser(intent?.getStringExtra(ARG_UA))
viewBinding.webView.webViewClient = cfClient
onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
onBackPressedDispatcher.addCallback(it)
}
@@ -91,16 +81,6 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
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 {
menuInflater.inflate(R.menu.opt_captcha, menu)
return super.onCreateOptionsMenu(menu)
@@ -125,15 +105,7 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
}
R.id.action_retry -> {
lifecycleScope.launch {
viewBinding.webView.stopLoading()
yield()
val targetUrl = intent?.dataString?.toHttpUrlOrNull()
if (targetUrl != null) {
clearCfCookies(targetUrl)
viewBinding.webView.loadUrl(targetUrl.toString())
}
}
restartCheck()
true
}
@@ -159,6 +131,10 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
viewBinding.progressBar.isInvisible = true
}
override fun onLoopDetected() {
restartCheck()
}
override fun onCheckPassed() {
pendingResult = RESULT_OK
finishAfterTransition()
@@ -178,10 +154,23 @@ class CloudFlareActivity : BaseActivity<ActivityBrowserBinding>(), CloudFlareCal
subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
}
private fun restartCheck() {
lifecycleScope.launch {
viewBinding.webView.stopLoading()
yield()
cfClient.reset()
val targetUrl = intent?.dataString?.toHttpUrlOrNull()
if (targetUrl != null) {
clearCfCookies(targetUrl)
viewBinding.webView.loadUrl(targetUrl.toString())
}
}
}
private suspend fun clearCfCookies(url: HttpUrl) = runInterruptible(Dispatchers.Default) {
cookieJar.removeCookies(url) { cookie ->
val name = cookie.name
name.startsWith("cf_") || name.startsWith("_cf") || name.startsWith("__cf")
name.startsWith("cf_") || name.startsWith("_cf") || name.startsWith("__cf") || name == "csrftoken"
}
}

View File

@@ -11,4 +11,6 @@ interface CloudFlareCallback : BrowserCallback {
fun onPageLoaded()
fun onCheckPassed()
fun onLoopDetected()
}

View File

@@ -7,6 +7,7 @@ import org.koitharu.kotatsu.browser.BrowserClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
private const val CF_CLEARANCE = "cf_clearance"
private const val LOOP_COUNTER = 3
class CloudFlareClient(
private val cookieJar: MutableCookieJar,
@@ -15,6 +16,7 @@ class CloudFlareClient(
) : BrowserClient(callback) {
private val oldClearance = getClearance()
private var counter = 0
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
@@ -31,10 +33,20 @@ class CloudFlareClient(
callback.onPageLoaded()
}
fun reset() {
counter = 0
}
private fun checkClearance() {
val clearance = getClearance()
if (clearance != null && clearance != oldClearance) {
callback.onCheckPassed()
} else {
counter++
if (counter >= LOOP_COUNTER) {
reset()
callback.onLoopDetected()
}
}
}

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core
import android.app.Application
import android.content.Context
import android.os.Build
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate
import androidx.hilt.work.HiltWorkerFactory
@@ -19,6 +20,7 @@ import org.acra.config.httpSender
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -27,6 +29,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.util.WorkServiceStopHelper
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import org.koitharu.kotatsu.settings.work.WorkScheduleManager
import java.security.Security
import javax.inject.Inject
import javax.inject.Provider
@@ -52,7 +55,7 @@ open class BaseApp : Application(), Configuration.Provider {
lateinit var appValidator: AppValidator
@Inject
lateinit var workScheduleManager: Provider<WorkScheduleManager>
lateinit var workScheduleManager: WorkScheduleManager
@Inject
lateinit var workManagerProvider: Provider<WorkManager>
@@ -66,6 +69,10 @@ open class BaseApp : Application(), Configuration.Provider {
super.onCreate()
AppCompatDelegate.setDefaultNightMode(settings.theme)
AppCompatDelegate.setApplicationLocales(settings.appLocales)
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
setupActivityLifecycleCallbacks()
processLifecycleScope.launch {
val isOriginalApp = withContext(Dispatchers.Default) {
@@ -76,7 +83,7 @@ open class BaseApp : Application(), Configuration.Provider {
processLifecycleScope.launch(Dispatchers.Default) {
setupDatabaseObservers()
}
workScheduleManager.get().init()
workScheduleManager.init()
WorkServiceStopHelper(workManagerProvider).setup()
}

View File

@@ -6,12 +6,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import okio.Closeable
import org.json.JSONArray
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
import java.io.File
import java.util.EnumSet
import java.util.zip.ZipException
import java.util.zip.ZipFile
class BackupZipInput(val file: File) : Closeable {
class BackupZipInput private constructor(val file: File) : Closeable {
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

@@ -54,6 +54,7 @@ class JsonDeserializer(private val json: JSONObject) {
page = json.getInt("page"),
scroll = json.getDouble("scroll").toFloat(),
percent = json.getFloatOrDefault("percent", -1f),
chaptersCount = json.getIntOrDefault("chapters", -1),
deletedAt = 0L,
)

View File

@@ -41,6 +41,7 @@ class JsonSerializer private constructor(private val json: JSONObject) {
put("page", e.page)
put("scroll", e.scroll)
put("percent", e.percent)
put("chapters", e.chaptersCount)
},
)

View File

@@ -20,6 +20,8 @@ interface ContentCache {
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

@@ -7,10 +7,12 @@ class ExpiringLruCache<T>(
val maxSize: Int,
private val lifetime: Long,
private val timeUnit: TimeUnit,
) {
) : Iterable<ContentCache.Key> {
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
override fun iterator(): Iterator<ContentCache.Key> = cache.snapshot().keys.iterator()
operator fun get(key: ContentCache.Key): T? {
val value = cache[key] ?: return null
if (value.isExpired) {
@@ -30,4 +32,8 @@ class ExpiringLruCache<T>(
fun trimToSize(size: Int) {
cache.trimToSize(size)
}
fun remove(key: ContentCache.Key) {
cache.remove(key)
}
}

View File

@@ -44,6 +44,12 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
relatedMangaCache[ContentCache.Key(source, url)] = related
}
override fun clear(source: MangaSource) {
clearCache(detailsCache, source)
clearCache(pagesCache, source)
clearCache(relatedMangaCache, source)
}
override fun onConfigurationChanged(newConfig: Configuration) = Unit
override fun onLowMemory() = Unit
@@ -67,4 +73,12 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
else -> cache.trimToSize(cache.maxSize / 2)
}
}
private fun clearCache(cache: ExpiringLruCache<*>, source: MangaSource) {
cache.forEach { key ->
if (key.source == source) {
cache.remove(key)
}
}
}
}

View File

@@ -19,4 +19,6 @@ class StubContentCache : ContentCache {
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

@@ -30,6 +30,8 @@ import org.koitharu.kotatsu.core.db.migrations.Migration14To15
import org.koitharu.kotatsu.core.db.migrations.Migration15To16
import org.koitharu.kotatsu.core.db.migrations.Migration16To17
import org.koitharu.kotatsu.core.db.migrations.Migration17To18
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.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
@@ -48,20 +50,22 @@ import org.koitharu.kotatsu.history.data.HistoryDao
import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingDao
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
import org.koitharu.kotatsu.stats.data.StatsDao
import org.koitharu.kotatsu.stats.data.StatsEntity
import org.koitharu.kotatsu.suggestions.data.SuggestionDao
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 18
const val DATABASE_VERSION = 20
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class,
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class,
ScrobblingEntity::class, MangaSourceEntity::class, StatsEntity::class,
],
version = DATABASE_VERSION,
)
@@ -90,6 +94,8 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun getScrobblingDao(): ScrobblingDao
abstract fun getSourcesDao(): MangaSourcesDao
abstract fun getStatsDao(): StatsDao
}
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
@@ -110,6 +116,8 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration15To16(),
Migration16To17(context),
Migration17To18(),
Migration18To19(),
Migration19To20(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -29,6 +29,9 @@ abstract class MangaSourcesDao {
@Query("SELECT * FROM sources ORDER BY sort_key")
abstract fun observeAll(): Flow<List<MangaSourceEntity>>
@Query("SELECT enabled FROM sources WHERE source = :source")
abstract fun observeIsEnabled(source: String): Flow<Boolean>
@Query("SELECT IFNULL(MAX(sort_key),0) FROM sources")
abstract suspend fun getMaxSortKey(): Int

View File

@@ -1,6 +1,10 @@
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 org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TrackLogWithManga
@@ -24,6 +28,9 @@ interface TrackLogsDao {
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
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")
suspend fun count(): Int
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration18To19 : Migration(18, 19) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE history ADD COLUMN `chapters` INTEGER NOT NULL DEFAULT -1")
db.execSQL("CREATE TABLE IF NOT EXISTS `stats` (`manga_id` INTEGER NOT NULL, `started_at` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `pages` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `started_at`), FOREIGN KEY(`manga_id`) REFERENCES `history`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )")
}
}

View File

@@ -0,0 +1,16 @@
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")
}
}

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

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.core.exceptions
import org.koitharu.kotatsu.parsers.model.Manga
class UnsupportedSourceException(
message: String?,
val manga: Manga?,
) : IllegalArgumentException(message)

View File

@@ -21,7 +21,7 @@ abstract class ErrorObserver(
private val onResolved: Consumer<Boolean>?,
) : FlowCollector<Throwable> {
protected val activity = host.context.findActivity()
protected open val activity = host.context.findActivity()
private val lifecycleScope: LifecycleCoroutineScope
get() = checkNotNull(fragment?.viewLifecycleScope ?: (activity as? LifecycleOwner)?.lifecycle?.coroutineScope)
@@ -36,7 +36,7 @@ abstract class ErrorObserver(
private fun isAlive(): Boolean {
return when {
fragment != null -> fragment.view != null
activity != null -> !activity.isDestroyed
activity != null -> activity?.isDestroyed == false
else -> true
}
}

View File

@@ -8,13 +8,16 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import okhttp3.Headers
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.TaggedActivityResult
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
import kotlin.coroutines.Continuation
@@ -59,6 +62,11 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
false
}
is UnsupportedSourceException -> {
e.manga?.let { openAlternatives(it) }
false
}
else -> false
}
@@ -74,7 +82,12 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
private fun openInBrowser(url: String) {
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) {
val context = activity ?: fragment?.activity ?: return
context.startActivity(AlternativesActivity.newIntent(context, manga))
}
private fun getFragmentManager() = checkNotNull(fragment?.childFragmentManager ?: activity?.supportFragmentManager)
@@ -86,6 +99,7 @@ class ExceptionResolver : ActivityResultCallback<TaggedActivityResult> {
is CloudFlareProtectedException -> R.string.captcha_solve
is AuthRequiredException -> R.string.sign_in
is NotFoundException -> if (e.url.isNotEmpty()) R.string.open_in_browser else 0
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
else -> 0
}

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.core.model
import android.net.Uri
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.collection.MutableObjectIntMap
import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.iterator
@@ -13,6 +14,8 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.util.mapToSet
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import com.google.android.material.R as materialR
@JvmName("mangaIds")
@@ -29,12 +32,14 @@ fun Collection<ChapterListItem>.countChaptersByBranch(): Int {
if (size <= 1) {
return size
}
val acc = HashMap<String?, Int>()
val acc = MutableObjectIntMap<String?>()
for (item in this) {
val branch = item.chapter.branch
acc[branch] = (acc[branch] ?: 0) + 1
acc[branch] = acc.getOrDefault(branch, 0) + 1
}
return acc.values.max()
var max = 0
acc.forEachValue { x -> if (x > max) max = x }
return max
}
@get:StringRes
@@ -113,3 +118,32 @@ val Manga.appUrl: Uri
.appendQueryParameter("name", title)
.appendQueryParameter("url", url)
.build()
private val chaptersNumberFormat = DecimalFormat("#.#").also { f ->
f.decimalFormatSymbols = DecimalFormatSymbols.getInstance().also {
it.decimalSeparator = '.'
}
}
fun MangaChapter.formatNumber(): String? {
if (number <= 0f) {
return null
}
return chaptersNumberFormat.format(number.toDouble())
}
fun Manga.chaptersCount(): Int {
if (chapters.isNullOrEmpty()) {
return 0
}
val counters = MutableObjectIntMap<String?>()
var max = 0
chapters?.forEach { x ->
val c = counters.getOrDefault(x.branch, 0) + 1
counters[x.branch] = c
if (max < c) {
max = c
}
}
return max
}

View File

@@ -19,7 +19,8 @@ data class ParcelableChapter(
MangaChapter(
id = parcel.readLong(),
name = parcel.readString().orEmpty(),
number = parcel.readInt(),
number = parcel.readFloat(),
volume = parcel.readInt(),
url = parcel.readString().orEmpty(),
scanlator = parcel.readString(),
uploadDate = parcel.readLong(),
@@ -31,7 +32,8 @@ data class ParcelableChapter(
override fun ParcelableChapter.write(parcel: Parcel, flags: Int) = with(chapter) {
parcel.writeLong(id)
parcel.writeString(name)
parcel.writeInt(number)
parcel.writeFloat(number)
parcel.writeInt(volume)
parcel.writeString(url)
parcel.writeString(scanlator)
parcel.writeLong(uploadDate)

View File

@@ -7,11 +7,11 @@ import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaLoaderContextImpl
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.mergeWith
import java.net.IDN
import javax.inject.Inject
@@ -20,6 +20,7 @@ import javax.inject.Singleton
@Singleton
class CommonHeadersInterceptor @Inject constructor(
private val mangaRepositoryFactoryLazy: Lazy<MangaRepository.Factory>,
private val mangaLoaderContextLazy: Lazy<MangaLoaderContextImpl>,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
@@ -38,7 +39,7 @@ class CommonHeadersInterceptor @Inject constructor(
headersBuilder.mergeWith(it, replaceExisting = false)
}
if (headersBuilder[CommonHeaders.USER_AGENT] == null) {
headersBuilder[CommonHeaders.USER_AGENT] = UserAgents.CHROME_MOBILE
headersBuilder[CommonHeaders.USER_AGENT] = mangaLoaderContextLazy.get().getDefaultUserAgent()
}
if (headersBuilder[CommonHeaders.REFERER] == null && repository != null) {
val idn = IDN.toASCII(repository.domain)

View File

@@ -7,6 +7,7 @@ import coil.request.ErrorResult
import coil.request.ImageResult
import coil.request.SuccessResult
import coil.size.Dimension
import coil.size.isOriginal
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
@@ -46,11 +47,13 @@ class ImageProxyInterceptor @Inject constructor(
.scheme("https")
.host("wsrv.nl")
.addQueryParameter("url", url.toString())
.addQueryParameter("fit", "outside")
.addQueryParameter("we", null)
val size = request.sizeResolver.size()
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
if (!size.isOriginal) {
newUrl.addQueryParameter("crop", "cover")
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
}
val newRequest = request.newBuilder()
.data(newUrl.build())

View File

@@ -1,16 +1,9 @@
package org.koitharu.kotatsu.core.os
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.pm.PackageInfoCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.parsers.util.byte2HexFormatted
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
@@ -18,29 +11,13 @@ import javax.inject.Singleton
class AppValidator @Inject constructor(
@ApplicationContext private val context: Context,
) {
@Suppress("NewApi")
val isOriginalApp by lazy {
getCertificateSHA1Fingerprint() == CERT_SHA1
val certificates = mapOf(CERT_SHA256.hexToByteArray() to PackageManager.CERT_INPUT_SHA256)
PackageInfoCompat.hasSignatures(context.packageManager, context.packageName, certificates, false)
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(): String? = runCatching {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
val signatures = requireNotNull(packageInfo?.signatures)
val cert: ByteArray = signatures.first().toByteArray()
val input: InputStream = ByteArrayInputStream(cert)
val cf = CertificateFactory.getInstance("X509")
val c = cf.generateCertificate(input) as X509Certificate
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
return publicKey.byte2HexFormatted()
}.onFailure { error ->
error.printStackTraceDebug()
}.getOrNull()
private companion object {
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
private const val CERT_SHA256 = "67e15100bb809301783edcb6348fa3bbf83034d91e62868a91053dbd70db3f18"
}
}

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.parser
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey
@@ -18,24 +19,20 @@ import java.util.EnumSet
class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("")
get() = ConfigKey.Domain("localhost")
override val availableSortOrders: Set<SortOrder>
get() = EnumSet.allOf(SortOrder::class.java)
override suspend fun getDetails(manga: Manga): Manga {
TODO("Not yet implemented")
}
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
TODO("Not yet implemented")
}
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = stub(null)
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
TODO("Not yet implemented")
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
override suspend fun getAvailableTags(): Set<MangaTag> {
TODO("Not yet implemented")
override suspend fun getAvailableTags(): Set<MangaTag> = stub(null)
private fun stub(manga: Manga?): Nothing {
throw UnsupportedSourceException("Usage of Dummy parser", manga)
}
}

View File

@@ -4,18 +4,25 @@ import android.annotation.SuppressLint
import android.content.Context
import android.util.Base64
import android.webkit.WebView
import androidx.annotation.MainThread
import androidx.core.os.LocaleListCompat
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import org.koitharu.kotatsu.core.network.MangaHttpClient
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.core.util.ext.configureForParser
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.core.util.ext.toList
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.network.UserAgents
import org.koitharu.kotatsu.parsers.util.SuspendLazy
import java.lang.ref.WeakReference
import java.util.Locale
import javax.inject.Inject
@@ -32,12 +39,15 @@ class MangaLoaderContextImpl @Inject constructor(
private var webViewCached: WeakReference<WebView>? = null
private val userAgentLazy = SuspendLazy {
withContext(Dispatchers.Main) {
obtainWebView().settings.userAgentString
}.sanitizeHeaderValue()
}
@SuppressLint("SetJavaScriptEnabled")
override suspend fun evaluateJs(script: String): String? = withContext(Dispatchers.Main) {
val webView = webViewCached?.get() ?: WebView(androidContext).also {
it.settings.javaScriptEnabled = true
webViewCached = WeakReference(it)
}
val webView = obtainWebView()
suspendCoroutine { cont ->
webView.evaluateJavascript(script) { result ->
cont.resume(result?.takeUnless { it == "null" })
@@ -45,6 +55,14 @@ class MangaLoaderContextImpl @Inject constructor(
}
}
override fun getDefaultUserAgent(): String = runCatching {
runBlocking {
userAgentLazy.get()
}
}.onFailure { e ->
e.printStackTraceDebug()
}.getOrDefault(UserAgents.FIREFOX_MOBILE)
override fun getConfig(source: MangaSource): MangaSourceConfig {
return SourceSettings(androidContext, source)
}
@@ -60,4 +78,12 @@ class MangaLoaderContextImpl @Inject constructor(
override fun getPreferredLocales(): List<Locale> {
return LocaleListCompat.getAdjustedDefault().toList()
}
@MainThread
private fun obtainWebView(): WebView {
return webViewCached?.get() ?: WebView(androidContext).also {
it.configureForParser(null)
webViewCached = WeakReference(it)
}
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.core.parser
import android.util.Log
import androidx.collection.MutableLongSet
import coil.request.CachePolicy
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -170,7 +171,11 @@ class RemoteMangaRepository(
return getConfig().isSlowdownEnabled
}
private fun getConfig() = parser.config as SourceSettings
fun invalidateCache() {
cache.clear(source)
}
fun getConfig() = parser.config as SourceSettings
private suspend fun <T> asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred<T> {
var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
@@ -189,7 +194,7 @@ class RemoteMangaRepository(
return emptyList()
}
val result = ArrayList<MangaPage>(size)
val set = HashSet<Long>(size)
val set = MutableLongSet(size)
for (page in this) {
if (set.add(page.id)) {
result.add(page)
@@ -226,6 +231,5 @@ class RemoteMangaRepository(
}
}
private fun Result<*>.isValidResult() = exceptionOrNull() !is ParseException
&& (getOrNull() as? Collection<*>)?.isEmpty() != true
private fun Result<*>.isValidResult() = isSuccess && (getOrNull() as? Collection<*>)?.isEmpty() != true
}

View File

@@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.ArraySet
import androidx.core.content.edit
import androidx.core.os.LocaleListCompat
import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager
import dagger.hilt.android.qualifiers.ApplicationContext
import org.json.JSONArray
@@ -70,10 +71,17 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
val isNavLabelsVisible: Boolean
get() = prefs.getBoolean(KEY_NAV_LABELS, true)
var gridSize: Int
get() = prefs.getInt(KEY_GRID_SIZE, 100)
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
get() = prefs.getEnumValue(KEY_LIST_MODE_HISTORY, listMode)
set(value) = prefs.edit { putEnumValue(KEY_LIST_MODE_HISTORY, value) }
@@ -101,14 +109,21 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
}
}
val readerPageSwitch: Set<String>
get() = prefs.getStringSet(KEY_READER_SWITCHERS, null) ?: setOf(PAGE_SWITCH_TAPS)
var isReaderDoubleOnLandscape: Boolean
get() = prefs.getBoolean(KEY_READER_DOUBLE_PAGES, false)
set(value) = prefs.edit { putBoolean(KEY_READER_DOUBLE_PAGES, value) }
val isReaderVolumeButtonsEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_VOLUME_BUTTONS, false)
val isReaderZoomButtonsEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_ZOOM_BUTTONS, false)
val isReaderTapsAdaptive: Boolean
get() = !prefs.getBoolean(KEY_READER_TAPS_LTR, false)
val isReaderControlAlwaysLTR: Boolean
get() = prefs.getBoolean(KEY_READER_CONTROL_LTR, false)
val isReaderFullscreenEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_FULLSCREEN, true)
val isReaderOptimizationEnabled: Boolean
get() = prefs.getBoolean(KEY_READER_OPTIMIZE, false)
@@ -167,10 +182,14 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_INCOGNITO_MODE, false)
set(value) = prefs.edit { putBoolean(KEY_INCOGNITO_MODE, value) }
var chaptersReverse: Boolean
var isChaptersReverse: Boolean
get() = prefs.getBoolean(KEY_REVERSE_CHAPTERS, false)
set(value) = prefs.edit { putBoolean(KEY_REVERSE_CHAPTERS, value) }
var isChaptersGridView: Boolean
get() = prefs.getBoolean(KEY_GRID_VIEW_CHAPTERS, false)
set(value) = prefs.edit { putBoolean(KEY_GRID_VIEW_CHAPTERS, value) }
val zoomMode: ZoomMode
get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER)
@@ -180,11 +199,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
var appPassword: String?
get() = prefs.getString(KEY_APP_PASSWORD, null)
set(value) = prefs.edit {
if (value != null) putString(KEY_APP_PASSWORD, value) else remove(
KEY_APP_PASSWORD,
)
if (value != null) putString(KEY_APP_PASSWORD, value) else remove(KEY_APP_PASSWORD)
}
var isAppPasswordNumeric: Boolean
get() = prefs.getBoolean(KEY_APP_PASSWORD_NUMERIC, false)
set(value) = prefs.edit { putBoolean(KEY_APP_PASSWORD_NUMERIC, value) }
val isLoggingEnabled: Boolean
get() = prefs.getBoolean(KEY_LOGGING_ENABLED, false)
@@ -204,8 +225,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isUnstableUpdatesAllowed: Boolean
get() = prefs.getBoolean(KEY_UPDATES_UNSTABLE, false)
val isPagesTabEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_TAB, true)
val defaultDetailsTab: Int
get() = prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull()?.coerceIn(0, 1) ?: 0
get() = if (isPagesTabEnabled) {
prefs.getString(KEY_DETAILS_TAB, null)?.toIntOrNull()?.coerceIn(0, 1) ?: 0
} else {
0
}
val isContentPrefetchEnabled: Boolean
get() {
@@ -266,6 +294,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isDownloadsWiFiOnly: Boolean
get() = prefs.getBoolean(KEY_DOWNLOADS_WIFI, false)
val preferredDownloadFormat: DownloadFormat
get() = prefs.getEnumValue(KEY_DOWNLOADS_FORMAT, DownloadFormat.AUTOMATIC)
var isSuggestionsEnabled: Boolean
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
set(value) = prefs.edit { putBoolean(KEY_SUGGESTIONS, value) }
@@ -360,6 +391,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
val isWebtoonZoomEnable: Boolean
get() = prefs.getBoolean(KEY_WEBTOON_ZOOM, true)
@get:FloatRange(from = 0.0, to = 0.5)
val defaultWebtoonZoomOut: Float
get() = prefs.getInt(KEY_WEBTOON_ZOOM_OUT, 0).coerceIn(0, 50) / 100f
@get:FloatRange(from = 0.0, to = 1.0)
var readerAutoscrollSpeed: Float
get() = prefs.getFloat(KEY_READER_AUTOSCROLL_SPEED, 0f)
@@ -395,6 +430,18 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
val isReadingTimeEstimationEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_TIME, true)
val isPagesSavingAskEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_SAVE_ASK, true)
val isStatsEnabled: Boolean
get() = prefs.getBoolean(KEY_STATS_ENABLED, false)
val isAutoLocalChaptersCleanupEnabled: Boolean
get() = prefs.getBoolean(KEY_CHAPTERS_CLEAR_AUTO, false)
fun isTipEnabled(tip: String): Boolean {
return prefs.getStringSet(KEY_TIPS_CLOSED, emptySet())?.contains(tip) != true
}
@@ -407,6 +454,15 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
prefs.edit { putStringSet(KEY_TIPS_CLOSED, closedTips + tip) }
}
fun getPagesSaveDir(context: Context): DocumentFile? =
prefs.getString(KEY_PAGES_SAVE_DIR, null)?.toUriOrNull()?.let {
DocumentFile.fromTreeUri(context, it)?.takeIf { it.canWrite() }
}
fun setPagesSaveDir(uri: Uri?) {
prefs.edit { putString(KEY_PAGES_SAVE_DIR, uri?.toString()) }
}
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}
@@ -453,7 +509,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
companion object {
const val PAGE_SWITCH_TAPS = "taps"
const val PAGE_SWITCH_VOLUME_KEYS = "volume"
const val TRACK_HISTORY = "history"
@@ -470,14 +525,20 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear"
const val KEY_COOKIES_CLEAR = "cookies_clear"
const val KEY_CHAPTERS_CLEAR = "chapters_clear"
const val KEY_CHAPTERS_CLEAR_AUTO = "chapters_clear_auto"
const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear"
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_LOCAL_STORAGE = "local_storage"
const val KEY_READER_SWITCHERS = "reader_switchers"
const val KEY_READER_DOUBLE_PAGES = "reader_double_pages"
const val KEY_READER_ZOOM_BUTTONS = "reader_zoom_buttons"
const val KEY_READER_CONTROL_LTR = "reader_taps_ltr"
const val KEY_READER_FULLSCREEN = "reader_fullscreen"
const val KEY_READER_VOLUME_BUTTONS = "reader_volume_buttons"
const val KEY_TRACKER_ENABLED = "tracker_enabled"
const val KEY_TRACKER_WIFI_ONLY = "tracker_wifi"
const val KEY_TRACK_SOURCES = "track_sources"
@@ -493,6 +554,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_MODE = "reader_mode"
const val KEY_READER_MODE_DETECT = "reader_mode_detect"
const val KEY_APP_PASSWORD = "app_password"
const val KEY_APP_PASSWORD_NUMERIC = "app_password_num"
const val KEY_PROTECT_APP = "protect_app"
const val KEY_PROTECT_APP_BIOMETRIC = "protect_app_bio"
const val KEY_APP_VERSION = "app_version"
@@ -506,6 +568,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_READING_INDICATORS = "reading_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
const val KEY_PAGES_NUMBERS = "pages_numbers"
const val KEY_SCREENSHOTS_POLICY = "screenshots_policy"
@@ -518,7 +581,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_SHIKIMORI = "shikimori"
const val KEY_ANILIST = "anilist"
const val KEY_MAL = "mal"
const val KEY_KITSU = "kitsu"
const val KEY_DOWNLOADS_WIFI = "downloads_wifi"
const val KEY_DOWNLOADS_FORMAT = "downloads_format"
const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible"
const val KEY_DOH = "doh"
const val KEY_EXIT_CONFIRM = "exit_confirm"
@@ -530,12 +595,13 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_READER_BACKGROUND = "reader_background"
const val KEY_READER_SCREEN_ON = "reader_screen_on"
const val KEY_SHORTCUTS = "dynamic_shortcuts"
const val KEY_READER_TAPS_LTR = "reader_taps_ltr"
const val KEY_READER_TAP_ACTIONS = "reader_tap_actions"
const val KEY_READER_OPTIMIZE = "reader_optimize"
const val KEY_LOCAL_LIST_ORDER = "local_order"
const val KEY_HISTORY_ORDER = "history_order"
const val KEY_FAVORITES_ORDER = "fav_order"
const val KEY_WEBTOON_ZOOM = "webtoon_zoom"
const val KEY_WEBTOON_ZOOM_OUT = "webtoon_zoom_out"
const val KEY_PREFETCH_CONTENT = "prefetch_content"
const val KEY_APP_LOCALE = "app_locale"
const val KEY_LOGGING_ENABLED = "logging"
@@ -559,6 +625,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_DISABLE_NSFW = "no_nsfw"
const val KEY_RELATED_MANGA = "related_manga"
const val KEY_NAV_MAIN = "nav_main"
const val KEY_NAV_LABELS = "nav_labels"
const val KEY_32BIT_COLOR = "enhanced_colors"
const val KEY_SOURCES_ORDER = "sources_sort_order"
const val KEY_SOURCES_CATALOG = "sources_catalog"
@@ -567,9 +634,12 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_CF_INVERTED = "cf_inverted"
const val KEY_CF_GRAYSCALE = "cf_grayscale"
const val KEY_IGNORE_DOZE = "ignore_dose"
const val KEY_PAGES_TAB = "pages_tab"
const val KEY_DETAILS_TAB = "details_tab"
// About
const val KEY_READING_TIME = "reading_time"
const val KEY_PAGES_SAVE_DIR = "pages_dir"
const val KEY_PAGES_SAVE_ASK = "pages_dir_ask"
const val KEY_STATS_ENABLED = "stats_on"
const val KEY_APP_UPDATE = "app_update"
const val KEY_APP_TRANSLATION = "about_app_translation"
}

View File

@@ -0,0 +1,8 @@
package org.koitharu.kotatsu.core.prefs
enum class DownloadFormat {
AUTOMATIC,
SINGLE_CBZ,
MULTIPLE_CBZ,
}

View File

@@ -4,13 +4,12 @@ import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.list.ui.model.ListModel
enum class NavItem(
@IdRes val id: Int,
@StringRes val title: Int,
@DrawableRes val icon: Int,
) : ListModel {
) {
HISTORY(R.id.nav_history, R.string.history, R.drawable.ic_history_selector),
FAVORITES(R.id.nav_favorites, R.string.favourites, R.drawable.ic_favourites_selector),
@@ -21,10 +20,6 @@ enum class NavItem(
BOOKMARKS(R.id.nav_bookmarks, R.string.bookmarks, R.drawable.ic_bookmark_selector),
;
override fun areItemsTheSame(other: ListModel): Boolean {
return other is NavItem && ordinal == other.ordinal
}
fun isAvailable(settings: AppSettings): Boolean = when (this) {
SUGGESTIONS -> settings.isSuggestionsEnabled
FEED -> settings.isTrackerEnabled

View File

@@ -4,7 +4,9 @@ enum class ReaderMode(val id: Int) {
STANDARD(1),
REVERSED(3),
WEBTOON(2);
VERTICAL(4),
WEBTOON(2),
;
companion object {

View File

@@ -1,18 +1,18 @@
package org.koitharu.kotatsu.core.prefs
import android.content.Context
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import androidx.core.content.edit
import okhttp3.internal.isSensitiveHeader
import org.koitharu.kotatsu.core.util.ext.getEnumValue
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.putEnumValue
import org.koitharu.kotatsu.core.util.ext.sanitizeHeaderValue
import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.SortOrder
private const val KEY_SORT_ORDER = "sort_order"
private const val KEY_SLOWDOWN = "slowdown"
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
private val prefs = context.getSharedPreferences(source.name, Context.MODE_PRIVATE)
@@ -27,9 +27,13 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
@Suppress("UNCHECKED_CAST")
override fun <T> get(key: ConfigKey<T>): T {
return when (key) {
is ConfigKey.UserAgent -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
is ConfigKey.UserAgent -> prefs.getString(key.key, key.defaultValue)
.ifNullOrEmpty { key.defaultValue }
.sanitizeHeaderValue()
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
} as T
}
@@ -37,7 +41,22 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
when (key) {
is ConfigKey.Domain -> putString(key.key, value as String?)
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
is ConfigKey.UserAgent -> putString(key.key, value as String?)
is ConfigKey.UserAgent -> putString(key.key, (value as String?)?.sanitizeHeaderValue())
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean)
}
}
fun subscribe(listener: OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}
fun unsubscribe(listener: OnSharedPreferenceChangeListener) {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
companion object {
const val KEY_SORT_ORDER = "sort_order"
const val KEY_SLOWDOWN = "slowdown"
}
}

View File

@@ -8,6 +8,7 @@ import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
@@ -29,6 +30,7 @@ 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.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.isWebViewUnavailable
@Suppress("LeakingThis")
abstract class BaseActivity<B : ViewBinding> :
@@ -58,11 +60,11 @@ abstract class BaseActivity<B : ViewBinding> :
if (isAmoledTheme) {
setTheme(R.style.ThemeOverlay_Kotatsu_Amoled)
}
putDataToExtras(intent)
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
insetsDelegate.handleImeInsets = true
insetsDelegate.addInsetsListener(this)
putDataToExtras(intent)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
@@ -125,11 +127,13 @@ abstract class BaseActivity<B : ViewBinding> :
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),
getThemeColor(com.google.android.material.R.attr.colorSurface),
)
} else {
ContextCompat.getColor(this, R.color.kotatsu_m3_background)
ContextCompat.getColor(this, R.color.kotatsu_background)
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar).apply {
@@ -138,8 +142,6 @@ abstract class BaseActivity<B : ViewBinding> :
topMargin = insets.top
}
}
defaultStatusBarColor = window.statusBarColor
window.statusBarColor = actionModeColor
}
@CallSuper
@@ -164,6 +166,21 @@ abstract class BaseActivity<B : ViewBinding> :
intent?.putExtra(EXTRA_DATA, intent.data)
}
protected fun setContentViewWebViewSafe(viewBindingProducer: () -> B): Boolean {
return try {
setContentView(viewBindingProducer())
true
} catch (e: Exception) {
if (e.isWebViewUnavailable()) {
Toast.makeText(this, R.string.web_view_unavailable, Toast.LENGTH_LONG).show()
finishAfterTransition()
false
} else {
throw e
}
}
}
companion object {
const val EXTRA_DATA = "data"

View File

@@ -29,7 +29,6 @@ abstract class BaseFullscreenActivity<B : ViewBinding> :
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
// insetsControllerCompat.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
systemUiController.setSystemUiVisible(true)
}
}

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.coroutines.suspendCoroutine
@@ -28,11 +29,23 @@ open class BaseListAdapter<T : ListModel> : AsyncListDifferDelegationAdapter<T>(
return this
}
fun addListListener(listListener: ListListener<T>) {
fun addListListener(listListener: ListListener<T>): BaseListAdapter<T> {
differ.addListListener(listListener)
return this
}
fun removeListListener(listListener: ListListener<T>) {
differ.removeListListener(listListener)
}
fun findHeader(position: Int): ListHeader? {
val snapshot = items
for (i in (0..position).reversed()) {
val item = snapshot.getOrNull(i) ?: continue
if (item is ListHeader) {
return item
}
}
return null
}
}

View File

@@ -68,6 +68,13 @@ abstract class BaseViewModel : ViewModel() {
errorEvent.call(error)
}
protected inline suspend fun <T> withLoading(block: () -> T): T = try {
loadingCounter.increment()
block()
} finally {
loadingCounter.decrement()
}
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }

View File

@@ -68,6 +68,14 @@ class RecyclerViewAlertDialog private constructor(
return this
}
fun setNeutralButton(
@StringRes textId: Int,
listener: DialogInterface.OnClickListener,
): Builder<T> {
delegate.setNeutralButton(textId, listener)
return this
}
fun setCancelable(isCancelable: Boolean): Builder<T> {
delegate.setCancelable(isCancelable)
return this

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.ui.image
import android.graphics.drawable.Drawable
import coil.target.GenericViewTarget
import com.google.android.material.chip.Chip
class ChipIconTarget(override val view: Chip) : GenericViewTarget<Chip>() {
override var drawable: Drawable?
get() = view.chipIcon
set(value) {
view.chipIcon = value
}
}

View File

@@ -12,11 +12,10 @@ import android.graphics.RectF
import android.graphics.drawable.Drawable
import androidx.annotation.StyleRes
import androidx.core.content.withStyledAttributes
import androidx.core.graphics.ColorUtils
import androidx.core.graphics.withClip
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.R
import kotlin.math.absoluteValue
import org.koitharu.kotatsu.core.util.KotatsuColors
class FaviconDrawable(
context: Context,
@@ -44,7 +43,7 @@ class FaviconDrawable(
}
paint.textAlign = Paint.Align.CENTER
paint.isFakeBoldText = true
colorForeground = MaterialColors.harmonize(colorOfString(name), colorBackground)
colorForeground = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
}
override fun draw(canvas: Canvas) {
@@ -104,9 +103,4 @@ class FaviconDrawable(
paint.getTextBounds(text, 0, text.length, tempRect)
return testTextSize * width / tempRect.width()
}
private fun colorOfString(str: String): Int {
val hue = (str.hashCode() % 360).absoluteValue.toFloat()
return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
}
}

View File

@@ -1,10 +1,9 @@
package org.koitharu.kotatsu.core.ui.list
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.view.ActionMode
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
@@ -20,7 +19,7 @@ private const val KEY_SELECTION = "selection"
private const val PROVIDER_NAME = "selection_decoration"
class ListSelectionController(
private val activity: Activity,
private val appCompatDelegate: AppCompatDelegate,
private val decoration: AbstractSelectionItemDecoration,
private val registryOwner: SavedStateRegistryOwner,
private val callback: Callback2,
@@ -108,7 +107,7 @@ class ListSelectionController(
private fun startActionMode() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
actionMode = appCompatDelegate.startSupportActionMode(this)
}
}

View File

@@ -91,7 +91,7 @@ abstract class AbstractSelectionItemDecoration : RecyclerView.ItemDecoration() {
canvas.restoreToCount(checkpoint)
}
protected open fun getItemId(parent: RecyclerView, child: View) = parent.getChildItemId(child)
abstract fun getItemId(parent: RecyclerView, child: View): Long
protected open fun onDrawBackground(
canvas: Canvas,

View File

@@ -1,25 +1,39 @@
package org.koitharu.kotatsu.core.ui.sheet
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.activity.OnBackPressedDispatcher
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.app.AppCompatDialogFragment
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.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.sidesheet.SideSheetDialog
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import com.google.android.material.R as materialR
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
private var waitingForDismissAllowingStateLoss = false
private var isFitToContentsDisabled = false
private var defaultStatusBarColor = Color.TRANSPARENT
var viewBinding: B? = null
private set
@@ -31,12 +45,19 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
protected val behavior: AdaptiveSheetBehavior?
get() = AdaptiveSheetBehavior.from(dialog)
@JvmField
val actionModeDelegate = ActionModeDelegate()
val isExpanded: Boolean
get() = behavior?.state == AdaptiveSheetBehavior.STATE_EXPANDED
val onBackPressedDispatcher: OnBackPressedDispatcher
get() = requireComponentDialog().onBackPressedDispatcher
var isLocked = false
private set
private var lockCounter = 0
final override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -60,11 +81,45 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext()
return if (context.resources.getBoolean(R.bool.is_tablet)) {
SideSheetDialog(context, theme)
val dialog = if (context.resources.getBoolean(R.bool.is_tablet)) {
SideSheetDialogImpl(context, theme)
} else {
BottomSheetDialog(context, theme)
BottomSheetDialogImpl(context, theme)
}
dialog.onBackPressedDispatcher.addCallback(actionModeDelegate)
return dialog
}
@CallSuper
protected open fun dispatchSupportActionModeStarted(mode: ActionMode) {
actionModeDelegate.onSupportActionModeStarted(mode)
val ctx = requireContext()
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors(
ContextCompat.getColor(ctx, com.google.android.material.R.color.m3_appbar_overlay_color),
ctx.getThemeColor(com.google.android.material.R.attr.colorSurface),
)
} else {
ContextCompat.getColor(ctx, R.color.kotatsu_surface)
}
dialog?.window?.let {
defaultStatusBarColor = it.statusBarColor
it.statusBarColor = actionModeColor
}
val insets = ViewCompat.getRootWindowInsets(requireView())
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return
dialog?.window?.decorView?.findViewById<ActionBarContextView?>(androidx.appcompat.R.id.action_mode_bar)?.apply {
setBackgroundColor(actionModeColor)
updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = insets.top
}
}
}
@CallSuper
protected open fun dispatchSupportActionModeFinished(mode: ActionMode) {
actionModeDelegate.onSupportActionModeFinished(mode)
dialog?.window?.statusBarColor = defaultStatusBarColor
}
fun addSheetCallback(callback: AdaptiveSheetCallback) {
@@ -81,7 +136,16 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
fun startSupportActionMode(callback: ActionMode.Callback): ActionMode? {
val appCompatDialog = dialog as? AppCompatDialog ?: return null
return appCompatDialog.delegate.startSupportActionMode(callback)
}
protected fun setExpanded(isExpanded: Boolean, isLocked: Boolean) {
this.isLocked = isLocked
if (!isLocked) {
lockCounter = 0
}
val b = behavior ?: return
if (isExpanded) {
b.state = BottomSheetBehavior.STATE_EXPANDED
@@ -109,6 +173,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) {
"Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()."
}
@@ -171,4 +249,38 @@ abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment() {
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)
}
}
}
}

View File

@@ -7,12 +7,16 @@ import org.koitharu.kotatsu.R
class ReversibleActionObserver(
private val snackbarHost: View,
private val snackbarAnchor: View? = null,
) : FlowCollector<ReversibleAction> {
override suspend fun emit(value: ReversibleAction) {
val handle = value.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make(snackbarHost, value.stringResId, length)
if (snackbarAnchor?.isShown == true) {
snackbar.anchorView = snackbarAnchor
}
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}

View File

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

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.content.Context
import android.util.ArrayMap
import android.util.AttributeSet
import androidx.collection.MutableScatterMap
import com.google.android.material.slider.Slider
import kotlin.math.cbrt
import kotlin.math.pow
@@ -12,7 +12,7 @@ class CubicSlider @JvmOverloads constructor(
attrs: AttributeSet? = null,
) : Slider(context, attrs) {
private val changeListeners = ArrayMap<OnChangeListener, OnChangeListenerMapper>(1)
private val changeListeners = MutableScatterMap<OnChangeListener, OnChangeListenerMapper>(1)
override fun setValue(value: Float) {
super.setValue(value.unmap())

View File

@@ -1,397 +0,0 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.CornerPathEffect
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.os.Build
import android.os.Parcelable
import android.text.Layout
import android.text.StaticLayout
import android.text.TextDirectionHeuristic
import android.text.TextDirectionHeuristics
import android.text.TextPaint
import android.util.AttributeSet
import android.view.View
import androidx.annotation.RequiresApi
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.draw
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.resolveDp
import org.koitharu.kotatsu.core.util.ext.resolveSp
class PieChart @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), PieChartInterface {
private var marginTextFirst: Float = context.resources.resolveDp(DEFAULT_MARGIN_TEXT_1)
private var marginTextSecond: Float = context.resources.resolveDp(DEFAULT_MARGIN_TEXT_2)
private var marginTextThird: Float = context.resources.resolveDp(DEFAULT_MARGIN_TEXT_3)
private var marginSmallCircle: Float = context.resources.resolveDp(DEFAULT_MARGIN_SMALL_CIRCLE)
private val marginText: Float = marginTextFirst + marginTextSecond
private val circleRect = RectF()
private var circleStrokeWidth: Float = context.resources.resolveDp(6f)
private var circleRadius: Float = 0f
private var circlePadding: Float = context.resources.resolveDp(8f)
private var circlePaintRoundSize: Boolean = true
private var circleSectionSpace: Float = 3f
private var circleCenterX: Float = 0f
private var circleCenterY: Float = 0f
private var numberTextPaint: TextPaint = TextPaint()
private var descriptionTextPain: TextPaint = TextPaint()
private var amountTextPaint: TextPaint = TextPaint()
private var textStartX: Float = 0f
private var textStartY: Float = 0f
private var textHeight: Int = 0
private var textCircleRadius: Float = context.resources.resolveDp(4f)
private var textAmountStr: String = ""
private var textAmountY: Float = 0f
private var textAmountXNumber: Float = 0f
private var textAmountXDescription: Float = 0f
private var textAmountYDescription: Float = 0f
private var totalAmount: Int = 0
private var pieChartColors: List<String> = listOf()
private var percentageCircleList: List<PieChartModel> = listOf()
private var textRowList: MutableList<StaticLayout> = mutableListOf()
private var dataList: List<Pair<Int, String>> = listOf()
private var animationSweepAngle: Int = 0
init {
var textAmountSize: Float = context.resources.resolveSp(22f)
var textNumberSize: Float = context.resources.resolveSp(20f)
var textDescriptionSize: Float = context.resources.resolveSp(14f)
var textAmountColor: Int = Color.WHITE
var textNumberColor: Int = Color.WHITE
var textDescriptionColor: Int = Color.GRAY
if (attrs != null) {
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.PieChart)
val colorResId = typeArray.getResourceId(R.styleable.PieChart_pieChartColors, 0)
pieChartColors = typeArray.resources.getStringArray(colorResId).toList()
marginTextFirst = typeArray.getDimension(R.styleable.PieChart_pieChartMarginTextFirst, marginTextFirst)
marginTextSecond = typeArray.getDimension(R.styleable.PieChart_pieChartMarginTextSecond, marginTextSecond)
marginTextThird = typeArray.getDimension(R.styleable.PieChart_pieChartMarginTextThird, marginTextThird)
marginSmallCircle =
typeArray.getDimension(R.styleable.PieChart_pieChartMarginSmallCircle, marginSmallCircle)
circleStrokeWidth =
typeArray.getDimension(R.styleable.PieChart_pieChartCircleStrokeWidth, circleStrokeWidth)
circlePadding = typeArray.getDimension(R.styleable.PieChart_pieChartCirclePadding, circlePadding)
circlePaintRoundSize =
typeArray.getBoolean(R.styleable.PieChart_pieChartCirclePaintRoundSize, circlePaintRoundSize)
circleSectionSpace = typeArray.getFloat(R.styleable.PieChart_pieChartCircleSectionSpace, circleSectionSpace)
textCircleRadius = typeArray.getDimension(R.styleable.PieChart_pieChartTextCircleRadius, textCircleRadius)
textAmountSize = typeArray.getDimension(R.styleable.PieChart_pieChartTextAmountSize, textAmountSize)
textNumberSize = typeArray.getDimension(R.styleable.PieChart_pieChartTextNumberSize, textNumberSize)
textDescriptionSize =
typeArray.getDimension(R.styleable.PieChart_pieChartTextDescriptionSize, textDescriptionSize)
textAmountColor = typeArray.getColor(R.styleable.PieChart_pieChartTextAmountColor, textAmountColor)
textNumberColor = typeArray.getColor(R.styleable.PieChart_pieChartTextNumberColor, textNumberColor)
textDescriptionColor =
typeArray.getColor(R.styleable.PieChart_pieChartTextDescriptionColor, textDescriptionColor)
textAmountStr = typeArray.getString(R.styleable.PieChart_pieChartTextAmount) ?: ""
typeArray.recycle()
}
circlePadding += circleStrokeWidth
// Инициализация кистей View
initPaints(amountTextPaint, textAmountSize, textAmountColor)
initPaints(numberTextPaint, textNumberSize, textNumberColor)
initPaints(descriptionTextPain, textDescriptionSize, textDescriptionColor, true)
}
@RequiresApi(Build.VERSION_CODES.M)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
textRowList.clear()
val initSizeWidth = resolveDefaultSize(widthMeasureSpec, DEFAULT_VIEW_SIZE_WIDTH)
val textTextWidth = (initSizeWidth * TEXT_WIDTH_PERCENT)
val initSizeHeight = calculateViewHeight(heightMeasureSpec, textTextWidth.toInt())
textStartX = initSizeWidth - textTextWidth.toFloat()
textStartY = initSizeHeight.toFloat() / 2 - textHeight / 2
calculateCircleRadius(initSizeWidth, initSizeHeight)
setMeasuredDimension(initSizeWidth, initSizeHeight)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawCircle(canvas)
drawText(canvas)
}
override fun onRestoreInstanceState(state: Parcelable?) {
val pieChartState = state as? PieChartState
super.onRestoreInstanceState(pieChartState?.superState ?: state)
dataList = pieChartState?.dataList ?: listOf()
}
override fun onSaveInstanceState(): Parcelable {
val superState = super.onSaveInstanceState()
return PieChartState(superState, dataList)
}
override fun setDataChart(list: List<Pair<Int, String>>) {
dataList = list
calculatePercentageOfData()
}
override fun startAnimation() {
val animator = ValueAnimator.ofInt(0, 360).apply {
duration = context.getAnimationDuration(android.R.integer.config_longAnimTime)
interpolator = FastOutSlowInInterpolator()
addUpdateListener { valueAnimator ->
animationSweepAngle = valueAnimator.animatedValue as Int
invalidate()
}
}
animator.start()
}
private fun drawCircle(canvas: Canvas) {
for (percent in percentageCircleList) {
if (animationSweepAngle > percent.percentToStartAt + percent.percentOfCircle) {
canvas.drawArc(circleRect, percent.percentToStartAt, percent.percentOfCircle, false, percent.paint)
} else if (animationSweepAngle > percent.percentToStartAt) {
canvas.drawArc(
circleRect,
percent.percentToStartAt,
animationSweepAngle - percent.percentToStartAt,
false,
percent.paint,
)
}
}
}
private fun drawText(canvas: Canvas) {
var textBuffY = textStartY
textRowList.forEachIndexed { index, staticLayout ->
if (index % 2 == 0) {
staticLayout.draw(canvas, textStartX + marginSmallCircle + textCircleRadius, textBuffY)
canvas.drawCircle(
textStartX + marginSmallCircle / 2,
textBuffY + staticLayout.height / 2 + textCircleRadius / 2,
textCircleRadius,
Paint().apply { color = Color.parseColor(pieChartColors[(index / 2) % pieChartColors.size]) },
)
textBuffY += staticLayout.height + marginTextFirst
} else {
staticLayout.draw(canvas, textStartX, textBuffY)
textBuffY += staticLayout.height + marginTextSecond
}
}
canvas.drawText(totalAmount.toString(), textAmountXNumber, textAmountY, amountTextPaint)
canvas.drawText(textAmountStr, textAmountXDescription, textAmountYDescription, descriptionTextPain)
}
private fun initPaints(textPaint: TextPaint, textSize: Float, textColor: Int, isDescription: Boolean = false) {
textPaint.color = textColor
textPaint.textSize = textSize
textPaint.isAntiAlias = true
if (!isDescription) textPaint.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
}
private fun resolveDefaultSize(spec: Int, defValue: Int): Int {
return when (MeasureSpec.getMode(spec)) {
MeasureSpec.UNSPECIFIED -> resources.resolveDp(defValue)
else -> MeasureSpec.getSize(spec)
}
}
@RequiresApi(Build.VERSION_CODES.M)
private fun calculateViewHeight(heightMeasureSpec: Int, textWidth: Int): Int {
val initSizeHeight = resolveDefaultSize(heightMeasureSpec, DEFAULT_VIEW_SIZE_HEIGHT)
textHeight = (dataList.size * marginText + getTextViewHeight(textWidth)).toInt()
val textHeightWithPadding = textHeight + paddingTop + paddingBottom
return if (textHeightWithPadding > initSizeHeight) textHeightWithPadding else initSizeHeight
}
private fun calculateCircleRadius(width: Int, height: Int) {
val circleViewWidth = (width * CIRCLE_WIDTH_PERCENT)
circleRadius = if (circleViewWidth > height) {
(height.toFloat() - circlePadding) / 2
} else {
circleViewWidth.toFloat() / 2
}
with(circleRect) {
left = circlePadding
top = height / 2 - circleRadius
right = circleRadius * 2 + circlePadding
bottom = height / 2 + circleRadius
}
circleCenterX = (circleRadius * 2 + circlePadding + circlePadding) / 2
circleCenterY = (height / 2 + circleRadius + (height / 2 - circleRadius)) / 2
textAmountY = circleCenterY
val sizeTextAmountNumber = getWidthOfAmountText(
totalAmount.toString(),
amountTextPaint,
)
textAmountXNumber = circleCenterX - sizeTextAmountNumber.width() / 2
textAmountXDescription = circleCenterX - getWidthOfAmountText(textAmountStr, descriptionTextPain).width() / 2
textAmountYDescription = circleCenterY + sizeTextAmountNumber.height() + marginTextThird
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getTextViewHeight(maxWidth: Int): Int {
var textHeight = 0
dataList.forEach {
val textLayoutNumber = getMultilineText(
text = it.first.toString(),
textPaint = numberTextPaint,
width = maxWidth,
)
val textLayoutDescription = getMultilineText(
text = it.second,
textPaint = descriptionTextPain,
width = maxWidth,
)
textRowList.apply {
add(textLayoutNumber)
add(textLayoutDescription)
}
textHeight += textLayoutNumber.height + textLayoutDescription.height
}
return textHeight
}
private fun calculatePercentageOfData() {
totalAmount = dataList.fold(0) { res, value -> res + value.first }
var startAt = circleSectionSpace
percentageCircleList = dataList.mapIndexed { index, pair ->
var percent = pair.first * 100 / totalAmount.toFloat() - circleSectionSpace
percent = if (percent < 0f) 0f else percent
val resultModel = PieChartModel(
percentOfCircle = percent,
percentToStartAt = startAt,
colorOfLine = Color.parseColor(pieChartColors[index % pieChartColors.size]),
stroke = circleStrokeWidth,
paintRound = circlePaintRoundSize,
)
if (percent != 0f) startAt += percent + circleSectionSpace
resultModel
}
}
private fun getWidthOfAmountText(text: String, textPaint: TextPaint): Rect {
val bounds = Rect()
textPaint.getTextBounds(text, 0, text.length, bounds)
return bounds
}
@RequiresApi(Build.VERSION_CODES.M)
private fun getMultilineText(
text: CharSequence,
textPaint: TextPaint,
width: Int,
start: Int = 0,
end: Int = text.length,
alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL,
textDir: TextDirectionHeuristic = TextDirectionHeuristics.LTR,
spacingMult: Float = 1f,
spacingAdd: Float = 0f
): StaticLayout {
return StaticLayout.Builder
.obtain(text, start, end, textPaint, width)
.setAlignment(alignment)
.setTextDirection(textDir)
.setLineSpacing(spacingAdd, spacingMult)
.build()
}
companion object {
private const val DEFAULT_MARGIN_TEXT_1 = 2f
private const val DEFAULT_MARGIN_TEXT_2 = 10f
private const val DEFAULT_MARGIN_TEXT_3 = 2f
private const val DEFAULT_MARGIN_SMALL_CIRCLE = 12f
private const val TEXT_WIDTH_PERCENT = 0.40
private const val CIRCLE_WIDTH_PERCENT = 0.50
const val DEFAULT_VIEW_SIZE_HEIGHT = 150
const val DEFAULT_VIEW_SIZE_WIDTH = 250
}
}
interface PieChartInterface {
fun setDataChart(list: List<Pair<Int, String>>)
fun startAnimation()
}
data class PieChartModel(
var percentOfCircle: Float = 0f,
var percentToStartAt: Float = 0f,
var colorOfLine: Int = 0,
var stroke: Float = 0f,
var paint: Paint = Paint(),
var paintRound: Boolean = true
) {
init {
if (percentOfCircle < 0 || percentOfCircle > 100) {
percentOfCircle = 100f
}
percentOfCircle = 360 * percentOfCircle / 100
if (percentToStartAt < 0 || percentToStartAt > 100) {
percentToStartAt = 0f
}
percentToStartAt = 360 * percentToStartAt / 100
if (colorOfLine == 0) {
colorOfLine = Color.parseColor("#000000")
}
paint = Paint()
paint.color = colorOfLine
paint.isAntiAlias = true
paint.style = Paint.Style.STROKE
paint.strokeWidth = stroke
paint.isDither = true
if (paintRound) {
paint.strokeJoin = Paint.Join.ROUND
paint.strokeCap = Paint.Cap.ROUND
paint.pathEffect = CornerPathEffect(8f)
}
}
}
class PieChartState(
superSavedState: Parcelable?,
val dataList: List<Pair<Int, String>>
) : View.BaseSavedState(superSavedState), Parcelable

View File

@@ -0,0 +1,155 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
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.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.getThemeColor
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 colorBase = context.getThemeColor(materialR.attr.colorPrimaryContainer)
private var colorProgress = context.getThemeColor(materialR.attr.colorPrimary)
private var colorText = context.getThemeColor(materialR.attr.colorOnPrimaryContainer)
private var progressAnimator: ValueAnimator? = null
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 = getColor(R.styleable.ProgressButton_baseColor, colorBase)
colorProgress = getColor(R.styleable.ProgressButton_progressColor, colorProgress)
colorText = getColor(R.styleable.ProgressButton_textColor, 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
paint.color = colorProgress
paint.alpha = 84 // 255 * 0.33F
applyGravity()
setWillNotDraw(false)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.drawColor(colorBase)
canvas.drawRect(0f, 0f, width * progress, height.toFloat(), paint)
}
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) {
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) {
progressAnimator?.cancel()
if (animate) {
progressAnimator = ValueAnimator.ofFloat(progress, value).apply {
duration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
interpolator = AccelerateDecelerateInterpolator()
addUpdateListener(this@ProgressButton)
start()
}
} else {
progressAnimator = null
progress = value
invalidate()
}
}
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.View
import android.view.ViewOutlineProvider
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.collection.MutableFloatList
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
@@ -25,7 +26,7 @@ class SegmentedBarView @JvmOverloads constructor(
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val segmentsData = ArrayList<Segment>()
private val segmentsSizes = ArrayList<Float>()
private val segmentsSizes = MutableFloatList()
private var cornerSize = 0f
private var scaleFactor = 1f
private var scaleAnimator: ValueAnimator? = null

View File

@@ -75,7 +75,7 @@ class TipView @JvmOverloads constructor(
val shapeAppearanceModel = ShapeAppearanceModel.builder(context, attrs, defStyleAttr, 0).build()
background = MaterialShapeDrawable(shapeAppearanceModel).also {
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.strokeColor = getColorStateList(R.styleable.TipView_strokeColor)
it.elevation = getDimension(R.styleable.TipView_elevation, 0f)

View File

@@ -1,70 +0,0 @@
package org.koitharu.kotatsu.core.util
import android.content.Context
import android.view.GestureDetector
import android.view.MotionEvent
import kotlin.math.roundToInt
class GridTouchHelper(
context: Context,
private val listener: OnGridTouchListener,
) : GestureDetector.SimpleOnGestureListener() {
private val detector = GestureDetector(context, this)
private val width = context.resources.displayMetrics.widthPixels
private val height = context.resources.displayMetrics.heightPixels
private var isDispatching = false
init {
detector.setIsLongpressEnabled(true)
detector.setOnDoubleTapListener(this)
}
fun dispatchTouchEvent(event: MotionEvent) {
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
isDispatching = listener.onProcessTouch(event.rawX.toInt(), event.rawY.toInt())
}
detector.onTouchEvent(event)
}
override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
if (!isDispatching) {
return true
}
val xIndex = (event.rawX * 2f / width).roundToInt()
val yIndex = (event.rawY * 2f / height).roundToInt()
listener.onGridTouch(
when (xIndex) {
0 -> AREA_LEFT
1 -> {
when (yIndex) {
0 -> AREA_TOP
1 -> AREA_CENTER
2 -> AREA_BOTTOM
else -> return false
}
}
2 -> AREA_RIGHT
else -> return false
},
)
return true
}
companion object {
const val AREA_CENTER = 1
const val AREA_LEFT = 2
const val AREA_RIGHT = 3
const val AREA_TOP = 4
const val AREA_BOTTOM = 5
}
interface OnGridTouchListener {
fun onGridTouch(area: Int)
fun onProcessTouch(rawX: Int, rawY: Int): Boolean
}
}

View File

@@ -0,0 +1,55 @@
package org.koitharu.kotatsu.core.util
import android.content.Context
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.core.graphics.ColorUtils
import com.google.android.material.R
import com.google.android.material.color.MaterialColors
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.math.absoluteValue
object KotatsuColors {
@ColorInt
fun segmentColor(context: Context, @AttrRes resId: Int): Int {
val colorHex = String.format("%06x", context.getThemeColor(resId))
val hue = getHue(colorHex)
val color = ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
val backgroundColor = context.getThemeColor(R.attr.colorSurfaceContainerHigh)
return MaterialColors.harmonize(color, backgroundColor)
}
@ColorInt
fun random(seed: Any): Int {
val hue = (seed.hashCode() % 360).absoluteValue.toFloat()
return ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
}
@ColorInt
fun ofManga(context: Context, manga: Manga?): Int {
val color = if (manga != null) {
val hue = (manga.id.absoluteValue % 360).toFloat()
ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
} else {
context.getThemeColor(R.attr.colorOutline)
}
val backgroundColor = context.getThemeColor(R.attr.colorSurfaceContainerHigh)
return MaterialColors.harmonize(color, backgroundColor)
}
private fun getHue(hex: String): Float {
val r = (hex.substring(0, 2).toInt(16)).toFloat()
val g = (hex.substring(2, 4).toInt(16)).toFloat()
val b = (hex.substring(4, 6).toInt(16)).toFloat()
var hue = 0F
if ((r >= g) && (g >= b)) {
hue = 60 * (g - b) / (r - b)
} else if ((g > r) && (r >= b)) {
hue = 60 * (2 - (r - b) / (g - b))
}
return hue
}
}

View File

@@ -14,7 +14,7 @@ import android.content.ContextWrapper
import android.content.OperationApplicationException
import android.content.SharedPreferences
import android.content.SyncResult
import android.content.pm.PackageManager
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.content.pm.ResolveInfo
import android.database.SQLException
import android.graphics.Bitmap
@@ -27,14 +27,19 @@ import android.provider.Settings
import android.view.View
import android.view.ViewPropertyAnimator
import android.view.Window
import android.widget.Toast
import android.webkit.WebView
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IntegerRes
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.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.os.LocaleListCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker
@@ -136,7 +141,7 @@ fun Window.setNavigationBarTransparentCompat(context: Context, elevation: Float,
} else {
// Set navbar scrim 70% of navigationBarColor
ElevationOverlayProvider(context).compositeOverlayIfNeeded(
context.getThemeColor(R.attr.m3ColorBottomMenuBackground, alphaFactor),
context.getThemeColor(com.google.android.material.R.attr.colorSurfaceContainer, alphaFactor),
elevation,
)
}
@@ -216,25 +221,26 @@ fun Context.findActivity(): Activity? = when (this) {
else -> null
}
inline fun Activity.catchingWebViewUnavailability(block: () -> Unit): Boolean {
return try {
block()
true
} catch (e: Exception) {
if (e.isWebViewUnavailable()) {
Toast.makeText(this, R.string.web_view_unavailable, Toast.LENGTH_LONG).show()
finishAfterTransition()
false
} else {
throw e
}
fun Fragment.findAppCompatDelegate(): AppCompatDelegate? {
((this as? DialogFragment)?.dialog as? AppCompatDialog)?.run {
return delegate
}
return parentFragment?.findAppCompatDelegate() ?: (activity as? AppCompatActivity)?.delegate
}
fun Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(this).areNotificationsEnabled()
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
@@ -251,3 +257,13 @@ fun Context.ensureRamAtLeast(requiredSize: Long) {
throw IllegalStateException("Not enough free memory")
}
}
fun WebView.configureForParser(userAgentOverride: String?) = with(settings) {
javaScriptEnabled = true
domStorageEnabled = true
mediaPlaybackRequiresUserGesture = false
databaseEnabled = true
if (userAgentOverride != null) {
userAgentString = userAgentOverride
}
}

View File

@@ -24,11 +24,15 @@ inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String)
}
inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: String): T? {
return getSerializableExtra(key) as T?
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
getSerializableExtra(key, T::class.java)
} else {
getSerializableExtra(key) as T?
}
}
inline fun <reified T : Serializable> Bundle.getSerializableCompat(key: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
getSerializable(key, T::class.java)
} else {
getSerializable(key) as T?

View File

@@ -25,7 +25,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
}
// disposeImageRequest()
return ImageRequest.Builder(context)
.data(data)
.data(data?.takeUnless { it == "" || it == 0 })
.lifecycle(lifecycleOwner)
.crossfade(context)
.target(this)

View File

@@ -18,6 +18,7 @@ fun calculateTimeAgo(instant: Instant, showMonths: Boolean = false): DateTimeAgo
if (instant.until(Instant.now(), ChronoUnit.MINUTES) < 3) DateTimeAgo.JustNow
else DateTimeAgo.Today
}
diffDays == 1L -> DateTimeAgo.Yesterday
diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays.toInt())
else -> {
@@ -30,3 +31,5 @@ fun calculateTimeAgo(instant: Instant, showMonths: Boolean = false): DateTimeAgo
}
}
}
fun Long.toInstantOrNull() = if (this == 0L) null else Instant.ofEpochMilli(this)

View File

@@ -10,7 +10,6 @@ import android.provider.OpenableColumns
import androidx.core.database.getStringOrNull
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.fs.FileSequence
import java.io.File
@@ -19,6 +18,7 @@ import java.nio.file.attribute.BasicFileAttributes
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.PathWalkOption
import kotlin.io.path.readAttributes
import kotlin.io.path.walk
@@ -52,7 +52,7 @@ fun File.getStorageName(context: Context): String = runCatching {
fun Uri.toFileOrNull() = if (scheme == URI_SCHEME_FILE) path?.let(::File) else null
suspend fun File.deleteAwait() = withContext(Dispatchers.IO) {
suspend fun File.deleteAwait() = runInterruptible(Dispatchers.IO) {
delete() || deleteRecursively()
}
@@ -72,7 +72,7 @@ fun ContentResolver.resolveName(uri: Uri): String? {
}
suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) {
walkCompat().sumOf { it.length() }
walkCompat(includeDirectories = false).sumOf { it.length() }
}
fun File.children() = FileSequence(this)
@@ -87,10 +87,16 @@ val File.creationTime
}
@OptIn(ExperimentalPathApi::class)
fun File.walkCompat() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
fun File.walkCompat(includeDirectories: Boolean): Sequence<File> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Use lazy loading on Android 8.0 and later
toPath().walk().map { it.toFile() }
val walk = if (includeDirectories) {
toPath().walk(PathWalkOption.INCLUDE_DIRECTORIES)
} else {
toPath().walk()
}
walk.map { it.toFile() }
} else {
// Directories are excluded by default in Path.walk(), so do it here as well
walk().filter { it.isFile }
val walk = walk()
if (includeDirectories) walk else walk.filter { it.isFile }
}

View File

@@ -11,8 +11,10 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
import org.koitharu.kotatsu.R
import java.util.concurrent.atomic.AtomicInteger
fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
var isFirstCall = true
@@ -37,6 +39,14 @@ fun <T> Flow<T>.onEachWhile(action: suspend (T) -> Boolean): Flow<T> {
}
}
fun <T> Flow<T>.onEachIndexed(action: suspend (index: Int, T) -> Unit): Flow<T> {
val counter = AtomicInteger(0)
return transform { value ->
action(counter.getAndIncrement(), value)
return@transform emit(value)
}
}
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
return map { list -> list.map(transform) }
}

View File

@@ -60,3 +60,25 @@ fun DialogFragment.showDistinct(fm: FragmentManager, tag: String) {
}
show(fm, tag)
}
tailrec fun Fragment.dismissParentDialog(): Boolean {
return when (val parent = parentFragment) {
null -> return false
is DialogFragment -> {
parent.dismiss()
true
}
else -> parent.dismissParentDialog()
}
}
@Suppress("UNCHECKED_CAST")
tailrec fun <T> Fragment.findParentCallback(cls: Class<T>): T? {
val parent = parentFragment
return when {
parent == null -> cls.castOrNull(activity)
cls.isInstance(parent) -> parent as T
else -> parent.findParentCallback(cls)
}
}

View File

@@ -1,11 +1,13 @@
package org.koitharu.kotatsu.core.util.ext
import okhttp3.Cookie
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.internal.closeQuietly
import okhttp3.internal.isSensitiveHeader
import okio.IOException
import org.json.JSONObject
import org.jsoup.HttpStatusException
@@ -59,3 +61,16 @@ fun Cookie.newBuilder(): Cookie.Builder = Cookie.Builder().also { c ->
c.httpOnly()
}
}
fun String.sanitizeHeaderValue(): String {
return if (all(Char::isValidForHeaderValue)) {
this // fast path
} else {
filter(Char::isValidForHeaderValue)
}
}
private fun Char.isValidForHeaderValue(): Boolean {
// from okhttp3.Headers$Companion.checkValue
return this == '\t' || this in '\u0020'..'\u007e'
}

View File

@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.core.util.ext
inline fun Int.ifZero(defaultValue: () -> Int): Int = if (this == 0) defaultValue() else this
inline fun Long.ifZero(defaultValue: () -> Long): Long = if (this == 0L) defaultValue() else this
fun longOf(a: Int, b: Int): Long {
return a.toLong() shl 32 or (b.toLong() and 0xffffffffL)
}

View File

@@ -2,22 +2,23 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.ActivityNotFoundException
import android.content.res.Resources
import android.util.AndroidRuntimeException
import androidx.annotation.DrawableRes
import androidx.collection.arraySetOf
import coil.network.HttpException
import okio.FileNotFoundException
import okio.IOException
import org.acra.ktx.sendWithAcra
import org.json.JSONException
import org.jsoup.HttpStatusException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.BadBackupFormatException
import org.koitharu.kotatsu.core.exceptions.CaughtException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
import org.koitharu.kotatsu.core.exceptions.SyncApiException
import org.koitharu.kotatsu.core.exceptions.TooManyRequestExceptions
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED
import org.koitharu.kotatsu.parsers.ErrorMessages.FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED
@@ -43,6 +44,7 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is TooManyRequestExceptions -> resources.getString(R.string.too_many_requests_message)
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is BadBackupFormatException -> resources.getString(R.string.unsupported_backup_message)
is FileNotFoundException -> resources.getString(R.string.file_not_found)
is AccessDeniedException -> resources.getString(R.string.no_access_to_file)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
@@ -55,8 +57,11 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is SocketTimeoutException,
-> resources.getString(R.string.network_error)
is NoDataReceivedException -> resources.getString(R.string.error_no_data_received)
is WrongPasswordException -> resources.getString(R.string.wrong_password)
is NotFoundException -> resources.getString(R.string.not_found_404)
is UnsupportedSourceException -> resources.getString(R.string.unsupported_source)
is HttpException -> getHttpDisplayMessage(response.code, resources)
is HttpStatusException -> getHttpDisplayMessage(statusCode, resources)
@@ -99,24 +104,27 @@ fun Throwable.isReportable(): Boolean {
return this is Error || this.javaClass in reportableExceptions
}
fun Throwable.isNetworkError(): Boolean {
return this is UnknownHostException || this is SocketTimeoutException
}
fun Throwable.report() {
val exception = CaughtException(this, "${javaClass.simpleName}($message)")
exception.sendWithAcra()
}
private val reportableExceptions = arraySetOf<Class<*>>(
ParseException::class.java,
JSONException::class.java,
RuntimeException::class.java,
IllegalStateException::class.java,
IllegalArgumentException::class.java,
ConcurrentModificationException::class.java,
UnsupportedOperationException::class.java,
NoDataReceivedException::class.java,
)
fun Throwable.isWebViewUnavailable(): Boolean {
return (this is AndroidRuntimeException && message?.contains("WebView") == true) ||
cause?.isWebViewUnavailable() == true
val trace = stackTraceToString()
return trace.contains("android.webkit.WebView.<init>")
}
@Suppress("FunctionName")

View File

@@ -1,15 +1,14 @@
package org.koitharu.kotatsu.core.util.ext
import android.app.Activity
import android.graphics.Rect
import android.os.Build
import android.view.View
import android.view.View.MeasureSpec
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Checkable
import androidx.appcompat.widget.ActionMenuView
import androidx.appcompat.widget.Toolbar
import androidx.core.view.SoftwareKeyboardControllerCompat
import androidx.core.view.children
import androidx.core.view.descendants
import androidx.core.view.isVisible
@@ -18,19 +17,18 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.progressindicator.BaseProgressIndicator
import com.google.android.material.slider.Slider
import com.google.android.material.tabs.TabLayout
import kotlin.math.roundToInt
fun View.hideKeyboard() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0)
SoftwareKeyboardControllerCompat(this).hide()
}
fun View.showKeyboard() {
val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(this, 0)
SoftwareKeyboardControllerCompat(this).show()
}
fun View.hasGlobalPoint(x: Int, y: Int): Boolean {
@@ -172,3 +170,11 @@ fun MaterialButton.setProgressIcon() {
icon = progressDrawable
progressDrawable.start()
}
fun Chip.setProgressIcon() {
val progressDrawable = CircularProgressDrawable(context)
progressDrawable.strokeWidth = resources.resolveDp(2f)
progressDrawable.setColorSchemeColors(currentTextColor)
chipIcon = progressDrawable
progressDrawable.start()
}

View File

@@ -1,6 +1,8 @@
package org.koitharu.kotatsu.core.util.progress
import android.os.SystemClock
import androidx.collection.IntList
import androidx.collection.MutableIntList
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@@ -10,7 +12,7 @@ private const val NO_TIME = -1L
class TimeLeftEstimator {
private var times = ArrayList<Int>()
private var times = MutableIntList()
private var lastTick: Tick? = null
private val tooLargeTime = TimeUnit.DAYS.toMillis(1)
@@ -50,6 +52,15 @@ class TimeLeftEstimator {
return if (etl == NO_TIME) NO_TIME else System.currentTimeMillis() + etl
}
private fun IntList.average(): Double {
if (isEmpty()) {
return 0.0
}
var acc = 0L
forEach { acc += it }
return acc / size.toDouble()
}
private class Tick(
@JvmField val value: Int,
@JvmField val total: Int,

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.details.data
import android.content.res.Resources
import org.koitharu.kotatsu.R
data class ReadingTime(
val minutes: Int,
val hours: Int,
val isContinue: Boolean,
) {
fun format(resources: Resources): String = when {
hours == 0 && minutes == 0 -> resources.getString(R.string.less_than_minute)
hours == 0 -> resources.getQuantityString(R.plurals.minutes, minutes, minutes)
minutes == 0 -> resources.getQuantityString(R.plurals.hours, hours, hours)
else -> resources.getString(
R.string.remaining_time_pattern,
resources.getQuantityString(R.plurals.hours, hours, hours),
resources.getQuantityString(R.plurals.minutes, minutes, minutes),
)
}
fun formatShort(resources: Resources): String? = when {
hours == 0 && minutes == 0 -> null
hours == 0 -> resources.getString(R.string.minutes_short, minutes)
minutes == 0 -> resources.getString(R.string.hours_short, hours)
else -> resources.getString(R.string.hours_minutes_short, hours, minutes)
}
}

View File

@@ -6,6 +6,7 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.details.data.MangaDetails
@@ -35,6 +36,10 @@ class DetailsInteractor @Inject constructor(
.map { it.isNotEmpty() }
}
fun observeFavourite(mangaId: Long): Flow<Set<FavouriteCategory>> {
return favouritesRepository.observeCategories(mangaId)
}
fun observeNewChapters(mangaId: Long): Flow<Int> {
return settings.observeAsFlow(AppSettings.KEY_TRACKER_ENABLED) { isTrackerEnabled }
.flatMapLatest { isEnabled ->

View File

@@ -54,7 +54,8 @@ class DetailsLoadUseCase @Inject constructor(
} catch (e: IOException) {
local?.await()?.manga?.also { localManga ->
send(MangaDetails(localManga, null, localManga.description?.parseAsHtml(withImages = false), true))
} ?: throw e
}
throw e
}
}

View File

@@ -0,0 +1,53 @@
package org.koitharu.kotatsu.details.domain
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.stats.data.StatsRepository
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.roundToInt
class ReadingTimeUseCase @Inject constructor(
private val settings: AppSettings,
private val statsRepository: StatsRepository,
) {
suspend fun invoke(manga: MangaDetails?, branch: String?, history: MangaHistory?): ReadingTime? {
if (!settings.isReadingTimeEstimationEnabled) {
return null
}
val chapters = manga?.chapters?.get(branch)
if (chapters.isNullOrEmpty()) {
return null
}
val isOnHistoryBranch = history != null && chapters.findById(history.chapterId) != null
// Impossible task, I guess. Good luck on this.
var averageTimeSec: Int = 20 /* pages */ * getSecondsPerPage(manga.id) * chapters.size
if (isOnHistoryBranch) {
averageTimeSec = (averageTimeSec * (1f - checkNotNull(history).percent)).roundToInt()
}
if (averageTimeSec < 60) {
return null
}
return ReadingTime(
minutes = (averageTimeSec / 60) % 60,
hours = averageTimeSec / 3600,
isContinue = isOnHistoryBranch,
)
}
private suspend fun getSecondsPerPage(mangaId: Long): Int {
var time = if (settings.isStatsEnabled) {
TimeUnit.MILLISECONDS.toSeconds(statsRepository.getTimePerPage(mangaId)).toInt()
} else {
0
}
if (time == 0) {
time = 10 // default
}
return time
}
}

View File

@@ -0,0 +1,126 @@
package org.koitharu.kotatsu.details.ui
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuProvider
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
import org.koitharu.kotatsu.core.util.ext.setValueRounded
import org.koitharu.kotatsu.core.util.progress.IntPercentLabelFormatter
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_CHAPTERS
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet.Companion.TAB_PAGES
import java.lang.ref.WeakReference
class ChapterPagesMenuProvider(
private val viewModel: DetailsViewModel,
private val sheet: BaseAdaptiveSheet<*>,
private val pager: ViewPager2,
private val settings: AppSettings,
) : OnBackPressedCallback(false), MenuProvider, SearchView.OnQueryTextListener, MenuItem.OnActionExpandListener,
Slider.OnChangeListener {
private var expandedItemRef: WeakReference<MenuItem>? = null
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
val tab = getCurrentTab()
when (tab) {
TAB_CHAPTERS -> {
menuInflater.inflate(R.menu.opt_chapters, menu)
menu.findItem(R.id.action_search)?.run {
setOnActionExpandListener(this@ChapterPagesMenuProvider)
(actionView as? SearchView)?.setupChaptersSearchView()
}
menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_grid_view)?.isChecked = viewModel.isChaptersInGridView.value == true
}
TAB_PAGES -> {
menuInflater.inflate(R.menu.opt_pages, menu)
menu.findItem(R.id.action_grid_size)?.run {
setOnActionExpandListener(this@ChapterPagesMenuProvider)
(actionView as? Slider)?.setupPagesSizeSlider()
}
}
}
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_reversed -> {
viewModel.setChaptersReversed(!menuItem.isChecked)
true
}
R.id.action_grid_view -> {
viewModel.setChaptersInGridView(!menuItem.isChecked)
true
}
else -> false
}
override fun handleOnBackPressed() {
expandedItemRef?.get()?.collapseActionView()
}
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
expandedItemRef = WeakReference(item)
sheet.expandAndLock()
isEnabled = true
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
expandedItemRef = null
isEnabled = false
(item.actionView as? SearchView)?.setQuery("", false)
viewModel.performChapterSearch(null)
sheet.unlock()
return true
}
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
viewModel.performChapterSearch(newText)
return true
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
if (fromUser) {
settings.gridSizePages = value.toInt()
}
}
private fun SearchView.setupChaptersSearchView() {
setOnQueryTextListener(this@ChapterPagesMenuProvider)
setIconifiedByDefault(false)
queryHint = context.getString(R.string.search_chapters)
}
private fun Slider.setupPagesSizeSlider() {
valueFrom = 50f
valueTo = 150f
stepSize = 5f
isTickVisible = false
labelBehavior = LabelFormatter.LABEL_FLOATING
setLabelFormatter(IntPercentLabelFormatter(context))
setValueRounded(settings.gridSizePages.toFloat())
addOnChangeListener(this@ChapterPagesMenuProvider)
}
private fun getCurrentTab(): Int {
var page = pager.currentItem
if (page > 0 && pager.adapter?.itemCount == 2) { // no Pages page
page++ // shift
}
return page
}
}

View File

@@ -1,10 +1,14 @@
package org.koitharu.kotatsu.details.ui
import android.content.Context
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.mapToSet
fun MangaDetails.mapChapters(
@@ -12,6 +16,7 @@ fun MangaDetails.mapChapters(
newCount: Int,
branch: String?,
bookmarks: List<Bookmark>,
isGrid: Boolean,
): List<ChapterListItem> {
val remoteChapters = chapters[branch].orEmpty()
val localChapters = local?.manga?.getChapters(branch).orEmpty()
@@ -43,6 +48,7 @@ fun MangaDetails.mapChapters(
isNew = isUnread && result.size >= newFrom,
isDownloaded = local != null,
isBookmarked = chapter.id in bookmarked,
isGrid = isGrid,
)
}
if (!localMap.isNullOrEmpty()) {
@@ -56,8 +62,28 @@ fun MangaDetails.mapChapters(
isNew = false,
isDownloaded = !isLocal,
isBookmarked = chapter.id in bookmarked,
isGrid = isGrid,
)
}
}
return result
}
fun List<ChapterListItem>.withVolumeHeaders(context: Context): List<ListModel> {
var prevVolume = 0
val result = ArrayList<ListModel>((size * 1.4).toInt())
for (item in this) {
val chapter = item.chapter
if (chapter.volume != prevVolume) {
val text = if (chapter.volume == 0) {
context.getString(R.string.volume_unknown)
} else {
context.getString(R.string.volume_, chapter.volume)
}
result.add(ListHeader(text))
prevVolume = chapter.volume
}
result.add(item)
}
return result
}

View File

@@ -28,8 +28,9 @@ class ChaptersMenuProvider(
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_grid_view)?.isChecked = viewModel.isChaptersInGridView.value == true
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
@@ -37,6 +38,10 @@ class ChaptersMenuProvider(
viewModel.setChaptersReversed(!menuItem.isChecked)
true
}
R.id.action_grid_view-> {
viewModel.setChaptersInGridView(!menuItem.isChecked)
true
}
else -> false
}

View File

@@ -4,7 +4,9 @@ import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.text.style.DynamicDrawableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.RelativeSizeSpan
import android.transition.AutoTransition
import android.transition.Slide
@@ -27,6 +29,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.preference.PreferenceManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
@@ -34,7 +37,6 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.filterNotNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaIntent
@@ -123,20 +125,11 @@ class DetailsActivity :
viewModel.manga.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onError.observeEvent(
viewModel.onError.observeEvent(this, DetailsErrorObserver(this, viewModel, exceptionResolver))
viewModel.onActionDone.observeEvent(
this,
SnackbarErrorObserver(
host = viewBinding.containerDetails,
fragment = null,
resolver = exceptionResolver,
onResolved = { isResolved ->
if (isResolved) {
viewModel.reload()
}
},
),
ReversibleActionObserver(viewBinding.containerDetails, viewBinding.layoutBottom),
)
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.containerDetails))
viewModel.onShowTip.observeEvent(this) { showTip() }
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.selectedBranch.observe(this) {
@@ -148,6 +141,7 @@ class DetailsActivity :
viewModel.isChaptersEmpty.observe(this, chaptersMenuInvalidator)
val menuInvalidator = MenuInvalidator(this)
viewModel.favouriteCategories.observe(this, menuInvalidator)
viewModel.isStatsAvailable.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) {
viewBinding.buttonDropdown.isVisible = it.size > 1
@@ -185,6 +179,9 @@ class DetailsActivity :
buttonTip = null
val menu = PopupMenu(v.context, v)
menu.inflate(R.menu.popup_read)
menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run {
!isIncognitoMode && history != null
}
menu.setOnMenuItemClickListener(this)
menu.setForceShowIcon(true)
menu.show()
@@ -201,6 +198,11 @@ class DetailsActivity :
true
}
R.id.action_forget -> {
viewModel.removeFromHistory()
true
}
R.id.action_pages_thumbs -> {
val history = viewModel.historyInfo.value.history
PagesThumbnailsSheet.show(
@@ -318,6 +320,18 @@ class DetailsActivity :
val branches = viewModel.branches.value
for ((i, branch) in branches.withIndex()) {
val title = buildSpannedString {
if (branch.isCurrent) {
inSpans(
ImageSpan(
this@DetailsActivity,
R.drawable.ic_current_chapter,
DynamicDrawableSpan.ALIGN_BASELINE,
),
) {
append(' ')
}
append(' ')
}
append(branch.name ?: getString(R.string.system_default))
append(' ')
append(' ')
@@ -363,12 +377,13 @@ class DetailsActivity :
}
private fun initPager() {
val adapter = DetailsPagerAdapter(this)
val adapter = DetailsPagerAdapter(this, settings)
viewBinding.pager.recyclerView?.isNestedScrollingEnabled = false
viewBinding.pager.offscreenPageLimit = 1
viewBinding.pager.adapter = adapter
TabLayoutMediator(viewBinding.tabs, viewBinding.pager, adapter).attach()
viewBinding.pager.setCurrentItem(settings.defaultDetailsTab, false)
viewBinding.tabs.isVisible = adapter.itemCount > 1
}
private fun showBottomSheet(isVisible: Boolean) {
@@ -408,15 +423,25 @@ class DetailsActivity :
companion object {
const val TIP_BUTTON = "btn_read"
private const val KEY_NEW_ACTIVITY = "new_details_screen"
fun newIntent(context: Context, manga: Manga): Intent {
return Intent(context, DetailsActivity::class.java)
return getActivityIntent(context)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
}
fun newIntent(context: Context, mangaId: Long): Intent {
return Intent(context, DetailsActivity::class.java)
return getActivityIntent(context)
.putExtra(MangaIntent.KEY_ID, mangaId)
}
private fun getActivityIntent(context: Context): Intent {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val useNewActivity = prefs.getBoolean(KEY_NEW_ACTIVITY, false)
return Intent(
context,
if (useNewActivity) DetailsActivity2::class.java else DetailsActivity::class.java,
)
}
}
}

View File

@@ -0,0 +1,703 @@
package org.koitharu.kotatsu.details.ui
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.text.style.DynamicDrawableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.RelativeSizeSpan
import android.transition.TransitionManager
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.Insets
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import androidx.core.text.method.LinkMovementMethodCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import coil.ImageLoader
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil.transform.CircleCropTransformation
import coil.util.CoilUtils
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.filterNotNull
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.FavouriteCategory
import org.koitharu.kotatsu.core.model.iconResId
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.model.titleResId
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.parser.favicon.faviconUri
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.ChipIconTarget
import org.koitharu.kotatsu.core.ui.image.CoverSizeResolver
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.ui.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.core.util.FileSize
import org.koitharu.kotatsu.core.util.ViewBadge
import org.koitharu.kotatsu.core.util.ext.crossfade
import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.getThemeColor
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.isTextTruncated
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.parentView
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ActivityDetailsNewBinding
import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
import org.koitharu.kotatsu.details.ui.scrobbling.ScrobblingItemDecoration
import org.koitharu.kotatsu.details.ui.scrobbling.ScrollingInfoAdapter
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.list.domain.ListExtraProvider
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.MangaListActivity
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class DetailsActivity2 :
BaseActivity<ActivityDetailsNewBinding>(),
View.OnClickListener,
View.OnLongClickListener, PopupMenu.OnMenuItemClickListener, View.OnLayoutChangeListener,
ViewTreeObserver.OnDrawListener, ChipsView.OnChipClickListener, OnListItemClickListener<Bookmark> {
@Inject
lateinit var shortcutManager: AppShortcutManager
@Inject
lateinit var coil: ImageLoader
@Inject
lateinit var tagHighlighter: ListExtraProvider
private val viewModel: DetailsViewModel by viewModels()
var bottomSheetMediator: ChaptersBottomSheetMediator? = null
private set
private lateinit var chaptersBadge: ViewBadge
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityDetailsNewBinding.inflate(layoutInflater))
supportActionBar?.run {
setDisplayHomeAsUpEnabled(true)
setDisplayShowTitleEnabled(false)
}
viewBinding.buttonRead.setOnClickListener(this)
viewBinding.buttonRead.setOnLongClickListener(this)
viewBinding.buttonRead.setOnContextClickListenerCompat(this)
viewBinding.buttonChapters.setOnClickListener(this)
viewBinding.infoLayout.chipBranch.setOnClickListener(this)
viewBinding.infoLayout.chipSize.setOnClickListener(this)
viewBinding.infoLayout.chipSource.setOnClickListener(this)
viewBinding.infoLayout.chipFavorite.setOnClickListener(this)
viewBinding.infoLayout.chipAuthor.setOnClickListener(this)
viewBinding.infoLayout.chipTime.setOnClickListener(this)
viewBinding.imageViewCover.setOnClickListener(this)
viewBinding.buttonDescriptionMore.setOnClickListener(this)
viewBinding.buttonScrobblingMore.setOnClickListener(this)
viewBinding.buttonRelatedMore.setOnClickListener(this)
viewBinding.infoLayout.chipSource.setOnClickListener(this)
viewBinding.infoLayout.chipSize.setOnClickListener(this)
viewBinding.textViewDescription.addOnLayoutChangeListener(this)
viewBinding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
viewBinding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
viewBinding.chipsTags.onChipClickListener = this
viewBinding.recyclerViewRelated.addItemDecoration(
SpacingItemDecoration(resources.getDimensionPixelOffset(R.dimen.grid_spacing)),
)
TitleScrollCoordinator(viewBinding.textViewTitle).attach(viewBinding.scrollView)
chaptersBadge = ViewBadge(viewBinding.buttonChapters, this)
viewModel.details.filterNotNull().observe(this, ::onMangaUpdated)
viewModel.onMangaRemoved.observeEvent(this, ::onMangaRemoved)
viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged)
viewModel.onError.observeEvent(
this,
SnackbarErrorObserver(viewBinding.scrollView, null, exceptionResolver) {
if (it) viewModel.reload()
},
)
viewModel.onActionDone.observeEvent(this, ReversibleActionObserver(viewBinding.scrollView, null))
viewModel.historyInfo.observe(this, ::onHistoryChanged)
viewModel.isLoading.observe(this, ::onLoadingStateChanged)
viewModel.scrobblingInfo.observe(this, ::onScrobblingInfoChanged)
viewModel.localSize.observe(this, ::onLocalSizeChanged)
viewModel.relatedManga.observe(this, ::onRelatedMangaChanged)
// viewModel.chapters.observe(this, ::onChaptersChanged)
viewModel.readingTime.observe(this, ::onReadingTimeChanged)
viewModel.selectedBranch.observe(this) {
viewBinding.infoLayout.chipBranch.text = it.ifNullOrEmpty { getString(R.string.system_default) }
}
viewModel.favouriteCategories.observe(this, ::onFavoritesChanged)
val menuInvalidator = MenuInvalidator(this)
viewModel.isStatsAvailable.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) {
viewBinding.infoLayout.chipBranch.isVisible = it.size > 1
}
viewModel.chapters.observe(this, PrefetchObserver(this))
viewModel.onDownloadStarted.observeEvent(
this,
DownloadStartedObserver(viewBinding.scrollView),
)
addMenuProvider(
DetailsMenuProvider(
activity = this,
viewModel = viewModel,
snackbarHost = viewBinding.scrollView,
appShortcutManager = shortcutManager,
),
)
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_read -> openReader(isIncognitoMode = false)
R.id.chip_branch -> showBranchPopupMenu(v)
R.id.button_chapters -> {
ChaptersPagesSheet.show(supportFragmentManager)
}
R.id.chip_author -> {
val manga = viewModel.manga.value ?: return
startActivity(
SearchActivity.newIntent(
context = v.context,
source = manga.source,
query = manga.author ?: return,
),
)
}
R.id.chip_source -> {
val manga = viewModel.manga.value ?: return
startActivity(
MangaListActivity.newIntent(
context = v.context,
source = manga.source,
),
)
}
R.id.chip_size -> {
val manga = viewModel.manga.value ?: return
LocalInfoDialog.show(supportFragmentManager, manga)
}
R.id.chip_favorite -> {
val manga = viewModel.manga.value ?: return
FavoriteSheet.show(supportFragmentManager, manga)
}
R.id.chip_time -> {
if (viewModel.isStatsAvailable.value) {
val manga = viewModel.manga.value ?: return
MangaStatsSheet.show(supportFragmentManager, manga)
} else {
// TODO
}
}
R.id.imageView_cover -> {
val manga = viewModel.manga.value ?: return
startActivity(
ImageActivity.newIntent(
v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
manga.source,
),
scaleUpActivityOptionsOf(v),
)
}
R.id.button_description_more -> {
val tv = viewBinding.textViewDescription
TransitionManager.beginDelayedTransition(tv.parentView)
if (tv.maxLines in 1 until Integer.MAX_VALUE) {
tv.maxLines = Integer.MAX_VALUE
} else {
tv.maxLines = resources.getInteger(R.integer.details_description_lines)
}
}
R.id.button_scrobbling_more -> {
val manga = viewModel.manga.value ?: return
ScrobblingSelectorSheet.show(supportFragmentManager, manga, null)
}
R.id.button_related_more -> {
val manga = viewModel.manga.value ?: return
startActivity(RelatedMangaActivity.newIntent(v.context, manga))
}
}
}
override fun onChipClick(chip: Chip, data: Any?) {
val tag = data as? MangaTag ?: return
startActivity(MangaListActivity.newIntent(this, setOf(tag)))
}
override fun onLongClick(v: View): Boolean = when (v.id) {
R.id.button_read -> {
val menu = PopupMenu(v.context, v)
menu.inflate(R.menu.popup_read)
menu.menu.findItem(R.id.action_forget)?.isVisible = viewModel.historyInfo.value.run {
!isIncognitoMode && history != null
}
menu.setOnMenuItemClickListener(this)
menu.setForceShowIcon(true)
menu.show()
true
}
else -> false
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_incognito -> {
openReader(isIncognitoMode = true)
true
}
R.id.action_forget -> {
viewModel.removeFromHistory()
true
}
else -> false
}
}
override fun onItemClick(item: Bookmark, view: View) {
startActivity(
IntentBuilder(view.context).bookmark(item).incognito(true).build(),
)
Toast.makeText(view.context, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
override fun onDraw() {
viewBinding.run {
buttonDescriptionMore.isVisible = textViewDescription.maxLines == Int.MAX_VALUE ||
textViewDescription.isTextTruncated
}
}
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
with(viewBinding) {
buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated
}
}
private fun onChaptersChanged(chapters: List<ChapterListItem>?) {
// TODO
}
private fun onFavoritesChanged(categories: Set<FavouriteCategory>) {
val chip = viewBinding.infoLayout.chipFavorite
chip.setChipIconResource(if (categories.isEmpty()) R.drawable.ic_heart_outline else R.drawable.ic_heart)
chip.text = if (categories.isEmpty()) {
getString(R.string.add_to_favourites)
} else {
if (categories.size == 1) {
categories.first().title.ellipsize(FAV_LABEL_LIMIT)
}
buildString(FAV_LABEL_LIMIT + 6) {
for ((i, cat) in categories.withIndex()) {
if (i == 0) {
append(cat.title.ellipsize(FAV_LABEL_LIMIT - 4))
} else if (length + cat.title.length > FAV_LABEL_LIMIT) {
append(", ")
append(getString(R.string.list_ellipsize_pattern, categories.size - i))
break
} else {
append(", ")
append(cat.title)
}
}
}
}
}
private fun onReadingTimeChanged(time: ReadingTime?) {
val chip = viewBinding.infoLayout.chipTime
chip.textAndVisible = time?.formatShort(chip.resources)
}
private fun onDescriptionChanged(description: CharSequence?) {
val tv = viewBinding.textViewDescription
if (description.isNullOrBlank()) {
tv.setText(R.string.no_description)
} else {
tv.text = description
}
}
private fun onLocalSizeChanged(size: Long) {
val chip = viewBinding.infoLayout.chipSize
if (size == 0L) {
chip.isVisible = false
} else {
chip.text = FileSize.BYTES.format(chip.context, size)
chip.isVisible = true
}
}
private fun onRelatedMangaChanged(related: List<MangaItemModel>) {
if (related.isEmpty()) {
viewBinding.groupRelated.isVisible = false
return
}
val rv = viewBinding.recyclerViewRelated
@Suppress("UNCHECKED_CAST")
val adapter = (rv.adapter as? BaseListAdapter<ListModel>) ?: BaseListAdapter<ListModel>()
.addDelegate(
ListItemType.MANGA_GRID,
mangaGridItemAD(
coil, this,
StaticItemSizeResolver(resources.getDimensionPixelSize(R.dimen.smaller_grid_width)),
) { item, view ->
startActivity(DetailsActivity.newIntent(view.context, item))
},
).also { rv.adapter = it }
adapter.items = related
viewBinding.groupRelated.isVisible = true
}
private fun onLoadingStateChanged(isLoading: Boolean) {
val button = viewBinding.buttonChapters
if (isLoading) {
button.setImageDrawable(
CircularProgressDrawable(this).also {
it.setStyle(CircularProgressDrawable.LARGE)
it.setColorSchemeColors(getThemeColor(materialR.attr.colorControlNormal))
it.start()
},
)
} else {
button.setImageResource(R.drawable.ic_list_sheet)
}
}
private fun onScrobblingInfoChanged(scrobblings: List<ScrobblingInfo>) {
var adapter = viewBinding.recyclerViewScrobbling.adapter as? ScrollingInfoAdapter
viewBinding.groupScrobbling.isGone = scrobblings.isEmpty()
if (adapter != null) {
adapter.items = scrobblings
} else {
adapter = ScrollingInfoAdapter(this, coil, supportFragmentManager)
adapter.items = scrobblings
viewBinding.recyclerViewScrobbling.adapter = adapter
viewBinding.recyclerViewScrobbling.addItemDecoration(ScrobblingItemDecoration())
}
}
private fun onMangaUpdated(details: MangaDetails) {
with(viewBinding) {
val manga = details.toManga()
val hasChapters = !manga.chapters.isNullOrEmpty()
// Main
loadCover(manga)
textViewTitle.text = manga.title
textViewSubtitle.textAndVisible = manga.altTitle
infoLayout.chipAuthor.textAndVisible = manga.author
if (manga.hasRating) {
ratingBar.rating = manga.rating * ratingBar.numStars
ratingBar.isVisible = true
} else {
ratingBar.isVisible = false
}
manga.state?.let { state ->
textViewState.textAndVisible = resources.getString(state.titleResId)
imageViewState.setImageResource(state.iconResId)
} ?: run {
textViewState.isVisible = false
imageViewState.isVisible = false
}
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) {
infoLayout.chipSource.isVisible = false
} else {
infoLayout.chipSource.text = manga.source.title
infoLayout.chipSource.isVisible = true
}
textViewNsfw.isVisible = manga.isNsfw
// Chips
bindTags(manga)
textViewDescription.text = details.description.ifNullOrEmpty { getString(R.string.no_description) }
viewBinding.infoLayout.chipSource.also { chip ->
ImageRequest.Builder(this@DetailsActivity2)
.data(manga.source.faviconUri())
.lifecycle(this@DetailsActivity2)
.crossfade(false)
.size(resources.getDimensionPixelSize(materialR.dimen.m3_chip_icon_size))
.target(ChipIconTarget(chip))
.placeholder(R.drawable.ic_web)
.fallback(R.drawable.ic_web)
.error(R.drawable.ic_web)
.source(manga.source)
.transformations(CircleCropTransformation())
.allowRgb565(true)
.enqueueWith(coil)
}
buttonChapters.isEnabled = hasChapters
title = manga.title
buttonRead.isEnabled = hasChapters
invalidateOptionsMenu()
}
}
private fun onMangaRemoved(manga: Manga) {
Toast.makeText(
this,
getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT,
).show()
finishAfterTransition()
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding.root.updatePadding(
left = insets.left,
right = insets.right,
bottom = insets.bottom
)
}
private fun onHistoryChanged(info: HistoryInfo) {
with(viewBinding.buttonRead) {
if (info.history != null) {
setTitle(R.string._continue)
} else {
setTitle(R.string.read)
}
}
viewBinding.buttonRead.subtitle = when {
!info.isValid -> getString(R.string.loading_)
info.currentChapter >= 0 -> getString(
R.string.chapter_d_of_d,
info.currentChapter + 1,
info.totalChapters,
)
info.totalChapters == 0 -> getString(R.string.no_chapters)
else -> resources.getQuantityString(
R.plurals.chapters,
info.totalChapters,
info.totalChapters,
)
}
viewBinding.buttonRead.setProgress(info.history?.percent?.coerceIn(0f, 1f) ?: 0f, true)
}
private fun onNewChaptersChanged(count: Int) {
chaptersBadge.counter = count
}
private fun showBranchPopupMenu(v: View) {
val menu = PopupMenu(v.context, v)
val branches = viewModel.branches.value
for ((i, branch) in branches.withIndex()) {
val title = buildSpannedString {
if (branch.isCurrent) {
inSpans(
ImageSpan(
this@DetailsActivity2,
R.drawable.ic_current_chapter,
DynamicDrawableSpan.ALIGN_BASELINE,
),
) {
append(' ')
}
append(' ')
}
append(branch.name ?: getString(R.string.system_default))
append(' ')
append(' ')
inSpans(
ForegroundColorSpan(
v.context.getThemeColor(
android.R.attr.textColorSecondary,
Color.LTGRAY,
),
),
RelativeSizeSpan(0.74f),
) {
append(branch.count.toString())
}
}
val item = menu.menu.add(R.id.group_branches, Menu.NONE, i, title)
item.isCheckable = true
item.isChecked = branch.isSelected
}
menu.menu.setGroupCheckable(R.id.group_branches, true, true)
menu.setOnMenuItemClickListener {
viewModel.setSelectedBranch(branches.getOrNull(it.order)?.name)
true
}
menu.show()
}
private fun openReader(isIncognitoMode: Boolean) {
val manga = viewModel.manga.value ?: return
val chapterId = viewModel.historyInfo.value.history?.chapterId
if (chapterId != null && manga.chapters?.none { x -> x.id == chapterId } == true) {
Snackbar.make(viewBinding.scrollView, R.string.chapter_is_missing, Snackbar.LENGTH_SHORT)
.show()
} else {
startActivity(
IntentBuilder(this)
.manga(manga)
.branch(viewModel.selectedBranchValue)
.incognito(isIncognitoMode)
.build(),
)
if (isIncognitoMode) {
Toast.makeText(this, R.string.incognito_mode, Toast.LENGTH_SHORT).show()
}
}
}
private fun bindTags(manga: Manga) {
viewBinding.chipsTags.isVisible = manga.tags.isNotEmpty()
viewBinding.chipsTags.setChips(
manga.tags.map { tag ->
ChipsView.ChipModel(
title = tag.title,
tint = tagHighlighter.getTagTint(tag),
icon = 0,
data = tag,
isCheckable = false,
isChecked = false,
)
},
)
}
private fun loadCover(manga: Manga) {
val imageUrl = manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }
val lastResult = CoilUtils.result(viewBinding.imageViewCover)
if (lastResult is SuccessResult && lastResult.request.data == imageUrl) {
return
}
val request = ImageRequest.Builder(this)
.target(viewBinding.imageViewCover)
.size(CoverSizeResolver(viewBinding.imageViewCover))
.data(imageUrl)
.tag(manga.source)
.crossfade(this)
.lifecycle(this)
.placeholderMemoryCacheKey(manga.coverUrl)
val previousDrawable = lastResult?.drawable
if (previousDrawable != null) {
request.fallback(previousDrawable)
.placeholder(previousDrawable)
.error(previousDrawable)
} else {
request.fallback(R.drawable.ic_placeholder)
.placeholder(R.drawable.ic_placeholder)
.error(R.drawable.ic_error_placeholder)
}
request.enqueueWith(coil)
}
private class PrefetchObserver(
private val context: Context,
) : FlowCollector<List<ChapterListItem>?> {
private var isCalled = false
override suspend fun emit(value: List<ChapterListItem>?) {
if (value.isNullOrEmpty()) {
return
}
if (!isCalled) {
isCalled = true
val item = value.find { it.isCurrent } ?: value.first()
MangaPrefetchService.prefetchPages(context, item.chapter)
}
}
}
companion object {
private const val FAV_LABEL_LIMIT = 10
fun newIntent(context: Context, manga: Manga): Intent {
return Intent(context, DetailsActivity2::class.java)
.putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga))
}
fun newIntent(context: Context, mangaId: Long): Intent {
return Intent(context, DetailsActivity2::class.java)
.putExtra(MangaIntent.KEY_ID, mangaId)
}
}
}

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.details.ui
import com.google.android.material.snackbar.Snackbar
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
import org.koitharu.kotatsu.core.exceptions.resolve.ErrorObserver
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.isNetworkError
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.exception.ParseException
class DetailsErrorObserver(
override val activity: DetailsActivity,
private val viewModel: DetailsViewModel,
resolver: ExceptionResolver?,
) : ErrorObserver(
activity.viewBinding.containerDetails, null, resolver,
{ isResolved ->
if (isResolved) {
viewModel.reload()
}
},
) {
override suspend fun emit(value: Throwable) {
val snackbar = Snackbar.make(host, value.getDisplayMessage(host.context.resources), Snackbar.LENGTH_SHORT)
if (value is NotFoundException || value is UnsupportedSourceException) {
snackbar.duration = Snackbar.LENGTH_INDEFINITE
}
when {
canResolve(value) -> {
snackbar.setAction(ExceptionResolver.getResolveStringId(value)) {
resolve(value)
}
}
value is ParseException -> {
val fm = fragmentManager
if (fm != null) {
snackbar.setAction(R.string.details) {
ErrorDetailsDialog.show(fm, value, value.url)
}
}
}
value.isNetworkError() -> {
snackbar.setAction(R.string.try_again) {
viewModel.reload()
}
}
}
snackbar.show()
}
}

View File

@@ -48,6 +48,7 @@ import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.showOrHide
import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.FragmentDetailsBinding
import org.koitharu.kotatsu.details.data.ReadingTime
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
import org.koitharu.kotatsu.details.ui.related.RelatedMangaActivity
@@ -61,6 +62,7 @@ import org.koitharu.kotatsu.list.ui.adapter.mangaGridItemAD
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaItemModel
import org.koitharu.kotatsu.list.ui.size.StaticItemSizeResolver
import org.koitharu.kotatsu.local.ui.info.LocalInfoDialog
import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -101,6 +103,7 @@ class DetailsFragment :
binding.buttonScrobblingMore.setOnClickListener(this)
binding.buttonRelatedMore.setOnClickListener(this)
binding.infoLayout.textViewSource.setOnClickListener(this)
binding.infoLayout.textViewSize.setOnClickListener(this)
binding.textViewDescription.addOnLayoutChangeListener(this)
binding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethodCompat.getInstance()
@@ -118,6 +121,7 @@ class DetailsFragment :
viewModel.localSize.observe(viewLifecycleOwner, ::onLocalSizeChanged)
viewModel.relatedManga.observe(viewLifecycleOwner, ::onRelatedMangaChanged)
viewModel.chapters.observe(viewLifecycleOwner, ::onChaptersChanged)
viewModel.readingTime.observe(viewLifecycleOwner, ::onReadingTimeChanged)
}
override fun onItemClick(item: Bookmark, view: View) {
@@ -185,7 +189,7 @@ class DetailsFragment :
isVisible = false
}
}
if (manga.source == MangaSource.LOCAL) {
if (manga.source == MangaSource.LOCAL || manga.source == MangaSource.DUMMY) {
infoLayout.textViewSource.isVisible = false
} else {
infoLayout.textViewSource.text = manga.source.title
@@ -211,6 +215,19 @@ class DetailsFragment :
}
}
private fun onReadingTimeChanged(time: ReadingTime?) {
val binding = viewBinding ?: return
if (time == null) {
binding.approximateReadTimeLayout.isVisible = false
return
}
binding.approximateReadTime.text = time.format(resources)
binding.approximateReadTimeTitle.setText(
if (time.isContinue) R.string.approximate_remaining_time else R.string.approximate_reading_time,
)
binding.approximateReadTimeLayout.isVisible = true
}
private fun onDescriptionChanged(description: CharSequence?) {
val tv = requireViewBinding().textViewDescription
if (description.isNullOrBlank()) {
@@ -309,6 +326,10 @@ class DetailsFragment :
)
}
R.id.textView_size -> {
LocalInfoDialog.show(parentFragmentManager, manga)
}
R.id.imageView_cover -> {
startActivity(
ImageActivity.newIntent(

View File

@@ -14,6 +14,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.alternatives.ui.AlternativesActivity
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.os.AppShortcutManager
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
@@ -23,6 +24,7 @@ import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.scrobbling.common.ui.selector.ScrobblingSelectorSheet
import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.stats.ui.sheet.MangaStatsSheet
class DetailsMenuProvider(
private val activity: FragmentActivity,
@@ -33,6 +35,7 @@ class DetailsMenuProvider(
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_details, menu)
menu.findItem(R.id.action_favourite).isVisible = activity is DetailsActivity
}
override fun onPrepareMenu(menu: Menu) {
@@ -40,11 +43,13 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_save).isVisible = manga?.source != null && manga.source != MangaSource.LOCAL
menu.findItem(R.id.action_delete).isVisible = manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_alternatives).isVisible = manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
menu.findItem(R.id.action_stats).isVisible = viewModel.isStatsAvailable.value
menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
if (viewModel.favouriteCategories.value.isNotEmpty()) R.drawable.ic_heart else R.drawable.ic_heart_outline,
)
}
@@ -85,7 +90,7 @@ class DetailsMenuProvider(
R.id.action_browser -> {
viewModel.manga.value?.let {
activity.startActivity(BrowserActivity.newIntent(activity, it.publicUrl, it.title))
activity.startActivity(BrowserActivity.newIntent(activity, it.publicUrl, it.source, it.title))
}
}
@@ -101,6 +106,18 @@ class DetailsMenuProvider(
}
}
R.id.action_alternatives -> {
viewModel.manga.value?.let {
activity.startActivity(AlternativesActivity.newIntent(activity, it))
}
}
R.id.action_stats -> {
viewModel.manga.value?.let {
MangaStatsSheet.show(activity.supportFragmentManager, it)
}
}
R.id.action_scrobbling -> {
viewModel.manga.value?.let {
ScrobblingSelectorSheet.show(activity.supportFragmentManager, it, null)

View File

@@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus
@@ -25,6 +26,7 @@ import okio.FileNotFoundException
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.model.findById
import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -34,6 +36,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.combine
import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.onEachWhile
import org.koitharu.kotatsu.core.util.ext.requireValue
@@ -42,6 +45,7 @@ import org.koitharu.kotatsu.details.domain.BranchComparator
import org.koitharu.kotatsu.details.domain.DetailsInteractor
import org.koitharu.kotatsu.details.domain.DetailsLoadUseCase
import org.koitharu.kotatsu.details.domain.ProgressUpdateUseCase
import org.koitharu.kotatsu.details.domain.ReadingTimeUseCase
import org.koitharu.kotatsu.details.domain.RelatedMangaUseCase
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.HistoryInfo
@@ -59,6 +63,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingInfo
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblingStatus
import org.koitharu.kotatsu.stats.data.StatsRepository
import javax.inject.Inject
@HiltViewModel
@@ -76,6 +81,8 @@ class DetailsViewModel @Inject constructor(
private val extraProvider: ListExtraProvider,
private val detailsLoadUseCase: DetailsLoadUseCase,
private val progressUpdateUseCase: ProgressUpdateUseCase,
private val readingTimeUseCase: ReadingTimeUseCase,
private val statsRepository: StatsRepository,
) : BaseViewModel() {
private val intent = MangaIntent(savedStateHandle)
@@ -94,7 +101,10 @@ class DetailsViewModel @Inject constructor(
val history = historyRepository.observeOne(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val favouriteCategories = interactor.observeIsFavourite(mangaId)
val favouriteCategories = interactor.observeFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptySet())
val isStatsAvailable = statsRepository.observeHasStats(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val remoteManga = MutableStateFlow<Manga?>(null)
@@ -113,7 +123,13 @@ class DetailsViewModel @Inject constructor(
val isChaptersReversed = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_REVERSE_CHAPTERS,
valueProducer = { chaptersReverse },
valueProducer = { isChaptersReverse },
)
val isChaptersInGridView = settings.observeAsStateFlow(
scope = viewModelScope + Dispatchers.Default,
key = AppSettings.KEY_GRID_VIEW_CHAPTERS,
valueProducer = { isChaptersGridView },
)
val historyInfo: StateFlow<HistoryInfo> = combine(
@@ -136,6 +152,7 @@ class DetailsViewModel @Inject constructor(
val localSize = details
.map { it?.local }
.distinctUntilChanged()
.combine(localStorageChanges.onStart { emit(null) }) { x, _ -> x }
.map { local ->
if (local != null) {
runCatchingCancellable {
@@ -169,10 +186,21 @@ class DetailsViewModel @Inject constructor(
val branches: StateFlow<List<MangaBranch>> = combine(
details,
selectedBranch,
) { m, b ->
(m?.chapters ?: return@combine emptyList())
.map { x -> MangaBranch(x.key, x.value.size, x.key == b) }
.sortedWith(BranchComparator())
history,
) { m, b, h ->
val c = m?.chapters
if (c.isNullOrEmpty()) {
return@combine emptyList()
}
val currentBranch = h?.let { m.allChapters.findById(it.chapterId) }?.branch
c.map { x ->
MangaBranch(
name = x.key,
count = x.value.size,
isSelected = x.key == b,
isCurrent = h != null && x.key == currentBranch,
)
}.sortedWith(BranchComparator())
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
val isChaptersEmpty: StateFlow<Boolean> = details.map {
@@ -186,12 +214,14 @@ class DetailsViewModel @Inject constructor(
selectedBranch,
newChaptersCount,
bookmarks,
) { manga, history, branch, news, bookmarks ->
isChaptersInGridView,
) { manga, history, branch, news, bookmarks, grid ->
manga?.mapChapters(
history,
news,
branch,
bookmarks,
grid,
).orEmpty()
},
isChaptersReversed,
@@ -200,6 +230,14 @@ class DetailsViewModel @Inject constructor(
(if (reversed) list.asReversed() else list).filterSearch(query)
}.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
val readingTime = combine(
details,
selectedBranch,
history,
) { m, b, h ->
readingTimeUseCase.invoke(m, b, h)
}.stateIn(viewModelScope, SharingStarted.Lazily, null)
val selectedBranchValue: String?
get() = selectedBranch.value
@@ -253,7 +291,11 @@ class DetailsViewModel @Inject constructor(
}
fun setChaptersReversed(newValue: Boolean) {
settings.chaptersReverse = newValue
settings.isChaptersReverse = newValue
}
fun setChaptersInGridView(newValue: Boolean) {
settings.isChaptersGridView = newValue
}
fun setSelectedBranch(branch: String?) {
@@ -298,6 +340,7 @@ class DetailsViewModel @Inject constructor(
page = 0,
scroll = 0,
percent = percent,
force = true,
)
}
}
@@ -324,6 +367,13 @@ class DetailsViewModel @Inject constructor(
settings.closeTip(DetailsActivity.TIP_BUTTON)
}
fun removeFromHistory() {
launchJob(Dispatchers.Default) {
val handle = historyRepository.delete(setOf(mangaId))
onActionDone.call(ReversibleAction(R.string.removed_from_history, handle))
}
}
private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
detailsLoadUseCase.invoke(intent)
.onEachWhile {

View File

@@ -8,6 +8,7 @@ import org.koitharu.kotatsu.core.ui.dialog.RecyclerViewAlertDialog
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.download.ui.dialog.DownloadOption
import org.koitharu.kotatsu.download.ui.dialog.downloadOptionAD
import org.koitharu.kotatsu.settings.SettingsActivity
class DownloadDialogHelper(
private val host: View,
@@ -57,6 +58,9 @@ class DownloadDialogHelper(
.setCancelable(true)
.setTitle(R.string.download)
.setNegativeButton(android.R.string.cancel)
.setNeutralButton(R.string.settings) { _, _ ->
host.context.startActivity(SettingsActivity.newDownloadsSettingsIntent(host.context))
}
.setItems(options)
.create()
.also { it.show() }

View File

@@ -0,0 +1,52 @@
package org.koitharu.kotatsu.details.ui.adapter
import android.graphics.Typeface
import androidx.core.view.isVisible
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.core.model.formatNumber
import org.koitharu.kotatsu.core.ui.list.AdapterDelegateClickListenerAdapter
import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.util.ext.getThemeColorStateList
import org.koitharu.kotatsu.databinding.ItemChapterGridBinding
import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.list.ui.model.ListModel
fun chapterGridItemAD(
clickListener: OnListItemClickListener<ChapterListItem>,
) = adapterDelegateViewBinding<ChapterListItem, ListModel, ItemChapterGridBinding>(
viewBinding = { inflater, parent -> ItemChapterGridBinding.inflate(inflater, parent, false) },
on = { item, _, _ -> item is ChapterListItem && item.isGrid },
) {
val eventListener = AdapterDelegateClickListenerAdapter(this, clickListener)
itemView.setOnClickListener(eventListener)
itemView.setOnLongClickListener(eventListener)
bind { payloads ->
if (payloads.isEmpty()) {
binding.textViewTitle.text = item.chapter.formatNumber() ?: "?"
}
binding.imageViewNew.isVisible = item.isNew
binding.imageViewCurrent.isVisible = item.isCurrent
binding.imageViewBookmarked.isVisible = item.isBookmarked
binding.imageViewDownloaded.isVisible = item.isDownloaded
when {
item.isCurrent -> {
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorPrimary))
binding.textViewTitle.typeface = Typeface.DEFAULT_BOLD
}
item.isUnread -> {
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorPrimary))
binding.textViewTitle.typeface = Typeface.DEFAULT
}
else -> {
binding.textViewTitle.setTextColor(context.getThemeColorStateList(android.R.attr.textColorHint))
binding.textViewTitle.typeface = Typeface.DEFAULT
}
}
}
}

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